fix: Resolve userSettings is null error in worker

This commit fixes the "userSettings is null" error that occurred in the worker when trying to join or create a game.

- Introduced IUserSettings interface to define the contract for user settings used in the worker.

- Updated UserSettings class to implement IUserSettings and provide a getData() method for serialization.

- Modified WorkerMessages to include serialized user settings in the InitMessage.

- Passed user settings from ClientGameRunner to WorkerClient, and then to the worker.

- Updated createGameRunner to accept IUserSettings and pass it to getConfig.

- Corrected type inconsistencies across various configuration and theme classes to align with IUserSettings.

- Re-added missing imports in relevant files.
This commit is contained in:
Restart2008
2025-10-26 17:44:11 -07:00
parent c79e8022b9
commit 215511de5d
14 changed files with 148 additions and 19 deletions
+1
View File
@@ -162,6 +162,7 @@ async function createClientGame(
const worker = new WorkerClient(
lobbyConfig.gameStartInfo,
lobbyConfig.clientID,
userSettings,
);
await worker.initialize();
const gameView = new GameView(
+3 -1
View File
@@ -27,6 +27,7 @@ import {
GameUpdateViewData,
} from "./game/GameUpdates";
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
import { IUserSettings } from "./game/UserSettings";
import { PseudoRandom } from "./PseudoRandom";
import { ClientID, GameStartInfo, Turn } from "./Schemas";
import { sanitize, simpleHash } from "./Util";
@@ -37,8 +38,9 @@ export async function createGameRunner(
clientID: ClientID,
mapLoader: GameMapLoader,
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
userSettings: IUserSettings,
): Promise<GameRunner> {
const config = await getConfig(gameStart.config, null);
const config = await getConfig(gameStart.config, userSettings);
const gameMap = await loadGameMap(
gameStart.config.gameMap,
gameStart.config.gameMapSize,
+2 -2
View File
@@ -3,7 +3,7 @@ import labPlugin from "colord/plugins/lab";
import lchPlugin from "colord/plugins/lch";
import Color from "colorjs.io";
import { ColoredTeams, Team } from "../game/Game";
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import {
@@ -31,7 +31,7 @@ export class ColorAllocator {
constructor(
colors: Colord[],
fallback: Colord[],
private userSettings: UserSettings,
private userSettings: IUserSettings,
) {
this.availableColors = [...colors];
this.fallbackColors = [...colors, ...fallback];
+2 -2
View File
@@ -16,7 +16,7 @@ import {
} from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import { NukeType } from "../StatsSchemas";
@@ -89,7 +89,7 @@ export interface Config {
donateTroops(): boolean;
instantBuild(): boolean;
numSpawnPhaseTurns(): number;
userSettings(): UserSettings;
userSettings(): IUserSettings;
playerTeams(): TeamCountConfig;
startManpower(playerInfo: PlayerInfo): number;
+2 -2
View File
@@ -1,4 +1,4 @@
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { GameConfig } from "../Schemas";
import { Config, GameEnv, ServerConfig } from "./Config";
import { DefaultConfig } from "./DefaultConfig";
@@ -10,7 +10,7 @@ export let cachedSC: ServerConfig | null = null;
export async function getConfig(
gameConfig: GameConfig,
userSettings: UserSettings | null,
userSettings: IUserSettings | null,
isReplay: boolean = false,
): Promise<Config> {
const sc = await getServerConfigFromClient();
+3 -3
View File
@@ -21,7 +21,7 @@ import {
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import { NukeType } from "../StatsSchemas";
import { assertNever, sigmoid, simpleHash, within } from "../Util";
@@ -225,7 +225,7 @@ export class DefaultConfig implements Config {
constructor(
private _serverConfig: ServerConfig,
private _gameConfig: GameConfig,
private _userSettings: UserSettings | null,
private _userSettings: IUserSettings | null,
private _isReplay: boolean,
) {
this.pastelTheme = new PastelTheme(this.userSettings());
@@ -269,7 +269,7 @@ export class DefaultConfig implements Config {
return this._serverConfig;
}
userSettings(): UserSettings {
userSettings(): IUserSettings {
if (this._userSettings === null) {
throw new Error("userSettings is null");
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { UnitInfo, UnitType } from "../game/Game";
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { GameConfig } from "../Schemas";
import { GameEnv, ServerConfig } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
@@ -52,7 +52,7 @@ export class DevConfig extends DefaultConfig {
constructor(
sc: ServerConfig,
gc: GameConfig,
us: UserSettings | null,
us: IUserSettings | null,
isReplay: boolean,
) {
super(sc, gc, us, isReplay);
+2 -2
View File
@@ -3,7 +3,7 @@ import { PseudoRandom } from "../PseudoRandom";
import { PlayerType, Team, TerrainType } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { ColorAllocator } from "./ColorAllocator";
import { botColors, fallbackColors, humanColors, nationColors } from "./Colors";
import { Theme } from "./Config";
@@ -18,7 +18,7 @@ export class PastelTheme implements Theme {
private teamColorAllocator: ColorAllocator;
private nationColorAllocator: ColorAllocator;
constructor(private userSettings: UserSettings) {
constructor(private userSettings: IUserSettings) {
this.humanColorAllocator = new ColorAllocator(
humanColors,
fallbackColors,
+2 -2
View File
@@ -1,7 +1,7 @@
import { Colord, colord } from "colord";
import { TerrainType } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { UserSettings } from "../game/UserSettings";
import { IUserSettings } from "../game/UserSettings";
import { PastelTheme } from "./PastelTheme";
export class PastelThemeDark extends PastelTheme {
@@ -10,7 +10,7 @@ export class PastelThemeDark extends PastelTheme {
private darkWater = colord({ r: 14, g: 11, b: 30 });
private darkShorelineWater = colord({ r: 50, g: 50, b: 50 });
constructor(userSettings: UserSettings) {
constructor(userSettings: IUserSettings) {
super(userSettings);
}
+54 -1
View File
@@ -3,7 +3,41 @@ import { PlayerPattern } from "../Schemas";
const PATTERN_KEY = "territoryPattern";
export class UserSettings {
export interface UserSettingsData {
emojis: boolean;
performanceOverlay: boolean;
alertFrame: boolean;
anonymousNames: boolean;
lobbyIdVisibility: boolean;
fxLayer: boolean;
structureSprites: boolean;
darkMode: boolean;
leftClickOpensMenu: boolean;
territoryPatterns: boolean;
focusLocked: boolean;
colorblindMode: boolean;
backgroundMusicVolume: number;
soundEffectsVolume: number;
}
export interface IUserSettings {
emojis(): boolean;
performanceOverlay(): boolean;
alertFrame(): boolean;
anonymousNames(): boolean;
lobbyIdVisibility(): boolean;
fxLayer(): boolean;
structureSprites(): boolean;
darkMode(): boolean;
leftClickOpensMenu(): boolean;
territoryPatterns(): boolean;
focusLocked(): boolean;
colorblindMode(): boolean;
backgroundMusicVolume(): number;
soundEffectsVolume(): number;
}
export class UserSettings implements IUserSettings {
get(key: string, defaultValue: boolean): boolean {
const value = localStorage.getItem(key);
if (!value) return defaultValue;
@@ -33,6 +67,25 @@ export class UserSettings {
localStorage.setItem(key, value.toString());
}
getData(): UserSettingsData {
return {
emojis: this.emojis(),
performanceOverlay: this.performanceOverlay(),
alertFrame: this.alertFrame(),
anonymousNames: this.anonymousNames(),
lobbyIdVisibility: this.lobbyIdVisibility(),
fxLayer: this.fxLayer(),
structureSprites: this.structureSprites(),
darkMode: this.darkMode(),
leftClickOpensMenu: this.leftClickOpensMenu(),
territoryPatterns: this.territoryPatterns(),
focusLocked: this.focusLocked(),
colorblindMode: this.colorblindMode(),
backgroundMusicVolume: this.backgroundMusicVolume(),
soundEffectsVolume: this.soundEffectsVolume(),
};
}
emojis() {
return this.get("settings.emojis", true);
}
+50
View File
@@ -2,6 +2,7 @@ import version from "../../../resources/version.txt";
import { createGameRunner, GameRunner } from "../GameRunner";
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
import { IUserSettings, UserSettingsData } from "../game/UserSettings";
import {
AttackAveragePositionResultMessage,
InitializedMessage,
@@ -17,6 +18,53 @@ const ctx: Worker = self as any;
let gameRunner: Promise<GameRunner> | null = null;
const mapLoader = new FetchGameMapLoader(`/maps`, version);
class MockUserSettings implements IUserSettings {
constructor(private data: UserSettingsData) {}
emojis(): boolean {
return this.data.emojis;
}
performanceOverlay(): boolean {
return this.data.performanceOverlay;
}
alertFrame(): boolean {
return this.data.alertFrame;
}
anonymousNames(): boolean {
return this.data.anonymousNames;
}
lobbyIdVisibility(): boolean {
return this.data.lobbyIdVisibility;
}
fxLayer(): boolean {
return this.data.fxLayer;
}
structureSprites(): boolean {
return this.data.structureSprites;
}
darkMode(): boolean {
return this.data.darkMode;
}
leftClickOpensMenu(): boolean {
return this.data.leftClickOpensMenu;
}
territoryPatterns(): boolean {
return this.data.territoryPatterns;
}
focusLocked(): boolean {
return this.data.focusLocked;
}
colorblindMode(): boolean {
return this.data.colorblindMode;
}
backgroundMusicVolume(): number {
return this.data.backgroundMusicVolume;
}
soundEffectsVolume(): number {
return this.data.soundEffectsVolume;
}
}
function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) {
// skip if ErrorUpdate
if (!("updates" in gu)) {
@@ -41,11 +89,13 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
break;
case "init":
try {
const userSettings = new MockUserSettings(message.userSettings);
gameRunner = createGameRunner(
message.gameStartInfo,
message.clientID,
mapLoader,
gameUpdate,
userSettings,
).then((gr) => {
sendMessage({
type: "initialized",
+3
View File
@@ -7,6 +7,7 @@ import {
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
import { UserSettings } from "../game/UserSettings";
import { ClientID, GameStartInfo, Turn } from "../Schemas";
import { generateID } from "../Util";
import { WorkerMessage } from "./WorkerMessages";
@@ -22,6 +23,7 @@ export class WorkerClient {
constructor(
private gameStartInfo: GameStartInfo,
private clientID: ClientID,
private userSettings: UserSettings,
) {
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
this.messageHandlers = new Map();
@@ -70,6 +72,7 @@ export class WorkerClient {
id: messageId,
gameStartInfo: this.gameStartInfo,
clientID: this.clientID,
userSettings: this.userSettings.getData(),
});
// Add timeout for initialization
+2
View File
@@ -6,6 +6,7 @@ import {
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { GameUpdateViewData } from "../game/GameUpdates";
import { UserSettingsData } from "../game/UserSettings";
import { ClientID, GameStartInfo, Turn } from "../Schemas";
export type WorkerMessageType =
@@ -40,6 +41,7 @@ export interface InitMessage extends BaseWorkerMessage {
type: "init";
gameStartInfo: GameStartInfo;
clientID: ClientID;
userSettings: UserSettingsData;
}
export interface TurnMessage extends BaseWorkerMessage {
+20 -2
View File
@@ -14,7 +14,7 @@ import {
yellow,
} from "../src/core/configuration/Colors";
import { ColoredTeams } from "../src/core/game/Game";
import { UserSettings } from "../src/core/game/UserSettings";
import { IUserSettings } from "../src/core/game/UserSettings";
const mockColors: Colord[] = [
colord({ r: 255, g: 0, b: 0 }),
@@ -31,7 +31,7 @@ const fallbackColors = [...fallbackMockColors, ...mockColors];
const mockUserSettings = {
colorblindMode: () => false,
} as UserSettings;
} as IUserSettings;
describe("ColorAllocator", () => {
let allocator: ColorAllocator;
@@ -157,6 +157,24 @@ describe("ColorAllocator", () => {
expect(redColorPlayerOne.isEqual(redColorPlayerTwo)).toBe(false);
});
test("assignTeamColor returns colorblind-friendly colors when colorblind mode is enabled", () => {
const mockUserSettingsColorblind = {
colorblindMode: () => true,
} as IUserSettings;
const allocator = new ColorAllocator(
mockColors,
fallbackMockColors,
mockUserSettingsColorblind,
);
const redColor = allocator.assignTeamColor(ColoredTeams.Red);
const greenColor = allocator.assignTeamColor(ColoredTeams.Green);
expect(redColor.toHex()).toBe(colord({ h: 30, s: 100, l: 50 }).toHex()); // Orange
expect(greenColor.toHex()).toBe(colord({ h: 210, s: 100, l: 50 }).toHex()); // Blue
});
});
describe("selectDistinctColor", () => {