mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 16:56:38 +00:00
WIP_DNS
This commit is contained in:
+76
-66
@@ -101,81 +101,91 @@ export class PublicLobby extends LitElement {
|
||||
render() {
|
||||
if (this.lobbies.length === 0) return html``;
|
||||
|
||||
const lobby = this.lobbies[0];
|
||||
if (!lobby?.gameConfig) {
|
||||
return;
|
||||
}
|
||||
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
|
||||
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
|
||||
const elements = this.lobbies.map((lobby) => {
|
||||
if (!lobby?.gameConfig) {
|
||||
return;
|
||||
}
|
||||
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
|
||||
const timeRemaining = Math.max(
|
||||
0,
|
||||
Math.floor((start - Date.now()) / 1000),
|
||||
);
|
||||
|
||||
// Format time to show minutes and seconds
|
||||
const minutes = Math.floor(timeRemaining / 60);
|
||||
const seconds = timeRemaining % 60;
|
||||
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
||||
// Format time to show minutes and seconds
|
||||
const minutes = Math.floor(timeRemaining / 60);
|
||||
const seconds = timeRemaining % 60;
|
||||
const timeDisplay =
|
||||
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
||||
|
||||
const teamCount =
|
||||
lobby.gameConfig.gameMode === GameMode.Team
|
||||
? lobby.gameConfig.playerTeams || 0
|
||||
: null;
|
||||
const teamCount =
|
||||
lobby.gameConfig.gameMode === GameMode.Team
|
||||
? lobby.gameConfig.playerTeams || 0
|
||||
: null;
|
||||
|
||||
const mapImageSrc = this.mapImages.get(lobby.gameID);
|
||||
const mapImageSrc = this.mapImages.get(lobby.gameID);
|
||||
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
?disabled=${this.isButtonDebounced}
|
||||
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${this
|
||||
.isLobbyHighlighted
|
||||
? "bg-gradient-to-r from-green-600 to-green-500"
|
||||
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
|
||||
.isButtonDebounced
|
||||
? "opacity-70 cursor-not-allowed"
|
||||
: ""}"
|
||||
>
|
||||
${mapImageSrc
|
||||
? html`<img
|
||||
src="${mapImageSrc}"
|
||||
alt="${lobby.gameConfig.gameMap}"
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10"
|
||||
style="mask-image: linear-gradient(to left, transparent, #fff)"
|
||||
/>`
|
||||
: html`<div
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10 bg-gray-300"
|
||||
></div>`}
|
||||
<div
|
||||
class="flex flex-col justify-between h-full col-span-full row-span-full p-4 md:p-6 text-right z-0"
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
?disabled=${this.isButtonDebounced}
|
||||
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${this
|
||||
.isLobbyHighlighted
|
||||
? "bg-gradient-to-r from-green-600 to-green-500"
|
||||
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
|
||||
.isButtonDebounced
|
||||
? "opacity-70 cursor-not-allowed"
|
||||
: ""}"
|
||||
>
|
||||
<div>
|
||||
<div class="text-lg md:text-2xl font-semibold">
|
||||
${translateText("public_lobby.join")}
|
||||
${mapImageSrc
|
||||
? html`<img
|
||||
src="${mapImageSrc}"
|
||||
alt="${lobby.gameConfig.gameMap}"
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10"
|
||||
style="mask-image: linear-gradient(to left, transparent, #fff)"
|
||||
/>`
|
||||
: html`<div
|
||||
class="place-self-start col-span-full row-span-full h-full -z-10 bg-gray-300"
|
||||
></div>`}
|
||||
<div
|
||||
class="flex flex-col justify-between h-full col-span-full row-span-full p-4 md:p-6 text-right z-0"
|
||||
>
|
||||
<div>
|
||||
<div class="text-lg md:text-2xl font-semibold">
|
||||
${translateText("public_lobby.join")}
|
||||
</div>
|
||||
<div class="text-md font-medium text-blue-100">
|
||||
<span
|
||||
class="text-sm ${this.isLobbyHighlighted
|
||||
? "text-green-600"
|
||||
: "text-blue-600"} bg-white rounded-sm px-1"
|
||||
>
|
||||
${lobby.gameConfig.gameMode === GameMode.Team
|
||||
? translateText("public_lobby.teams", {
|
||||
num: teamCount ?? 0,
|
||||
})
|
||||
: translateText("game_mode.ffa")}</span
|
||||
>
|
||||
<span
|
||||
>${translateText(
|
||||
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/\s+/g, "")}`,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-md font-medium text-blue-100">
|
||||
<span
|
||||
class="text-sm ${this.isLobbyHighlighted
|
||||
? "text-green-600"
|
||||
: "text-blue-600"} bg-white rounded-sm px-1"
|
||||
>
|
||||
${lobby.gameConfig.gameMode === GameMode.Team
|
||||
? translateText("public_lobby.teams", { num: teamCount ?? 0 })
|
||||
: translateText("game_mode.ffa")}</span
|
||||
>
|
||||
<span
|
||||
>${translateText(
|
||||
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/\s+/g, "")}`,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-md font-medium text-blue-100">
|
||||
${lobby.numClients} / ${lobby.gameConfig.maxPlayers}
|
||||
<div>
|
||||
<div class="text-md font-medium text-blue-100">
|
||||
${lobby.numClients} / ${lobby.gameConfig.maxPlayers}
|
||||
</div>
|
||||
<div class="text-md font-medium text-blue-100">
|
||||
${timeDisplay}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-md font-medium text-blue-100">${timeDisplay}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
|
||||
leaveLobby() {
|
||||
|
||||
@@ -196,10 +196,7 @@
|
||||
class="w-[20%] md:w-[15%] component-hideable"
|
||||
></news-button>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<public-lobby class="block"></public-lobby>
|
||||
</div>
|
||||
<public-lobby class="block"></public-lobby>
|
||||
<div class="container__row container__row--equal">
|
||||
<o-button
|
||||
id="host-lobby-button"
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { z } from "zod/v4";
|
||||
import { GameConfigSchema } from "./Schemas";
|
||||
|
||||
export const CreateGameInputSchema = GameConfigSchema.or(
|
||||
export const CreateGameConfigSchema = GameConfigSchema.or(
|
||||
z
|
||||
.object({})
|
||||
.strict()
|
||||
.transform((val) => undefined),
|
||||
);
|
||||
|
||||
export const CreateGameInputSchema = z.object({
|
||||
config: CreateGameConfigSchema,
|
||||
startTime: z.number().optional(),
|
||||
});
|
||||
export type CreateGameInput = z.infer<typeof CreateGameInputSchema>;
|
||||
|
||||
export const GameInputSchema = GameConfigSchema.partial();
|
||||
|
||||
@@ -28,8 +28,12 @@ export class GameManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
createGame(id: GameID, gameConfig: GameConfig | undefined) {
|
||||
const game = new GameServer(id, this.log, Date.now(), this.config, {
|
||||
createGame(
|
||||
id: GameID,
|
||||
gameConfig: GameConfig | undefined,
|
||||
startTime: number,
|
||||
) {
|
||||
const game = new GameServer(id, this.log, startTime, this.config, {
|
||||
gameMap: GameMapType.World,
|
||||
gameType: GameType.Private,
|
||||
difficulty: Difficulty.Medium,
|
||||
|
||||
@@ -44,7 +44,7 @@ export class GameServer {
|
||||
// Used for record record keeping
|
||||
private allClients: Map<ClientID, Client> = new Map();
|
||||
private _hasStarted = false;
|
||||
private _startTime: number | null = null;
|
||||
private readonly createdAt = Date.now();
|
||||
|
||||
private endTurnIntervalID;
|
||||
|
||||
@@ -64,7 +64,7 @@ export class GameServer {
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
readonly log_: Logger,
|
||||
public readonly createdAt: number,
|
||||
private _startTime: number,
|
||||
private config: ServerConfig,
|
||||
public gameConfig: GameConfig,
|
||||
) {
|
||||
@@ -278,12 +278,7 @@ export class GameServer {
|
||||
}
|
||||
|
||||
public startTime(): number {
|
||||
if (this._startTime !== null && this._startTime > 0) {
|
||||
return this._startTime;
|
||||
} else {
|
||||
//game hasn't started yet, only works for public games
|
||||
return this.createdAt + this.config.gameCreationRate();
|
||||
}
|
||||
return this._startTime;
|
||||
}
|
||||
|
||||
public prestart() {
|
||||
@@ -478,36 +473,16 @@ export class GameServer {
|
||||
const noRecentPings = now > this.lastPingUpdate + 20 * 1000;
|
||||
const noActive = this.activeClients.length === 0;
|
||||
|
||||
if (this.gameConfig.gameType !== GameType.Public) {
|
||||
if (this._hasStarted) {
|
||||
if (noActive && noRecentPings) {
|
||||
this.log.info("private game complete", {
|
||||
gameID: this.id,
|
||||
});
|
||||
return GamePhase.Finished;
|
||||
} else {
|
||||
return GamePhase.Active;
|
||||
}
|
||||
} else {
|
||||
return GamePhase.Lobby;
|
||||
}
|
||||
}
|
||||
|
||||
const msSinceCreation = now - this.createdAt;
|
||||
const lessThanLifetime = msSinceCreation < this.config.gameCreationRate();
|
||||
const notEnoughPlayers =
|
||||
this.gameConfig.gameType === GameType.Public &&
|
||||
this.gameConfig.maxPlayers &&
|
||||
this.activeClients.length < this.gameConfig.maxPlayers;
|
||||
if (lessThanLifetime && notEnoughPlayers) {
|
||||
if (!this._hasStarted) {
|
||||
return GamePhase.Lobby;
|
||||
}
|
||||
const warmupOver =
|
||||
now > this.createdAt + this.config.gameCreationRate() + 30 * 1000;
|
||||
if (noActive && warmupOver && noRecentPings) {
|
||||
if (noActive && noRecentPings) {
|
||||
this.log.info("game complete", {
|
||||
type: this.gameConfig.gameType,
|
||||
gameID: this.id,
|
||||
});
|
||||
return GamePhase.Finished;
|
||||
}
|
||||
|
||||
return GamePhase.Active;
|
||||
}
|
||||
|
||||
|
||||
+20
-10
@@ -7,6 +7,7 @@ import { fileURLToPath } from "url";
|
||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import { GameInfo, PublicLobbies } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { CreateGameInput } from "../core/WorkerSchemas";
|
||||
import { gatekeeper, LimiterType } from "./Gatekeeper";
|
||||
import { logger } from "./Logger";
|
||||
import { MapPlaylist } from "./MapPlaylist";
|
||||
@@ -61,8 +62,6 @@ app.use(
|
||||
|
||||
let publicLobbiesJsonStr = "";
|
||||
|
||||
const publicLobbyIDs: Set<string> = new Set();
|
||||
|
||||
// Start the master process
|
||||
export async function startMaster() {
|
||||
if (!cluster.isPrimary) {
|
||||
@@ -103,7 +102,7 @@ export async function startMaster() {
|
||||
setInterval(
|
||||
() =>
|
||||
fetchLobbies().then((lobbies) => {
|
||||
if (lobbies === 0) {
|
||||
if (lobbies < 3) {
|
||||
scheduleLobbies();
|
||||
}
|
||||
}),
|
||||
@@ -193,10 +192,12 @@ app.post(
|
||||
}),
|
||||
);
|
||||
|
||||
const publicLobbies: Map<string, number> = new Map();
|
||||
|
||||
async function fetchLobbies(): Promise<number> {
|
||||
const fetchPromises: Promise<GameInfo | null>[] = [];
|
||||
|
||||
for (const gameID of new Set(publicLobbyIDs)) {
|
||||
for (const [gameID] of publicLobbies) {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
||||
const port = config.workerPort(gameID);
|
||||
@@ -211,7 +212,7 @@ async function fetchLobbies(): Promise<number> {
|
||||
.catch((error) => {
|
||||
log.error(`Error fetching game ${gameID}:`, error);
|
||||
// Return null or a placeholder if fetch fails
|
||||
publicLobbyIDs.delete(gameID);
|
||||
publicLobbies.delete(gameID);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -239,7 +240,7 @@ async function fetchLobbies(): Promise<number> {
|
||||
l.msUntilStart !== undefined &&
|
||||
l.msUntilStart <= 250
|
||||
) {
|
||||
publicLobbyIDs.delete(l.gameID);
|
||||
publicLobbies.delete(l.gameID);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,7 +253,7 @@ async function fetchLobbies(): Promise<number> {
|
||||
l.numClients !== undefined &&
|
||||
l.gameConfig.maxPlayers <= l.numClients
|
||||
) {
|
||||
publicLobbyIDs.delete(l.gameID);
|
||||
publicLobbies.delete(l.gameID);
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -262,16 +263,22 @@ async function fetchLobbies(): Promise<number> {
|
||||
lobbies: lobbyInfos,
|
||||
} satisfies PublicLobbies);
|
||||
|
||||
return publicLobbyIDs.size;
|
||||
return publicLobbies.size;
|
||||
}
|
||||
|
||||
// Function to schedule a new public game
|
||||
async function schedulePublicGame(playlist: MapPlaylist) {
|
||||
const gameID = generateID();
|
||||
publicLobbyIDs.add(gameID);
|
||||
|
||||
const workerPath = config.workerPath(gameID);
|
||||
|
||||
let lastGameCreatedTime = Date.now() - config.gameCreationRate();
|
||||
for (const value of publicLobbies.values()) {
|
||||
lastGameCreatedTime = Math.max(value, lastGameCreatedTime);
|
||||
}
|
||||
const createdAt = lastGameCreatedTime + config.gameCreationRate();
|
||||
publicLobbies.set(gameID, createdAt);
|
||||
|
||||
// Send request to the worker to start the game
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -282,7 +289,10 @@ async function schedulePublicGame(playlist: MapPlaylist) {
|
||||
"Content-Type": "application/json",
|
||||
[config.adminHeader()]: config.adminToken(),
|
||||
},
|
||||
body: JSON.stringify(playlist.gameConfig()),
|
||||
body: JSON.stringify({
|
||||
config: playlist.gameConfig(),
|
||||
createdAt,
|
||||
} satisfies CreateGameInput),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export function startWorker() {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
|
||||
const gc = result.data;
|
||||
const gc = result.data.config;
|
||||
if (
|
||||
gc?.gameType === GameType.Public &&
|
||||
req.headers[config.adminHeader()] !== config.adminToken()
|
||||
@@ -120,7 +120,9 @@ export function startWorker() {
|
||||
return res.status(400).json({ error: "Worker, game id mismatch" });
|
||||
}
|
||||
|
||||
const game = gm.createGame(id, gc);
|
||||
const startTime =
|
||||
result.data.startTime ?? Date.now() + config.gameCreationRate();
|
||||
const game = gm.createGame(id, gc, startTime);
|
||||
|
||||
log.info(
|
||||
`Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating game ${game.isPublic() ? "Public" : "Private"} with id ${id}`,
|
||||
|
||||
Reference in New Issue
Block a user