diff --git a/client/src/views/Lobby.vue b/client/src/views/Lobby.vue index 3c89404..99b8a3e 100644 --- a/client/src/views/Lobby.vue +++ b/client/src/views/Lobby.vue @@ -115,8 +115,12 @@ onMounted(async () => { const room = await colyseusService.joinLobby(); colorInput.value = colyseusService.playerColor.value || '#667eea'; + let resumed = false; + const guardResume = () => { if (resumed) return true; resumed = true; return false; }; + // Prefer reconnection token path to bypass locked rooms room.onMessage("resumeReconnection", async (data: any) => { + if (guardResume()) return; try { await colyseusService.reconnectWithToken(data.token); // Leave lobby before navigating @@ -132,6 +136,7 @@ onMounted(async () => { // Listen for server-initiated resume to existing game (fallback joinById) room.onMessage("resumeGame", async (data: any) => { + if (guardResume()) return; try { const gameRoom = await colyseusService.joinGameRoom(data.roomId); // Leave lobby before navigating diff --git a/server/src/adminApi.ts b/server/src/adminApi.ts index f982eeb..ec94bb6 100644 --- a/server/src/adminApi.ts +++ b/server/src/adminApi.ts @@ -368,12 +368,18 @@ adminRouter.post("/admin/shuffle-players", async (req: Request, res: Response) = // 6. Assign players to rooms and roles randomly const assignments: { [roomId: string]: { p1: any; p2: any } } = {}; + // Shuffle rooms so pairs go to random rooms + const shuffledRooms = [...roomsInfo]; + for (let i = shuffledRooms.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledRooms[i], shuffledRooms[j]] = [shuffledRooms[j], shuffledRooms[i]]; + } - for (let i = 0; i < playerGroups.length && i < roomsInfo.length; i++) { + for (let i = 0; i < playerGroups.length && i < shuffledRooms.length; i++) { const group = playerGroups[i]; - const roomInfo = roomsInfo[i]; + const roomInfo = shuffledRooms[i]; - // Randomly assign P1 and P2 roles + // Randomize roles P1/P2 const [player1, player2] = Math.random() < 0.5 ? [group[0], group[1]] : [group[1], group[0]]; assignments[roomInfo.roomId] = { @@ -393,6 +399,17 @@ adminRouter.post("/admin/shuffle-players", async (req: Request, res: Response) = .catch(error => console.error(`Failed to clear room ${room.roomId}:`, error)) ); + // Clear current-room and reconnection tokens for all players to avoid resume during shuffle + try { + allPlayers.forEach(p => { + const u = p.uuid; + if (u) { + NameManager.getInstance().clearCurrentRoom(u); + (NameManager.getInstance() as any).clearReconnectToken?.(u); + } + }); + } catch {} + await Promise.allSettled(clearPromises); // 8. Wait a bit for rooms to clear, then assign new players diff --git a/server/src/rooms/LobbyRoom.ts b/server/src/rooms/LobbyRoom.ts index ed60577..502383e 100644 --- a/server/src/rooms/LobbyRoom.ts +++ b/server/src/rooms/LobbyRoom.ts @@ -49,7 +49,7 @@ export class LobbyRoom extends Room { }, 2000); } - onJoin(client: Client, options: any) { + async onJoin(client: Client, options: any) { console.log(`[LobbyRoom] ${client.sessionId} joined lobby with UUID: ${options.uuid}`); // Enforce UUID presence and allowlist (if configured) if (!options.uuid || !isUuidAllowed(options.uuid)) { @@ -62,32 +62,7 @@ export class LobbyRoom extends Room { if (options.uuid) { this.sessionToUuid.set(client.sessionId, options.uuid); - // If this UUID has a current active game room assignment, redirect there - try { - const currentRoomId = NameManager.getInstance().getCurrentRoom(options.uuid); - if (currentRoomId) { - // Verify room exists and is not finished - matchMaker.query({ roomId: currentRoomId }).then((rooms) => { - const room = rooms[0]; - const status = room?.metadata?.gameStatus || "waiting"; - if (room && status !== "finished") { - const token = NameManager.getInstance().getReconnectToken(options.uuid); - if (token) { - // Prefer reconnection token to bypass room lock - client.send("resumeReconnection", { token }); - } else { - // Fallback to joinById path - client.send("resumeGame", { roomId: currentRoomId }); - } - // Mark as in game on lobby state to avoid duplicate quickPlay - const p = this.state.players.get(client.sessionId); - if (p) p.inGame = true; - return; - } - }).catch(() => {}); - } - } catch {} - + // (duplicate resume check removed; handled below with await) // Check for shuffle redirect FIRST if (NameManager.getInstance().isShuffleInProgress()) { const assignment = NameManager.getInstance().getPlayerRoomAssignment(options.uuid); @@ -123,6 +98,25 @@ export class LobbyRoom extends Room { } } + // If not in shuffle, check if UUID has active current room to resume + try { + const currentRoomId = NameManager.getInstance().getCurrentRoom(options.uuid); + if (currentRoomId) { + const rooms = await matchMaker.query({ roomId: currentRoomId }); + const room = rooms[0]; + const status = room?.metadata?.gameStatus || "waiting"; + if (room && status !== "finished") { + const token = NameManager.getInstance().getReconnectToken(options.uuid); + if (token) { + client.send("resumeReconnection", { token }); + } else { + client.send("resumeGame", { roomId: currentRoomId }); + } + return; // Don't proceed with normal lobby join + } + } + } catch {} + // Normal lobby join flow // Check if this UUID already has a name const existingName = NameManager.getInstance().getPlayerName(options.uuid); diff --git a/server/src/rooms/schemas/LobbyState.ts b/server/src/rooms/schemas/LobbyState.ts index 824d3f1..d5b939b 100644 --- a/server/src/rooms/schemas/LobbyState.ts +++ b/server/src/rooms/schemas/LobbyState.ts @@ -45,7 +45,11 @@ export class LobbyState extends Schema { } removePlayer(sessionId: string): void { - this.players.delete(sessionId); + try { + if (this.players.has(sessionId)) { + this.players.delete(sessionId); + } + } catch {} this.totalPlayers = this.players.size; }