From 3b07f78e976b4dac898b2c634de0e2a8b661b21b Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:16:37 -0400 Subject: [PATCH] strictNullChecks --- src/client/ClientGameRunner.ts | 24 +++-- src/client/HostLobbyModal.ts | 4 +- src/client/InputHandler.ts | 6 +- src/client/JoinPrivateLobbyModal.ts | 6 +- src/client/LocalPersistantStats.ts | 4 +- src/client/LocalServer.ts | 17 +++- src/client/Main.ts | 49 ++++++---- src/client/PublicLobby.ts | 13 +-- src/client/Transport.ts | 19 +++- src/client/UsernameInput.ts | 2 +- src/client/graphics/GameRenderer.ts | 4 +- src/client/graphics/TransformHandler.ts | 6 +- src/client/graphics/layers/BuildMenu.ts | 2 +- src/client/graphics/layers/ControlPanel.ts | 2 +- src/client/graphics/layers/EventsDisplay.ts | 16 +++- src/client/graphics/layers/NameLayer.ts | 13 +-- src/client/graphics/layers/OptionsMenu.ts | 7 +- .../graphics/layers/PlayerInfoOverlay.ts | 4 +- src/client/graphics/layers/PlayerPanel.ts | 30 +++--- src/client/graphics/layers/RadialMenu.ts | 14 ++- src/client/graphics/layers/StructureLayer.ts | 21 ++-- src/client/graphics/layers/TerrainLayer.ts | 4 +- src/client/graphics/layers/TerritoryLayer.ts | 17 ++-- src/client/graphics/layers/TopBar.ts | 21 ++-- src/client/graphics/layers/UILayer.ts | 7 +- src/client/graphics/layers/UnitLayer.ts | 40 +++++--- src/client/graphics/layers/WinModal.ts | 13 ++- src/core/GameRunner.ts | 4 +- src/core/Schemas.ts | 2 +- src/core/Util.ts | 15 ++- src/core/configuration/Config.ts | 2 +- src/core/configuration/ConfigLoader.ts | 4 +- src/core/configuration/DefaultConfig.ts | 17 ++-- src/core/configuration/DevConfig.ts | 2 +- src/core/execution/AttackExecution.ts | 12 ++- src/core/execution/DonateGoldExecution.ts | 5 +- src/core/execution/DonateTroopExecution.ts | 5 +- src/core/execution/EmbargoExecution.ts | 4 - src/core/execution/EmojiExecution.ts | 4 - src/core/execution/ExecutionManager.ts | 4 +- src/core/execution/FakeHumanExecution.ts | 95 +++++++++++-------- src/core/execution/NoOpExecution.ts | 5 +- src/core/execution/NukeExecution.ts | 2 +- src/core/execution/PlayerExecution.ts | 2 + src/core/execution/SAMLauncherExecution.ts | 2 +- .../execution/SetTargetTroopRatioExecution.ts | 4 - src/core/execution/ShellExecution.ts | 2 +- src/core/execution/SpawnExecution.ts | 5 +- src/core/execution/TargetPlayerExecution.ts | 4 - src/core/execution/TransportShipExecution.ts | 6 +- src/core/execution/Util.ts | 2 +- src/core/execution/WarshipExecution.ts | 30 ++++-- src/core/execution/WinCheckExecution.ts | 10 +- .../alliance/AllianceRequestExecution.ts | 6 -- .../alliance/AllianceRequestReplyExecution.ts | 6 -- .../alliance/BreakAllianceExecution.ts | 4 - src/core/game/Game.ts | 20 ++-- src/core/game/GameImpl.ts | 26 ++--- src/core/game/GameMap.ts | 1 + src/core/game/GameUpdates.ts | 6 +- src/core/game/GameView.ts | 46 +++++---- src/core/game/PlayerImpl.ts | 29 +++--- src/core/game/Stats.ts | 6 +- src/core/game/TerraNulliusImpl.ts | 4 +- src/core/game/TerrainMapLoader.ts | 5 +- src/core/game/UnitImpl.ts | 50 +++++----- src/core/game/UserSettings.ts | 4 +- src/core/pathfinding/PathFinding.ts | 14 ++- src/core/pathfinding/SerialAStar.ts | 5 +- src/core/worker/Worker.worker.ts | 2 +- src/scripts/TerrainMapGenerator.ts | 55 ++++++----- src/server/Archive.ts | 1 + src/server/GameManager.ts | 2 +- src/server/GameServer.ts | 44 ++++----- src/server/Gatekeeper.ts | 2 +- src/server/MapPlaylist.ts | 2 +- src/server/Master.ts | 22 ++++- src/server/MasterMetrics.ts | 4 +- tests/MissileSilo.test.ts | 2 +- tests/Warship.test.ts | 4 + tests/util/Setup.ts | 18 +++- tsconfig.json | 1 + 82 files changed, 582 insertions(+), 423 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index fad943459..4c668addc 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -61,7 +61,7 @@ export function joinLobby( ); const userSettings: UserSettings = new UserSettings(); - startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config); + startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {}); const transport = new Transport(lobbyConfig, eventBus); @@ -107,6 +107,9 @@ export async function createClientGame( userSettings: UserSettings, terrainLoad: Promise | null, ): Promise { + if (typeof lobbyConfig.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } const config = await getConfig( lobbyConfig.gameStartInfo.config, userSettings, @@ -198,6 +201,9 @@ export class ClientGameRunner { winner = update.winner as Team; } + if (typeof this.lobby.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } const record = createGameRecord( this.lobby.gameStartInfo.gameID, this.lobby.gameStartInfo, @@ -229,16 +235,20 @@ export class ClientGameRunner { this.renderer.initialize(); this.input.initialize(); this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => { + if (typeof this.lobby.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } if ("errMsg" in gu) { showErrorModal( gu.errMsg, - gu.stack, + gu.stack ?? "missing", this.lobby.gameStartInfo.gameID, this.lobby.clientID, ); this.stop(true); return; } + if (gu.updates === null) return; gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash)); }); @@ -282,6 +292,9 @@ export class ClientGameRunner { } } if (message.type == "desync") { + if (typeof this.lobby.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } showErrorModal( `desync from server: ${JSON.stringify(message)}`, "", @@ -344,10 +357,9 @@ export class ClientGameRunner { return; } if (this.myPlayer == null) { - this.myPlayer = this.gameView.playerByClientID(this.lobby.clientID); - if (this.myPlayer == null) { - return; - } + const myPlayer = this.gameView.playerByClientID(this.lobby.clientID); + if (myPlayer === null) return; + this.myPlayer = myPlayer; } this.myPlayer.actions(tile).then((actions) => { console.log(`got actions: ${JSON.stringify(actions)}`); diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 599141cd6..8c8b646a6 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -33,7 +33,7 @@ export class HostLobbyModal extends LitElement { @state() private players: string[] = []; @state() private useRandomMap: boolean = false; - private playersInterval = null; + private playersInterval: NodeJS.Timeout | null = null; // Add a new timer for debouncing bot changes private botsUpdateTimer: number | null = null; @@ -493,7 +493,7 @@ export class HostLobbyModal extends LitElement { .then((response) => response.json()) .then((data: GameInfo) => { console.log(`got game info response: ${JSON.stringify(data)}`); - this.players = data.clients.map((p) => p.username); + this.players = data.clients?.map((p) => p.username) ?? []; }); } } diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index ae495e9fe..9ced3c9a8 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -99,7 +99,7 @@ export class InputHandler { private alternateView = false; - private moveInterval: NodeJS.Timeout = null; + private moveInterval: NodeJS.Timeout | null = null; private activeKeys = new Set(); private readonly PAN_SPEED = 5; @@ -392,7 +392,9 @@ export class InputHandler { } destroy() { - clearInterval(this.moveInterval); + if (this.moveInterval !== null) { + clearInterval(this.moveInterval); + } this.activeKeys.clear(); } } diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index 651e37440..01424e26a 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -19,7 +19,7 @@ export class JoinPrivateLobbyModal extends LitElement { @state() private hasJoined = false; @state() private players: string[] = []; - private playersInterval = null; + private playersInterval: NodeJS.Timeout | null = null; render() { return html` @@ -98,7 +98,7 @@ export class JoinPrivateLobbyModal extends LitElement { } public close() { - this.lobbyIdInput.value = null; + this.lobbyIdInput.value = ""; this.modalEl?.close(); if (this.playersInterval) { clearInterval(this.playersInterval); @@ -263,7 +263,7 @@ export class JoinPrivateLobbyModal extends LitElement { ) .then((response) => response.json()) .then((data: GameInfo) => { - this.players = data.clients.map((p) => p.username); + this.players = data.clients?.map((p) => p.username) ?? []; }) .catch((error) => { consolex.error("Error polling players:", error); diff --git a/src/client/LocalPersistantStats.ts b/src/client/LocalPersistantStats.ts index d659332c1..dc4694bb4 100644 --- a/src/client/LocalPersistantStats.ts +++ b/src/client/LocalPersistantStats.ts @@ -3,7 +3,7 @@ import { GameConfig, GameID, GameRecord } from "../core/Schemas"; export interface LocalStatsData { [key: GameID]: { - lobby: GameConfig; + lobby: Partial; // Only once the game is over gameRecord?: GameRecord; }; @@ -26,7 +26,7 @@ function save(stats: LocalStatsData) { // The user can quit the game anytime so better save the lobby as soon as the // game starts. -export function startGame(id: GameID, lobby: GameConfig) { +export function startGame(id: GameID, lobby: Partial) { if (typeof localStorage === "undefined") { return; } diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 8248a1603..94751d743 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -20,11 +20,11 @@ export class LocalServer { private intents: Intent[] = []; private startedAt: number; - private endTurnIntervalID; + private endTurnIntervalID: NodeJS.Timeout; private paused = false; - private winner: ClientSendWinnerMessage = null; + private winner: ClientSendWinnerMessage | null = null; private allPlayersStats: AllPlayersStats = {}; constructor( @@ -46,6 +46,9 @@ export class LocalServer { this.turns = decompressGameRecord(this.lobbyConfig.gameRecord).turns; console.log(`loaded turns: ${JSON.stringify(this.turns)}`); } + if (typeof this.lobbyConfig.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } this.clientMessage( ServerStartGameMessageSchema.parse({ type: "start", @@ -125,6 +128,9 @@ export class LocalServer { if (this.paused) { return; } + if (typeof this.lobbyConfig.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } const pastTurn: Turn = { turnNumber: this.turns.length, gameID: this.lobbyConfig.gameStartInfo.gameID, @@ -149,6 +155,9 @@ export class LocalServer { clientID: this.lobbyConfig.clientID, }, ]; + if (typeof this.lobbyConfig.gameStartInfo === "undefined") { + throw new Error("missing gameStartInfo"); + } const record = createGameRecord( this.lobbyConfig.gameStartInfo.gameID, this.lobbyConfig.gameStartInfo, @@ -156,8 +165,8 @@ export class LocalServer { this.turns, this.startedAt, Date.now(), - this.winner?.winner, - this.winner?.winnerType, + this.winner?.winner ?? null, + this.winner?.winnerType ?? null, this.allPlayersStats, ); if (!saveFullGame) { diff --git a/src/client/Main.ts b/src/client/Main.ts index 6b4b623c9..d58a374fb 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -40,7 +40,7 @@ export interface JoinLobbyEvent { } class Client { - private gameStop: () => void; + private gameStop: (() => void) | null; private usernameInput: UsernameInput | null = null; private flagInput: FlagInput | null = null; @@ -106,15 +106,19 @@ class Client { "single-player-modal", ) as SinglePlayerModal; spModal instanceof SinglePlayerModal; - document.getElementById("single-player").addEventListener("click", () => { - if (this.usernameInput.isValid()) { + const singlePlayer = document.getElementById("single-player"); + if (singlePlayer === null) throw new Error("Missing single-player"); + singlePlayer.addEventListener("click", () => { + if (this.usernameInput?.isValid()) { spModal.open(); } }); const hlpModal = document.querySelector("help-modal") as HelpModal; hlpModal instanceof HelpModal; - document.getElementById("help-button").addEventListener("click", () => { + const helpButton = document.getElementById("help-button"); + if (helpButton === null) throw new Error("Missing help-button"); + helpButton.addEventListener("click", () => { hlpModal.open(); }); @@ -122,26 +126,29 @@ class Client { "host-lobby-modal", ) as HostPrivateLobbyModal; hostModal instanceof HostPrivateLobbyModal; - document - .getElementById("host-lobby-button") - .addEventListener("click", () => { - if (this.usernameInput.isValid()) { - hostModal.open(); - this.publicLobby.leaveLobby(); - } - }); + const hostLobbyButton = document.getElementById("host-lobby-button"); + if (hostLobbyButton === null) throw new Error("Missing host-lobby-button"); + hostLobbyButton.addEventListener("click", () => { + if (this.usernameInput?.isValid()) { + hostModal.open(); + this.publicLobby.leaveLobby(); + } + }); this.joinModal = document.querySelector( "join-private-lobby-modal", ) as JoinPrivateLobbyModal; this.joinModal instanceof JoinPrivateLobbyModal; - document - .getElementById("join-private-lobby-button") - .addEventListener("click", () => { - if (this.usernameInput.isValid()) { - this.joinModal.open(); - } - }); + const joinPrivateLobbyButton = document.getElementById( + "join-private-lobby-button", + ); + if (joinPrivateLobbyButton === null) + throw new Error("Missing join-private-lobby-button"); + joinPrivateLobbyButton.addEventListener("click", () => { + if (this.usernameInput?.isValid()) { + this.joinModal.open(); + } + }); if (this.userSettings.darkMode()) { document.documentElement.classList.add("dark"); @@ -190,10 +197,10 @@ class Client { gameID: lobby.gameID, serverConfig: config, flag: - this.flagInput.getCurrentFlag() == "xx" + this.flagInput === null || this.flagInput.getCurrentFlag() == "xx" ? "" : this.flagInput.getCurrentFlag(), - playerName: this.usernameInput.getCurrentUsername(), + playerName: this.usernameInput?.getCurrentUsername() ?? "", persistentID: getPersistentIDFromCookie(), clientID: lobby.clientID, gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo, diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 9170ca22c..dea9d690b 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -14,7 +14,7 @@ export class PublicLobby extends LitElement { @state() public isLobbyHighlighted: boolean = false; @state() private isButtonDebounced: boolean = false; private lobbiesInterval: number | null = null; - private currLobby: GameInfo = null; + private currLobby: GameInfo | null = null; private debounceDelay: number = 750; private lobbyIDToStart = new Map(); @@ -46,7 +46,8 @@ export class PublicLobby extends LitElement { // Store the start time on first fetch because endpoint is cached, causing // the time to appear irregular. if (!this.lobbyIDToStart.has(l.gameID)) { - this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now()); + const msUntilStart = l.msUntilStart ?? 0; + this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now()); } }); } catch (error) { @@ -82,17 +83,13 @@ export class PublicLobby extends LitElement { if (!lobby?.gameConfig) { return; } - const timeRemaining = Math.max( - 0, - Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000), - ); + const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0; + const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000)); // Format time to show minutes and seconds const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; - const playersRemainingBeforeMax = - lobby.gameConfig.maxPlayers - lobby.numClients; return html`