mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:20:16 +00:00
refactor: collapse per-env Configs into ClientEnv + ServerEnv (#3906)
## Description: This is a refactor to simplify config handling. Replaces the per-environment DevConfig/PreprodConfig/ProdConfig class hierarchy with two static classes: ClientEnv (browser main thread, reads from window.BOOTSTRAP_CONFIG) and ServerEnv (Node server, reads from process.env). The four config classes are deleted, the abstract DefaultServerConfig is gone, and DefaultConfig is renamed to Config. The values that flow server → client (gameEnv, numWorkers, turnstileSiteKey, jwtAudience, instanceId) used to be baked into the hardcoded per-env classes. They're now real env vars on the server, embedded into a single window.BOOTSTRAP_CONFIG object in index.html at request time (alongside the existing gitCommit/assetManifest/cdnBase globals, which moved into the same object), and read back by ClientEnv on the client. The dev defaults previously hidden inside DevServerConfig are now explicit in start:server-dev (NUM_WORKERS=2, TURNSTILE_SITE_KEY=1x..., JWT_AUDIENCE=localhost, etc.) and in vite.config.ts's html plugin inject.data. Production deploys plumb NUM_WORKERS and TURNSTILE_SITE_KEY through deploy.yml (GitHub vars) into the remote env file; JWT_AUDIENCE is derived from DOMAIN in deploy.sh. The dynamic /api/instance endpoint is gone — INSTANCE_ID rides along in BOOTSTRAP_CONFIG now. ServerEnv is the only thing server code touches; ClientEnv is browser-only. The two classes have intentional overlap (env, numWorkers, jwtIssuer, gameCreationRate, workerIndex, etc.) since they derive identical logic from different sources — there's a TODO in each to consolidate via a shared helper later. The game-logic Config no longer stores a ServerConfig/ClientEnv reference and its serverConfig() getter is gone; the one caller (MultiTabModal) now reads ClientEnv.env() directly. Worker init no longer carries server-config values since nothing in the worker actually reads them. ## 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
This commit is contained in:
@@ -139,6 +139,8 @@ jobs:
|
|||||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||||
API_KEY: ${{ secrets.API_KEY }}
|
API_KEY: ${{ secrets.API_KEY }}
|
||||||
|
NUM_WORKERS: ${{ vars.NUM_WORKERS }}
|
||||||
|
TURNSTILE_SITE_KEY: ${{ vars.TURNSTILE_SITE_KEY }}
|
||||||
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
|
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
|
||||||
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
|
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
|
||||||
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ else
|
|||||||
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
|
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -z "$DOMAIN" ]; then
|
||||||
|
echo "Error: DOMAIN not defined in .env file or environment"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$HOST" == "staging" ]; then
|
if [ "$HOST" == "staging" ]; then
|
||||||
print_header "DEPLOYING TO STAGING HOST"
|
print_header "DEPLOYING TO STAGING HOST"
|
||||||
SERVER_HOST=$SERVER_HOST_STAGING
|
SERVER_HOST=$SERVER_HOST_STAGING
|
||||||
@@ -138,6 +143,8 @@ API_KEY=$API_KEY
|
|||||||
DOMAIN=$DOMAIN
|
DOMAIN=$DOMAIN
|
||||||
SUBDOMAIN=$SUBDOMAIN
|
SUBDOMAIN=$SUBDOMAIN
|
||||||
CDN_BASE=$CDN_BASE
|
CDN_BASE=$CDN_BASE
|
||||||
|
NUM_WORKERS=$NUM_WORKERS
|
||||||
|
TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT
|
OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
OTEL_AUTH_HEADER=$OTEL_AUTH_HEADER
|
OTEL_AUTH_HEADER=$OTEL_AUTH_HEADER
|
||||||
EOL
|
EOL
|
||||||
|
|||||||
+7
-3
@@ -60,11 +60,15 @@
|
|||||||
|
|
||||||
<!-- Injected from Server env -->
|
<!-- Injected from Server env -->
|
||||||
<script>
|
<script>
|
||||||
window.GIT_COMMIT = <%- gitCommit %>;
|
|
||||||
window.ASSET_MANIFEST = <%- assetManifest %>;
|
|
||||||
window.CDN_BASE = <%- cdnBase %>;
|
|
||||||
window.BOOTSTRAP_CONFIG = {
|
window.BOOTSTRAP_CONFIG = {
|
||||||
|
gitCommit: <%- gitCommit %>,
|
||||||
|
assetManifest: <%- assetManifest %>,
|
||||||
|
cdnBase: <%- cdnBase %>,
|
||||||
gameEnv: <%- gameEnv %>,
|
gameEnv: <%- gameEnv %>,
|
||||||
|
numWorkers: <%- numWorkers %>,
|
||||||
|
turnstileSiteKey: <%- turnstileSiteKey %>,
|
||||||
|
jwtAudience: <%- jwtAudience %>,
|
||||||
|
instanceId: <%- instanceId %>,
|
||||||
};
|
};
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--background-image-url",
|
"--background-image-url",
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"build-prod": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"vite build\"",
|
"build-prod": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"vite build\"",
|
||||||
"start:client": "vite",
|
"start:client": "vite",
|
||||||
"start:server": "tsx src/server/Server.ts",
|
"start:server": "tsx src/server/Server.ts",
|
||||||
"start:server-dev": "cross-env GAME_ENV=dev tsx src/server/Server.ts",
|
"start:server-dev": "cross-env GAME_ENV=dev NUM_WORKERS=2 TURNSTILE_SITE_KEY=1x00000000000000000000AA API_KEY=WARNING_DEV_API_KEY_DO_NOT_USE_IN_PRODUCTION DOMAIN=localhost GIT_COMMIT=DEV tsx src/server/Server.ts",
|
||||||
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||||
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||||
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { html, TemplateResult } from "lit";
|
import { html, TemplateResult } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import {
|
import {
|
||||||
PlayerGame,
|
PlayerGame,
|
||||||
PlayerStatsTree,
|
PlayerStatsTree,
|
||||||
UserMeResponse,
|
UserMeResponse,
|
||||||
} from "../core/ApiSchemas";
|
} from "../core/ApiSchemas";
|
||||||
import { assetUrl } from "../core/AssetUrls";
|
import { assetUrl } from "../core/AssetUrls";
|
||||||
import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader";
|
|
||||||
import { fetchPlayerById, getUserMe } from "./Api";
|
import { fetchPlayerById, getUserMe } from "./Api";
|
||||||
import { discordLogin, logOut, sendMagicLink } from "./Auth";
|
import { discordLogin, logOut, sendMagicLink } from "./Auth";
|
||||||
import "./components/baseComponents/stats/DiscordUserHeader";
|
import "./components/baseComponents/stats/DiscordUserHeader";
|
||||||
@@ -229,9 +229,8 @@ export class AccountModal extends BaseModal {
|
|||||||
|
|
||||||
private async viewGame(gameId: string): Promise<void> {
|
private async viewGame(gameId: string): Promise<void> {
|
||||||
this.close();
|
this.close();
|
||||||
const config = await getRuntimeClientServerConfig();
|
|
||||||
const encodedGameId = encodeURIComponent(gameId);
|
const encodedGameId = encodeURIComponent(gameId);
|
||||||
const newUrl = `/${config.workerPath(gameId)}/game/${encodedGameId}`;
|
const newUrl = `/${ClientEnv.workerPath(gameId)}/game/${encodedGameId}`;
|
||||||
|
|
||||||
history.pushState({ join: gameId }, "", newUrl);
|
history.pushState({ join: gameId }, "", newUrl);
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { JWK } from "jose";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { GameID } from "../core/Schemas";
|
||||||
|
import { simpleHash } from "../core/Util";
|
||||||
|
import {
|
||||||
|
GameEnv,
|
||||||
|
JwksSchema,
|
||||||
|
parseGameEnv,
|
||||||
|
} from "../core/configuration/Config";
|
||||||
|
|
||||||
|
export class ClientEnv {
|
||||||
|
private static values: ClientEnvValues | null = null;
|
||||||
|
private static publicKey: JWK | null = null;
|
||||||
|
|
||||||
|
/** Test-only. */
|
||||||
|
static reset(): void {
|
||||||
|
ClientEnv.values = null;
|
||||||
|
ClientEnv.publicKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static get(): ClientEnvValues {
|
||||||
|
if (ClientEnv.values) return ClientEnv.values;
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
throw new Error("ClientEnv is only available on the browser main thread");
|
||||||
|
}
|
||||||
|
const bc = window.BOOTSTRAP_CONFIG;
|
||||||
|
if (
|
||||||
|
!bc ||
|
||||||
|
bc.gameEnv === undefined ||
|
||||||
|
bc.numWorkers === undefined ||
|
||||||
|
bc.turnstileSiteKey === undefined ||
|
||||||
|
bc.jwtAudience === undefined ||
|
||||||
|
bc.instanceId === undefined ||
|
||||||
|
bc.gitCommit === undefined
|
||||||
|
) {
|
||||||
|
throw new Error("Missing BOOTSTRAP_CONFIG");
|
||||||
|
}
|
||||||
|
ClientEnv.values = {
|
||||||
|
gameEnv: parseGameEnv(bc.gameEnv),
|
||||||
|
numWorkers: bc.numWorkers,
|
||||||
|
turnstileSiteKey: bc.turnstileSiteKey,
|
||||||
|
jwtAudience: bc.jwtAudience,
|
||||||
|
instanceId: bc.instanceId,
|
||||||
|
gitCommit: bc.gitCommit,
|
||||||
|
};
|
||||||
|
return ClientEnv.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: the following methods are duplicated on ServerEnv. The two classes
|
||||||
|
// read from different sources (window.BOOTSTRAP_CONFIG vs process.env) but
|
||||||
|
// the derived logic is identical. Consolidate into a shared helper that
|
||||||
|
// takes a source so we don't have to keep them in sync by hand.
|
||||||
|
static env(): GameEnv {
|
||||||
|
return ClientEnv.get().gameEnv;
|
||||||
|
}
|
||||||
|
static numWorkers(): number {
|
||||||
|
return ClientEnv.get().numWorkers;
|
||||||
|
}
|
||||||
|
static turnstileSiteKey(): string {
|
||||||
|
return ClientEnv.get().turnstileSiteKey;
|
||||||
|
}
|
||||||
|
static jwtAudience(): string {
|
||||||
|
return ClientEnv.get().jwtAudience;
|
||||||
|
}
|
||||||
|
static instanceId(): string {
|
||||||
|
return ClientEnv.get().instanceId;
|
||||||
|
}
|
||||||
|
static gitCommit(): string {
|
||||||
|
return ClientEnv.get().gitCommit;
|
||||||
|
}
|
||||||
|
static jwtIssuer(): string {
|
||||||
|
const audience = ClientEnv.jwtAudience();
|
||||||
|
return audience === "localhost"
|
||||||
|
? "http://localhost:8787"
|
||||||
|
: `https://api.${audience}`;
|
||||||
|
}
|
||||||
|
static async jwkPublicKey(): Promise<JWK> {
|
||||||
|
if (ClientEnv.publicKey) return ClientEnv.publicKey;
|
||||||
|
const jwksUrl = ClientEnv.jwtIssuer() + "/.well-known/jwks.json";
|
||||||
|
console.log(`Fetching JWKS from ${jwksUrl}`);
|
||||||
|
const response = await fetch(jwksUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
throw new Error(`JWKS fetch failed: ${response.status} ${body}`);
|
||||||
|
}
|
||||||
|
const result = JwksSchema.safeParse(await response.json());
|
||||||
|
if (!result.success) {
|
||||||
|
const error = z.prettifyError(result.error);
|
||||||
|
console.error("Error parsing JWKS", error);
|
||||||
|
throw new Error("Invalid JWKS");
|
||||||
|
}
|
||||||
|
ClientEnv.publicKey = result.data.keys[0];
|
||||||
|
return ClientEnv.publicKey;
|
||||||
|
}
|
||||||
|
static turnIntervalMs(): number {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
static gameCreationRate(): number {
|
||||||
|
return ClientEnv.env() === GameEnv.Dev ? 5 * 1000 : 2 * 60 * 1000;
|
||||||
|
}
|
||||||
|
static workerIndex(gameID: GameID): number {
|
||||||
|
return simpleHash(gameID) % ClientEnv.numWorkers();
|
||||||
|
}
|
||||||
|
static workerPath(gameID: GameID): string {
|
||||||
|
return `w${ClientEnv.workerIndex(gameID)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Values that flow from server → client via index.html. Set on the server from
|
||||||
|
* process.env, then re-hydrated on the client from window.BOOTSTRAP_CONFIG.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ClientEnvValues {
|
||||||
|
gameEnv: GameEnv;
|
||||||
|
numWorkers: number;
|
||||||
|
turnstileSiteKey: string;
|
||||||
|
jwtAudience: string;
|
||||||
|
instanceId: string;
|
||||||
|
gitCommit: string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Config } from "src/core/configuration/Config";
|
||||||
import { translateText } from "../client/Utils";
|
import { translateText } from "../client/Utils";
|
||||||
import { EventBus } from "../core/EventBus";
|
import { EventBus } from "../core/EventBus";
|
||||||
import {
|
import {
|
||||||
@@ -11,8 +12,6 @@ import {
|
|||||||
ServerMessage,
|
ServerMessage,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { createPartialGameRecord, findClosestBy, replacer } from "../core/Util";
|
import { createPartialGameRecord, findClosestBy, replacer } from "../core/Util";
|
||||||
import { ServerConfig } from "../core/configuration/Config";
|
|
||||||
import { getGameLogicConfig } from "../core/configuration/ConfigLoader";
|
|
||||||
import {
|
import {
|
||||||
BuildableUnit,
|
BuildableUnit,
|
||||||
PlayerType,
|
PlayerType,
|
||||||
@@ -63,7 +62,6 @@ import { GoToPlayerEvent } from "./graphics/TransformHandler";
|
|||||||
import { SoundManager } from "./sound/SoundManager";
|
import { SoundManager } from "./sound/SoundManager";
|
||||||
|
|
||||||
export interface LobbyConfig {
|
export interface LobbyConfig {
|
||||||
serverConfig: ServerConfig;
|
|
||||||
cosmetics: PlayerCosmeticRefs;
|
cosmetics: PlayerCosmeticRefs;
|
||||||
playerName: string;
|
playerName: string;
|
||||||
playerClanTag: string | null;
|
playerClanTag: string | null;
|
||||||
@@ -238,7 +236,7 @@ async function createClientGame(
|
|||||||
if (lobbyConfig.gameStartInfo === undefined) {
|
if (lobbyConfig.gameStartInfo === undefined) {
|
||||||
throw new Error("missing gameStartInfo");
|
throw new Error("missing gameStartInfo");
|
||||||
}
|
}
|
||||||
const config = await getGameLogicConfig(
|
const config = new Config(
|
||||||
lobbyConfig.gameStartInfo.config,
|
lobbyConfig.gameStartInfo.config,
|
||||||
userSettings,
|
userSettings,
|
||||||
lobbyConfig.gameRecord !== undefined,
|
lobbyConfig.gameRecord !== undefined,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { getRuntimeClientServerConfig } from "src/core/configuration/ConfigLoader";
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import {
|
import {
|
||||||
Duos,
|
Duos,
|
||||||
GameMapType,
|
GameMapType,
|
||||||
@@ -59,9 +59,7 @@ export class GameModeSelector extends LitElement {
|
|||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.lobbySocket.start();
|
this.lobbySocket.start();
|
||||||
getRuntimeClientServerConfig().then((config) => {
|
this.defaultLobbyTime = ClientEnv.gameCreationRate() / 1000;
|
||||||
this.defaultLobbyTime = config.gameCreationRate() / 1000;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { translateText } from "../client/Utils";
|
import { translateText } from "../client/Utils";
|
||||||
import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader";
|
|
||||||
import { EventBus } from "../core/EventBus";
|
import { EventBus } from "../core/EventBus";
|
||||||
import {
|
import {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
@@ -121,8 +121,7 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const config = await getRuntimeClientServerConfig();
|
return `${window.location.origin}/${ClientEnv.workerPath(this.lobbyId)}/game/${this.lobbyId}?lobby&s=${encodeURIComponent(this.lobbyUrlSuffix)}`;
|
||||||
return `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}?lobby&s=${encodeURIComponent(this.lobbyUrlSuffix)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async constructUrl(): Promise<string> {
|
private async constructUrl(): Promise<string> {
|
||||||
@@ -1050,13 +1049,12 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createLobby(gameID: string): Promise<GameInfo> {
|
async function createLobby(gameID: string): Promise<GameInfo> {
|
||||||
const config = await getRuntimeClientServerConfig();
|
|
||||||
// Send JWT token for creator identification - server extracts persistentID from it
|
// Send JWT token for creator identification - server extracts persistentID from it
|
||||||
// persistentID should never be exposed to other clients
|
// persistentID should never be exposed to other clients
|
||||||
const token = await getPlayToken();
|
const token = await getPlayToken();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/${config.workerPath(gameID)}/api/create_game/${gameID}`,
|
`/${ClientEnv.workerPath(gameID)}/api/create_game/${gameID}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { html, TemplateResult } from "lit";
|
import { html, TemplateResult } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators.js";
|
import { customElement, property, query, state } from "lit/decorators.js";
|
||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import {
|
import {
|
||||||
calculateServerTimeOffset,
|
calculateServerTimeOffset,
|
||||||
getMapName,
|
getMapName,
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
LobbyInfoEvent,
|
LobbyInfoEvent,
|
||||||
PublicGameInfo,
|
PublicGameInfo,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader";
|
|
||||||
import {
|
import {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
GameMapSize,
|
GameMapSize,
|
||||||
@@ -967,8 +967,7 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkActiveLobby(lobbyId: string): Promise<boolean> {
|
private async checkActiveLobby(lobbyId: string): Promise<boolean> {
|
||||||
const config = await getRuntimeClientServerConfig();
|
const url = `/${ClientEnv.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
||||||
const url = `/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -1037,10 +1036,8 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
return "version_mismatch";
|
return "version_mismatch";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const gitCommit = ClientEnv.gitCommit();
|
||||||
window.GIT_COMMIT !== "DEV" &&
|
if (gitCommit !== "DEV" && parsed.data.gitCommit !== gitCommit) {
|
||||||
parsed.data.gitCommit !== window.GIT_COMMIT
|
|
||||||
) {
|
|
||||||
const safeLobbyId = this.sanitizeForLog(lobbyId);
|
const safeLobbyId = this.sanitizeForLog(lobbyId);
|
||||||
console.warn(
|
console.warn(
|
||||||
`Git commit hash mismatch for game ${safeLobbyId}`,
|
`Git commit hash mismatch for game ${safeLobbyId}`,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader";
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { PublicGames, PublicGamesSchema } from "../core/Schemas";
|
import { PublicGames, PublicGamesSchema } from "../core/Schemas";
|
||||||
|
|
||||||
interface LobbySocketOptions {
|
interface LobbySocketOptions {
|
||||||
@@ -35,8 +35,7 @@ export class PublicLobbySocket {
|
|||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
this.wsConnectionAttempts = 0;
|
this.wsConnectionAttempts = 0;
|
||||||
// Get config to determine number of workers, then pick a random one
|
// Get config to determine number of workers, then pick a random one
|
||||||
const config = await getRuntimeClientServerConfig();
|
this.workerPath = getRandomWorkerPath(ClientEnv.numWorkers());
|
||||||
this.workerPath = getRandomWorkerPath(config.numWorkers());
|
|
||||||
this.connectWebSocket();
|
this.connectWebSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EventBus } from "../core/EventBus";
|
import { EventBus } from "../core/EventBus";
|
||||||
import {
|
import {
|
||||||
@@ -81,8 +82,7 @@ export class LocalServer {
|
|||||||
console.log("local server starting");
|
console.log("local server starting");
|
||||||
this.turnCheckInterval = setInterval(() => {
|
this.turnCheckInterval = setInterval(() => {
|
||||||
const turnIntervalMs =
|
const turnIntervalMs =
|
||||||
this.lobbyConfig.serverConfig.turnIntervalMs() *
|
ClientEnv.turnIntervalMs() * this.replaySpeedMultiplier;
|
||||||
this.replaySpeedMultiplier;
|
|
||||||
const backlog = Math.max(0, this.turns.length - this.turnsExecuted);
|
const backlog = Math.max(0, this.turns.length - this.turnsExecuted);
|
||||||
const allowReplayBacklog =
|
const allowReplayBacklog =
|
||||||
this.replaySpeedMultiplier === ReplaySpeedMultiplier.fastest &&
|
this.replaySpeedMultiplier === ReplaySpeedMultiplier.fastest &&
|
||||||
@@ -297,7 +297,7 @@ export class LocalServer {
|
|||||||
console.error("Error parsing game record", error);
|
console.error("Error parsing game record", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workerPath = this.lobbyConfig.serverConfig.workerPath(
|
const workerPath = ClientEnv.workerPath(
|
||||||
this.lobbyConfig.gameStartInfo.gameID,
|
this.lobbyConfig.gameStartInfo.gameID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+7
-15
@@ -1,4 +1,5 @@
|
|||||||
import version from "resources/version.txt?raw";
|
import version from "resources/version.txt?raw";
|
||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { UserMeResponse } from "../core/ApiSchemas";
|
import { UserMeResponse } from "../core/ApiSchemas";
|
||||||
import { assetUrl } from "../core/AssetUrls";
|
import { assetUrl } from "../core/AssetUrls";
|
||||||
import { EventBus } from "../core/EventBus";
|
import { EventBus } from "../core/EventBus";
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
PublicGameInfo,
|
PublicGameInfo,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader";
|
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
import {
|
import {
|
||||||
DARK_MODE_KEY,
|
DARK_MODE_KEY,
|
||||||
@@ -169,7 +169,6 @@ function updateAccountNavButton(userMeResponse: UserMeResponse | false) {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
GIT_COMMIT: string;
|
|
||||||
turnstile: any;
|
turnstile: any;
|
||||||
adsEnabled: boolean;
|
adsEnabled: boolean;
|
||||||
PageOS: {
|
PageOS: {
|
||||||
@@ -770,16 +769,14 @@ class Client {
|
|||||||
if (lobby.source === "public") {
|
if (lobby.source === "public") {
|
||||||
this.joinModal?.open(lobby.gameID, lobby.publicLobbyInfo);
|
this.joinModal?.open(lobby.gameID, lobby.publicLobbyInfo);
|
||||||
}
|
}
|
||||||
const config = await getRuntimeClientServerConfig();
|
|
||||||
// Only update URL immediately for private lobbies, not public ones
|
// Only update URL immediately for private lobbies, not public ones
|
||||||
if (lobby.source !== "public") {
|
if (lobby.source !== "public") {
|
||||||
this.updateJoinUrlForShare(lobby.gameID, config);
|
this.updateJoinUrlForShare(lobby.gameID);
|
||||||
}
|
}
|
||||||
const auth = await userAuth();
|
const auth = await userAuth();
|
||||||
const playerRole = auth !== false ? (auth.claims.role ?? null) : null;
|
const playerRole = auth !== false ? (auth.claims.role ?? null) : null;
|
||||||
const newLobbyHandle = joinLobby(this.eventBus, {
|
const newLobbyHandle = joinLobby(this.eventBus, {
|
||||||
gameID: lobby.gameID,
|
gameID: lobby.gameID,
|
||||||
serverConfig: config,
|
|
||||||
cosmetics: await getPlayerCosmeticsRefs(),
|
cosmetics: await getPlayerCosmeticsRefs(),
|
||||||
turnstileToken: await this.getTurnstileToken(lobby),
|
turnstileToken: await this.getTurnstileToken(lobby),
|
||||||
playerName: this.usernameInput?.getUsername() ?? genAnonUsername(),
|
playerName: this.usernameInput?.getUsername() ?? genAnonUsername(),
|
||||||
@@ -881,7 +878,7 @@ class Client {
|
|||||||
"",
|
"",
|
||||||
lobbyIdHidden
|
lobbyIdHidden
|
||||||
? "/streamer-mode"
|
? "/streamer-mode"
|
||||||
: `/${config.workerPath(lobby.gameID)}/game/${lobby.gameID}?live`,
|
: `/${ClientEnv.workerPath(lobby.gameID)}/game/${lobby.gameID}?live`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store current URL for popstate confirmation
|
// Store current URL for popstate confirmation
|
||||||
@@ -889,14 +886,11 @@ class Client {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateJoinUrlForShare(
|
private updateJoinUrlForShare(lobbyId: string) {
|
||||||
lobbyId: string,
|
|
||||||
config: Awaited<ReturnType<typeof getRuntimeClientServerConfig>>,
|
|
||||||
) {
|
|
||||||
const lobbyIdHidden = !this.userSettings.lobbyIdVisibility();
|
const lobbyIdHidden = !this.userSettings.lobbyIdVisibility();
|
||||||
const targetUrl = lobbyIdHidden
|
const targetUrl = lobbyIdHidden
|
||||||
? "/streamer-mode"
|
? "/streamer-mode"
|
||||||
: `/${config.workerPath(lobbyId)}/game/${lobbyId}`;
|
: `/${ClientEnv.workerPath(lobbyId)}/game/${lobbyId}`;
|
||||||
const currentUrl = window.location.pathname;
|
const currentUrl = window.location.pathname;
|
||||||
|
|
||||||
if (currentUrl !== targetUrl) {
|
if (currentUrl !== targetUrl) {
|
||||||
@@ -970,9 +964,8 @@ class Client {
|
|||||||
private async getTurnstileToken(
|
private async getTurnstileToken(
|
||||||
lobby: JoinLobbyEvent,
|
lobby: JoinLobbyEvent,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const config = await getRuntimeClientServerConfig();
|
|
||||||
if (
|
if (
|
||||||
config.env() === GameEnv.Dev ||
|
ClientEnv.env() === GameEnv.Dev ||
|
||||||
lobby.gameStartInfo?.config.gameType === GameType.Singleplayer
|
lobby.gameStartInfo?.config.gameType === GameType.Singleplayer
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1048,9 +1041,8 @@ async function getTurnstileToken(): Promise<{
|
|||||||
throw new Error("Failed to load Turnstile script");
|
throw new Error("Failed to load Turnstile script");
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getRuntimeClientServerConfig();
|
|
||||||
const widgetId = window.turnstile.render("#turnstile-container", {
|
const widgetId = window.turnstile.render("#turnstile-container", {
|
||||||
sitekey: config.turnstileSiteKey(),
|
sitekey: ClientEnv.turnstileSiteKey(),
|
||||||
size: "normal",
|
size: "normal",
|
||||||
appearance: "interaction-only",
|
appearance: "interaction-only",
|
||||||
theme: "light",
|
theme: "light",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { UserMeResponse } from "../core/ApiSchemas";
|
import { UserMeResponse } from "../core/ApiSchemas";
|
||||||
import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader";
|
|
||||||
import { getUserMe, hasLinkedAccount } from "./Api";
|
import { getUserMe, hasLinkedAccount } from "./Api";
|
||||||
import { getPlayToken } from "./Auth";
|
import { getPlayToken } from "./Auth";
|
||||||
import { BaseModal } from "./components/BaseModal";
|
import { BaseModal } from "./components/BaseModal";
|
||||||
@@ -12,7 +12,6 @@ import { translateText } from "./Utils";
|
|||||||
|
|
||||||
@customElement("matchmaking-modal")
|
@customElement("matchmaking-modal")
|
||||||
export class MatchmakingModal extends BaseModal {
|
export class MatchmakingModal extends BaseModal {
|
||||||
private static instanceIdPromise: Promise<string> | null = null;
|
|
||||||
private gameCheckInterval: ReturnType<typeof setInterval> | null = null;
|
private gameCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
private connectTimeout: ReturnType<typeof setTimeout> | null = null;
|
private connectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
@state() private connected = false;
|
@state() private connected = false;
|
||||||
@@ -86,11 +85,8 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async connect() {
|
private async connect() {
|
||||||
const config = await getRuntimeClientServerConfig();
|
|
||||||
const instanceId = await MatchmakingModal.getInstanceId();
|
|
||||||
|
|
||||||
this.socket = new WebSocket(
|
this.socket = new WebSocket(
|
||||||
`${config.jwtIssuer()}/matchmaking/join?instance_id=${encodeURIComponent(instanceId)}`,
|
`${ClientEnv.jwtIssuer()}/matchmaking/join?instance_id=${encodeURIComponent(ClientEnv.instanceId())}`,
|
||||||
);
|
);
|
||||||
this.socket.onopen = async () => {
|
this.socket.onopen = async () => {
|
||||||
console.log("Connected to matchmaking server");
|
console.log("Connected to matchmaking server");
|
||||||
@@ -131,32 +127,6 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getInstanceId(): Promise<string> {
|
|
||||||
MatchmakingModal.instanceIdPromise ??= fetch("/api/instance", {
|
|
||||||
cache: "no-store",
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to load instance id: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as { instanceId?: string };
|
|
||||||
if (!data.instanceId) {
|
|
||||||
throw new Error("Missing instance id");
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.instanceId;
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
MatchmakingModal.instanceIdPromise = null;
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
return MatchmakingModal.instanceIdPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onOpen(): Promise<void> {
|
protected async onOpen(): Promise<void> {
|
||||||
const userMe = await getUserMe();
|
const userMe = await getUserMe();
|
||||||
// Early return if modal was closed during async operation
|
// Early return if modal was closed during async operation
|
||||||
@@ -209,8 +179,7 @@ export class MatchmakingModal extends BaseModal {
|
|||||||
if (this.gameID === null) {
|
if (this.gameID === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const config = await getRuntimeClientServerConfig();
|
const url = `/${ClientEnv.workerPath(this.gameID)}/api/game/${this.gameID}/exists`;
|
||||||
const url = `/${config.workerPath(this.gameID)}/api/game/${this.gameID}/exists`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EventBus, GameEvent } from "../core/EventBus";
|
import { EventBus, GameEvent } from "../core/EventBus";
|
||||||
import {
|
import {
|
||||||
@@ -330,9 +331,7 @@ export class Transport {
|
|||||||
this.killExistingSocket();
|
this.killExistingSocket();
|
||||||
const wsHost = window.location.host;
|
const wsHost = window.location.host;
|
||||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const workerPath = this.lobbyConfig.serverConfig.workerPath(
|
const workerPath = ClientEnv.workerPath(this.lobbyConfig.gameID);
|
||||||
this.lobbyConfig.gameID,
|
|
||||||
);
|
|
||||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`);
|
this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`);
|
||||||
this.onconnect = onconnect;
|
this.onconnect = onconnect;
|
||||||
this.onmessage = onmessage;
|
this.onmessage = onmessage;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { getRuntimeClientServerConfig } from "../../core/configuration/ConfigLoader";
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { UserSettings } from "../../core/game/UserSettings";
|
import { UserSettings } from "../../core/game/UserSettings";
|
||||||
import { crazyGamesSDK } from "../CrazyGamesSDK";
|
import { crazyGamesSDK } from "../CrazyGamesSDK";
|
||||||
import { copyToClipboard, translateText } from "../Utils";
|
import { copyToClipboard, translateText } from "../Utils";
|
||||||
@@ -63,8 +63,7 @@ export class CopyButton extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async buildCopyUrl(): Promise<string> {
|
private async buildCopyUrl(): Promise<string> {
|
||||||
const config = await getRuntimeClientServerConfig();
|
let url = `${window.location.origin}/${ClientEnv.workerPath(this.lobbyId)}/game/${this.lobbyId}`;
|
||||||
let url = `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}`;
|
|
||||||
if (this.includeLobbyQuery) {
|
if (this.includeLobbyQuery) {
|
||||||
url += `?lobby&s=${encodeURIComponent(this.lobbySuffix)}`;
|
url += `?lobby&s=${encodeURIComponent(this.lobbySuffix)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
|
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
|
||||||
import buildingExplosion from "../../../resources/sprites/buildingExplosion.png";
|
import buildingExplosion from "../../../resources/sprites/buildingExplosion.png";
|
||||||
import conquestSword from "../../../resources/sprites/conquestSword.png";
|
import conquestSword from "../../../resources/sprites/conquestSword.png";
|
||||||
@@ -10,7 +11,6 @@ import sinkingShip from "../../../resources/sprites/sinkingShip.png";
|
|||||||
import miniSmoke from "../../../resources/sprites/smoke.png";
|
import miniSmoke from "../../../resources/sprites/smoke.png";
|
||||||
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
|
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
|
||||||
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
|
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
|
||||||
import { Theme } from "../../core/configuration/Config";
|
|
||||||
import { PlayerView } from "../../core/game/GameView";
|
import { PlayerView } from "../../core/game/GameView";
|
||||||
import { AnimatedSprite } from "./AnimatedSprite";
|
import { AnimatedSprite } from "./AnimatedSprite";
|
||||||
import { FxType } from "./fx/Fx";
|
import { FxType } from "./fx/Fx";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Colord } from "colord";
|
import { Colord } from "colord";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { assetUrl } from "../../core/AssetUrls";
|
import { assetUrl } from "../../core/AssetUrls";
|
||||||
import { Theme } from "../../core/configuration/Config";
|
|
||||||
import { TrainType, UnitType } from "../../core/game/Game";
|
import { TrainType, UnitType } from "../../core/game/Game";
|
||||||
import { UnitView } from "../../core/game/GameView";
|
import { UnitView } from "../../core/game/GameView";
|
||||||
const atomBombSprite = assetUrl("sprites/atombomb.png");
|
const atomBombSprite = assetUrl("sprites/atombomb.png");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Theme } from "../../../core/configuration/Config";
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { PlayerView } from "../../../core/game/GameView";
|
import { PlayerView } from "../../../core/game/GameView";
|
||||||
import { AnimatedSprite } from "../AnimatedSprite";
|
import { AnimatedSprite } from "../AnimatedSprite";
|
||||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Theme } from "../../../core/configuration/Config";
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { UnitType } from "../../../core/game/Game";
|
import { UnitType } from "../../../core/game/Game";
|
||||||
import { TileRef } from "../../../core/game/GameMap";
|
import { TileRef } from "../../../core/game/GameMap";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { GameEnv } from "../../../core/configuration/Config";
|
import { GameEnv } from "../../../core/configuration/Config";
|
||||||
import { GameType } from "../../../core/game/Game";
|
import { GameType } from "../../../core/game/Game";
|
||||||
import { GameView } from "../../../core/game/GameView";
|
import { GameView } from "../../../core/game/GameView";
|
||||||
@@ -31,7 +32,7 @@ export class MultiTabModal extends LitElement implements Layer {
|
|||||||
if (
|
if (
|
||||||
this.game.inSpawnPhase() ||
|
this.game.inSpawnPhase() ||
|
||||||
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
|
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
|
||||||
this.game.config().serverConfig().env() === GameEnv.Dev ||
|
ClientEnv.env() === GameEnv.Dev ||
|
||||||
this.game.config().isReplay()
|
this.game.config().isReplay()
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { assetUrl } from "src/core/AssetUrls";
|
import { assetUrl } from "src/core/AssetUrls";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||||
import { Config, Theme } from "../../../core/configuration/Config";
|
import { Config } from "../../../core/configuration/Config";
|
||||||
import { Cell } from "../../../core/game/Game";
|
import { Cell } from "../../../core/game/Game";
|
||||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||||
import { UserSettings } from "../../../core/game/UserSettings";
|
import { UserSettings } from "../../../core/game/UserSettings";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as PIXI from "pixi.js";
|
import * as PIXI from "pixi.js";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { assetUrl } from "../../../core/AssetUrls";
|
import { assetUrl } from "../../../core/AssetUrls";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
PlayerBuildableUnitType,
|
PlayerBuildableUnitType,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { extend } from "colord";
|
|||||||
import a11yPlugin from "colord/plugins/a11y";
|
import a11yPlugin from "colord/plugins/a11y";
|
||||||
import { OutlineFilter } from "pixi-filters";
|
import { OutlineFilter } from "pixi-filters";
|
||||||
import * as PIXI from "pixi.js";
|
import * as PIXI from "pixi.js";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { assetUrl } from "../../../core/AssetUrls";
|
import { assetUrl } from "../../../core/AssetUrls";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { wouldNukeBreakAlliance } from "../../../core/execution/Util";
|
import { wouldNukeBreakAlliance } from "../../../core/execution/Util";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { colord, Colord } from "colord";
|
import { colord, Colord } from "colord";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { assetUrl } from "../../../core/AssetUrls";
|
import { assetUrl } from "../../../core/AssetUrls";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { TransformHandler } from "../TransformHandler";
|
import { TransformHandler } from "../TransformHandler";
|
||||||
import { Layer } from "./Layer";
|
import { Layer } from "./Layer";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Config, Theme } from "../../../core/configuration/Config";
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
|
import { Config } from "../../../core/configuration/Config";
|
||||||
import { GameView } from "../../../core/game/GameView";
|
import { GameView } from "../../../core/game/GameView";
|
||||||
import { TransformHandler } from "../TransformHandler";
|
import { TransformHandler } from "../TransformHandler";
|
||||||
import { Layer } from "./Layer";
|
import { Layer } from "./Layer";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||||
import { Colord } from "colord";
|
import { Colord } from "colord";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Colord } from "colord";
|
import { Colord } from "colord";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
|
||||||
import { UnitType } from "../../../core/game/Game";
|
import { UnitType } from "../../../core/game/Game";
|
||||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { colord, Colord } from "colord";
|
import { colord, Colord } from "colord";
|
||||||
|
import { Theme } from "src/core/configuration/Theme";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
|
||||||
import { Cell, UnitType } from "../../../core/game/Game";
|
import { Cell, UnitType } from "../../../core/game/Game";
|
||||||
import { TileRef } from "../../../core/game/GameMap";
|
import { TileRef } from "../../../core/game/GameMap";
|
||||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||||
|
|||||||
+10
-9
@@ -70,16 +70,14 @@ export function buildAssetUrl(
|
|||||||
declare global {
|
declare global {
|
||||||
var __ASSET_MANIFEST__: AssetManifest | undefined;
|
var __ASSET_MANIFEST__: AssetManifest | undefined;
|
||||||
var __CDN_BASE__: string | undefined;
|
var __CDN_BASE__: string | undefined;
|
||||||
|
|
||||||
interface Window {
|
|
||||||
ASSET_MANIFEST?: AssetManifest;
|
|
||||||
CDN_BASE?: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAssetManifest(): AssetManifest {
|
export function getAssetManifest(): AssetManifest {
|
||||||
if (typeof window !== "undefined" && window.ASSET_MANIFEST !== undefined) {
|
if (
|
||||||
return window.ASSET_MANIFEST;
|
typeof window !== "undefined" &&
|
||||||
|
window.BOOTSTRAP_CONFIG?.assetManifest !== undefined
|
||||||
|
) {
|
||||||
|
return window.BOOTSTRAP_CONFIG.assetManifest;
|
||||||
}
|
}
|
||||||
return globalThis.__ASSET_MANIFEST__ ?? {};
|
return globalThis.__ASSET_MANIFEST__ ?? {};
|
||||||
}
|
}
|
||||||
@@ -89,8 +87,11 @@ export function getAssetManifest(): AssetManifest {
|
|||||||
// Without this fallback, asset fetches inside workers (e.g. map binaries)
|
// Without this fallback, asset fetches inside workers (e.g. map binaries)
|
||||||
// would silently bypass the CDN.
|
// would silently bypass the CDN.
|
||||||
export function getCdnBase(): string {
|
export function getCdnBase(): string {
|
||||||
if (typeof window !== "undefined" && window.CDN_BASE !== undefined) {
|
if (
|
||||||
return window.CDN_BASE;
|
typeof window !== "undefined" &&
|
||||||
|
window.BOOTSTRAP_CONFIG?.cdnBase !== undefined
|
||||||
|
) {
|
||||||
|
return window.BOOTSTRAP_CONFIG.cdnBase;
|
||||||
}
|
}
|
||||||
return globalThis.__CDN_BASE__ ?? "";
|
return globalThis.__CDN_BASE__ ?? "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { placeName } from "../client/graphics/NameBoxCalculator";
|
import { placeName } from "../client/graphics/NameBoxCalculator";
|
||||||
import { getGameLogicConfig } from "./configuration/ConfigLoader";
|
import { Config } from "./configuration/Config";
|
||||||
import { Executor } from "./execution/ExecutionManager";
|
import { Executor } from "./execution/ExecutionManager";
|
||||||
import { RecomputeRailClusterExecution } from "./execution/RecomputeRailClusterExecution";
|
import { RecomputeRailClusterExecution } from "./execution/RecomputeRailClusterExecution";
|
||||||
import { SpawnTimerExecution } from "./execution/SpawnTimerExecution";
|
import { SpawnTimerExecution } from "./execution/SpawnTimerExecution";
|
||||||
@@ -37,7 +37,7 @@ export async function createGameRunner(
|
|||||||
mapLoader: GameMapLoader,
|
mapLoader: GameMapLoader,
|
||||||
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||||
): Promise<GameRunner> {
|
): Promise<GameRunner> {
|
||||||
const config = await getGameLogicConfig(gameStart.config, null);
|
const config = new Config(gameStart.config, null, false);
|
||||||
const gameMap = await loadGameMap(
|
const gameMap = await loadGameMap(
|
||||||
gameStart.config.gameMap,
|
gameStart.config.gameMap,
|
||||||
gameStart.config.gameMapSize,
|
gameStart.config.gameMapSize,
|
||||||
|
|||||||
+907
-167
File diff suppressed because it is too large
Load Diff
@@ -1,119 +0,0 @@
|
|||||||
import { UserSettings } from "../game/UserSettings";
|
|
||||||
import { GameConfig } from "../Schemas";
|
|
||||||
import { Config, ServerConfig } from "./Config";
|
|
||||||
import { DefaultConfig } from "./DefaultConfig";
|
|
||||||
import { DevConfig, DevServerConfig } from "./DevConfig";
|
|
||||||
import { Env } from "./Env";
|
|
||||||
import { preprodConfig } from "./PreprodConfig";
|
|
||||||
import { prodConfig } from "./ProdConfig";
|
|
||||||
|
|
||||||
export enum GameLogicEnv {
|
|
||||||
Dev = "dev",
|
|
||||||
Default = "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
export let cachedRuntimeClientServerConfig: ServerConfig | null = null;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
BOOTSTRAP_CONFIG?: {
|
|
||||||
gameEnv?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getGameLogicConfig(
|
|
||||||
gameConfig: GameConfig,
|
|
||||||
userSettings: UserSettings | null,
|
|
||||||
isReplay: boolean = false,
|
|
||||||
): Promise<Config> {
|
|
||||||
const gameLogicEnv = getBuildTimeGameLogicEnv();
|
|
||||||
const serverConfig = getServerConfigForGameLogicEnv(gameLogicEnv);
|
|
||||||
|
|
||||||
switch (gameLogicEnv) {
|
|
||||||
case GameLogicEnv.Dev:
|
|
||||||
return new DevConfig(serverConfig, gameConfig, userSettings, isReplay);
|
|
||||||
case GameLogicEnv.Default:
|
|
||||||
return new DefaultConfig(
|
|
||||||
serverConfig,
|
|
||||||
gameConfig,
|
|
||||||
userSettings,
|
|
||||||
isReplay,
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
throw Error(`unsupported game logic environment: ${gameLogicEnv}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBuildTimeGameLogicEnv(): GameLogicEnv {
|
|
||||||
const bundledGameEnv = process.env.GAME_ENV;
|
|
||||||
|
|
||||||
switch (bundledGameEnv) {
|
|
||||||
case "dev":
|
|
||||||
return GameLogicEnv.Dev;
|
|
||||||
case "staging":
|
|
||||||
case "prod":
|
|
||||||
return GameLogicEnv.Default;
|
|
||||||
case undefined:
|
|
||||||
throw new Error("Missing bundled game logic env");
|
|
||||||
default:
|
|
||||||
throw Error(`unsupported bundled game logic env: ${bundledGameEnv}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getServerConfigForGameLogicEnv(
|
|
||||||
gameLogicEnv: GameLogicEnv,
|
|
||||||
): ServerConfig {
|
|
||||||
switch (gameLogicEnv) {
|
|
||||||
case GameLogicEnv.Dev:
|
|
||||||
return new DevServerConfig();
|
|
||||||
case GameLogicEnv.Default:
|
|
||||||
console.log("using default game logic config");
|
|
||||||
return prodConfig;
|
|
||||||
default:
|
|
||||||
throw Error(`unsupported game logic environment: ${gameLogicEnv}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRuntimeClientServerConfig(): Promise<ServerConfig> {
|
|
||||||
if (cachedRuntimeClientServerConfig) {
|
|
||||||
return cachedRuntimeClientServerConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
throw new Error(
|
|
||||||
"Runtime client server config is only available on the browser main thread",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtimeClientEnv = window.BOOTSTRAP_CONFIG?.gameEnv;
|
|
||||||
if (!runtimeClientEnv) {
|
|
||||||
throw new Error("Missing runtime client server config");
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedRuntimeClientServerConfig = getServerConfig(runtimeClientEnv);
|
|
||||||
return cachedRuntimeClientServerConfig;
|
|
||||||
}
|
|
||||||
export function getServerConfigFromServer(): ServerConfig {
|
|
||||||
const gameEnv = Env.GAME_ENV;
|
|
||||||
return getServerConfig(gameEnv);
|
|
||||||
}
|
|
||||||
export function getServerConfig(gameEnv: string) {
|
|
||||||
switch (gameEnv) {
|
|
||||||
case "dev":
|
|
||||||
console.log("using dev server config");
|
|
||||||
return new DevServerConfig();
|
|
||||||
case "staging":
|
|
||||||
console.log("using preprod server config");
|
|
||||||
return preprodConfig;
|
|
||||||
case "prod":
|
|
||||||
console.log("using prod server config");
|
|
||||||
return prodConfig;
|
|
||||||
default:
|
|
||||||
throw Error(`unsupported server configuration: ${gameEnv}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearCachedRuntimeClientServerConfig(): void {
|
|
||||||
cachedRuntimeClientServerConfig = null;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
|||||||
import { UserSettings } from "../game/UserSettings";
|
|
||||||
import { GameConfig } from "../Schemas";
|
|
||||||
import { GameEnv, ServerConfig } from "./Config";
|
|
||||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
|
||||||
|
|
||||||
export class DevServerConfig extends DefaultServerConfig {
|
|
||||||
turnstileSiteKey(): string {
|
|
||||||
return "1x00000000000000000000AA";
|
|
||||||
}
|
|
||||||
|
|
||||||
adminToken(): string {
|
|
||||||
return "WARNING_DEV_ADMIN_KEY_DO_NOT_USE_IN_PRODUCTION";
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKey(): string {
|
|
||||||
return "WARNING_DEV_API_KEY_DO_NOT_USE_IN_PRODUCTION";
|
|
||||||
}
|
|
||||||
|
|
||||||
env(): GameEnv {
|
|
||||||
return GameEnv.Dev;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameCreationRate(): number {
|
|
||||||
return 5 * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
numWorkers(): number {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
jwtAudience(): string {
|
|
||||||
return "localhost";
|
|
||||||
}
|
|
||||||
gitCommit(): string {
|
|
||||||
return "DEV";
|
|
||||||
}
|
|
||||||
|
|
||||||
domain(): string {
|
|
||||||
return "localhost";
|
|
||||||
}
|
|
||||||
|
|
||||||
subdomain(): string {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DevConfig extends DefaultConfig {
|
|
||||||
constructor(
|
|
||||||
sc: ServerConfig,
|
|
||||||
gc: GameConfig,
|
|
||||||
us: UserSettings | null,
|
|
||||||
isReplay: boolean,
|
|
||||||
) {
|
|
||||||
super(sc, gc, us, isReplay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* Safely access environment variables in both Node.js and Vite environments.
|
|
||||||
* - In Vite (Browser), it uses `import.meta.env`.
|
|
||||||
* - In Node.js (Server), it uses `process.env`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
[key: string]: string | boolean | undefined;
|
|
||||||
}
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEnv(key: string, viteKey?: string): string | undefined {
|
|
||||||
const vKey = viteKey ?? key;
|
|
||||||
|
|
||||||
// Try import.meta.env (Vite/Browser)
|
|
||||||
// We use a try-catch block or check existence to avoid ReferenceErrors
|
|
||||||
try {
|
|
||||||
if (typeof import.meta !== "undefined" && import.meta.env) {
|
|
||||||
const val = import.meta.env[vKey] ?? import.meta.env[key];
|
|
||||||
if (val !== undefined) {
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors accessing import.meta
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try process.env (Node.js)
|
|
||||||
try {
|
|
||||||
if (typeof process !== "undefined" && process.env) {
|
|
||||||
const val = process.env[key];
|
|
||||||
if (val !== undefined) {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors accessing process
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Env = {
|
|
||||||
get GAME_ENV(): string {
|
|
||||||
// Check MODE for Vite, GAME_ENV for Node
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
typeof import.meta !== "undefined" &&
|
|
||||||
import.meta.env &&
|
|
||||||
import.meta.env.MODE
|
|
||||||
) {
|
|
||||||
return import.meta.env.MODE;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors accessing import.meta
|
|
||||||
}
|
|
||||||
|
|
||||||
return getEnv("GAME_ENV") ?? "dev";
|
|
||||||
},
|
|
||||||
|
|
||||||
get STRIPE_PUBLISHABLE_KEY() {
|
|
||||||
return getEnv("STRIPE_PUBLISHABLE_KEY");
|
|
||||||
},
|
|
||||||
get DOMAIN() {
|
|
||||||
return getEnv("DOMAIN");
|
|
||||||
},
|
|
||||||
get SUBDOMAIN() {
|
|
||||||
return getEnv("SUBDOMAIN");
|
|
||||||
},
|
|
||||||
get OTEL_EXPORTER_OTLP_ENDPOINT() {
|
|
||||||
return getEnv("OTEL_EXPORTER_OTLP_ENDPOINT");
|
|
||||||
},
|
|
||||||
get OTEL_AUTH_HEADER() {
|
|
||||||
return getEnv("OTEL_AUTH_HEADER");
|
|
||||||
},
|
|
||||||
get GIT_COMMIT() {
|
|
||||||
return getEnv("GIT_COMMIT");
|
|
||||||
},
|
|
||||||
get API_KEY() {
|
|
||||||
return getEnv("API_KEY");
|
|
||||||
},
|
|
||||||
get ADMIN_TOKEN() {
|
|
||||||
return getEnv("ADMIN_TOKEN");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -5,7 +5,7 @@ import { GameMap, TileRef } from "../game/GameMap";
|
|||||||
import { PlayerView } from "../game/GameView";
|
import { PlayerView } from "../game/GameView";
|
||||||
import { ColorAllocator } from "./ColorAllocator";
|
import { ColorAllocator } from "./ColorAllocator";
|
||||||
import { botColors, fallbackColors, humanColors, nationColors } from "./Colors";
|
import { botColors, fallbackColors, humanColors, nationColors } from "./Colors";
|
||||||
import { Theme } from "./Config";
|
import { Theme } from "./Theme";
|
||||||
|
|
||||||
export class PastelTheme implements Theme {
|
export class PastelTheme implements Theme {
|
||||||
private rand = new PseudoRandom(123);
|
private rand = new PseudoRandom(123);
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { GameEnv } from "./Config";
|
|
||||||
import { DefaultServerConfig } from "./DefaultConfig";
|
|
||||||
|
|
||||||
export const preprodConfig = new (class extends DefaultServerConfig {
|
|
||||||
env(): GameEnv {
|
|
||||||
return GameEnv.Preprod;
|
|
||||||
}
|
|
||||||
numWorkers(): number {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
turnstileSiteKey(): string {
|
|
||||||
return "0x4AAAAAAB7QetxHwRCKw-aP";
|
|
||||||
}
|
|
||||||
jwtAudience(): string {
|
|
||||||
return "openfront.dev";
|
|
||||||
}
|
|
||||||
allowedFlares(): string[] | undefined {
|
|
||||||
return undefined;
|
|
||||||
// TODO: Uncomment this after testing.
|
|
||||||
// Allow access without login for now to test
|
|
||||||
// the new login flow.
|
|
||||||
// return [
|
|
||||||
// // "access:openfront.dev"
|
|
||||||
// ];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { GameEnv } from "./Config";
|
|
||||||
import { DefaultServerConfig } from "./DefaultConfig";
|
|
||||||
|
|
||||||
export const prodConfig = new (class extends DefaultServerConfig {
|
|
||||||
numWorkers(): number {
|
|
||||||
return 20;
|
|
||||||
}
|
|
||||||
env(): GameEnv {
|
|
||||||
return GameEnv.Prod;
|
|
||||||
}
|
|
||||||
jwtAudience(): string {
|
|
||||||
return "openfront.io";
|
|
||||||
}
|
|
||||||
turnstileSiteKey(): string {
|
|
||||||
return "0x4AAAAAACFLkaecN39lS8sk";
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Colord } from "colord";
|
||||||
|
import { Team } from "../game/Game";
|
||||||
|
import { GameMap, TileRef } from "../game/GameMap";
|
||||||
|
import { PlayerView } from "../game/GameView";
|
||||||
|
|
||||||
|
export interface Theme {
|
||||||
|
teamColor(team: Team): Colord;
|
||||||
|
// Don't call directly, use PlayerView
|
||||||
|
territoryColor(playerInfo: PlayerView): Colord;
|
||||||
|
// Don't call directly, use PlayerView
|
||||||
|
structureColors(territoryColor: Colord): { light: Colord; dark: Colord };
|
||||||
|
// Don't call directly, use PlayerView
|
||||||
|
borderColor(territoryColor: Colord): Colord;
|
||||||
|
// Don't call directly, use PlayerView
|
||||||
|
defendedBorderColors(territoryColor: Colord): { light: Colord; dark: Colord };
|
||||||
|
focusedBorderColor(): Colord;
|
||||||
|
terrainColor(gm: GameMap, tile: TileRef): Colord;
|
||||||
|
backgroundColor(): Colord;
|
||||||
|
falloutColor(): Colord;
|
||||||
|
font(): string;
|
||||||
|
textColor(playerInfo: PlayerView): string;
|
||||||
|
// unit color for alternate view
|
||||||
|
selfColor(): Colord;
|
||||||
|
allyColor(): Colord;
|
||||||
|
neutralColor(): Colord;
|
||||||
|
enemyColor(): Colord;
|
||||||
|
spawnHighlightColor(): Colord;
|
||||||
|
spawnHighlightSelfColor(): Colord;
|
||||||
|
spawnHighlightTeamColor(): Colord;
|
||||||
|
spawnHighlightEnemyColor(): Colord;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { assetUrl } from "../AssetUrls";
|
import { assetUrl } from "../AssetUrls";
|
||||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
|
||||||
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
|
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
|
||||||
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||||
|
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||||
import {
|
import {
|
||||||
AttackClusteredPositionsResultMessage,
|
AttackClusteredPositionsResultMessage,
|
||||||
InitializedMessage,
|
InitializedMessage,
|
||||||
|
|||||||
+8
-10
@@ -1,5 +1,4 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
import {
|
import {
|
||||||
GameID,
|
GameID,
|
||||||
@@ -10,8 +9,7 @@ import {
|
|||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { replacer } from "../core/Util";
|
import { replacer } from "../core/Util";
|
||||||
import { logger } from "./Logger";
|
import { logger } from "./Logger";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
const config = getServerConfigFromServer();
|
|
||||||
|
|
||||||
const log = logger.child({ component: "Archive" });
|
const log = logger.child({ component: "Archive" });
|
||||||
|
|
||||||
@@ -31,13 +29,13 @@ export async function archive(
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = `${config.jwtIssuer()}/game/${gameRecord.info.gameID}`;
|
const url = `${ServerEnv.jwtIssuer()}/game/${gameRecord.info.gameID}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(gameRecord, replacer),
|
body: JSON.stringify(gameRecord, replacer),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-api-key": config.apiKey(),
|
"x-api-key": ServerEnv.apiKey(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -62,12 +60,12 @@ export async function readGameRecord(
|
|||||||
log.error(`invalid game ID: ${gameId}`);
|
log.error(`invalid game ID: ${gameId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const url = `${config.jwtIssuer()}/game/${gameId}`;
|
const url = `${ServerEnv.jwtIssuer()}/game/${gameId}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-api-key": config.apiKey(),
|
"x-api-key": ServerEnv.apiKey(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const record = await response.json();
|
const record = await response.json();
|
||||||
@@ -91,9 +89,9 @@ export function finalizeGameRecord(
|
|||||||
): GameRecord {
|
): GameRecord {
|
||||||
return {
|
return {
|
||||||
...clientRecord,
|
...clientRecord,
|
||||||
gitCommit: config.gitCommit(),
|
gitCommit: ServerEnv.gitCommit(),
|
||||||
subdomain: config.subdomain(),
|
subdomain: ServerEnv.subdomain(),
|
||||||
domain: config.domain(),
|
domain: ServerEnv.domain(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Logger } from "winston";
|
import { Logger } from "winston";
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
import { ServerConfig } from "../core/configuration/Config";
|
|
||||||
import {
|
import {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
GameMapSize,
|
GameMapSize,
|
||||||
@@ -15,10 +14,7 @@ import { GamePhase, GameServer } from "./GameServer";
|
|||||||
export class GameManager {
|
export class GameManager {
|
||||||
private games: Map<GameID, GameServer> = new Map();
|
private games: Map<GameID, GameServer> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(private log: Logger) {
|
||||||
private config: ServerConfig,
|
|
||||||
private log: Logger,
|
|
||||||
) {
|
|
||||||
setInterval(() => this.tick(), 1000);
|
setInterval(() => this.tick(), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +65,6 @@ export class GameManager {
|
|||||||
id,
|
id,
|
||||||
this.log,
|
this.log,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
this.config,
|
|
||||||
{
|
{
|
||||||
donateGold: false,
|
donateGold: false,
|
||||||
donateTroops: false,
|
donateTroops: false,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ClanTagSchema, GameInfo, UsernameSchema } from "../core/Schemas";
|
|||||||
import { formatPlayerDisplayName } from "../core/Util";
|
import { formatPlayerDisplayName } from "../core/Util";
|
||||||
import { GameMode } from "../core/game/Game";
|
import { GameMode } from "../core/game/Game";
|
||||||
import { getRuntimeAssetManifest } from "./RuntimeAssetManifest";
|
import { getRuntimeAssetManifest } from "./RuntimeAssetManifest";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
export const PlayerInfoSchema = z.object({
|
export const PlayerInfoSchema = z.object({
|
||||||
clientID: z.string().optional(),
|
clientID: z.string().optional(),
|
||||||
@@ -141,7 +142,7 @@ export async function buildPreview(
|
|||||||
publicInfo: ExternalGameInfo | null,
|
publicInfo: ExternalGameInfo | null,
|
||||||
): Promise<PreviewMeta> {
|
): Promise<PreviewMeta> {
|
||||||
const assetManifest = await getRuntimeAssetManifest();
|
const assetManifest = await getRuntimeAssetManifest();
|
||||||
const cdnBase = process.env.CDN_BASE ?? "";
|
const cdnBase = ServerEnv.cdnBase();
|
||||||
const buildAbsoluteAssetUrl = (path: string) =>
|
const buildAbsoluteAssetUrl = (path: string) =>
|
||||||
new URL(buildAssetUrl(path, assetManifest, cdnBase), origin).toString();
|
new URL(buildAssetUrl(path, assetManifest, cdnBase), origin).toString();
|
||||||
const isFinished = !!publicInfo?.info?.end;
|
const isFinished = !!publicInfo?.info?.end;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { parse } from "node-html-parser";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import type { Logger } from "winston";
|
import type { Logger } from "winston";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { ServerConfig } from "../core/configuration/Config";
|
|
||||||
import { GAME_ID_REGEX, GameInfo } from "../core/Schemas";
|
import { GAME_ID_REGEX, GameInfo } from "../core/Schemas";
|
||||||
import { replacer } from "../core/Util";
|
import { replacer } from "../core/Util";
|
||||||
import type { GameManager } from "./GameManager";
|
import type { GameManager } from "./GameManager";
|
||||||
@@ -16,17 +15,19 @@ import {
|
|||||||
} from "./GamePreviewBuilder";
|
} from "./GamePreviewBuilder";
|
||||||
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
||||||
import { getAppShellContent, setHtmlNoCacheHeaders } from "./RenderHtml";
|
import { getAppShellContent, setHtmlNoCacheHeaders } from "./RenderHtml";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
const requestOrigin = (req: Request, config: ServerConfig): string => {
|
const requestOrigin = (req: Request): string => {
|
||||||
const protoHeader = (req.headers["x-forwarded-proto"] as string) ?? "";
|
const protoHeader = (req.headers["x-forwarded-proto"] as string) ?? "";
|
||||||
const proto = protoHeader.split(",")[0]?.trim() || req.protocol || "https";
|
const proto = protoHeader.split(",")[0]?.trim() || req.protocol || "https";
|
||||||
const host = req.get("host") ?? `${config.subdomain()}.${config.domain()}`;
|
const host =
|
||||||
|
req.get("host") ?? `${ServerEnv.subdomain()}.${ServerEnv.domain()}`;
|
||||||
|
|
||||||
// Force https only for the configured public domain (and its subdomains).
|
// Force https only for the configured public domain (and its subdomains).
|
||||||
// This avoids hardcoding hostnames while ensuring we don't force https on
|
// This avoids hardcoding hostnames while ensuring we don't force https on
|
||||||
// localhost or arbitrary custom hosts.
|
// localhost or arbitrary custom hosts.
|
||||||
const hostname = host.split(":")[0].toLowerCase();
|
const hostname = host.split(":")[0].toLowerCase();
|
||||||
const domain = config.domain().toLowerCase();
|
const domain = ServerEnv.domain().toLowerCase();
|
||||||
const forceHttps = hostname === domain || hostname.endsWith(`.${domain}`);
|
const forceHttps = hostname === domain || hostname.endsWith(`.${domain}`);
|
||||||
|
|
||||||
return `${forceHttps ? "https" : proto}://${host}`;
|
return `${forceHttps ? "https" : proto}://${host}`;
|
||||||
@@ -35,12 +36,11 @@ const requestOrigin = (req: Request, config: ServerConfig): string => {
|
|||||||
export function registerGamePreviewRoute(opts: {
|
export function registerGamePreviewRoute(opts: {
|
||||||
app: Express;
|
app: Express;
|
||||||
gm: GameManager;
|
gm: GameManager;
|
||||||
config: ServerConfig;
|
|
||||||
workerId: number;
|
workerId: number;
|
||||||
log: Logger;
|
log: Logger;
|
||||||
baseDir: string;
|
baseDir: string;
|
||||||
}) {
|
}) {
|
||||||
const { app, gm, config, log, baseDir } = opts;
|
const { app, gm, log, baseDir } = opts;
|
||||||
|
|
||||||
const gameIDSchema = z.string().regex(GAME_ID_REGEX);
|
const gameIDSchema = z.string().regex(GAME_ID_REGEX);
|
||||||
|
|
||||||
@@ -52,11 +52,11 @@ export function registerGamePreviewRoute(opts: {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 1500);
|
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||||
try {
|
try {
|
||||||
const apiDomain = config.jwtIssuer();
|
const apiDomain = ServerEnv.jwtIssuer();
|
||||||
const encodedID = encodeURIComponent(gameID);
|
const encodedID = encodeURIComponent(gameID);
|
||||||
const response = await fetch(`${apiDomain}/game/${encodedID}`, {
|
const response = await fetch(`${apiDomain}/game/${encodedID}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"x-api-key": config.apiKey(),
|
"x-api-key": ServerEnv.apiKey(),
|
||||||
},
|
},
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
@@ -99,11 +99,11 @@ export function registerGamePreviewRoute(opts: {
|
|||||||
return res.redirect(302, "/");
|
return res.redirect(302, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const origin = requestOrigin(req, config);
|
const origin = requestOrigin(req);
|
||||||
const meta = await buildPreview(
|
const meta = await buildPreview(
|
||||||
gameID,
|
gameID,
|
||||||
origin,
|
origin,
|
||||||
config.workerPath(gameID),
|
ServerEnv.workerPath(gameID),
|
||||||
lobby,
|
lobby,
|
||||||
publicInfo,
|
publicInfo,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Logger } from "winston";
|
|||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { isAdminRole } from "../core/ApiSchemas";
|
import { isAdminRole } from "../core/ApiSchemas";
|
||||||
import { GameEnv, ServerConfig } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
import {
|
import {
|
||||||
ClientID,
|
ClientID,
|
||||||
@@ -28,6 +28,7 @@ import { createPartialGameRecord } from "../core/Util";
|
|||||||
import { archive, finalizeGameRecord } from "./Archive";
|
import { archive, finalizeGameRecord } from "./Archive";
|
||||||
import { Client } from "./Client";
|
import { Client } from "./Client";
|
||||||
import { ClientMsgRateLimiter } from "./ClientMsgRateLimiter";
|
import { ClientMsgRateLimiter } from "./ClientMsgRateLimiter";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
export enum GamePhase {
|
export enum GamePhase {
|
||||||
Lobby = "LOBBY",
|
Lobby = "LOBBY",
|
||||||
Active = "ACTIVE",
|
Active = "ACTIVE",
|
||||||
@@ -96,7 +97,6 @@ export class GameServer {
|
|||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
readonly log_: Logger,
|
readonly log_: Logger,
|
||||||
public readonly createdAt: number,
|
public readonly createdAt: number,
|
||||||
private config: ServerConfig,
|
|
||||||
public gameConfig: GameConfig,
|
public gameConfig: GameConfig,
|
||||||
private creatorPersistentID?: string,
|
private creatorPersistentID?: string,
|
||||||
private startsAt?: number,
|
private startsAt?: number,
|
||||||
@@ -236,7 +236,7 @@ export class GameServer {
|
|||||||
return "rejected";
|
return "rejected";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.env() === GameEnv.Prod) {
|
if (ServerEnv.env() === GameEnv.Prod) {
|
||||||
// Prevent multiple clients from using the same account in prod
|
// Prevent multiple clients from using the same account in prod
|
||||||
const conflicting = this.activeClients.find(
|
const conflicting = this.activeClients.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -751,7 +751,7 @@ export class GameServer {
|
|||||||
|
|
||||||
this.endTurnIntervalID = setInterval(
|
this.endTurnIntervalID = setInterval(
|
||||||
() => this.endTurn(),
|
() => this.endTurn(),
|
||||||
this.config.turnIntervalMs(),
|
ServerEnv.turnIntervalMs(),
|
||||||
);
|
);
|
||||||
this.activeClients.forEach((c) => {
|
this.activeClients.forEach((c) => {
|
||||||
this.log.info("sending start message", {
|
this.log.info("sending start message", {
|
||||||
|
|||||||
@@ -7,22 +7,20 @@ import {
|
|||||||
import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport";
|
import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport";
|
||||||
import * as dotenv from "dotenv";
|
import * as dotenv from "dotenv";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
|
||||||
import { getOtelResource } from "./OtelResource";
|
import { getOtelResource } from "./OtelResource";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const config = getServerConfigFromServer();
|
|
||||||
|
|
||||||
const resource = getOtelResource();
|
const resource = getOtelResource();
|
||||||
|
|
||||||
if (config.otelEnabled()) {
|
if (ServerEnv.otelEnabled()) {
|
||||||
console.log("OTEL enabled");
|
console.log("OTEL enabled");
|
||||||
// Configure OpenTelemetry endpoint with basic auth (if provided)
|
// Configure OpenTelemetry endpoint with basic auth (if provided)
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
headers["Authorization"] = "Basic " + config.otelAuthHeader();
|
headers["Authorization"] = "Basic " + ServerEnv.otelAuthHeader();
|
||||||
// Add OTLP exporter for logs
|
// Add OTLP exporter for logs
|
||||||
const logExporter = new OTLPLogExporter({
|
const logExporter = new OTLPLogExporter({
|
||||||
url: `${config.otelEndpoint()}/v1/logs`,
|
url: `${ServerEnv.otelEndpoint()}/v1/logs`,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ const logger = winston.createLogger({
|
|||||||
),
|
),
|
||||||
defaultMeta: {
|
defaultMeta: {
|
||||||
service: "openfront",
|
service: "openfront",
|
||||||
environment: process.env.GAME_ENV ?? "prod",
|
environment: ServerEnv.gameEnvName(),
|
||||||
},
|
},
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console(),
|
new winston.transports.Console(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SAM_CONSTRUCTION_TICKS } from "../core/configuration/DefaultConfig";
|
import { SAM_CONSTRUCTION_TICKS } from "../core/configuration/Config";
|
||||||
import {
|
import {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
Duos,
|
Duos,
|
||||||
|
|||||||
+5
-12
@@ -6,15 +6,14 @@ import http from "http";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
|
||||||
import { logger } from "./Logger";
|
import { logger } from "./Logger";
|
||||||
import { MapPlaylist } from "./MapPlaylist";
|
import { MapPlaylist } from "./MapPlaylist";
|
||||||
import { MasterLobbyService } from "./MasterLobbyService";
|
import { MasterLobbyService } from "./MasterLobbyService";
|
||||||
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
||||||
import { renderAppShell } from "./RenderHtml";
|
import { renderAppShell } from "./RenderHtml";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
import { applyStaticAssetCacheControl } from "./StaticAssetCache";
|
import { applyStaticAssetCacheControl } from "./StaticAssetCache";
|
||||||
|
|
||||||
const config = getServerConfigFromServer();
|
|
||||||
const playlist = new MapPlaylist();
|
const playlist = new MapPlaylist();
|
||||||
let lobbyService: MasterLobbyService;
|
let lobbyService: MasterLobbyService;
|
||||||
|
|
||||||
@@ -79,16 +78,16 @@ export async function startMaster() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Primary ${process.pid} is running`);
|
log.info(`Primary ${process.pid} is running`);
|
||||||
log.info(`Setting up ${config.numWorkers()} workers...`);
|
log.info(`Setting up ${ServerEnv.numWorkers()} workers...`);
|
||||||
|
|
||||||
lobbyService = new MasterLobbyService(config, playlist, log);
|
lobbyService = new MasterLobbyService(playlist, log);
|
||||||
|
|
||||||
// Generate admin token for worker authentication
|
// Generate admin token for worker authentication
|
||||||
const ADMIN_TOKEN = crypto.randomBytes(16).toString("hex");
|
const ADMIN_TOKEN = crypto.randomBytes(16).toString("hex");
|
||||||
process.env.ADMIN_TOKEN = ADMIN_TOKEN;
|
process.env.ADMIN_TOKEN = ADMIN_TOKEN;
|
||||||
|
|
||||||
const INSTANCE_ID =
|
const INSTANCE_ID =
|
||||||
config.env() === GameEnv.Dev
|
ServerEnv.env() === GameEnv.Dev
|
||||||
? "DEV_ID"
|
? "DEV_ID"
|
||||||
: crypto.randomBytes(4).toString("hex");
|
: crypto.randomBytes(4).toString("hex");
|
||||||
process.env.INSTANCE_ID = INSTANCE_ID;
|
process.env.INSTANCE_ID = INSTANCE_ID;
|
||||||
@@ -96,7 +95,7 @@ export async function startMaster() {
|
|||||||
log.info(`Instance ID: ${INSTANCE_ID}`);
|
log.info(`Instance ID: ${INSTANCE_ID}`);
|
||||||
|
|
||||||
// Fork workers
|
// Fork workers
|
||||||
for (let i = 0; i < config.numWorkers(); i++) {
|
for (let i = 0; i < ServerEnv.numWorkers(); i++) {
|
||||||
const worker = cluster.fork({
|
const worker = cluster.fork({
|
||||||
WORKER_ID: i,
|
WORKER_ID: i,
|
||||||
ADMIN_TOKEN,
|
ADMIN_TOKEN,
|
||||||
@@ -151,12 +150,6 @@ app.get("/api/health", (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/instance", (_req, res) => {
|
|
||||||
res.json({
|
|
||||||
instanceId: process.env.INSTANCE_ID ?? "undefined",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// SPA fallback route
|
// SPA fallback route
|
||||||
app.get("/{*splat}", async function (_req, res) {
|
app.get("/{*splat}", async function (_req, res) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Worker } from "cluster";
|
import { Worker } from "cluster";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
import { ServerConfig } from "../core/configuration/Config";
|
|
||||||
import { PublicGameInfo, PublicGameType } from "../core/Schemas";
|
import { PublicGameInfo, PublicGameType } from "../core/Schemas";
|
||||||
import { generateID } from "../core/Util";
|
import { generateID } from "../core/Util";
|
||||||
import {
|
import {
|
||||||
@@ -12,9 +11,9 @@ import {
|
|||||||
import { logger } from "./Logger";
|
import { logger } from "./Logger";
|
||||||
import { MapPlaylist } from "./MapPlaylist";
|
import { MapPlaylist } from "./MapPlaylist";
|
||||||
import { startPolling } from "./PollingLoop";
|
import { startPolling } from "./PollingLoop";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
export interface MasterLobbyServiceOptions {
|
export interface MasterLobbyServiceOptions {
|
||||||
config: ServerConfig;
|
|
||||||
playlist: MapPlaylist;
|
playlist: MapPlaylist;
|
||||||
log: typeof logger;
|
log: typeof logger;
|
||||||
}
|
}
|
||||||
@@ -27,7 +26,6 @@ export class MasterLobbyService {
|
|||||||
private started = false;
|
private started = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private config: ServerConfig,
|
|
||||||
private playlist: MapPlaylist,
|
private playlist: MapPlaylist,
|
||||||
private log: winston.Logger,
|
private log: winston.Logger,
|
||||||
) {}
|
) {}
|
||||||
@@ -63,16 +61,16 @@ export class MasterLobbyService {
|
|||||||
isHealthy(): boolean {
|
isHealthy(): boolean {
|
||||||
// We consider the lobby service healthy if at least half of the workers are ready.
|
// We consider the lobby service healthy if at least half of the workers are ready.
|
||||||
// This allows for some leeway if a worker crashes.
|
// This allows for some leeway if a worker crashes.
|
||||||
const minWorkers = Math.max(this.config.numWorkers() / 2, 1);
|
const minWorkers = Math.max(ServerEnv.numWorkers() / 2, 1);
|
||||||
return this.started && this.readyWorkers.size >= minWorkers;
|
return this.started && this.readyWorkers.size >= minWorkers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleWorkerReady(workerId: number) {
|
private handleWorkerReady(workerId: number) {
|
||||||
this.readyWorkers.add(workerId);
|
this.readyWorkers.add(workerId);
|
||||||
this.log.info(
|
this.log.info(
|
||||||
`Worker ${workerId} is ready. (${this.readyWorkers.size}/${this.config.numWorkers()} ready)`,
|
`Worker ${workerId} is ready. (${this.readyWorkers.size}/${ServerEnv.numWorkers()} ready)`,
|
||||||
);
|
);
|
||||||
if (this.readyWorkers.size === this.config.numWorkers() && !this.started) {
|
if (this.readyWorkers.size === ServerEnv.numWorkers() && !this.started) {
|
||||||
this.started = true;
|
this.started = true;
|
||||||
this.log.info("All workers ready, starting game scheduling");
|
this.log.info("All workers ready, starting game scheduling");
|
||||||
startPolling(async () => this.broadcastLobbies(), 500);
|
startPolling(async () => this.broadcastLobbies(), 500);
|
||||||
@@ -145,7 +143,7 @@ export class MasterLobbyService {
|
|||||||
this.sendMessageToWorker({
|
this.sendMessageToWorker({
|
||||||
type: "updateLobby",
|
type: "updateLobby",
|
||||||
gameID: nextLobby.gameID,
|
gameID: nextLobby.gameID,
|
||||||
startsAt: Date.now() + this.config.gameCreationRate(),
|
startsAt: Date.now() + ServerEnv.gameCreationRate(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +161,7 @@ export class MasterLobbyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendMessageToWorker(msg: MasterCreateGame | MasterUpdateGame): void {
|
private sendMessageToWorker(msg: MasterCreateGame | MasterUpdateGame): void {
|
||||||
const workerId = this.config.workerIndex(msg.gameID);
|
const workerId = ServerEnv.workerIndex(msg.gameID);
|
||||||
const worker = this.workers.get(workerId);
|
const worker = this.workers.get(workerId);
|
||||||
if (!worker) {
|
if (!worker) {
|
||||||
this.log.error(`Worker ${workerId} not found`);
|
this.log.error(`Worker ${workerId} not found`);
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import {
|
|||||||
ATTR_SERVICE_NAME,
|
ATTR_SERVICE_NAME,
|
||||||
ATTR_SERVICE_VERSION,
|
ATTR_SERVICE_VERSION,
|
||||||
} from "@opentelemetry/semantic-conventions";
|
} from "@opentelemetry/semantic-conventions";
|
||||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
const config = getServerConfigFromServer();
|
|
||||||
|
|
||||||
export function getOtelResource() {
|
export function getOtelResource() {
|
||||||
return resourceFromAttributes({
|
return resourceFromAttributes({
|
||||||
@@ -16,14 +14,14 @@ export function getOtelResource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPromLabels() {
|
export function getPromLabels() {
|
||||||
|
const workerId = ServerEnv.workerId();
|
||||||
return {
|
return {
|
||||||
"service.instance.id": process.env.HOSTNAME,
|
"service.instance.id": ServerEnv.hostname(),
|
||||||
"openfront.environment": config.env(),
|
"openfront.environment": ServerEnv.env(),
|
||||||
"openfront.host": process.env.HOST,
|
"openfront.host": ServerEnv.host(),
|
||||||
"openfront.domain": process.env.DOMAIN,
|
"openfront.domain": ServerEnv.domain(),
|
||||||
"openfront.subdomain": process.env.SUBDOMAIN,
|
"openfront.subdomain": ServerEnv.subdomain(),
|
||||||
"openfront.component": process.env.WORKER_ID
|
"openfront.component":
|
||||||
? "Worker " + process.env.WORKER_ID
|
workerId !== undefined ? "Worker " + workerId : "Master",
|
||||||
: "Master",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fs from "fs/promises";
|
|||||||
import { buildAssetUrl } from "../core/AssetUrls";
|
import { buildAssetUrl } from "../core/AssetUrls";
|
||||||
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
||||||
import { getRuntimeAssetManifest } from "./RuntimeAssetManifest";
|
import { getRuntimeAssetManifest } from "./RuntimeAssetManifest";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
const APP_SHELL_CACHE_CONTROL =
|
const APP_SHELL_CACHE_CONTROL =
|
||||||
"public, max-age=0, s-maxage=300, stale-while-revalidate=86400";
|
"public, max-age=0, s-maxage=300, stale-while-revalidate=86400";
|
||||||
@@ -13,9 +14,9 @@ const appShellContentCache = new Map<string, Promise<string>>();
|
|||||||
export async function renderHtmlContent(htmlPath: string): Promise<string> {
|
export async function renderHtmlContent(htmlPath: string): Promise<string> {
|
||||||
const htmlContent = await fs.readFile(htmlPath, "utf-8");
|
const htmlContent = await fs.readFile(htmlPath, "utf-8");
|
||||||
const assetManifest = await getRuntimeAssetManifest();
|
const assetManifest = await getRuntimeAssetManifest();
|
||||||
const cdnBase = process.env.CDN_BASE ?? "";
|
const cdnBase = ServerEnv.cdnBase();
|
||||||
return ejs.render(htmlContent, {
|
return ejs.render(htmlContent, {
|
||||||
gitCommit: JSON.stringify(process.env.GIT_COMMIT ?? "undefined"),
|
gitCommit: JSON.stringify(ServerEnv.gitCommit()),
|
||||||
assetManifest: JSON.stringify(assetManifest),
|
assetManifest: JSON.stringify(assetManifest),
|
||||||
cdnBase: JSON.stringify(cdnBase),
|
cdnBase: JSON.stringify(cdnBase),
|
||||||
// Raw (unquoted) value for use as a URL prefix in the index.html template,
|
// Raw (unquoted) value for use as a URL prefix in the index.html template,
|
||||||
@@ -23,7 +24,11 @@ export async function renderHtmlContent(htmlPath: string): Promise<string> {
|
|||||||
// build plugin inject-cdn-base-template rewrites Vite's emitted /assets/
|
// build plugin inject-cdn-base-template rewrites Vite's emitted /assets/
|
||||||
// refs to use this placeholder.
|
// refs to use this placeholder.
|
||||||
cdnBaseRaw: cdnBase,
|
cdnBaseRaw: cdnBase,
|
||||||
gameEnv: JSON.stringify(process.env.GAME_ENV ?? "dev"),
|
gameEnv: JSON.stringify(ServerEnv.gameEnvName()),
|
||||||
|
numWorkers: JSON.stringify(ServerEnv.numWorkers()),
|
||||||
|
turnstileSiteKey: JSON.stringify(ServerEnv.turnstileSiteKey()),
|
||||||
|
jwtAudience: JSON.stringify(ServerEnv.jwtAudience()),
|
||||||
|
instanceId: JSON.stringify(ServerEnv.instanceId()),
|
||||||
manifestHref: buildAssetUrl("manifest.json", assetManifest, cdnBase),
|
manifestHref: buildAssetUrl("manifest.json", assetManifest, cdnBase),
|
||||||
faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest, cdnBase),
|
faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest, cdnBase),
|
||||||
gameplayScreenshotUrl: buildAssetUrl(
|
gameplayScreenshotUrl: buildAssetUrl(
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { JWK } from "jose";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { GameEnv, parseGameEnv } from "../core/configuration/Config";
|
||||||
|
import { GameID } from "../core/Schemas";
|
||||||
|
import { simpleHash } from "../core/Util";
|
||||||
|
|
||||||
|
const JwksSchema = z.object({
|
||||||
|
keys: z
|
||||||
|
.object({
|
||||||
|
alg: z.literal("EdDSA"),
|
||||||
|
crv: z.literal("Ed25519"),
|
||||||
|
kty: z.literal("OKP"),
|
||||||
|
x: z.string(),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export class ServerEnv {
|
||||||
|
private static readonly gameEnv: GameEnv = parseGameEnv(process.env.GAME_ENV);
|
||||||
|
private static publicKey: JWK | null = null;
|
||||||
|
|
||||||
|
// Values that also flow to the client via index.html, but on the server
|
||||||
|
// are read from process.env directly. Server code never reaches into
|
||||||
|
// ClientEnv — that's reserved for the browser/worker hydrated path.
|
||||||
|
//
|
||||||
|
// TODO: the following methods are duplicated on ClientEnv. The two classes
|
||||||
|
// read from different sources (process.env vs window.BOOTSTRAP_CONFIG) but
|
||||||
|
// the derived logic is identical. Consolidate into a shared helper that
|
||||||
|
// takes a source so we don't have to keep them in sync by hand.
|
||||||
|
static env(): GameEnv {
|
||||||
|
return ServerEnv.gameEnv;
|
||||||
|
}
|
||||||
|
static gameEnvName(): string {
|
||||||
|
switch (ServerEnv.gameEnv) {
|
||||||
|
case GameEnv.Dev:
|
||||||
|
return "dev";
|
||||||
|
case GameEnv.Preprod:
|
||||||
|
return "staging";
|
||||||
|
case GameEnv.Prod:
|
||||||
|
return "prod";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static numWorkers(): number {
|
||||||
|
const raw = process.env.NUM_WORKERS;
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error("NUM_WORKERS not set");
|
||||||
|
}
|
||||||
|
const n = parseInt(raw, 10);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
|
throw new Error(`Invalid NUM_WORKERS: ${raw}`);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
static turnstileSiteKey(): string {
|
||||||
|
const v = process.env.TURNSTILE_SITE_KEY;
|
||||||
|
if (!v) {
|
||||||
|
throw new Error("TURNSTILE_SITE_KEY not set");
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
static jwtAudience(): string {
|
||||||
|
const v = process.env.DOMAIN;
|
||||||
|
if (!v) {
|
||||||
|
throw new Error("DOMAIN not set");
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
static instanceId(): string {
|
||||||
|
return process.env.INSTANCE_ID ?? "";
|
||||||
|
}
|
||||||
|
static workerId(): number | undefined {
|
||||||
|
const raw = process.env.WORKER_ID;
|
||||||
|
if (raw === undefined) return undefined;
|
||||||
|
return parseInt(raw, 10);
|
||||||
|
}
|
||||||
|
static hostname(): string {
|
||||||
|
return process.env.HOSTNAME ?? "";
|
||||||
|
}
|
||||||
|
static host(): string {
|
||||||
|
return process.env.HOST ?? "";
|
||||||
|
}
|
||||||
|
static cdnBase(): string {
|
||||||
|
return process.env.CDN_BASE ?? "";
|
||||||
|
}
|
||||||
|
static jwtIssuer(): string {
|
||||||
|
const audience = ServerEnv.jwtAudience();
|
||||||
|
return audience === "localhost"
|
||||||
|
? "http://localhost:8787"
|
||||||
|
: `https://api.${audience}`;
|
||||||
|
}
|
||||||
|
static async jwkPublicKey(): Promise<JWK> {
|
||||||
|
if (ServerEnv.publicKey) return ServerEnv.publicKey;
|
||||||
|
const jwksUrl = ServerEnv.jwtIssuer() + "/.well-known/jwks.json";
|
||||||
|
console.log(`Fetching JWKS from ${jwksUrl}`);
|
||||||
|
const response = await fetch(jwksUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
throw new Error(`JWKS fetch failed: ${response.status} ${body}`);
|
||||||
|
}
|
||||||
|
const result = JwksSchema.safeParse(await response.json());
|
||||||
|
if (!result.success) {
|
||||||
|
const error = z.prettifyError(result.error);
|
||||||
|
console.error("Error parsing JWKS", error);
|
||||||
|
throw new Error("Invalid JWKS");
|
||||||
|
}
|
||||||
|
ServerEnv.publicKey = result.data.keys[0];
|
||||||
|
return ServerEnv.publicKey;
|
||||||
|
}
|
||||||
|
static turnIntervalMs(): number {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
static gameCreationRate(): number {
|
||||||
|
return ServerEnv.gameEnv === GameEnv.Dev ? 5 * 1000 : 2 * 60 * 1000;
|
||||||
|
}
|
||||||
|
static workerIndex(gameID: GameID): number {
|
||||||
|
return simpleHash(gameID) % ServerEnv.numWorkers();
|
||||||
|
}
|
||||||
|
static workerPath(gameID: GameID): string {
|
||||||
|
return `w${ServerEnv.workerIndex(gameID)}`;
|
||||||
|
}
|
||||||
|
static workerPort(gameID: GameID): number {
|
||||||
|
return ServerEnv.workerPortByIndex(ServerEnv.workerIndex(gameID));
|
||||||
|
}
|
||||||
|
static workerPortByIndex(index: number): number {
|
||||||
|
return 3001 + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only env values
|
||||||
|
static domain(): string {
|
||||||
|
return process.env.DOMAIN ?? "";
|
||||||
|
}
|
||||||
|
static subdomain(): string {
|
||||||
|
return process.env.SUBDOMAIN ?? "";
|
||||||
|
}
|
||||||
|
static otelEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
ServerEnv.gameEnv !== GameEnv.Dev &&
|
||||||
|
Boolean(ServerEnv.otelEndpoint()) &&
|
||||||
|
Boolean(ServerEnv.otelAuthHeader())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static otelEndpoint(): string {
|
||||||
|
return process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
|
||||||
|
}
|
||||||
|
static otelAuthHeader(): string {
|
||||||
|
return process.env.OTEL_AUTH_HEADER ?? "";
|
||||||
|
}
|
||||||
|
static gitCommit(): string {
|
||||||
|
const v = process.env.GIT_COMMIT;
|
||||||
|
if (!v) {
|
||||||
|
throw new Error("GIT_COMMIT not set");
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
static apiKey(): string {
|
||||||
|
return process.env.API_KEY ?? "";
|
||||||
|
}
|
||||||
|
static adminHeader(): string {
|
||||||
|
return "x-admin-key";
|
||||||
|
}
|
||||||
|
static adminToken(): string {
|
||||||
|
const token = process.env.ADMIN_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("ADMIN_TOKEN not set");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
static allowedFlares(): string[] | undefined {
|
||||||
|
const raw = process.env.ALLOWED_FLARES;
|
||||||
|
if (!raw) return undefined;
|
||||||
|
return raw
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ServerConfig } from "../core/configuration/Config";
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
const TurnstileVerdictSchema = z.discriminatedUnion("status", [
|
const TurnstileVerdictSchema = z.discriminatedUnion("status", [
|
||||||
z.object({ status: z.literal("approved") }),
|
z.object({ status: z.literal("approved") }),
|
||||||
@@ -15,7 +15,6 @@ export type TurnstileResponse =
|
|||||||
export async function verifyTurnstileToken(
|
export async function verifyTurnstileToken(
|
||||||
ip: string,
|
ip: string,
|
||||||
turnstileToken: string | null,
|
turnstileToken: string | null,
|
||||||
config: ServerConfig,
|
|
||||||
): Promise<TurnstileResponse> {
|
): Promise<TurnstileResponse> {
|
||||||
if (!turnstileToken) {
|
if (!turnstileToken) {
|
||||||
return { status: "rejected", reason: "No turnstile token provided" };
|
return { status: "rejected", reason: "No turnstile token provided" };
|
||||||
@@ -25,11 +24,11 @@ export async function verifyTurnstileToken(
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||||
|
|
||||||
const response = await fetch(`${config.jwtIssuer()}/turnstile`, {
|
const response = await fetch(`${ServerEnv.jwtIssuer()}/turnstile`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-api-key": config.apiKey(),
|
"x-api-key": ServerEnv.apiKey(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ ip, token: turnstileToken }),
|
body: JSON.stringify({ ip, token: turnstileToken }),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
|
|||||||
+21
-25
@@ -7,7 +7,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { WebSocket, WebSocketServer } from "ws";
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
import {
|
import {
|
||||||
ClientMessageSchema,
|
ClientMessageSchema,
|
||||||
@@ -24,19 +24,17 @@ import { registerGamePreviewRoute } from "./GamePreviewRoute";
|
|||||||
import { getUserMe, verifyClientToken } from "./jwt";
|
import { getUserMe, verifyClientToken } from "./jwt";
|
||||||
import { logger } from "./Logger";
|
import { logger } from "./Logger";
|
||||||
|
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
|
||||||
import { MapPlaylist } from "./MapPlaylist";
|
import { MapPlaylist } from "./MapPlaylist";
|
||||||
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
import { setNoStoreHeaders } from "./NoStoreHeaders";
|
||||||
import { startPolling } from "./PollingLoop";
|
import { startPolling } from "./PollingLoop";
|
||||||
import { PrivilegeRefresher } from "./PrivilegeRefresher";
|
import { PrivilegeRefresher } from "./PrivilegeRefresher";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
import { applyStaticAssetCacheControl } from "./StaticAssetCache";
|
import { applyStaticAssetCacheControl } from "./StaticAssetCache";
|
||||||
import { verifyTurnstileToken } from "./Turnstile";
|
import { verifyTurnstileToken } from "./Turnstile";
|
||||||
import { WorkerLobbyService } from "./WorkerLobbyService";
|
import { WorkerLobbyService } from "./WorkerLobbyService";
|
||||||
import { initWorkerMetrics } from "./WorkerMetrics";
|
import { initWorkerMetrics } from "./WorkerMetrics";
|
||||||
|
|
||||||
const config = getServerConfigFromServer();
|
const workerId = ServerEnv.workerId() ?? 0;
|
||||||
|
|
||||||
const workerId = parseInt(process.env.WORKER_ID ?? "0");
|
|
||||||
const log = logger.child({ comp: `w_${workerId}` });
|
const log = logger.child({ comp: `w_${workerId}` });
|
||||||
const playlist = new MapPlaylist();
|
const playlist = new MapPlaylist();
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ export async function startWorker() {
|
|||||||
maxPayload: 1024 * 1024, // 1MB
|
maxPayload: 1024 * 1024, // 1MB
|
||||||
});
|
});
|
||||||
|
|
||||||
const gm = new GameManager(config, log);
|
const gm = new GameManager(log);
|
||||||
|
|
||||||
// Initialize lobby service (handles WebSocket upgrade routing)
|
// Initialize lobby service (handles WebSocket upgrade routing)
|
||||||
const lobbyService = new WorkerLobbyService(server, wss, gm, log);
|
const lobbyService = new WorkerLobbyService(server, wss, gm, log);
|
||||||
@@ -67,14 +65,14 @@ export async function startWorker() {
|
|||||||
1000 + Math.random() * 2000,
|
1000 + Math.random() * 2000,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (config.otelEnabled()) {
|
if (ServerEnv.otelEnabled()) {
|
||||||
initWorkerMetrics(gm);
|
initWorkerMetrics(gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
const privilegeRefresher = new PrivilegeRefresher(
|
const privilegeRefresher = new PrivilegeRefresher(
|
||||||
config.jwtIssuer() + "/cosmetics.json",
|
ServerEnv.jwtIssuer() + "/cosmetics.json",
|
||||||
config.jwtIssuer() + "/profane_words_game_server",
|
ServerEnv.jwtIssuer() + "/profane_words_game_server",
|
||||||
config.apiKey(),
|
ServerEnv.apiKey(),
|
||||||
log,
|
log,
|
||||||
);
|
);
|
||||||
privilegeRefresher.start();
|
privilegeRefresher.start();
|
||||||
@@ -150,7 +148,7 @@ export async function startWorker() {
|
|||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (authHeader?.startsWith("Bearer ")) {
|
if (authHeader?.startsWith("Bearer ")) {
|
||||||
const token = authHeader.substring("Bearer ".length);
|
const token = authHeader.substring("Bearer ".length);
|
||||||
const result = await verifyClientToken(token, config);
|
const result = await verifyClientToken(token);
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
creatorPersistentID = result.persistentId;
|
creatorPersistentID = result.persistentId;
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +156,7 @@ export async function startWorker() {
|
|||||||
return res.status(401).json({ error: "Invalid creator token" });
|
return res.status(401).json({ error: "Invalid creator token" });
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
!req.headers[config.adminHeader()] // Public games use admin token instead
|
!req.headers[ServerEnv.adminHeader()] // Public games use admin token instead
|
||||||
) {
|
) {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
@@ -180,7 +178,7 @@ export async function startWorker() {
|
|||||||
const gc = result.data;
|
const gc = result.data;
|
||||||
if (
|
if (
|
||||||
gc?.gameType === GameType.Public &&
|
gc?.gameType === GameType.Public &&
|
||||||
req.headers[config.adminHeader()] !== config.adminToken()
|
req.headers[ServerEnv.adminHeader()] !== ServerEnv.adminToken()
|
||||||
) {
|
) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`cannot create public game ${id}, ip ${ipAnonymize(clientIP)} incorrect admin token`,
|
`cannot create public game ${id}, ip ${ipAnonymize(clientIP)} incorrect admin token`,
|
||||||
@@ -189,7 +187,7 @@ export async function startWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Double-check this worker should host this game
|
// Double-check this worker should host this game
|
||||||
const expectedWorkerId = config.workerIndex(id);
|
const expectedWorkerId = ServerEnv.workerIndex(id);
|
||||||
if (expectedWorkerId !== workerId) {
|
if (expectedWorkerId !== workerId) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`This game ${id} should be on worker ${expectedWorkerId}, but this is worker ${workerId}`,
|
`This game ${id} should be on worker ${expectedWorkerId}, but this is worker ${workerId}`,
|
||||||
@@ -229,7 +227,6 @@ export async function startWorker() {
|
|||||||
registerGamePreviewRoute({
|
registerGamePreviewRoute({
|
||||||
app,
|
app,
|
||||||
gm,
|
gm,
|
||||||
config,
|
|
||||||
workerId,
|
workerId,
|
||||||
log,
|
log,
|
||||||
baseDir: __dirname,
|
baseDir: __dirname,
|
||||||
@@ -316,7 +313,7 @@ export async function startWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify this worker should handle this game
|
// Verify this worker should handle this game
|
||||||
const expectedWorkerId = config.workerIndex(clientMsg.gameID);
|
const expectedWorkerId = ServerEnv.workerIndex(clientMsg.gameID);
|
||||||
if (expectedWorkerId !== workerId) {
|
if (expectedWorkerId !== workerId) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`Worker mismatch: Game ${clientMsg.gameID} should be on worker ${expectedWorkerId}, but this is worker ${workerId}`,
|
`Worker mismatch: Game ${clientMsg.gameID} should be on worker ${expectedWorkerId}, but this is worker ${workerId}`,
|
||||||
@@ -325,7 +322,7 @@ export async function startWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify token signature
|
// Verify token signature
|
||||||
const result = await verifyClientToken(clientMsg.token, config);
|
const result = await verifyClientToken(clientMsg.token);
|
||||||
if (result.type === "error") {
|
if (result.type === "error") {
|
||||||
log.warn(`Invalid token: ${result.message}`, {
|
log.warn(`Invalid token: ${result.message}`, {
|
||||||
gameID: clientMsg.gameID,
|
gameID: clientMsg.gameID,
|
||||||
@@ -381,7 +378,7 @@ export async function startWorker() {
|
|||||||
|
|
||||||
let flares: string[] | undefined;
|
let flares: string[] | undefined;
|
||||||
|
|
||||||
const allowedFlares = config.allowedFlares();
|
const allowedFlares = ServerEnv.allowedFlares();
|
||||||
if (claims === null) {
|
if (claims === null) {
|
||||||
if (allowedFlares !== undefined) {
|
if (allowedFlares !== undefined) {
|
||||||
log.warn("Unauthorized: Anonymous user attempted to join game");
|
log.warn("Unauthorized: Anonymous user attempted to join game");
|
||||||
@@ -390,7 +387,7 @@ export async function startWorker() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Verify token and get player permissions
|
// Verify token and get player permissions
|
||||||
const result = await getUserMe(clientMsg.token, config);
|
const result = await getUserMe(clientMsg.token);
|
||||||
if (result.type === "error") {
|
if (result.type === "error") {
|
||||||
log.warn(`Unauthorized: ${result.message}`, {
|
log.warn(`Unauthorized: ${result.message}`, {
|
||||||
persistentID: persistentId,
|
persistentID: persistentId,
|
||||||
@@ -428,11 +425,10 @@ export async function startWorker() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.env() !== GameEnv.Dev) {
|
if (ServerEnv.env() !== GameEnv.Dev) {
|
||||||
const turnstileResult = await verifyTurnstileToken(
|
const turnstileResult = await verifyTurnstileToken(
|
||||||
ip,
|
ip,
|
||||||
clientMsg.turnstileToken,
|
clientMsg.turnstileToken,
|
||||||
config,
|
|
||||||
);
|
);
|
||||||
switch (turnstileResult.status) {
|
switch (turnstileResult.status) {
|
||||||
case "approved":
|
case "approved":
|
||||||
@@ -511,7 +507,7 @@ export async function startWorker() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The load balancer will handle routing to this server based on path
|
// The load balancer will handle routing to this server based on path
|
||||||
const PORT = config.workerPortByIndex(workerId);
|
const PORT = ServerEnv.workerPortByIndex(workerId);
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
log.info(`running on http://localhost:${PORT}`);
|
log.info(`running on http://localhost:${PORT}`);
|
||||||
log.info(`Handling requests with path prefix /w${workerId}/`);
|
log.info(`Handling requests with path prefix /w${workerId}/`);
|
||||||
@@ -540,7 +536,7 @@ async function startMatchmakingPolling(gm: GameManager) {
|
|||||||
startPolling(
|
startPolling(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const url = `${config.jwtIssuer() + "/matchmaking/checkin"}`;
|
const url = `${ServerEnv.jwtIssuer() + "/matchmaking/checkin"}`;
|
||||||
const gameId = generateGameIdForWorker();
|
const gameId = generateGameIdForWorker();
|
||||||
if (gameId === null) {
|
if (gameId === null) {
|
||||||
log.warn(`Failed to generate game ID for worker ${workerId}`);
|
log.warn(`Failed to generate game ID for worker ${workerId}`);
|
||||||
@@ -553,7 +549,7 @@ async function startMatchmakingPolling(gm: GameManager) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-api-key": config.apiKey(),
|
"x-api-key": ServerEnv.apiKey(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: workerId,
|
id: workerId,
|
||||||
@@ -605,7 +601,7 @@ function generateGameIdForWorker(): GameID | null {
|
|||||||
let attempts = 1000;
|
let attempts = 1000;
|
||||||
while (attempts > 0) {
|
while (attempts > 0) {
|
||||||
const gameId = generateID();
|
const gameId = generateID();
|
||||||
if (workerId === config.workerIndex(gameId)) {
|
if (workerId === ServerEnv.workerIndex(gameId)) {
|
||||||
return gameId;
|
return gameId;
|
||||||
}
|
}
|
||||||
attempts--;
|
attempts--;
|
||||||
|
|||||||
@@ -4,28 +4,25 @@ import {
|
|||||||
PeriodicExportingMetricReader,
|
PeriodicExportingMetricReader,
|
||||||
} from "@opentelemetry/sdk-metrics";
|
} from "@opentelemetry/sdk-metrics";
|
||||||
import * as dotenv from "dotenv";
|
import * as dotenv from "dotenv";
|
||||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
|
||||||
import { GameManager } from "./GameManager";
|
import { GameManager } from "./GameManager";
|
||||||
import { getOtelResource, getPromLabels } from "./OtelResource";
|
import { getOtelResource, getPromLabels } from "./OtelResource";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
export function initWorkerMetrics(gameManager: GameManager): void {
|
export function initWorkerMetrics(gameManager: GameManager): void {
|
||||||
// Get server configuration
|
|
||||||
const config = getServerConfigFromServer();
|
|
||||||
|
|
||||||
// Create resource with worker information
|
// Create resource with worker information
|
||||||
const resource = getOtelResource();
|
const resource = getOtelResource();
|
||||||
|
|
||||||
// Configure auth headers
|
// Configure auth headers
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (config.otelEnabled()) {
|
if (ServerEnv.otelEnabled()) {
|
||||||
headers["Authorization"] = "Basic " + config.otelAuthHeader();
|
headers["Authorization"] = "Basic " + ServerEnv.otelAuthHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create metrics exporter
|
// Create metrics exporter
|
||||||
const metricExporter = new OTLPMetricExporter({
|
const metricExporter = new OTLPMetricExporter({
|
||||||
url: `${config.otelEndpoint()}/v1/metrics`,
|
url: `${ServerEnv.otelEndpoint()}/v1/metrics`,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+8
-9
@@ -6,8 +6,9 @@ import {
|
|||||||
UserMeResponse,
|
UserMeResponse,
|
||||||
UserMeResponseSchema,
|
UserMeResponseSchema,
|
||||||
} from "../core/ApiSchemas";
|
} from "../core/ApiSchemas";
|
||||||
import { GameEnv, ServerConfig } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { PersistentIdSchema } from "../core/Schemas";
|
import { PersistentIdSchema } from "../core/Schemas";
|
||||||
|
import { ServerEnv } from "./ServerEnv";
|
||||||
|
|
||||||
type TokenVerificationResult =
|
type TokenVerificationResult =
|
||||||
| {
|
| {
|
||||||
@@ -19,10 +20,9 @@ type TokenVerificationResult =
|
|||||||
|
|
||||||
export async function verifyClientToken(
|
export async function verifyClientToken(
|
||||||
token: string,
|
token: string,
|
||||||
config: ServerConfig,
|
|
||||||
): Promise<TokenVerificationResult> {
|
): Promise<TokenVerificationResult> {
|
||||||
if (PersistentIdSchema.safeParse(token).success) {
|
if (PersistentIdSchema.safeParse(token).success) {
|
||||||
if (config.env() === GameEnv.Dev) {
|
if (ServerEnv.env() === GameEnv.Dev) {
|
||||||
return { type: "success", persistentId: token, claims: null };
|
return { type: "success", persistentId: token, claims: null };
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@@ -32,9 +32,9 @@ export async function verifyClientToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const issuer = config.jwtIssuer();
|
const issuer = ServerEnv.jwtIssuer();
|
||||||
const audience = config.jwtAudience();
|
const audience = ServerEnv.jwtAudience();
|
||||||
const key = await config.jwkPublicKey();
|
const key = await ServerEnv.jwkPublicKey();
|
||||||
const { payload } = await jwtVerify(token, key, {
|
const { payload } = await jwtVerify(token, key, {
|
||||||
algorithms: ["EdDSA"],
|
algorithms: ["EdDSA"],
|
||||||
issuer,
|
issuer,
|
||||||
@@ -64,17 +64,16 @@ export async function verifyClientToken(
|
|||||||
|
|
||||||
export async function getUserMe(
|
export async function getUserMe(
|
||||||
token: string,
|
token: string,
|
||||||
config: ServerConfig,
|
|
||||||
): Promise<
|
): Promise<
|
||||||
| { type: "success"; response: UserMeResponse }
|
| { type: "success"; response: UserMeResponse }
|
||||||
| { type: "error"; message: string }
|
| { type: "error"; message: string }
|
||||||
> {
|
> {
|
||||||
try {
|
try {
|
||||||
// Get the user object
|
// Get the user object
|
||||||
const response = await fetch(config.jwtIssuer() + "/users/@me", {
|
const response = await fetch(ServerEnv.jwtIssuer() + "/users/@me", {
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${token}`,
|
authorization: `Bearer ${token}`,
|
||||||
"x-api-key": config.apiKey(),
|
"x-api-key": ServerEnv.apiKey(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
apiMockFactory,
|
apiMockFactory,
|
||||||
authMockFactory,
|
authMockFactory,
|
||||||
clanApiMockFactory,
|
clanApiMockFactory,
|
||||||
configLoaderMockFactory,
|
|
||||||
crazyGamesSdkMockFactory,
|
crazyGamesSdkMockFactory,
|
||||||
flushAsync,
|
flushAsync,
|
||||||
getElState,
|
getElState,
|
||||||
@@ -22,9 +21,6 @@ vi.mock("../../../src/client/Api", () => apiMockFactory());
|
|||||||
vi.mock("../../../src/client/ClanApi", () => clanApiMockFactory());
|
vi.mock("../../../src/client/ClanApi", () => clanApiMockFactory());
|
||||||
vi.mock("../../../src/client/Utils", () => utilsMockFactory());
|
vi.mock("../../../src/client/Utils", () => utilsMockFactory());
|
||||||
vi.mock("../../../src/client/Auth", () => authMockFactory());
|
vi.mock("../../../src/client/Auth", () => authMockFactory());
|
||||||
vi.mock("../../../src/core/configuration/ConfigLoader", () =>
|
|
||||||
configLoaderMockFactory(),
|
|
||||||
);
|
|
||||||
vi.mock("../../../src/client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
|
vi.mock("../../../src/client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
|
||||||
|
|
||||||
stubLocalStorage();
|
stubLocalStorage();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
apiMockFactory,
|
apiMockFactory,
|
||||||
authMockFactory,
|
authMockFactory,
|
||||||
clanApiMockFactory,
|
clanApiMockFactory,
|
||||||
configLoaderMockFactory,
|
|
||||||
crazyGamesSdkMockFactory,
|
crazyGamesSdkMockFactory,
|
||||||
getElState,
|
getElState,
|
||||||
makeClan,
|
makeClan,
|
||||||
@@ -20,9 +19,6 @@ vi.mock("../../../src/client/Api", () => apiMockFactory());
|
|||||||
vi.mock("../../../src/client/ClanApi", () => clanApiMockFactory());
|
vi.mock("../../../src/client/ClanApi", () => clanApiMockFactory());
|
||||||
vi.mock("../../../src/client/Utils", () => utilsMockFactory());
|
vi.mock("../../../src/client/Utils", () => utilsMockFactory());
|
||||||
vi.mock("../../../src/client/Auth", () => authMockFactory());
|
vi.mock("../../../src/client/Auth", () => authMockFactory());
|
||||||
vi.mock("../../../src/core/configuration/ConfigLoader", () =>
|
|
||||||
configLoaderMockFactory(),
|
|
||||||
);
|
|
||||||
vi.mock("../../../src/client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
|
vi.mock("../../../src/client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
|
||||||
|
|
||||||
stubLocalStorage();
|
stubLocalStorage();
|
||||||
|
|||||||
@@ -128,12 +128,6 @@ export function authMockFactory() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configLoaderMockFactory() {
|
|
||||||
return {
|
|
||||||
getRuntimeClientServerConfig: vi.fn(() => ({})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function crazyGamesSdkMockFactory() {
|
export function crazyGamesSdkMockFactory() {
|
||||||
return {
|
return {
|
||||||
crazyGamesSDK: { isAvailable: false },
|
crazyGamesSDK: { isAvailable: false },
|
||||||
|
|||||||
@@ -1,44 +1,87 @@
|
|||||||
|
import { ClientEnv } from "src/client/ClientEnv";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { GameEnv } from "../../../src/core/configuration/Config";
|
import { GameEnv, parseGameEnv } from "../../../src/core/configuration/Config";
|
||||||
import {
|
|
||||||
clearCachedRuntimeClientServerConfig,
|
|
||||||
GameLogicEnv,
|
|
||||||
getBuildTimeGameLogicEnv,
|
|
||||||
getGameLogicConfig,
|
|
||||||
getRuntimeClientServerConfig,
|
|
||||||
getServerConfigForGameLogicEnv,
|
|
||||||
} from "../../../src/core/configuration/ConfigLoader";
|
|
||||||
|
|
||||||
describe("ConfigLoader", () => {
|
describe("parseGameEnv", () => {
|
||||||
const originalGameEnv = process.env.GAME_ENV;
|
test("maps 'dev' to GameEnv.Dev", () => {
|
||||||
|
expect(parseGameEnv("dev")).toBe(GameEnv.Dev);
|
||||||
|
});
|
||||||
|
test("maps 'staging' to GameEnv.Preprod", () => {
|
||||||
|
expect(parseGameEnv("staging")).toBe(GameEnv.Preprod);
|
||||||
|
});
|
||||||
|
test("maps 'prod' to GameEnv.Prod", () => {
|
||||||
|
expect(parseGameEnv("prod")).toBe(GameEnv.Prod);
|
||||||
|
});
|
||||||
|
test("throws on undefined", () => {
|
||||||
|
expect(() => parseGameEnv(undefined)).toThrow(/unsupported game env/);
|
||||||
|
});
|
||||||
|
test("throws on unknown value", () => {
|
||||||
|
expect(() => parseGameEnv("production")).toThrow(/unsupported game env/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ClientEnv", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
window.BOOTSTRAP_CONFIG = undefined;
|
window.BOOTSTRAP_CONFIG = undefined;
|
||||||
process.env.GAME_ENV = originalGameEnv;
|
ClientEnv.reset();
|
||||||
clearCachedRuntimeClientServerConfig();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses runtime bootstrap config without fetching /api/env", async () => {
|
test("reads from window.BOOTSTRAP_CONFIG without fetching", () => {
|
||||||
window.BOOTSTRAP_CONFIG = { gameEnv: "staging" };
|
window.BOOTSTRAP_CONFIG = {
|
||||||
|
gameEnv: "staging",
|
||||||
|
numWorkers: 4,
|
||||||
|
turnstileSiteKey: "test-key",
|
||||||
|
jwtAudience: "openfront.dev",
|
||||||
|
instanceId: "TEST_ID",
|
||||||
|
gitCommit: "abc123",
|
||||||
|
};
|
||||||
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
||||||
|
|
||||||
const config = await getRuntimeClientServerConfig();
|
expect(ClientEnv.env()).toBe(GameEnv.Preprod);
|
||||||
|
expect(ClientEnv.numWorkers()).toBe(4);
|
||||||
expect(config.env()).toBe(GameEnv.Preprod);
|
expect(ClientEnv.turnstileSiteKey()).toBe("test-key");
|
||||||
|
expect(ClientEnv.jwtAudience()).toBe("openfront.dev");
|
||||||
|
expect(ClientEnv.instanceId()).toBe("TEST_ID");
|
||||||
expect(fetchSpy).not.toHaveBeenCalled();
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("maps staging builds to the default game logic config", async () => {
|
test("throws when BOOTSTRAP_CONFIG is undefined", () => {
|
||||||
process.env.GAME_ENV = "staging";
|
expect(() => ClientEnv.env()).toThrow(/Missing BOOTSTRAP_CONFIG/);
|
||||||
|
});
|
||||||
|
|
||||||
expect(getBuildTimeGameLogicEnv()).toBe(GameLogicEnv.Default);
|
test("throws when a required field is missing", () => {
|
||||||
expect(getServerConfigForGameLogicEnv(GameLogicEnv.Default).env()).toBe(
|
window.BOOTSTRAP_CONFIG = {
|
||||||
GameEnv.Prod,
|
gameEnv: "dev",
|
||||||
);
|
numWorkers: 1,
|
||||||
|
turnstileSiteKey: "k",
|
||||||
|
jwtAudience: "localhost",
|
||||||
|
// instanceId missing
|
||||||
|
};
|
||||||
|
expect(() => ClientEnv.instanceId()).toThrow(/Missing BOOTSTRAP_CONFIG/);
|
||||||
|
});
|
||||||
|
|
||||||
const config = await getGameLogicConfig({} as any, null);
|
test("jwtIssuer maps 'localhost' to http://localhost:8787", () => {
|
||||||
|
window.BOOTSTRAP_CONFIG = {
|
||||||
|
gameEnv: "dev",
|
||||||
|
numWorkers: 1,
|
||||||
|
turnstileSiteKey: "k",
|
||||||
|
jwtAudience: "localhost",
|
||||||
|
instanceId: "x",
|
||||||
|
gitCommit: "DEV",
|
||||||
|
};
|
||||||
|
expect(ClientEnv.jwtIssuer()).toBe("http://localhost:8787");
|
||||||
|
});
|
||||||
|
|
||||||
expect(config.serverConfig().env()).toBe(GameEnv.Prod);
|
test("jwtIssuer derives api.<audience> for non-localhost", () => {
|
||||||
|
window.BOOTSTRAP_CONFIG = {
|
||||||
|
gameEnv: "prod",
|
||||||
|
numWorkers: 1,
|
||||||
|
turnstileSiteKey: "k",
|
||||||
|
jwtAudience: "openfront.io",
|
||||||
|
instanceId: "x",
|
||||||
|
gitCommit: "abc123",
|
||||||
|
};
|
||||||
|
expect(ClientEnv.jwtIssuer()).toBe("https://api.openfront.io");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { GameUpdateType } from "src/core/game/GameUpdates";
|
import { GameUpdateType } from "src/core/game/GameUpdates";
|
||||||
import { vi, type Mocked } from "vitest";
|
import { vi, type Mocked } from "vitest";
|
||||||
import { DefaultConfig } from "../../../src/core/configuration/DefaultConfig";
|
import { Config } from "../../../src/core/configuration/Config";
|
||||||
import { TrainExecution } from "../../../src/core/execution/TrainExecution";
|
import { TrainExecution } from "../../../src/core/execution/TrainExecution";
|
||||||
import {
|
import {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
|
import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
|
||||||
import { UserSettings } from "../../../src/core/game/UserSettings";
|
import { UserSettings } from "../../../src/core/game/UserSettings";
|
||||||
import { GameConfig } from "../../../src/core/Schemas";
|
import { GameConfig } from "../../../src/core/Schemas";
|
||||||
import { TestServerConfig } from "../../util/TestServerConfig";
|
|
||||||
|
|
||||||
vi.mock("../../../src/core/game/Game");
|
vi.mock("../../../src/core/game/Game");
|
||||||
vi.mock("../../../src/core/execution/TrainExecution");
|
vi.mock("../../../src/core/execution/TrainExecution");
|
||||||
@@ -206,12 +205,11 @@ describe("TrainStation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DefaultConfig.trainGold trade stop penalty", () => {
|
describe("Config.trainGold trade stop penalty", () => {
|
||||||
let config: DefaultConfig;
|
let config: Config;
|
||||||
let mockPlayer: Player;
|
let mockPlayer: Player;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const serverConfig = new TestServerConfig();
|
|
||||||
const gameConfig: GameConfig = {
|
const gameConfig: GameConfig = {
|
||||||
gameMap: GameMapType.Asia,
|
gameMap: GameMapType.Asia,
|
||||||
gameMapSize: GameMapSize.Normal,
|
gameMapSize: GameMapSize.Normal,
|
||||||
@@ -228,12 +226,7 @@ describe("DefaultConfig.trainGold trade stop penalty", () => {
|
|||||||
disableNavMesh: false,
|
disableNavMesh: false,
|
||||||
randomSpawn: false,
|
randomSpawn: false,
|
||||||
};
|
};
|
||||||
config = new DefaultConfig(
|
config = new Config(gameConfig, new UserSettings(), false);
|
||||||
serverConfig,
|
|
||||||
gameConfig,
|
|
||||||
new UserSettings(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
mockPlayer = { isLobbyCreator: () => false } as unknown as Player;
|
mockPlayer = { isLobbyCreator: () => false } as unknown as Player;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { GameMapImpl } from "../../../src/core/game/GameMap";
|
|||||||
import { UserSettings } from "../../../src/core/game/UserSettings";
|
import { UserSettings } from "../../../src/core/game/UserSettings";
|
||||||
import { GameConfig } from "../../../src/core/Schemas";
|
import { GameConfig } from "../../../src/core/Schemas";
|
||||||
import { TestConfig } from "../../util/TestConfig";
|
import { TestConfig } from "../../util/TestConfig";
|
||||||
import { TestServerConfig } from "../../util/TestServerConfig";
|
|
||||||
|
|
||||||
export const W = "W"; // Water
|
export const W = "W"; // Water
|
||||||
export const L = "L"; // Land
|
export const L = "L"; // Land
|
||||||
@@ -131,7 +130,6 @@ export function createGame(data: TestMapData): Game {
|
|||||||
miniNumLand,
|
miniNumLand,
|
||||||
);
|
);
|
||||||
|
|
||||||
const serverConfig = new TestServerConfig();
|
|
||||||
const gameConfig: GameConfig = {
|
const gameConfig: GameConfig = {
|
||||||
gameMap: GameMapType.Asia,
|
gameMap: GameMapType.Asia,
|
||||||
gameMapSize: GameMapSize.Normal,
|
gameMapSize: GameMapSize.Normal,
|
||||||
@@ -148,12 +146,7 @@ export function createGame(data: TestMapData): Game {
|
|||||||
disableNavMesh: false,
|
disableNavMesh: false,
|
||||||
randomSpawn: false,
|
randomSpawn: false,
|
||||||
};
|
};
|
||||||
const config = new TestConfig(
|
const config = new TestConfig(gameConfig, new UserSettings(), false);
|
||||||
serverConfig,
|
|
||||||
gameConfig,
|
|
||||||
new UserSettings(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return createGameImpl([], [], gameMap, miniGameMap, config);
|
return createGameImpl([], [], gameMap, miniGameMap, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,9 +255,7 @@ export async function setupFromPath(
|
|||||||
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
|
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
|
||||||
const miniGameMap = await genTerrainFromBin(manifest.map4x, miniMapBinBuffer);
|
const miniGameMap = await genTerrainFromBin(manifest.map4x, miniMapBinBuffer);
|
||||||
|
|
||||||
// Configure the game
|
|
||||||
const config = new TestConfig(
|
const config = new TestConfig(
|
||||||
new (await import("../util/TestServerConfig")).TestServerConfig(),
|
|
||||||
{
|
{
|
||||||
gameMap: GameMapType.Asia,
|
gameMap: GameMapType.Asia,
|
||||||
gameMapSize: GameMapSize.Normal,
|
gameMapSize: GameMapSize.Normal,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("../../src/core/configuration/ConfigLoader", () => ({
|
vi.mock("../../src/server/ServerEnv", () => ({
|
||||||
getServerConfigFromServer: () => ({
|
ServerEnv: {
|
||||||
jwtIssuer: () => "https://archive.test.invalid",
|
jwtIssuer: () => "https://archive.test.invalid",
|
||||||
apiKey: () => "test-key",
|
apiKey: () => "test-key",
|
||||||
gitCommit: () => "DEV",
|
gitCommit: () => "DEV",
|
||||||
subdomain: () => "test",
|
subdomain: () => "test",
|
||||||
domain: () => "test",
|
domain: () => "test",
|
||||||
}),
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../src/server/Logger", () => ({
|
vi.mock("../../src/server/Logger", () => ({
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("../../src/core/configuration/ConfigLoader", () => ({
|
|
||||||
getServerConfigFromServer: () => ({
|
|
||||||
otelEnabled: () => false,
|
|
||||||
otelAuthHeader: () => "",
|
|
||||||
otelEndpoint: () => "",
|
|
||||||
env: () => 0, // GameEnv.Dev
|
|
||||||
}),
|
|
||||||
getServerConfig: () => ({
|
|
||||||
otelEnabled: () => false,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../../src/core/Schemas", async () => {
|
vi.mock("../../src/core/Schemas", async () => {
|
||||||
const actual = (await vi.importActual("../../src/core/Schemas")) as any;
|
const actual = (await vi.importActual("../../src/core/Schemas")) as any;
|
||||||
return {
|
return {
|
||||||
@@ -25,13 +13,11 @@ vi.mock("../../src/core/Schemas", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { GameEnv } from "../../src/core/configuration/Config";
|
|
||||||
import { GameType } from "../../src/core/game/Game";
|
import { GameType } from "../../src/core/game/Game";
|
||||||
import { GameServer } from "../../src/server/GameServer";
|
import { GameServer } from "../../src/server/GameServer";
|
||||||
|
|
||||||
describe("GameLifecycle", () => {
|
describe("GameLifecycle", () => {
|
||||||
let mockLogger: any;
|
let mockLogger: any;
|
||||||
let mockConfig: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -41,11 +27,6 @@ describe("GameLifecycle", () => {
|
|||||||
warn: vi.fn(),
|
warn: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
};
|
};
|
||||||
mockConfig = {
|
|
||||||
turnIntervalMs: () => 100,
|
|
||||||
gameCreationRate: () => 1000,
|
|
||||||
env: () => GameEnv.Dev,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -54,13 +35,9 @@ describe("GameLifecycle", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not start turn interval if game has ended", async () => {
|
it("should not start turn interval if game has ended", async () => {
|
||||||
const game = new GameServer(
|
const game = new GameServer("test-game", mockLogger, Date.now(), {
|
||||||
"test-game",
|
gameType: GameType.Private,
|
||||||
mockLogger,
|
} as any);
|
||||||
Date.now(),
|
|
||||||
mockConfig,
|
|
||||||
{ gameType: GameType.Private } as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Call end() first - this should set _hasEnded
|
// Call end() first - this should set _hasEnded
|
||||||
await game.end();
|
await game.end();
|
||||||
@@ -77,17 +54,11 @@ describe("GameLifecycle", () => {
|
|||||||
|
|
||||||
it("should clear turn interval and set _hasEnded on end()", async () => {
|
it("should clear turn interval and set _hasEnded on end()", async () => {
|
||||||
// We need to initialize the game such that start() can succeed
|
// We need to initialize the game such that start() can succeed
|
||||||
const game = new GameServer(
|
const game = new GameServer("test-game", mockLogger, Date.now(), {
|
||||||
"test-game",
|
gameType: GameType.Private,
|
||||||
mockLogger,
|
gameMap: "plains",
|
||||||
Date.now(),
|
gameMapSize: 100,
|
||||||
mockConfig,
|
} as any);
|
||||||
{
|
|
||||||
gameType: GameType.Private,
|
|
||||||
gameMap: "plains",
|
|
||||||
gameMapSize: 100,
|
|
||||||
} as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Manually trigger prestart to fulfill some internal checks if necessary
|
// Manually trigger prestart to fulfill some internal checks if necessary
|
||||||
game.prestart();
|
game.prestart();
|
||||||
@@ -103,13 +74,9 @@ describe("GameLifecycle", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should be resilient to multiple end() calls", async () => {
|
it("should be resilient to multiple end() calls", async () => {
|
||||||
const game = new GameServer(
|
const game = new GameServer("test-game", mockLogger, Date.now(), {
|
||||||
"test-game",
|
gameType: GameType.Private,
|
||||||
mockLogger,
|
} as any);
|
||||||
Date.now(),
|
|
||||||
mockConfig,
|
|
||||||
{ gameType: GameType.Private } as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
await game.end();
|
await game.end();
|
||||||
expect((game as any)._hasEnded).toBe(true);
|
expect((game as any)._hasEnded).toBe(true);
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("../../src/core/configuration/ConfigLoader", () => ({
|
|
||||||
getServerConfigFromServer: () => ({
|
|
||||||
otelEnabled: () => false,
|
|
||||||
otelAuthHeader: () => "",
|
|
||||||
otelEndpoint: () => "",
|
|
||||||
env: () => 0, // GameEnv.Dev
|
|
||||||
}),
|
|
||||||
getServerConfig: () => ({
|
|
||||||
otelEnabled: () => false,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../../src/core/Schemas", async () => {
|
vi.mock("../../src/core/Schemas", async () => {
|
||||||
const actual = (await vi.importActual("../../src/core/Schemas")) as any;
|
const actual = (await vi.importActual("../../src/core/Schemas")) as any;
|
||||||
return {
|
return {
|
||||||
@@ -69,7 +57,6 @@ function makeClient(
|
|||||||
|
|
||||||
describe("GameServer - kick_player authorization", () => {
|
describe("GameServer - kick_player authorization", () => {
|
||||||
let mockLogger: any;
|
let mockLogger: any;
|
||||||
let mockConfig: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -79,11 +66,6 @@ describe("GameServer - kick_player authorization", () => {
|
|||||||
warn: vi.fn(),
|
warn: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
};
|
};
|
||||||
mockConfig = {
|
|
||||||
turnIntervalMs: () => 100,
|
|
||||||
gameCreationRate: () => 1000,
|
|
||||||
env: () => 0,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -96,7 +78,6 @@ describe("GameServer - kick_player authorization", () => {
|
|||||||
"test-game",
|
"test-game",
|
||||||
mockLogger,
|
mockLogger,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
mockConfig,
|
|
||||||
{ gameType: GameType.Private } as any,
|
{ gameType: GameType.Private } as any,
|
||||||
creatorPersistentID,
|
creatorPersistentID,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { MasterLobbyService } from "../../src/server/MasterLobbyService";
|
import { MasterLobbyService } from "../../src/server/MasterLobbyService";
|
||||||
import { TestServerConfig } from "../util/TestServerConfig";
|
import { ServerEnv } from "../../src/server/ServerEnv";
|
||||||
|
|
||||||
vi.mock("../../src/server/Logger", () => ({
|
vi.mock("../../src/server/Logger", () => ({
|
||||||
logger: {
|
logger: {
|
||||||
@@ -27,10 +27,9 @@ function sendWorkerReady(worker: EventEmitter, workerId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createService(numWorkers: number): MasterLobbyService {
|
function createService(numWorkers: number): MasterLobbyService {
|
||||||
const config = new TestServerConfig();
|
vi.spyOn(ServerEnv, "numWorkers").mockReturnValue(numWorkers);
|
||||||
vi.spyOn(config, "numWorkers").mockReturnValue(numWorkers);
|
|
||||||
const log = { info: vi.fn(), error: vi.fn() } as any;
|
const log = { info: vi.fn(), error: vi.fn() } as any;
|
||||||
return new MasterLobbyService(config, {} as any, log);
|
return new MasterLobbyService({} as any, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAllWorkers(
|
function startAllWorkers(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { afterEach, describe, expect, test } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
clearAppShellContentCache,
|
clearAppShellContentCache,
|
||||||
getAppShellContent,
|
getAppShellContent,
|
||||||
@@ -12,7 +12,14 @@ describe("RenderHtml", () => {
|
|||||||
const originalGitCommit = process.env.GIT_COMMIT;
|
const originalGitCommit = process.env.GIT_COMMIT;
|
||||||
let tempDir: string | null = null;
|
let tempDir: string | null = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("NUM_WORKERS", "1");
|
||||||
|
vi.stubEnv("TURNSTILE_SITE_KEY", "test-key");
|
||||||
|
vi.stubEnv("DOMAIN", "localhost");
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
process.env.GIT_COMMIT = originalGitCommit;
|
process.env.GIT_COMMIT = originalGitCommit;
|
||||||
clearAppShellContentCache();
|
clearAppShellContentCache();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { ServerEnv } from "../../src/server/ServerEnv";
|
||||||
|
|
||||||
|
describe("ServerEnv.numWorkers", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns parsed value when valid", () => {
|
||||||
|
vi.stubEnv("NUM_WORKERS", "4");
|
||||||
|
expect(ServerEnv.numWorkers()).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when unset", () => {
|
||||||
|
vi.stubEnv("NUM_WORKERS", "");
|
||||||
|
expect(() => ServerEnv.numWorkers()).toThrow(/NUM_WORKERS not set/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on non-numeric", () => {
|
||||||
|
vi.stubEnv("NUM_WORKERS", "abc");
|
||||||
|
expect(() => ServerEnv.numWorkers()).toThrow(/Invalid NUM_WORKERS/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on zero", () => {
|
||||||
|
vi.stubEnv("NUM_WORKERS", "0");
|
||||||
|
expect(() => ServerEnv.numWorkers()).toThrow(/Invalid NUM_WORKERS/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on negative", () => {
|
||||||
|
vi.stubEnv("NUM_WORKERS", "-2");
|
||||||
|
expect(() => ServerEnv.numWorkers()).toThrow(/Invalid NUM_WORKERS/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ServerEnv.turnstileSiteKey", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns value when set", () => {
|
||||||
|
vi.stubEnv("TURNSTILE_SITE_KEY", "site-key");
|
||||||
|
expect(ServerEnv.turnstileSiteKey()).toBe("site-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when unset", () => {
|
||||||
|
vi.stubEnv("TURNSTILE_SITE_KEY", "");
|
||||||
|
expect(() => ServerEnv.turnstileSiteKey()).toThrow(
|
||||||
|
/TURNSTILE_SITE_KEY not set/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ServerEnv.jwtAudience", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns DOMAIN when set", () => {
|
||||||
|
vi.stubEnv("DOMAIN", "openfront.io");
|
||||||
|
expect(ServerEnv.jwtAudience()).toBe("openfront.io");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when DOMAIN unset", () => {
|
||||||
|
vi.stubEnv("DOMAIN", "");
|
||||||
|
expect(() => ServerEnv.jwtAudience()).toThrow(/DOMAIN not set/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ServerEnv.jwtIssuer", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("maps 'localhost' to http://localhost:8787", () => {
|
||||||
|
vi.stubEnv("DOMAIN", "localhost");
|
||||||
|
expect(ServerEnv.jwtIssuer()).toBe("http://localhost:8787");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("derives api.<audience> for non-localhost", () => {
|
||||||
|
vi.stubEnv("DOMAIN", "openfront.io");
|
||||||
|
expect(ServerEnv.jwtIssuer()).toBe("https://api.openfront.io");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ServerEnv.allowedFlares", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when unset", () => {
|
||||||
|
vi.stubEnv("ALLOWED_FLARES", "");
|
||||||
|
expect(ServerEnv.allowedFlares()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses a single value", () => {
|
||||||
|
vi.stubEnv("ALLOWED_FLARES", "admin");
|
||||||
|
expect(ServerEnv.allowedFlares()).toEqual(["admin"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses CSV", () => {
|
||||||
|
vi.stubEnv("ALLOWED_FLARES", "admin,beta,internal");
|
||||||
|
expect(ServerEnv.allowedFlares()).toEqual(["admin", "beta", "internal"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims whitespace and drops empties", () => {
|
||||||
|
vi.stubEnv("ALLOWED_FLARES", " admin , , beta ");
|
||||||
|
expect(ServerEnv.allowedFlares()).toEqual(["admin", "beta"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-9
@@ -18,7 +18,6 @@ import {
|
|||||||
import { UserSettings } from "../../src/core/game/UserSettings";
|
import { UserSettings } from "../../src/core/game/UserSettings";
|
||||||
import { GameConfig } from "../../src/core/Schemas";
|
import { GameConfig } from "../../src/core/Schemas";
|
||||||
import { TestConfig } from "./TestConfig";
|
import { TestConfig } from "./TestConfig";
|
||||||
import { TestServerConfig } from "./TestServerConfig";
|
|
||||||
|
|
||||||
export async function setup(
|
export async function setup(
|
||||||
mapName: string,
|
mapName: string,
|
||||||
@@ -54,8 +53,6 @@ export async function setup(
|
|||||||
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
|
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
|
||||||
const miniGameMap = await genTerrainFromBin(manifest.map4x, miniMapBinBuffer);
|
const miniGameMap = await genTerrainFromBin(manifest.map4x, miniMapBinBuffer);
|
||||||
|
|
||||||
// Configure the game
|
|
||||||
const serverConfig = new TestServerConfig();
|
|
||||||
const gameConfig: GameConfig = {
|
const gameConfig: GameConfig = {
|
||||||
gameMap: GameMapType.Asia,
|
gameMap: GameMapType.Asia,
|
||||||
gameMapSize: GameMapSize.Normal,
|
gameMapSize: GameMapSize.Normal,
|
||||||
@@ -72,12 +69,7 @@ export async function setup(
|
|||||||
randomSpawn: false,
|
randomSpawn: false,
|
||||||
..._gameConfig,
|
..._gameConfig,
|
||||||
};
|
};
|
||||||
const config = new ConfigClass(
|
const config = new ConfigClass(gameConfig, new UserSettings(), false);
|
||||||
serverConfig,
|
|
||||||
gameConfig,
|
|
||||||
new UserSettings(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const game = createGame(humans, [], gameMap, miniGameMap, config);
|
const game = createGame(humans, [], gameMap, miniGameMap, config);
|
||||||
if (autoEndSpawnPhase) game.endSpawnPhase();
|
if (autoEndSpawnPhase) game.endSpawnPhase();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { NukeMagnitude } from "../../src/core/configuration/Config";
|
import { Config, NukeMagnitude } from "../../src/core/configuration/Config";
|
||||||
import { DefaultConfig } from "../../src/core/configuration/DefaultConfig";
|
|
||||||
import {
|
import {
|
||||||
Game,
|
Game,
|
||||||
Player,
|
Player,
|
||||||
@@ -9,7 +8,7 @@ import {
|
|||||||
} from "../../src/core/game/Game";
|
} from "../../src/core/game/Game";
|
||||||
import { TileRef } from "../../src/core/game/GameMap";
|
import { TileRef } from "../../src/core/game/GameMap";
|
||||||
|
|
||||||
export class TestConfig extends DefaultConfig {
|
export class TestConfig extends Config {
|
||||||
private _proximityBonusPortsNb: number = 0;
|
private _proximityBonusPortsNb: number = 0;
|
||||||
private _defaultNukeSpeed: number = 4;
|
private _defaultNukeSpeed: number = 4;
|
||||||
private _spawnImmunityDuration: number = 0;
|
private _spawnImmunityDuration: number = 0;
|
||||||
@@ -100,7 +99,6 @@ export class TestConfig extends DefaultConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class UseRealAttackLogic extends TestConfig {
|
export class UseRealAttackLogic extends TestConfig {
|
||||||
// Override to use DefaultConfig's real attackLogic
|
|
||||||
attackLogic(
|
attackLogic(
|
||||||
gm: Game,
|
gm: Game,
|
||||||
attackTroops: number,
|
attackTroops: number,
|
||||||
@@ -112,7 +110,7 @@ export class UseRealAttackLogic extends TestConfig {
|
|||||||
defenderTroopLoss: number;
|
defenderTroopLoss: number;
|
||||||
tilesPerTickUsed: number;
|
tilesPerTickUsed: number;
|
||||||
} {
|
} {
|
||||||
return DefaultConfig.prototype.attackLogic.call(
|
return Config.prototype.attackLogic.call(
|
||||||
this,
|
this,
|
||||||
gm,
|
gm,
|
||||||
attackTroops,
|
attackTroops,
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { JWK } from "jose";
|
|
||||||
import { GameEnv, ServerConfig } from "../../src/core/configuration/Config";
|
|
||||||
import { PublicGameModifiers } from "../../src/core/game/Game";
|
|
||||||
import { GameID } from "../../src/core/Schemas";
|
|
||||||
|
|
||||||
export class TestServerConfig implements ServerConfig {
|
|
||||||
turnstileSiteKey(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
apiKey(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
allowedFlares(): string[] | undefined {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
stripePublishableKey(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
domain(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
subdomain(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
jwtAudience(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
jwtIssuer(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
jwkPublicKey(): Promise<JWK> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
otelEnabled(): boolean {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
otelEndpoint(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
otelAuthHeader(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
turnIntervalMs(): number {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
gameCreationRate(): number {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
async lobbyMaxPlayers(): Promise<number> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
numWorkers(): number {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
workerIndex(gameID: GameID): number {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
workerPath(gameID: GameID): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
workerPort(gameID: GameID): number {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
workerPortByIndex(workerID: number): number {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
env(): GameEnv {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
adminToken(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
adminHeader(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
gitCommit(): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
getRandomPublicGameModifiers(): PublicGameModifiers {
|
|
||||||
return {
|
|
||||||
isCompact: false,
|
|
||||||
isRandomSpawn: false,
|
|
||||||
isCrowded: false,
|
|
||||||
isHardNations: false,
|
|
||||||
isAlliancesDisabled: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
async supportsCompactMapForTeams(): Promise<boolean> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -65,6 +65,12 @@ export default defineConfig(({ mode }) => {
|
|||||||
assetManifest: JSON.stringify(assetManifest),
|
assetManifest: JSON.stringify(assetManifest),
|
||||||
cdnBase: JSON.stringify(cdnBase),
|
cdnBase: JSON.stringify(cdnBase),
|
||||||
gameEnv: JSON.stringify(env.GAME_ENV ?? "dev"),
|
gameEnv: JSON.stringify(env.GAME_ENV ?? "dev"),
|
||||||
|
numWorkers: JSON.stringify(parseInt(env.NUM_WORKERS ?? "2", 10)),
|
||||||
|
turnstileSiteKey: JSON.stringify(
|
||||||
|
env.TURNSTILE_SITE_KEY ?? "1x00000000000000000000AA",
|
||||||
|
),
|
||||||
|
jwtAudience: JSON.stringify(env.DOMAIN ?? "localhost"),
|
||||||
|
instanceId: JSON.stringify(env.INSTANCE_ID ?? "DEV_ID"),
|
||||||
manifestHref: buildAssetUrl("manifest.json", assetManifest, cdnBase),
|
manifestHref: buildAssetUrl("manifest.json", assetManifest, cdnBase),
|
||||||
faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest, cdnBase),
|
faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest, cdnBase),
|
||||||
gameplayScreenshotUrl: buildAssetUrl(
|
gameplayScreenshotUrl: buildAssetUrl(
|
||||||
|
|||||||
Reference in New Issue
Block a user