mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 18:13:29 +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:
+10
-9
@@ -70,16 +70,14 @@ export function buildAssetUrl(
|
||||
declare global {
|
||||
var __ASSET_MANIFEST__: AssetManifest | undefined;
|
||||
var __CDN_BASE__: string | undefined;
|
||||
|
||||
interface Window {
|
||||
ASSET_MANIFEST?: AssetManifest;
|
||||
CDN_BASE?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAssetManifest(): AssetManifest {
|
||||
if (typeof window !== "undefined" && window.ASSET_MANIFEST !== undefined) {
|
||||
return window.ASSET_MANIFEST;
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.BOOTSTRAP_CONFIG?.assetManifest !== undefined
|
||||
) {
|
||||
return window.BOOTSTRAP_CONFIG.assetManifest;
|
||||
}
|
||||
return globalThis.__ASSET_MANIFEST__ ?? {};
|
||||
}
|
||||
@@ -89,8 +87,11 @@ export function getAssetManifest(): AssetManifest {
|
||||
// Without this fallback, asset fetches inside workers (e.g. map binaries)
|
||||
// would silently bypass the CDN.
|
||||
export function getCdnBase(): string {
|
||||
if (typeof window !== "undefined" && window.CDN_BASE !== undefined) {
|
||||
return window.CDN_BASE;
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.BOOTSTRAP_CONFIG?.cdnBase !== undefined
|
||||
) {
|
||||
return window.BOOTSTRAP_CONFIG.cdnBase;
|
||||
}
|
||||
return globalThis.__CDN_BASE__ ?? "";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { placeName } from "../client/graphics/NameBoxCalculator";
|
||||
import { getGameLogicConfig } from "./configuration/ConfigLoader";
|
||||
import { Config } from "./configuration/Config";
|
||||
import { Executor } from "./execution/ExecutionManager";
|
||||
import { RecomputeRailClusterExecution } from "./execution/RecomputeRailClusterExecution";
|
||||
import { SpawnTimerExecution } from "./execution/SpawnTimerExecution";
|
||||
@@ -37,7 +37,7 @@ export async function createGameRunner(
|
||||
mapLoader: GameMapLoader,
|
||||
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||
): Promise<GameRunner> {
|
||||
const config = await getGameLogicConfig(gameStart.config, null);
|
||||
const config = new Config(gameStart.config, null, false);
|
||||
const gameMap = await loadGameMap(
|
||||
gameStart.config.gameMap,
|
||||
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 { ColorAllocator } from "./ColorAllocator";
|
||||
import { botColors, fallbackColors, humanColors, nationColors } from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
import { Theme } from "./Theme";
|
||||
|
||||
export class PastelTheme implements Theme {
|
||||
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 { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
|
||||
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import {
|
||||
AttackClusteredPositionsResultMessage,
|
||||
InitializedMessage,
|
||||
|
||||
Reference in New Issue
Block a user