Files
OpenFrontIO/src/server/GameManager.ts
T
Ryan aa451e217f [BUGFIX] allow users to update username pre-game (#3298)
## Description:

Fix player rename in pre-game lobby on rejoin

Previously, when a player left a lobby, changed their name, and
rejoined, the server reused the original Client object without updating
the username. Now rejoinClient accepts the new username and applies it
if the game hasn't started yet, while still preserving names mid-game
for consistency.


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n
2026-02-28 22:22:10 +00:00

146 lines
3.5 KiB
TypeScript

import { Logger } from "winston";
import WebSocket from "ws";
import { ServerConfig } from "../core/configuration/Config";
import {
Difficulty,
GameMapSize,
GameMapType,
GameMode,
GameType,
} from "../core/game/Game";
import { GameConfig, GameID, PublicGameType } from "../core/Schemas";
import { Client } from "./Client";
import { GamePhase, GameServer } from "./GameServer";
export class GameManager {
private games: Map<GameID, GameServer> = new Map();
constructor(
private config: ServerConfig,
private log: Logger,
) {
setInterval(() => this.tick(), 1000);
}
public game(id: GameID): GameServer | null {
return this.games.get(id) ?? null;
}
public publicLobbies(): GameServer[] {
return Array.from(this.games.values()).filter(
(g) => g.phase() === GamePhase.Lobby && g.isPublic(),
);
}
joinClient(
client: Client,
gameID: GameID,
): "joined" | "kicked" | "rejected" | "not_found" {
const game = this.games.get(gameID);
if (!game) return "not_found";
return game.joinClient(client);
}
rejoinClient(
ws: WebSocket,
persistentID: string,
gameID: GameID,
lastTurn: number = 0,
newUsername?: string,
): boolean {
const game = this.games.get(gameID);
if (!game) return false;
return game.rejoinClient(ws, persistentID, lastTurn, newUsername);
}
createGame(
id: GameID,
gameConfig: GameConfig | undefined,
creatorPersistentID?: string,
startsAt?: number,
publicGameType?: PublicGameType,
) {
const game = new GameServer(
id,
this.log,
Date.now(),
this.config,
{
donateGold: false,
donateTroops: false,
gameMap: GameMapType.World,
gameType: GameType.Private,
gameMapSize: GameMapSize.Normal,
difficulty: Difficulty.Medium,
disableNations: false,
infiniteGold: false,
infiniteTroops: false,
maxTimerValue: undefined,
instantBuild: false,
randomSpawn: false,
gameMode: GameMode.FFA,
bots: 400,
disabledUnits: [],
...gameConfig,
},
creatorPersistentID,
startsAt,
publicGameType,
);
this.games.set(id, game);
return game;
}
activeGames(): number {
return this.games.size;
}
activeClients(): number {
let totalClients = 0;
this.games.forEach((game: GameServer) => {
totalClients += game.activeClients.length;
});
return totalClients;
}
desyncCount(): number {
let totalDesyncs = 0;
this.games.forEach((game: GameServer) => {
totalDesyncs += game.desyncCount;
});
return totalDesyncs;
}
tick() {
const active = new Map<GameID, GameServer>();
for (const [id, game] of this.games) {
const phase = game.phase();
if (phase === GamePhase.Active) {
if (!game.hasStarted()) {
// Prestart tells clients to start loading the game.
game.prestart();
// Start game on delay to allow time for clients to connect.
setTimeout(() => {
try {
game.start();
} catch (error) {
this.log.error(`error starting game ${id}: ${error}`);
}
}, 2000);
}
}
if (phase === GamePhase.Finished) {
try {
game.end();
} catch (error) {
this.log.error(`error ending game ${id}: ${error}`);
}
} else {
active.set(id, game);
}
}
this.games = active;
}
}