mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 18:40:34 +00:00
feature: basic matchmaking (#2227)
## Description: Implement a basic matchmaking modal that connects to the api service and waits for a game id. It then waits until the game starts and connects to it. Workers use long polling to check in with the matchmaking server and receive player assignments. ## 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: evan --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -71,6 +71,8 @@ const TEAM_COUNTS = [
|
||||
export class MapPlaylist {
|
||||
private mapsPlaylist: MapWithMode[] = [];
|
||||
|
||||
constructor(private disableTeams: boolean = false) {}
|
||||
|
||||
public gameConfig(): GameConfig {
|
||||
const { map, mode } = this.getNextMap();
|
||||
|
||||
@@ -135,8 +137,10 @@ export class MapPlaylist {
|
||||
if (!this.addNextMap(this.mapsPlaylist, ffa, GameMode.FFA)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) {
|
||||
return false;
|
||||
if (!this.disableTeams) {
|
||||
if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -291,11 +291,6 @@ async function schedulePublicGame(playlist: MapPlaylist) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// SPA fallback route
|
||||
app.get("*", function (req, res) {
|
||||
res.sendFile(path.join(__dirname, "../../static/index.html"));
|
||||
|
||||
+90
-1
@@ -11,11 +11,12 @@ import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import { GameType } from "../core/game/Game";
|
||||
import {
|
||||
ClientMessageSchema,
|
||||
GameID,
|
||||
ID,
|
||||
PartialGameRecordSchema,
|
||||
ServerErrorMessage,
|
||||
} from "../core/Schemas";
|
||||
import { replacer } from "../core/Util";
|
||||
import { generateID, replacer } from "../core/Util";
|
||||
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
|
||||
import { archive, finalizeGameRecord } from "./Archive";
|
||||
import { Client } from "./Client";
|
||||
@@ -23,6 +24,7 @@ import { GameManager } from "./GameManager";
|
||||
import { getUserMe, verifyClientToken } from "./jwt";
|
||||
import { logger } from "./Logger";
|
||||
|
||||
import { MapPlaylist } from "./MapPlaylist";
|
||||
import { PrivilegeRefresher } from "./PrivilegeRefresher";
|
||||
import { initWorkerMetrics } from "./WorkerMetrics";
|
||||
|
||||
@@ -30,11 +32,24 @@ const config = getServerConfigFromServer();
|
||||
|
||||
const workerId = parseInt(process.env.WORKER_ID ?? "0");
|
||||
const log = logger.child({ comp: `w_${workerId}` });
|
||||
const playlist = new MapPlaylist(true);
|
||||
|
||||
// Worker setup
|
||||
export async function startWorker() {
|
||||
log.info(`Worker starting...`);
|
||||
|
||||
if (config.enableMatchmaking()) {
|
||||
log.info("Starting matchmaking");
|
||||
setTimeout(
|
||||
() => {
|
||||
pollLobby(gm);
|
||||
},
|
||||
1000 + Math.random() * 2000,
|
||||
);
|
||||
} else {
|
||||
log.info("Matchmaking disabled");
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -450,3 +465,77 @@ export async function startWorker() {
|
||||
log.error(`unhandled rejection at:`, promise, "reason:", reason);
|
||||
});
|
||||
}
|
||||
|
||||
async function pollLobby(gm: GameManager) {
|
||||
try {
|
||||
const url = `${config.jwtIssuer() + "/matchmaking/checkin"}`;
|
||||
const gameId = generateGameIdForWorker();
|
||||
if (gameId === null) {
|
||||
log.warn(`Failed to generate game ID for worker ${workerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": config.apiKey(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: workerId,
|
||||
gameId: gameId,
|
||||
ccu: gm.activeClients(),
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
log.warn(
|
||||
`Failed to poll lobby: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
log.info(`Lobby poll successful:`, data);
|
||||
|
||||
if (data.assignment) {
|
||||
// TODO: Only allow specified players to join the game.
|
||||
console.log(`Creating game ${gameId}`);
|
||||
const game = gm.createGame(gameId, playlist.gameConfig());
|
||||
setTimeout(() => {
|
||||
// Wait a few seconds to allow clients to connect.
|
||||
console.log(`Starting game ${gameId}`);
|
||||
game.start();
|
||||
}, 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error polling lobby:`, error);
|
||||
} finally {
|
||||
setTimeout(
|
||||
() => {
|
||||
pollLobby(gm);
|
||||
},
|
||||
5000 + Math.random() * 1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This is a hack to generate a game ID for the worker.
|
||||
// It should be replaced with a more robust solution.
|
||||
function generateGameIdForWorker(): GameID | null {
|
||||
let attempts = 1000;
|
||||
while (attempts > 0) {
|
||||
const gameId = generateID();
|
||||
if (workerId === config.workerIndex(gameId)) {
|
||||
return gameId;
|
||||
}
|
||||
attempts--;
|
||||
}
|
||||
log.warn(`Failed to generate game ID for worker ${workerId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user