Skip to content

Commit a0891d6

Browse files
fix(ravengard): dungeon instantiate over the service channel plus admin flow
The instantiate push never reached the dungeon server because the handler was registered on the proxy channel while the orchestrator sends on the service channel, which is what the timeout was; it now registers through getServiceHandlers the way the bedwars game does. Admin generation no longer transfers the runner: it allocates the instance and answers with its id and a clickable /dungeon admin join invite, and joining an admin instance teleports the runner into creative high above the dungeon's centre looking straight down. Instances with nobody inside self-destruct after thirty seconds, tracked from creation, and the listing shows the remaining countdown on each empty instance through the heartbeat's map field.
1 parent cd2f56a commit a0891d6

6 files changed

Lines changed: 139 additions & 20 deletions

File tree

commons/src/main/java/net/swofty/commons/protocol/objects/orchestrator/ListGamesProtocol.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public record ListGamesMessage(ServerType type) { }
2929
public record GameSummary(String gameId, String gameTypeName, String map,
3030
int playerCount, boolean acceptingJoins) { }
3131

32-
public record ServerGames(String shortName, int onlinePlayers, int maxPlayers,
32+
public record ServerGames(String shortName, String serverUuid, int onlinePlayers, int maxPlayers,
3333
Integer remainingGameSlots, List<GameSummary> games) { }
3434

3535
public record ListGamesResponse(List<ServerGames> servers, boolean success, String error) { }

service.orchestrator/src/main/java/net/swofty/service/orchestrator/endpoints/ListGamesEndpoint.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public ListGamesProtocol.ListGamesResponse handle(ListGamesProtocol.ListGamesMes
3434
game.game().isAcceptingJoins()));
3535
}
3636
servers.add(new ListGamesProtocol.ServerGames(server.shortName(),
37-
server.onlinePlayers(), server.maxPlayers(),
37+
server.uuid().toString(), server.onlinePlayers(), server.maxPlayers(),
3838
server.remainingGameSlots(), games));
3939
}
4040
return new ListGamesProtocol.ListGamesResponse(servers, true, null);

