From b659bc21d86b9a27d6c2504b4aa21bbce6ab1c3a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 18 Sep 2025 17:24:05 -0400 Subject: [PATCH] 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