mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 18:56:38 +00:00
Private Lobbies: Add kick player functionality (#1436)
## Description: Added player management features so lobby hosts can kick players from private games. This includes both UI changes and backend work. ### What's new: - Hosts can now kick players from private lobbies with a simple button - Added host badges and remove buttons to the UI - Made sure only hosts can kick people, and hosts can't kick themselves ### How it works: - When someone creates a private game, they automatically become the host - Kicking happens through WebSocket "kick-player" events - Server checks that you're actually the host before letting you kick anyone <img width="1291" height="871" alt="Screenshot 2025-07-15 002114" src="https://github.com/user-attachments/assets/ea575f83-a0f4-45d1-9cfe-7521d373f3d5" /> ### Known Issues: - Kicked player gets general message (same when kicked for multi tab) ### Other Issues: - Host abandoment still existent (host clicks on x; or is closing tab) ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: [UN]nvm --------- Co-authored-by: floriankilian <floriankilian@users.noreply.github.com>
This commit is contained in:
+25
-14
@@ -28,20 +28,31 @@ export class GameManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
createGame(id: GameID, gameConfig: GameConfig | undefined) {
|
||||
const game = new GameServer(id, this.log, Date.now(), this.config, {
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium,
|
||||
disableNPCs: false,
|
||||
infiniteGold: false,
|
||||
infiniteTroops: false,
|
||||
instantBuild: false,
|
||||
gameMode: GameMode.FFA,
|
||||
bots: 400,
|
||||
disabledUnits: [],
|
||||
...gameConfig,
|
||||
});
|
||||
createGame(
|
||||
id: GameID,
|
||||
gameConfig: GameConfig | undefined,
|
||||
creatorClientID?: string,
|
||||
) {
|
||||
const game = new GameServer(
|
||||
id,
|
||||
this.log,
|
||||
Date.now(),
|
||||
this.config,
|
||||
{
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium,
|
||||
disableNPCs: false,
|
||||
infiniteGold: false,
|
||||
infiniteTroops: false,
|
||||
instantBuild: false,
|
||||
gameMode: GameMode.FFA,
|
||||
bots: 400,
|
||||
disabledUnits: [],
|
||||
...gameConfig,
|
||||
},
|
||||
creatorClientID,
|
||||
);
|
||||
this.games.set(id, game);
|
||||
return game;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class GameServer {
|
||||
private turns: Turn[] = [];
|
||||
private intents: Intent[] = [];
|
||||
public activeClients: Client[] = [];
|
||||
// Used for record record keeping
|
||||
private LobbyCreatorID: string | undefined;
|
||||
private allClients: Map<ClientID, Client> = new Map();
|
||||
private clientsDisconnectedStatus: Map<ClientID, boolean> = new Map();
|
||||
private _hasStarted = false;
|
||||
@@ -71,8 +71,10 @@ export class GameServer {
|
||||
public readonly createdAt: number,
|
||||
private config: ServerConfig,
|
||||
public gameConfig: GameConfig,
|
||||
lobbyCreatorID?: string,
|
||||
) {
|
||||
this.log = log_.child({ gameID: id });
|
||||
this.LobbyCreatorID = lobbyCreatorID ?? undefined;
|
||||
}
|
||||
|
||||
public updateGameConfig(gameConfig: Partial<GameConfig>): void {
|
||||
@@ -118,6 +120,13 @@ export class GameServer {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Log when lobby creator joins private game
|
||||
if (client.clientID === this.LobbyCreatorID) {
|
||||
this.log.info("Lobby creator joined", {
|
||||
gameID: this.id,
|
||||
creatorID: this.LobbyCreatorID,
|
||||
});
|
||||
}
|
||||
this.log.info("client (re)joining game", {
|
||||
clientID: client.clientID,
|
||||
persistentID: client.persistentID,
|
||||
@@ -223,6 +232,42 @@ export class GameServer {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle kick_player intent via WebSocket
|
||||
if (clientMsg.intent.type === "kick_player") {
|
||||
const authenticatedClientID = client.clientID;
|
||||
|
||||
// Check if the authenticated client is the lobby creator
|
||||
if (authenticatedClientID !== this.LobbyCreatorID) {
|
||||
this.log.warn(`Only lobby creator can kick players`, {
|
||||
clientID: authenticatedClientID,
|
||||
creatorID: this.LobbyCreatorID,
|
||||
target: clientMsg.intent.target,
|
||||
gameID: this.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow lobby creator to kick themselves
|
||||
if (authenticatedClientID === clientMsg.intent.target) {
|
||||
this.log.warn(`Cannot kick yourself`, {
|
||||
clientID: authenticatedClientID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Log and execute the kick
|
||||
this.log.info(`Lobby creator initiated kick of player`, {
|
||||
creatorID: authenticatedClientID,
|
||||
target: clientMsg.intent.target,
|
||||
gameID: this.id,
|
||||
kickMethod: "websocket",
|
||||
});
|
||||
|
||||
this.kickClient(clientMsg.intent.target);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addIntent(clientMsg.intent);
|
||||
}
|
||||
if (clientMsg.type === "ping") {
|
||||
@@ -453,6 +498,10 @@ export class GameServer {
|
||||
}
|
||||
}
|
||||
|
||||
public isPrivateLobbyCreator(clientID: string): boolean {
|
||||
return this.LobbyCreatorID === clientID;
|
||||
}
|
||||
|
||||
phase(): GamePhase {
|
||||
const now = Date.now();
|
||||
const alive: Client[] = [];
|
||||
|
||||
+11
-2
@@ -15,6 +15,7 @@ import {
|
||||
ClientMessageSchema,
|
||||
GameRecord,
|
||||
GameRecordSchema,
|
||||
ID,
|
||||
ServerErrorMessage,
|
||||
} from "../core/Schemas";
|
||||
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
|
||||
@@ -90,6 +91,13 @@ export function startWorker() {
|
||||
"/api/create_game/:id",
|
||||
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const creatorClientID = (() => {
|
||||
if (typeof req.query.creatorClientID !== "string") return undefined;
|
||||
|
||||
const trimmed = req.query.creatorClientID.trim();
|
||||
return ID.safeParse(trimmed).success ? trimmed : undefined;
|
||||
})();
|
||||
|
||||
if (!id) {
|
||||
log.warn(`cannot create game, id not found`);
|
||||
return res.status(400).json({ error: "Game ID is required" });
|
||||
@@ -122,10 +130,11 @@ export function startWorker() {
|
||||
return res.status(400).json({ error: "Worker, game id mismatch" });
|
||||
}
|
||||
|
||||
const game = gm.createGame(id, gc);
|
||||
// Pass creatorClientID to createGame
|
||||
const game = gm.createGame(id, gc, creatorClientID);
|
||||
|
||||
log.info(
|
||||
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating game ${game.isPublic() ? "Public" : "Private"} with id ${id}`,
|
||||
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating ${game.isPublic() ? "Public" : "Private"}${gc?.gameMode ? ` ${gc.gameMode}` : ""} game with id ${id}${creatorClientID ? `, creator: ${creatorClientID}` : ""}`,
|
||||
);
|
||||
res.json(game.gameInfo());
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user