type.ravengarddungeon/src/main/java/net/swofty/type/ravengarddungeon/TypeRavengardDungeonLoader.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public void onInitialize(MinecraftServer server) {
6969

7070
@Override
7171
public void afterInitialize(MinecraftServer server) {
72+
net.swofty.type.ravengarddungeon.game.DungeonInstanceRegistry.startExpiryTask();
7273
startOrchestratorHeartbeat();
7374
}
7475

@@ -79,7 +80,8 @@ private void startOrchestratorHeartbeat() {
7980
net.swofty.type.game.game.GameObject game = new net.swofty.type.game.game.GameObject();
8081
game.setGameId(instance.getGameId());
8182
game.setType(ServerType.RAVENGARD_DUNGEON);
82-
game.setMap("generated");
83+
game.setMap(instance.getPlayers().isEmpty()
84+
? "empty:" + instance.getRemainingLifeSeconds() : "generated");
8385
game.setGameTypeName(instance.getMode());
8486
game.setAcceptingJoins(instance.isAcceptingJoins());
8587
game.setInvolvedPlayers(new java.util.ArrayList<>(instance.getPlayers()));
@@ -141,8 +143,12 @@ public List<HypixelNPC> getNPCs() {
141143

142144
@Override
143145
public List<RedisMessageHandler<?, ?>> getProxyHandlers() {
144-
return List.of(new net.swofty.type.ravengarddungeon.redis.DungeonInstantiateGameHandler(),
145-
new net.swofty.type.generic.redis.service.GameInformationHandler());
146+
return List.of();
147+
}
148+
149+
@Override
150+
public List<RedisMessageHandler<?, ?>> getServiceHandlers() {
151+
return List.of(new net.swofty.type.ravengarddungeon.redis.DungeonInstantiateGameHandler());
146152
}
147153

148154
@Override

type.ravengarddungeon/src/main/java/net/swofty/type/ravengarddungeon/events/ActionPlayerDungeonAssign.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,42 @@ public void onSpawn(PlayerSpawnEvent event) {
3535
player.sendMessage("§cYour dungeon instance no longer exists.");
3636
return;
3737
}
38-
instance.getPlayers().add(player.getUuid());
38+
instance.markPlayerJoined(player.getUuid());
3939
player.sendMessage("§7Preparing your dungeon (seed §f" + instance.getSeed() + "§7)...");
40+
boolean aerial = instance.getMode().equals("ADMIN");
4041
instance.whenReady().thenRun(() -> player.scheduler().scheduleNextTick(() -> {
41-
Pos spawn = instance.getGenerated().spawn().withY(67);
42+
Pos spawn;
43+
if (aerial) {
44+
double[] center = boundsCenter(instance);
45+
spawn = new Pos(center[0], 140, center[1], 0, 90);
46+
player.setGameMode(net.minestom.server.entity.GameMode.CREATIVE);
47+
} else {
48+
spawn = instance.getGenerated().spawn().withY(67);
49+
}
4250
player.setInstance(instance.getInstance(), spawn);
4351
player.sendMessage("§aEntered dungeon §f" + instance.getGameId().toString().substring(0, 8)
4452
+ "§a (" + instance.getGenerated().dungeon().getRoomCount() + " rooms, mode "
4553
+ instance.getMode() + ").");
4654
}));
4755
}
4856

57+
private static double[] boundsCenter(DungeonInstanceRegistry.DungeonInstance instance) {
58+
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE;
59+
int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE;
60+
for (var placement : instance.getGenerated().dungeon().getPlacements()) {
61+
minX = Math.min(minX, placement.originX());
62+
maxX = Math.max(maxX, placement.originX() + placement.getFootprintWidth());
63+
minZ = Math.min(minZ, placement.originZ());
64+
maxZ = Math.max(maxZ, placement.originZ() + placement.getFootprintDepth());
65+
}
66+
return new double[]{(minX + maxX) / 2.0, (minZ + maxZ) / 2.0};
67+
}
68+
4969
@PhasedEvent(node = EventNodes.PLAYER, requireDataLoaded = false, phase = EventPhase.DISCONNECT)
5070
public void onDisconnect(PlayerDisconnectEvent event) {
5171
UUID uuid = event.getPlayer().getUuid();
5272
for (DungeonInstanceRegistry.DungeonInstance instance : DungeonInstanceRegistry.all()) {
53-
if (instance.getPlayers().remove(uuid)) {
54-
DungeonInstanceRegistry.removeIfEmpty(instance.getGameId());
55-
}
73+
instance.markPlayerLeft(uuid);
5674
}
5775
}
5876
}

type.ravengarddungeon/src/main/java/net/swofty/type/ravengarddungeon/game/DungeonInstanceRegistry.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public final class DungeonInstanceRegistry {
2424
private DungeonInstanceRegistry() {
2525
}
2626

27+
public static final long EMPTY_LIFETIME_MILLIS = 30_000;
28+
2729
public static final class DungeonInstance {
2830
private final UUID gameId = UUID.randomUUID();
2931
private final String mode;
@@ -32,6 +34,28 @@ public static final class DungeonInstance {
3234
private final CompletableFuture<Void> ready;
3335
private final RavengardDungeonGenerator.GeneratedDungeon generated;
3436
private final Set<UUID> players = ConcurrentHashMap.newKeySet();
37+
private volatile long emptySince = System.currentTimeMillis();
38+
39+
public void markPlayerJoined(UUID player) {
40+
players.add(player);
41+
emptySince = 0;
42+
}
43+
44+
public void markPlayerLeft(UUID player) {
45+
if (players.remove(player) && players.isEmpty()) {
46+
emptySince = System.currentTimeMillis();
47+
}
48+
}
49+
50+
public long getRemainingLifeSeconds() {
51+
if (!players.isEmpty() || emptySince == 0) return -1;
52+
return Math.max(0, (EMPTY_LIFETIME_MILLIS - (System.currentTimeMillis() - emptySince)) / 1000);
53+
}
54+
55+
public boolean isExpired() {
56+
return players.isEmpty() && emptySince > 0
57+
&& System.currentTimeMillis() - emptySince > EMPTY_LIFETIME_MILLIS;
58+
}
3559

3660
private DungeonInstance(String mode, long seed, InstanceContainer instance,
3761
RavengardDungeonGenerator.GeneratedDungeon generated,
@@ -111,11 +135,14 @@ public static int remainingSlots() {
111135
return Math.max(0, MAX_INSTANCES - INSTANCES.size());
112136
}
113137

114-
public static void removeIfEmpty(UUID gameId) {
115-
DungeonInstance dungeonInstance = INSTANCES.get(gameId);
116-
if (dungeonInstance != null && dungeonInstance.getPlayers().isEmpty()) {
117-
INSTANCES.remove(gameId);
118-
MinecraftServer.getInstanceManager().unregisterInstance(dungeonInstance.getInstance());
119-
}
138+
public static void startExpiryTask() {
139+
MinecraftServer.getSchedulerManager().buildTask(() -> {
140+
for (DungeonInstance instance : INSTANCES.values()) {
141+
if (instance.isExpired()) {
142+
INSTANCES.remove(instance.getGameId());
143+
MinecraftServer.getInstanceManager().unregisterInstance(instance.getInstance());
144+
}
145+
}
146+
}).repeat(net.minestom.server.timer.TaskSchedule.seconds(1)).schedule();
120147
}
121148
}

type.ravengardgeneric/src/main/java/net/swofty/type/ravengardgeneric/commands/DungeonCommand.java

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public void registerUsage(MinestomCommand command) {
3232
if (player.getRank().isStaff()) {
3333
player.sendMessage("§e/dungeon list §7- every dungeon server and its instances");
3434
player.sendMessage("§e/dungeon admin generate [seed] [rooms] §7- dedicated instance");
35+
player.sendMessage("§e/dungeon admin join <instance> §7- fly over an instance");
3536
}
3637
});
3738

@@ -49,21 +50,86 @@ public void registerUsage(MinestomCommand command) {
4950

5051
command.addSyntax((sender, context) -> {
5152
if (!permissionCheck(sender, Rank.STAFF)) return;
52-
queue((RavengardPlayer) sender,
53+
adminGenerate((RavengardPlayer) sender,
5354
"ADMIN:" + ThreadLocalRandom.current().nextLong() + ":" + DEFAULT_ADMIN_ROOMS);
5455
}, ArgumentType.Literal("admin"), ArgumentType.Literal("generate"));
5556

5657
command.addSyntax((sender, context) -> {
5758
if (!permissionCheck(sender, Rank.STAFF)) return;
58-
queue((RavengardPlayer) sender,
59+
adminGenerate((RavengardPlayer) sender,
5960
"ADMIN:" + context.get(seedArg) + ":" + DEFAULT_ADMIN_ROOMS);
6061
}, ArgumentType.Literal("admin"), ArgumentType.Literal("generate"), seedArg);
6162

6263
command.addSyntax((sender, context) -> {
6364
if (!permissionCheck(sender, Rank.STAFF)) return;
64-
queue((RavengardPlayer) sender,
65+
adminGenerate((RavengardPlayer) sender,
6566
"ADMIN:" + context.get(seedArg) + ":" + context.get(roomsArg));
6667
}, ArgumentType.Literal("admin"), ArgumentType.Literal("generate"), seedArg, roomsArg);
68+
69+
var instanceArg = ArgumentType.Word("instance");
70+
command.addSyntax((sender, context) -> {
71+
if (!permissionCheck(sender, Rank.STAFF)) return;
72+
adminJoin((RavengardPlayer) sender, context.get(instanceArg));
73+
}, ArgumentType.Literal("admin"), ArgumentType.Literal("join"), instanceArg);
74+
}
75+
76+
private static void adminGenerate(RavengardPlayer player, String mode) {
77+
player.sendMessage("§7Allocating a dungeon instance...");
78+
GetServerForMapProtocol.GetServerForMapMessage request =
79+
new GetServerForMapProtocol.GetServerForMapMessage(
80+
ServerType.RAVENGARD_DUNGEON, null, mode, 1);
81+
ORCHESTRATOR.handleRequest(request).thenAccept(response -> {
82+
if (!(response instanceof GetServerForMapProtocol.GetServerForMapResponse(
83+
UnderstandableProxyServer server, String gameId, boolean success, String error))
84+
|| server == null || gameId == null) {
85+
String reason = response instanceof GetServerForMapProtocol.GetServerForMapResponse r
86+
&& r.error() != null ? r.error() : "no servers available";
87+
player.sendMessage("§cCould not allocate an instance: " + reason);
88+
return;
89+
}
90+
player.sendMessage("§aInstance §f" + gameId + "§a created on §f" + server.shortName()
91+
+ "§a. It self-destructs after 30s with nobody inside.");
92+
player.sendMessage(net.kyori.adventure.text.Component
93+
.text("§e§l[CLICK TO JOIN] §7or run /dungeon admin join " + gameId.substring(0, 8))
94+
.clickEvent(net.kyori.adventure.text.event.ClickEvent
95+
.runCommand("/dungeon admin join " + gameId)));
96+
}).exceptionally(throwable -> {
97+
player.sendMessage("§cAllocation failed: " + throwable.getMessage());
98+
return null;
99+
});
100+
}
101+
102+
private static void adminJoin(RavengardPlayer player, String instanceId) {
103+
ORCHESTRATOR.handleRequest(new ListGamesProtocol.ListGamesMessage(ServerType.RAVENGARD_DUNGEON))
104+
.thenAccept(response -> {
105+
if (!(response instanceof ListGamesProtocol.ListGamesResponse listing)
106+
|| !listing.success()) {
107+
player.sendMessage("§cFailed to look the instance up.");
108+
return;
109+
}
110+
for (ListGamesProtocol.ServerGames server : listing.servers()) {
111+
for (ListGamesProtocol.GameSummary game : server.games()) {
112+
if (!game.gameId().equals(instanceId)
113+
&& !game.gameId().startsWith(instanceId)) continue;
114+
UnderstandableProxyServer proxy = new UnderstandableProxyServer(
115+
server.shortName(),
116+
java.util.UUID.fromString(server.serverUuid()),
117+
ServerType.RAVENGARD_DUNGEON, -1,
118+
new java.util.ArrayList<>(), server.maxPlayers(),
119+
server.shortName());
120+
ORCHESTRATOR.handleRequest(new ChooseGameProtocol.ChooseGameMessage(
121+
player.getUuid(), proxy, game.gameId())).thenRun(() -> {
122+
player.sendMessage("§aSending you to §f" + server.shortName() + "§a!");
123+
player.asProxyPlayer().transferToWithIndication(proxy.uuid());
124+
});
125+
return;
126+
}
127+
}
128+
player.sendMessage("§cNo instance found matching §f" + instanceId + "§c.");
129+
}).exceptionally(throwable -> {
130+
player.sendMessage("§cLookup failed: " + throwable.getMessage());
131+
return null;
132+
});
67133
}
68134

69135
private boolean permissionCheck(net.minestom.server.command.CommandSender sender, Rank rank) {
@@ -123,10 +189,12 @@ private static void list(RavengardPlayer player) {
123189
+ " players§7, §f" + (server.remainingGameSlots() == null
124190
? "?" : server.remainingGameSlots()) + "§7 free instance slots");
125191
for (ListGamesProtocol.GameSummary game : server.games()) {
192+
String expiry = game.map() != null && game.map().startsWith("empty:")
193+
? " §c(dies in " + game.map().substring(6) + "s)" : "";
126194
player.sendMessage(" §8- §f" + game.gameId().substring(0, 8)
127195
+ " §7" + game.gameTypeName()
128196
+ " §f" + game.playerCount() + " players "
129-
+ (game.acceptingJoins() ? "§aopen" : "§cclosed"));
197+
+ (game.acceptingJoins() ? "§aopen" : "§cclosed") + expiry);
130198
}
131199
if (server.games().isEmpty()) {
132200
player.sendMessage(" §8- §7no instances");

0 commit comments

Comments
 (0)