From 1dca29d2ebcaf6ddcd24d2fd658da4b55b2ef52d Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 14:33:27 -0400 Subject: [PATCH 1/7] [Refactor] Moved `initSessionFromData` into a private method --- src/system/game-data.ts | 313 +++++++++++++++++++--------------------- 1 file changed, 148 insertions(+), 165 deletions(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8c2a1219245..67df1382d79 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -922,177 +922,160 @@ export class GameData { return !(success !== null && !success); } + // TODO: This is jank async loadSession(slotId: number, sessionData?: SessionSaveData): Promise { - const { promise, resolve, reject } = Promise.withResolvers(); - try { - const initSessionFromData = (fromSession: SessionSaveData) => { - if (isLocal || isBeta) { - try { - console.debug( - this.parseSessionData( - JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v)), - ), - ); - } catch (err) { - console.debug("Attempt to log session data failed:", err); - } - } + sessionData ??= (await this.getSession(slotId)) ?? undefined; + if (!sessionData) { + return false; + } + this.initSessionFromData(sessionData); + return true; + } - globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC); - if (fromSession.challenges) { - globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge()); - } - - globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]); - globalScene.resetSeed(); - - console.log("Seed:", globalScene.seed); - - globalScene.sessionPlayTime = fromSession.playTime || 0; - globalScene.lastSavePlayTime = 0; - - const loadPokemonAssets: Promise[] = []; - - const party = globalScene.getPlayerParty(); - party.splice(0, party.length); - - for (const p of fromSession.party) { - const pokemon = p.toPokemon() as PlayerPokemon; - pokemon.setVisible(false); - loadPokemonAssets.push(pokemon.loadAssets(false)); - party.push(pokemon); - } - - Object.keys(globalScene.pokeballCounts).forEach((key: string) => { - globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0; - }); - if (Overrides.POKEBALL_OVERRIDE.active) { - globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; - } - - globalScene.money = Math.floor(fromSession.money || 0); - globalScene.updateMoneyText(); - - if (globalScene.money > this.gameStats.highestMoney) { - this.gameStats.highestMoney = globalScene.money; - } - - globalScene.score = fromSession.score; - globalScene.updateScoreText(); - - globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData); - - globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints); - - const battleType = fromSession.battleType || 0; - const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null; - const mysteryEncounterType = - fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined; - const battle = globalScene.newBattle( - fromSession.waveIndex, - battleType, - fromSession.trainer, - battleType === BattleType.TRAINER - ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE - : fromSession.enemyParty.length > 1, - mysteryEncounterType, + // TODO: This needs a giant refactor and overhaul + private async initSessionFromData(fromSession: SessionSaveData): Promise { + if (isLocal || isBeta) { + try { + console.debug( + this.parseSessionData(JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), ); - battle.enemyLevels = fromSession.enemyParty.map(p => p.level); - - globalScene.arena.init(); - - fromSession.enemyParty.forEach((enemyData, e) => { - const enemyPokemon = enemyData.toPokemon( - battleType, - e, - fromSession.trainer?.variant === TrainerVariant.DOUBLE, - ) as EnemyPokemon; - battle.enemyParty[e] = enemyPokemon; - if (battleType === BattleType.WILD) { - battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); - } - - loadPokemonAssets.push(enemyPokemon.loadAssets()); - }); - - globalScene.arena.weather = fromSession.arena.weather; - globalScene.arena.eventTarget.dispatchEvent( - new WeatherChangedEvent( - WeatherType.NONE, - globalScene.arena.weather?.weatherType!, - globalScene.arena.weather?.turnsLeft!, - ), - ); // TODO: is this bang correct? - - globalScene.arena.terrain = fromSession.arena.terrain; - globalScene.arena.eventTarget.dispatchEvent( - new TerrainChangedEvent( - TerrainType.NONE, - globalScene.arena.terrain?.terrainType!, - globalScene.arena.terrain?.turnsLeft!, - ), - ); // TODO: is this bang correct? - - globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed; - - globalScene.arena.tags = fromSession.arena.tags; - if (globalScene.arena.tags) { - for (const tag of globalScene.arena.tags) { - if (tag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; - globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), - ); - } else { - globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); - } - } - } - - globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag => - loadPositionalTag(tag), - ); - - if (globalScene.modifiers.length > 0) { - console.warn("Existing modifiers not cleared on session load, deleting..."); - globalScene.modifiers = []; - } - for (const modifierData of fromSession.modifiers) { - const modifier = modifierData.toModifier(Modifier[modifierData.className]); - if (modifier) { - globalScene.addModifier(modifier, true); - } - } - globalScene.updateModifiers(true); - - for (const enemyModifierData of fromSession.enemyModifiers) { - const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); - if (modifier) { - globalScene.addEnemyModifier(modifier, true); - } - } - - globalScene.updateModifiers(false); - - Promise.all(loadPokemonAssets).then(() => resolve(true)); - }; - if (sessionData) { - initSessionFromData(sessionData); - } else { - this.getSession(slotId) - .then(data => { - return data && initSessionFromData(data); - }) - .catch(err => { - reject(err); - return; - }); + } catch (err) { + console.debug("Attempt to log session data failed:", err); } - } catch (err) { - reject(err); } - return promise; + globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC); + if (fromSession.challenges) { + globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge()); + } + + globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]); + globalScene.resetSeed(); + + console.log("Seed:", globalScene.seed); + + globalScene.sessionPlayTime = fromSession.playTime || 0; + globalScene.lastSavePlayTime = 0; + + const loadPokemonAssets: Promise[] = []; + + const party = globalScene.getPlayerParty(); + party.splice(0, party.length); + + for (const p of fromSession.party) { + const pokemon = p.toPokemon() as PlayerPokemon; + pokemon.setVisible(false); + loadPokemonAssets.push(pokemon.loadAssets(false)); + party.push(pokemon); + } + + Object.keys(globalScene.pokeballCounts).forEach((key: string) => { + globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0; + }); + if (Overrides.POKEBALL_OVERRIDE.active) { + globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; + } + + globalScene.money = Math.floor(fromSession.money || 0); + globalScene.updateMoneyText(); + + if (globalScene.money > this.gameStats.highestMoney) { + this.gameStats.highestMoney = globalScene.money; + } + + globalScene.score = fromSession.score; + globalScene.updateScoreText(); + + globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData); + + globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints); + + const battleType = fromSession.battleType || 0; + const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null; + const mysteryEncounterType = fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined; + const battle = globalScene.newBattle( + fromSession.waveIndex, + battleType, + fromSession.trainer, + battleType === BattleType.TRAINER + ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE + : fromSession.enemyParty.length > 1, + mysteryEncounterType, + ); + battle.enemyLevels = fromSession.enemyParty.map(p => p.level); + + globalScene.arena.init(); + + fromSession.enemyParty.forEach((enemyData, e) => { + const enemyPokemon = enemyData.toPokemon( + battleType, + e, + fromSession.trainer?.variant === TrainerVariant.DOUBLE, + ) as EnemyPokemon; + battle.enemyParty[e] = enemyPokemon; + if (battleType === BattleType.WILD) { + battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); + } + + loadPokemonAssets.push(enemyPokemon.loadAssets()); + }); + + globalScene.arena.weather = fromSession.arena.weather; + globalScene.arena.eventTarget.dispatchEvent( + new WeatherChangedEvent( + WeatherType.NONE, + globalScene.arena.weather?.weatherType!, + globalScene.arena.weather?.turnsLeft!, + ), + ); // TODO: is this bang correct? + + globalScene.arena.terrain = fromSession.arena.terrain; + globalScene.arena.eventTarget.dispatchEvent( + new TerrainChangedEvent( + TerrainType.NONE, + globalScene.arena.terrain?.terrainType!, + globalScene.arena.terrain?.turnsLeft!, + ), + ); // TODO: is this bang correct? + + globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed; + + globalScene.arena.tags = fromSession.arena.tags; + if (globalScene.arena.tags) { + for (const tag of globalScene.arena.tags) { + if (tag instanceof EntryHazardTag) { + const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; + globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); + } else { + globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + } + } + } + + globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag => loadPositionalTag(tag)); + + if (globalScene.modifiers.length > 0) { + console.warn("Existing modifiers not cleared on session load, deleting..."); + globalScene.modifiers = []; + } + for (const modifierData of fromSession.modifiers) { + const modifier = modifierData.toModifier(Modifier[modifierData.className]); + if (modifier) { + globalScene.addModifier(modifier, true); + } + } + globalScene.updateModifiers(true); + + for (const enemyModifierData of fromSession.enemyModifiers) { + const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); + if (modifier) { + globalScene.addEnemyModifier(modifier, true); + } + } + + globalScene.updateModifiers(false); + + await Promise.all(loadPokemonAssets); } /** From b499a06585cdeb09b18af3c1177c0d2e1267b84a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 16:28:49 -0400 Subject: [PATCH 2/7] Removed coment --- src/system/game-data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 67df1382d79..078b3cebfa6 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -922,7 +922,6 @@ export class GameData { return !(success !== null && !success); } - // TODO: This is jank async loadSession(slotId: number, sessionData?: SessionSaveData): Promise { sessionData ??= (await this.getSession(slotId)) ?? undefined; if (!sessionData) { From b659bc21d86b9a27d6c2504b4aa21bbce6ab1c3a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 17:24:05 -0400 Subject: [PATCH 3/7] Hopefully cleaned up `TitlePhase` and similar jank without causing conflicts --- src/@types/user-info.ts | 1 + src/phases/title-phase.ts | 101 +++++++++++++---------- src/system/game-data.ts | 89 +++++++++----------- src/utils/game-data-utils.ts | 15 ++++ test/test-utils/helpers/reload-helper.ts | 2 +- 5 files changed, 116 insertions(+), 92 deletions(-) create mode 100644 src/utils/game-data-utils.ts diff --git a/src/@types/user-info.ts b/src/@types/user-info.ts index c8a0c6ecb26..31a329a474d 100644 --- a/src/@types/user-info.ts +++ b/src/@types/user-info.ts @@ -1,3 +1,4 @@ +// TODO: Document this with default values export interface UserInfo { username: string; lastSessionSlot: number; diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 414be4c820c..796ab1fb1f8 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -15,19 +15,19 @@ import { getBiomeKey } from "#field/arena"; import type { Modifier } from "#modifiers/modifier"; import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; import { vouchers } from "#system/voucher"; -import type { SessionSaveData } from "#types/save-data"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import { isLocal, isLocalServerConnected } from "#utils/common"; import i18next from "i18next"; +const NO_SAVE_SLOT = -1; + export class TitlePhase extends Phase { public readonly phaseName = "TitlePhase"; private loaded = false; - private lastSessionData: SessionSaveData; public gameMode: GameModes; - start(): void { + async start(): Promise { super.start(); globalScene.ui.clearText(); @@ -35,30 +35,46 @@ export class TitlePhase extends Phase { globalScene.playBgm("title", true); - globalScene.gameData - .getSession(loggedInUser?.lastSessionSlot ?? -1) - .then(sessionData => { - if (sessionData) { - this.lastSessionData = sessionData; - const biomeKey = getBiomeKey(sessionData.arena.biome); - const bgTexture = `${biomeKey}_bg`; - globalScene.arenaBg.setTexture(bgTexture); - } - this.showOptions(); - }) - .catch(err => { - console.error(err); - this.showOptions(); - }); + const lastSlot = await this.checkLastSaveSlot(); + await this.showOptions(lastSlot); } - showOptions(): void { + /** + * If a user is logged in, check the last save slot they loaded and adjust various variables + * to account for it. + * @returns A Promise that resolves with the last loaded session's slot ID. + * Returns `NO_SAVE_SLOT` if not logged in or no session was found. + */ + private async checkLastSaveSlot(): Promise { + if (loggedInUser == null) { + return NO_SAVE_SLOT; + } + try { + const sessionData = await globalScene.gameData.getSession(loggedInUser.lastSessionSlot); + if (!sessionData) { + return NO_SAVE_SLOT; + } + + globalScene.sessionSlotId = loggedInUser.lastSessionSlot; + // Set the BG texture to the last save's current biome + const biomeKey = getBiomeKey(sessionData.arena.biome); + const bgTexture = `${biomeKey}_bg`; + globalScene.arenaBg.setTexture(bgTexture); + return loggedInUser.lastSessionSlot; + } catch (err) { + console.error(err); + return NO_SAVE_SLOT; + } + } + + private async showOptions(lastSessionSlot: number): Promise { const options: OptionSelectItem[] = []; - if (loggedInUser && loggedInUser.lastSessionSlot > -1) { + // Add a "continue" menu if the session slot ID is >-1 + if (lastSessionSlot > NO_SAVE_SLOT) { options.push({ label: i18next.t("continue", { ns: "menu" }), handler: () => { - this.loadSaveSlot(this.lastSessionData || !loggedInUser ? -1 : loggedInUser.lastSessionSlot); + this.loadSaveSlot(lastSessionSlot); return true; }, }); @@ -135,8 +151,9 @@ export class TitlePhase extends Phase { label: i18next.t("menu:loadGame"), handler: () => { globalScene.ui.setOverlayMode(UiMode.SAVE_SLOT, SaveSlotUiMode.LOAD, (slotId: number) => { - if (slotId === -1) { - return this.showOptions(); + if (slotId === NO_SAVE_SLOT) { + console.warn("Attempted to load save slot of -1 through load game menu!"); + return this.showOptions(slotId); } this.loadSaveSlot(slotId); }); @@ -165,30 +182,26 @@ export class TitlePhase extends Phase { noCancel: true, yOffset: 47, }; - globalScene.ui.setMode(UiMode.TITLE, config); + await globalScene.ui.setMode(UiMode.TITLE, config); } - loadSaveSlot(slotId: number): void { - globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; + // TODO: Make callers actually wait for the save slot to load + private async loadSaveSlot(slotId: number): Promise { + // TODO: Do we need to `await` this? globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); - globalScene.gameData - .loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) - .then((success: boolean) => { - if (success) { - this.loaded = true; - if (loggedInUser) { - loggedInUser.lastSessionSlot = slotId; - } - globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); - } - }) - .catch(err => { - console.error(err); - globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - }); + try { + const success = await globalScene.gameData.loadSession(slotId); + if (success) { + this.loaded = true; + globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); + } else { + this.end(); + } + } catch (err) { + console.error(err); + globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); + } } initDailyRun(): void { @@ -297,6 +310,7 @@ export class TitlePhase extends Phase { }); } + // TODO: Refactor this end(): void { if (!this.loaded && !globalScene.gameMode.isDaily) { globalScene.arena.preloadBgm(); @@ -335,6 +349,7 @@ export class TitlePhase extends Phase { } } + // TODO: Move this to a migrate script instead of running it on save slot load for (const achv of Object.keys(globalScene.gameData.achvUnlocks)) { if (vouchers.hasOwnProperty(achv) && achv !== "CLASSIC_VICTORY") { globalScene.validateVoucher(vouchers[achv]); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 078b3cebfa6..608e3d86972 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -77,6 +77,7 @@ import { applyChallenges } from "#utils/challenge-utils"; import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common"; import { decrypt, encrypt } from "#utils/data"; import { getEnumKeys } from "#utils/enums"; +import { getSaveDataLocalStorageKey } from "#utils/game-data-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { isBeta } from "#utils/utility-vars"; import { AES, enc } from "crypto-js"; @@ -838,57 +839,45 @@ export class GameData { } as SessionSaveData; } - async getSession(slotId: number): Promise { - const { promise, resolve, reject } = Promise.withResolvers(); + async getSession(slotId: number): Promise { + // TODO: Do we need this fallback anymore? if (slotId < 0) { - resolve(null); - return promise; + return; } - const handleSessionData = async (sessionDataStr: string) => { - try { - const sessionData = this.parseSessionData(sessionDataStr); - resolve(sessionData); - } catch (err) { - reject(err); + + // Check local storage for the cached session data + if (bypassLogin || localStorage.getItem(getSaveDataLocalStorageKey(slotId))) { + const sessionData = localStorage.getItem(getSaveDataLocalStorageKey(slotId)); + if (!sessionData) { + console.error("No session data found!"); return; } - }; - - if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { - const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); - - if (!response || response?.length === 0 || response?.[0] !== "{") { - console.error(response); - resolve(null); - return promise; - } - - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(response, bypassLogin), - ); - - await handleSessionData(response); - return promise; + return this.parseSessionData(decrypt(sessionData, bypassLogin)); } - const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); - if (sessionData) { - await handleSessionData(decrypt(sessionData, bypassLogin)); - return promise; + + // Ask the server API for the save data and store it in localstorage + const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); + + // TODO: This is a far cry from proper JSON validation + if (response == null || response.length === 0 || response.charAt(0) !== "{") { + console.error("Invalid save data JSON detected!", response); + return; } - resolve(null); - return promise; + + localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypt(response, bypassLogin)); + + return this.parseSessionData(response); } async renameSession(slotId: number, newName: string): Promise { if (slotId < 0) { return false; } + // TODO: Why do we consider renaming to an empty string successful if it does nothing? if (newName === "") { return true; } - const sessionData: SessionSaveData | null = await this.getSession(slotId); - + const sessionData = await this.getSession(slotId); if (!sessionData) { return false; } @@ -902,10 +891,7 @@ export class GameData { const trainerId = this.trainerId; if (bypassLogin) { - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(updatedDataStr, bypassLogin), - ); + localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypt(updatedDataStr, bypassLogin)); return true; } @@ -917,13 +903,20 @@ export class GameData { if (response) { return false; } - localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); + localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypted); const success = await updateUserInfo(); return !(success !== null && !success); } - async loadSession(slotId: number, sessionData?: SessionSaveData): Promise { - sessionData ??= (await this.getSession(slotId)) ?? undefined; + /** + * Load stored session data and re-initialize the game with its contents. + * @param slotIndex - The 0-indexed position of the save slot to load. + * Values `<=0` will be considered invalid. + * @returns A Promise that resolves with whether the session load succeeded + * (i.e. whether a save in the given slot exists) + */ + public async loadSession(slotIndex: number): Promise { + const sessionData = await this.getSession(slotIndex); if (!sessionData) { return false; } @@ -939,7 +932,7 @@ export class GameData { this.parseSessionData(JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), ); } catch (err) { - console.debug("Attempt to log session data failed:", err); + console.debug("Attempt to log session data failed: ", err); } } @@ -1086,7 +1079,7 @@ export class GameData { deleteSession(slotId: number): Promise { return new Promise(resolve => { if (bypassLogin) { - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); return resolve(true); } @@ -1107,7 +1100,7 @@ export class GameData { loggedInUser.lastSessionSlot = -1; } - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); resolve(true); } }); @@ -1151,7 +1144,7 @@ export class GameData { let result: [boolean, boolean] = [false, false]; if (bypassLogin) { - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); result = [true, true]; } else { const sessionData = this.getSessionSaveData(); @@ -1166,7 +1159,7 @@ export class GameData { if (loggedInUser) { loggedInUser!.lastSessionSlot = -1; } - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); } else { if (jsonResponse?.error?.startsWith("session out of date")) { globalScene.phaseManager.clearPhaseQueue(); diff --git a/src/utils/game-data-utils.ts b/src/utils/game-data-utils.ts new file mode 100644 index 00000000000..69a926f57db --- /dev/null +++ b/src/utils/game-data-utils.ts @@ -0,0 +1,15 @@ +import { loggedInUser } from "#app/account"; + +/** + * Utility function to obtain the local storage key for a given save slot. + * @param slotId - The numerical save slot ID. + * Will throw an error if `<0` (in line with standard util functions) + * @returns The local storage key used to access the save data for the given slot.. + */ +export function getSaveDataLocalStorageKey(slotId: number): string { + if (slotId < 0) { + throw new Error("Cannot access a negative save slot ID from localstorage!"); + } + + return `sessionData${slotId || ""}_${loggedInUser?.username}`; +} diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index e46096f3fab..7f275e97333 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -56,7 +56,7 @@ export class ReloadHelper extends GameManagerHelper { ); this.game.scene.modifiers = []; } - titlePhase.loadSaveSlot(-1); // Load the desired session data + titlePhase["loadSaveSlot"](0); // Load the desired session data this.game.phaseInterceptor.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up // Run through prompts for switching Pokemon, copied from classicModeHelper.ts From 49aaf1bff80f80bc8e9bd961108d34b4f3a8c22d Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 17:26:23 -0400 Subject: [PATCH 4/7] updated TODO --- src/phases/title-phase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 796ab1fb1f8..6d36148cf2c 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -25,6 +25,7 @@ const NO_SAVE_SLOT = -1; export class TitlePhase extends Phase { public readonly phaseName = "TitlePhase"; private loaded = false; + // TODO: Make `end` take a `GameModes` as a parameter rather than storing it on the class itself public gameMode: GameModes; async start(): Promise { From 495c928e5de99dec8844943130588b51e7be42d5 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:44:50 -0400 Subject: [PATCH 5/7] Update game-data-utils.ts Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/utils/game-data-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/game-data-utils.ts b/src/utils/game-data-utils.ts index 69a926f57db..38ff0068a5f 100644 --- a/src/utils/game-data-utils.ts +++ b/src/utils/game-data-utils.ts @@ -4,7 +4,7 @@ import { loggedInUser } from "#app/account"; * Utility function to obtain the local storage key for a given save slot. * @param slotId - The numerical save slot ID. * Will throw an error if `<0` (in line with standard util functions) - * @returns The local storage key used to access the save data for the given slot.. + * @returns The local storage key used to access the save data for the given slot. */ export function getSaveDataLocalStorageKey(slotId: number): string { if (slotId < 0) { From 177e7545129f90791da581382cdb59acc52df5e9 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:44:56 -0400 Subject: [PATCH 6/7] Update game-data-utils.ts Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/utils/game-data-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/game-data-utils.ts b/src/utils/game-data-utils.ts index 38ff0068a5f..15fd1202d09 100644 --- a/src/utils/game-data-utils.ts +++ b/src/utils/game-data-utils.ts @@ -2,7 +2,7 @@ import { loggedInUser } from "#app/account"; /** * Utility function to obtain the local storage key for a given save slot. - * @param slotId - The numerical save slot ID. + * @param slotId - The numerical save slot ID * Will throw an error if `<0` (in line with standard util functions) * @returns The local storage key used to access the save data for the given slot. */ From 36a2e526206031425dad38cfc48aaa943db14bf2 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:45:13 -0400 Subject: [PATCH 7/7] Update game-data-utils.ts Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/utils/game-data-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/game-data-utils.ts b/src/utils/game-data-utils.ts index 15fd1202d09..8027f8f166d 100644 --- a/src/utils/game-data-utils.ts +++ b/src/utils/game-data-utils.ts @@ -1,7 +1,7 @@ import { loggedInUser } from "#app/account"; /** - * Utility function to obtain the local storage key for a given save slot. + * Obtain the local storage key corresponding to a given save slot. * @param slotId - The numerical save slot ID * Will throw an error if `<0` (in line with standard util functions) * @returns The local storage key used to access the save data for the given slot.