mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 17:03:25 +00:00
Merge branch 'v30'
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
Trios,
|
||||
} from "../core/game/Game";
|
||||
import { PublicGameInfo, PublicGames } from "../core/Schemas";
|
||||
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
||||
import { HostLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinLobbyModal } from "./JoinLobbyModal";
|
||||
import { PublicLobbySocket } from "./LobbySocket";
|
||||
@@ -135,11 +136,13 @@ export class GameModeSelector extends LitElement {
|
||||
this.openHostLobby,
|
||||
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
|
||||
)}
|
||||
${!crazyGamesSDK.isOnCrazyGames()
|
||||
? this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
|
||||
)
|
||||
: html`<div class="invisible"></div>`}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.join"),
|
||||
this.openJoinLobby,
|
||||
@@ -204,11 +207,13 @@ export class GameModeSelector extends LitElement {
|
||||
this.openHostLobby,
|
||||
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
|
||||
)}
|
||||
${!crazyGamesSDK.isOnCrazyGames()
|
||||
? this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
|
||||
)
|
||||
: html`<div class="invisible"></div>`}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.join"),
|
||||
this.openJoinLobby,
|
||||
|
||||
@@ -115,6 +115,7 @@ export abstract class BaseModal extends LitElement {
|
||||
* Subclasses can override onOpen() for custom behavior.
|
||||
*/
|
||||
public open(): void {
|
||||
if (this.isModalOpen) return;
|
||||
this.registerEscapeHandler();
|
||||
this.onOpen();
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
const { greenPercent, orangePercent } = this.calculateTroopBar();
|
||||
return html`
|
||||
<div
|
||||
class="w-full h-8 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
class="w-full h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
>
|
||||
<div class="h-full flex">
|
||||
${greenPercent > 0
|
||||
@@ -225,7 +225,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
: ""}
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center text-xl font-bold leading-none pointer-events-none"
|
||||
class="absolute inset-0 flex items-center text-lg font-bold leading-none pointer-events-none"
|
||||
translate="no"
|
||||
>
|
||||
<span class="flex-1 flex justify-end h-full items-center pr-0.5">
|
||||
@@ -261,10 +261,10 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
private renderDesktop() {
|
||||
return html`
|
||||
<!-- Row 1: troop rate | troop bar | gold -->
|
||||
<div class="flex gap-1.5 items-center mb-1.5">
|
||||
<div class="flex gap-1.5 items-center mb-1">
|
||||
<!-- Troop rate -->
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 border rounded-md font-bold text-sm p-1 w-[5.5rem] ${this
|
||||
class="flex items-center gap-1 shrink-0 border rounded-md font-bold text-sm py-0.5 px-1 w-[5.5rem] ${this
|
||||
._troopRateIsIncreasing
|
||||
? "border-green-400"
|
||||
: "border-orange-400"}"
|
||||
@@ -292,7 +292,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
<div class="flex-1">${this.renderDesktopTroopBar()}</div>
|
||||
<!-- Gold -->
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 border rounded-md border-yellow-400 font-bold text-yellow-400 text-sm p-1 w-[4.5rem]"
|
||||
class="flex items-center gap-1 shrink-0 border rounded-md border-yellow-400 font-bold text-yellow-400 text-sm py-0.5 px-1 w-[4.5rem]"
|
||||
translate="no"
|
||||
>
|
||||
<img src=${goldCoinIcon} width="13" height="13" class="shrink-0" />
|
||||
@@ -300,9 +300,9 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2: attack ratio | slider -->
|
||||
<div class="flex items-center gap-2" translate="no">
|
||||
<div class="flex items-center gap-1.5" translate="no">
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 border border-gray-600 rounded-md p-1 text-sm font-bold text-white cursor-pointer w-[8rem]"
|
||||
class="flex items-center gap-1 shrink-0 border border-gray-600 rounded-md px-1 py-0.5 text-sm font-bold text-white cursor-pointer w-[8rem]"
|
||||
>
|
||||
<img
|
||||
src=${swordIcon}
|
||||
@@ -326,7 +326,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
.value=${String(Math.round(this.attackRatio * 100))}
|
||||
@input=${(e: Event) => this.handleRatioSliderInput(e)}
|
||||
@pointerup=${(e: Event) => this.handleRatioSliderPointerUp(e)}
|
||||
class="flex-1 h-2 accent-blue-500 cursor-pointer"
|
||||
class="flex-1 h-1.5 accent-blue-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -384,7 +384,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
return html`
|
||||
<div
|
||||
class="relative pointer-events-auto ${this._isVisible
|
||||
? "relative w-full text-sm px-2 py-1.5"
|
||||
? "relative w-full text-sm px-2 py-1"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -288,7 +288,10 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
renderPlayerInfo(render: RenderInfo) {
|
||||
if (!render.player.nameLocation() || !render.player.isAlive()) {
|
||||
if (!render.player.nameLocation()) {
|
||||
return;
|
||||
}
|
||||
if (!render.player.isAlive()) {
|
||||
this.renders = this.renders.filter((r) => r !== render);
|
||||
render.element.remove();
|
||||
return;
|
||||
|
||||
@@ -318,12 +318,12 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
const playerTeam = getTranslatedPlayerTeamLabel(player.team());
|
||||
|
||||
return html`
|
||||
<div class="flex items-start gap-1 lg:gap-2 p-1.5 lg:p-2">
|
||||
<div class="flex items-start gap-1 lg:gap-2 p-1 lg:p-1.5">
|
||||
<!-- Left: Gold & Troop bar -->
|
||||
<div class="flex flex-col gap-1 shrink-0 w-28 md:w-36">
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="flex flex-1 items-center justify-center p-1 border rounded-md border-yellow-400 font-bold text-yellow-400 text-sm lg:gap-1"
|
||||
class="flex flex-1 items-center justify-center px-1 py-0.5 border rounded-md border-yellow-400 font-bold text-yellow-400 text-sm lg:gap-1"
|
||||
translate="no"
|
||||
>
|
||||
<img src=${goldCoinIcon} width="13" height="13" />
|
||||
@@ -402,7 +402,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
>`}
|
||||
${this.renderPlayerNameIcons(player)} ${allianceHtml ?? ""}
|
||||
</div>
|
||||
<div class="flex gap-0.5 lg:gap-1 items-center mt-1">
|
||||
<div class="flex gap-0.5 lg:gap-1 items-center mt-0.5">
|
||||
${this.displayUnitCount(player, UnitType.City, cityIcon)}
|
||||
${this.displayUnitCount(player, UnitType.Factory, factoryIcon)}
|
||||
${this.displayUnitCount(player, UnitType.Port, portIcon)}
|
||||
@@ -440,7 +440,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="w-full mt-0.5 lg:mt-1 h-5 lg:h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
class="w-full h-5 lg:h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
|
||||
>
|
||||
<div class="h-full flex">
|
||||
${greenPercent > 0
|
||||
@@ -518,13 +518,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 min-[1200px]:top-4 left-0 right-0 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-[1001]"
|
||||
class="fixed top-0 left-0 right-0 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-[1001]"
|
||||
style="margin-top: ${this.barOffset}px;"
|
||||
@click=${() => this.hide()}
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/70 backdrop-blur-xs shadow-xs min-[1200px]:rounded-lg sm:rounded-b-lg shadow-lg text-white text-lg lg:text-base w-full sm:w-auto sm:min-w-[400px] overflow-hidden ${containerClasses}"
|
||||
class="bg-gray-800/70 backdrop-blur-xs shadow-xs min-[1200px]:rounded-lg sm:rounded-b-lg shadow-lg text-white text-lg lg:text-base w-full sm:w-[500px] overflow-hidden ${containerClasses}"
|
||||
>
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
|
||||
@@ -31,8 +31,8 @@ function generateTeamColors(baseColor: Colord): Colord[] {
|
||||
return Array.from({ length: colorCount }, (_, index) => {
|
||||
if (index === 0) return baseColor;
|
||||
|
||||
// Spread hues evenly across ±12° band using golden angle within that range
|
||||
const hueShift = ((index * goldenAngle) % 24) - 12;
|
||||
// Spread hues evenly across ±6° band using golden angle within that range
|
||||
const hueShift = ((index * goldenAngle) % 12) - 6;
|
||||
const h = (lch.h + hueShift + 360) % 360;
|
||||
|
||||
// Chroma oscillates ±10% around the base to add variety without washing out
|
||||
|
||||
@@ -271,14 +271,14 @@ export class DefaultConfig implements Config {
|
||||
trainSpawnRate(numPlayerFactories: number): number {
|
||||
// hyperbolic decay, midpoint at 10 factories
|
||||
// expected number of trains = numPlayerFactories / trainSpawnRate(numPlayerFactories)
|
||||
return (numPlayerFactories + 10) * 18;
|
||||
return (numPlayerFactories + 10) * 15;
|
||||
}
|
||||
trainGold(
|
||||
rel: "self" | "team" | "ally" | "other",
|
||||
citiesVisited: number,
|
||||
): Gold {
|
||||
// No penalty for the first 5 cities.
|
||||
citiesVisited = Math.max(0, citiesVisited - 5);
|
||||
// No penalty for the first 10 cities.
|
||||
citiesVisited = Math.max(0, citiesVisited - 9);
|
||||
let baseGold: number;
|
||||
switch (rel) {
|
||||
case "ally":
|
||||
@@ -311,7 +311,7 @@ export class DefaultConfig implements Config {
|
||||
// Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under range debuff.
|
||||
const debuff = this.tradeShipShortRangeDebuff();
|
||||
const baseGold =
|
||||
50_000 / (1 + Math.exp(-0.03 * (dist - debuff))) + 50 * dist;
|
||||
75_000 / (1 + Math.exp(-0.03 * (dist - debuff))) + 50 * dist;
|
||||
const multiplier = this.goldMultiplier();
|
||||
return BigInt(Math.floor(baseGold * multiplier));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { RateLimiter } from "limiter";
|
||||
import { ClientID } from "../core/Schemas";
|
||||
|
||||
const INTENTS_PER_SECOND = 10;
|
||||
const INTENTS_PER_MINUTE = 150;
|
||||
const MAX_BYTES_PER_MINUTE = 25 * 1024; // 25KB/min per client
|
||||
const MAX_INTENT_BYTES = 500; // intents are stored in turns, keep them small
|
||||
export type RateLimitResult = "ok" | "limit" | "kick";
|
||||
|
||||
// Allow 3 winner messages per client since a player can rejoin and resend.
|
||||
const MAX_WINNER_MSGS = 3;
|
||||
|
||||
interface ClientBucket {
|
||||
perSecond: RateLimiter;
|
||||
perMinute: RateLimiter;
|
||||
bytesPerMinute: RateLimiter;
|
||||
winnerMsgCount: number;
|
||||
}
|
||||
|
||||
export class ClientMsgRateLimiter {
|
||||
private buckets = new Map<ClientID, ClientBucket>();
|
||||
|
||||
check(clientID: ClientID, type: string, bytes: number): RateLimitResult {
|
||||
const bucket = this.getOrCreate(clientID);
|
||||
|
||||
// Winner message contains stats for all players and can be large (100s of KB).
|
||||
// It bypasses the byte rate limit but is strictly limited to one per client.
|
||||
if (type === "winner") {
|
||||
if (bucket.winnerMsgCount >= MAX_WINNER_MSGS) return "kick";
|
||||
bucket.winnerMsgCount++;
|
||||
return "ok";
|
||||
}
|
||||
|
||||
// Intents are stored in turn history for the duration of the game, so
|
||||
// oversized intents would accumulate and fill up server RAM.
|
||||
if (type === "intent" && bytes > MAX_INTENT_BYTES) return "kick";
|
||||
|
||||
if (!bucket.bytesPerMinute.tryRemoveTokens(bytes)) return "kick";
|
||||
|
||||
if (
|
||||
!bucket.perSecond.tryRemoveTokens(1) ||
|
||||
!bucket.perMinute.tryRemoveTokens(1)
|
||||
)
|
||||
return "limit";
|
||||
|
||||
return "ok";
|
||||
}
|
||||
|
||||
private getOrCreate(clientID: ClientID): ClientBucket {
|
||||
const existing = this.buckets.get(clientID);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const bucket = {
|
||||
perSecond: new RateLimiter({
|
||||
tokensPerInterval: INTENTS_PER_SECOND,
|
||||
interval: "second",
|
||||
}),
|
||||
perMinute: new RateLimiter({
|
||||
tokensPerInterval: INTENTS_PER_MINUTE,
|
||||
interval: "minute",
|
||||
}),
|
||||
bytesPerMinute: new RateLimiter({
|
||||
tokensPerInterval: MAX_BYTES_PER_MINUTE,
|
||||
interval: "minute",
|
||||
}),
|
||||
winnerMsgCount: 0,
|
||||
};
|
||||
this.buckets.set(clientID, bucket);
|
||||
return bucket;
|
||||
}
|
||||
}
|
||||
+52
-16
@@ -26,6 +26,7 @@ import {
|
||||
import { createPartialGameRecord, getClanTag } from "../core/Util";
|
||||
import { archive, finalizeGameRecord } from "./Archive";
|
||||
import { Client } from "./Client";
|
||||
import { ClientMsgRateLimiter } from "./ClientMsgRateLimiter";
|
||||
export enum GamePhase {
|
||||
Lobby = "LOBBY",
|
||||
Active = "ACTIVE",
|
||||
@@ -34,10 +35,14 @@ export enum GamePhase {
|
||||
|
||||
const KICK_REASON_DUPLICATE_SESSION = "kick_reason.duplicate_session";
|
||||
const KICK_REASON_LOBBY_CREATOR = "kick_reason.lobby_creator";
|
||||
const KICK_REASON_TOO_MUCH_DATA = "kick_reason.too_much_data";
|
||||
const KICK_REASON_INVALID_MESSAGE = "kick_reason.invalid_message";
|
||||
|
||||
export class GameServer {
|
||||
private sentDesyncMessageClients = new Set<ClientID>();
|
||||
|
||||
private intentRateLimiter = new ClientMsgRateLimiter();
|
||||
|
||||
private maxGameDuration = 3 * 60 * 60 * 1000; // 3 hours
|
||||
|
||||
private disconnectedTimeout = 1 * 30 * 1000; // 30 seconds
|
||||
@@ -51,6 +56,7 @@ export class GameServer {
|
||||
private clientsDisconnectedStatus: Map<ClientID, boolean> = new Map();
|
||||
private _hasStarted = false;
|
||||
private _startTime: number | null = null;
|
||||
private hasReachedMaxPlayerCount: boolean = false;
|
||||
|
||||
private endTurnIntervalID: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
@@ -247,6 +253,10 @@ export class GameServer {
|
||||
this.addListeners(client);
|
||||
this.startLobbyInfoBroadcast();
|
||||
|
||||
if (this.activeClients.length >= (this.gameConfig.maxPlayers ?? Infinity)) {
|
||||
this.hasReachedMaxPlayerCount = true;
|
||||
}
|
||||
|
||||
// In case a client joined the game late and missed the start message.
|
||||
if (this._hasStarted) {
|
||||
this.sendStartGameMsg(client.ws, 0);
|
||||
@@ -306,22 +316,48 @@ export class GameServer {
|
||||
client.ws.removeAllListeners("message");
|
||||
client.ws.on("message", async (message: string) => {
|
||||
try {
|
||||
const parsed = ClientMessageSchema.safeParse(JSON.parse(message));
|
||||
if (!parsed.success) {
|
||||
const error = z.prettifyError(parsed.error);
|
||||
this.log.warn(`Failed to parse client message ${error}`, {
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(message);
|
||||
} catch (e) {
|
||||
this.log.warn(`Failed to parse client message JSON, kicking`, {
|
||||
clientID: client.clientID,
|
||||
error: String(e),
|
||||
});
|
||||
client.ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error,
|
||||
message: `Server could not parse message from client: ${message}`,
|
||||
} satisfies ServerErrorMessage),
|
||||
);
|
||||
this.kickClient(client.clientID, KICK_REASON_INVALID_MESSAGE);
|
||||
return;
|
||||
}
|
||||
const parsed = ClientMessageSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
this.log.warn(`Failed to parse client message, kicking`, {
|
||||
clientID: client.clientID,
|
||||
error: z.prettifyError(parsed.error),
|
||||
});
|
||||
this.kickClient(client.clientID, KICK_REASON_INVALID_MESSAGE);
|
||||
return;
|
||||
}
|
||||
const clientMsg = parsed.data;
|
||||
const bytes = Buffer.byteLength(message, "utf8");
|
||||
const rateResult = this.intentRateLimiter.check(
|
||||
client.clientID,
|
||||
clientMsg.type,
|
||||
bytes,
|
||||
);
|
||||
if (rateResult === "kick") {
|
||||
this.log.warn(`Client rate limit exceeded, kicking`, {
|
||||
clientID: client.clientID,
|
||||
type: clientMsg.type,
|
||||
});
|
||||
this.kickClient(client.clientID, KICK_REASON_TOO_MUCH_DATA);
|
||||
return;
|
||||
}
|
||||
if (rateResult === "limit") {
|
||||
this.log.warn(`Client message rate limit exceeded, dropping`, {
|
||||
clientID: client.clientID,
|
||||
type: clientMsg.type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
switch (clientMsg.type) {
|
||||
case "rejoin": {
|
||||
// Client is already connected, no auth required, send start game message if game has started
|
||||
@@ -813,11 +849,11 @@ export class GameServer {
|
||||
// Public Games
|
||||
|
||||
const lessThanLifetime = this.startsAt ? Date.now() < this.startsAt : true;
|
||||
const notEnoughPlayers =
|
||||
this.gameConfig.gameType === GameType.Public &&
|
||||
this.gameConfig.maxPlayers &&
|
||||
this.activeClients.length < this.gameConfig.maxPlayers;
|
||||
if (lessThanLifetime && notEnoughPlayers) {
|
||||
if (
|
||||
lessThanLifetime &&
|
||||
!this.hasStarted() &&
|
||||
!this.hasReachedMaxPlayerCount
|
||||
) {
|
||||
return GamePhase.Lobby;
|
||||
}
|
||||
const warmupOver = now > this.startsAt! + 30 * 1000;
|
||||
|
||||
@@ -75,7 +75,7 @@ export class MasterLobbyService {
|
||||
if (this.readyWorkers.size === this.config.numWorkers() && !this.started) {
|
||||
this.started = true;
|
||||
this.log.info("All workers ready, starting game scheduling");
|
||||
startPolling(async () => this.broadcastLobbies(), 250);
|
||||
startPolling(async () => this.broadcastLobbies(), 500);
|
||||
startPolling(async () => await this.maybeScheduleLobby(), 1000);
|
||||
}
|
||||
}
|
||||
@@ -117,10 +117,14 @@ export class MasterLobbyService {
|
||||
games: this.getAllLobbies(),
|
||||
},
|
||||
} satisfies MasterLobbiesBroadcast;
|
||||
for (const worker of this.workers.values()) {
|
||||
for (const [workerId, worker] of this.workers.entries()) {
|
||||
worker.send(msg, (e) => {
|
||||
if (e) {
|
||||
this.log.error("Failed to send lobbies broadcast to worker:", e);
|
||||
this.log.error(
|
||||
`Failed to send lobbies broadcast to worker ${workerId}, killing worker:`,
|
||||
e,
|
||||
);
|
||||
worker.kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -131,12 +135,13 @@ export class MasterLobbyService {
|
||||
|
||||
for (const type of Object.keys(lobbiesByType) as PublicGameType[]) {
|
||||
const lobbies = lobbiesByType[type];
|
||||
if (lobbies.length >= 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always ensure the next lobby has a timer, even if we already have 2+
|
||||
// lobbies. This prevents a race where two lobbies are created before
|
||||
// either receives a startsAt (IPC round-trip delay), leaving both stuck
|
||||
// without a countdown.
|
||||
const nextLobby = lobbies[0];
|
||||
if (nextLobby && nextLobby.startsAt === undefined) {
|
||||
// The previous game has started, so we need to set the timer on the next game.
|
||||
this.sendMessageToWorker({
|
||||
type: "updateLobby",
|
||||
gameID: nextLobby.gameID,
|
||||
@@ -144,6 +149,10 @@ export class MasterLobbyService {
|
||||
});
|
||||
}
|
||||
|
||||
if (lobbies.length >= 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.sendMessageToWorker({
|
||||
type: "createGame",
|
||||
gameID: generateID(),
|
||||
@@ -162,7 +171,11 @@ export class MasterLobbyService {
|
||||
}
|
||||
worker.send(msg, (e) => {
|
||||
if (e) {
|
||||
this.log.error("Failed to send message to worker:", e);
|
||||
this.log.error(
|
||||
`Failed to send message to worker ${workerId}, killing worker:`,
|
||||
e,
|
||||
);
|
||||
worker.kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ export async function startWorker() {
|
||||
const app = express();
|
||||
app.use(express.json({ limit: "5mb" }));
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const wss = new WebSocketServer({
|
||||
noServer: true,
|
||||
maxPayload: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
const gm = new GameManager(config, log);
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ export class WorkerLobbyService {
|
||||
private readonly gm: GameManager,
|
||||
private readonly log: typeof logger,
|
||||
) {
|
||||
this.lobbiesWss = new WebSocketServer({ noServer: true });
|
||||
this.lobbiesWss = new WebSocketServer({
|
||||
noServer: true,
|
||||
maxPayload: 256 * 1024,
|
||||
});
|
||||
this.setupUpgradeHandler();
|
||||
this.setupLobbiesWebSocket();
|
||||
this.setupIPCListener();
|
||||
@@ -109,6 +112,9 @@ export class WorkerLobbyService {
|
||||
private setupLobbiesWebSocket() {
|
||||
this.lobbiesWss.on("connection", (ws: WebSocket) => {
|
||||
this.lobbyClients.add(ws);
|
||||
ws.on("message", () => {
|
||||
ws.terminate();
|
||||
});
|
||||
ws.on("close", () => {
|
||||
this.lobbyClients.delete(ws);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user