Hopefully cleaned up TitlePhase and similar jank without causing conflicts

This commit is contained in:
Bertie690 2025-09-18 17:24:05 -04:00
parent b499a06585
commit b659bc21d8
5 changed files with 116 additions and 92 deletions

View File

@ -1,3 +1,4 @@
// TODO: Document this with default values
export interface UserInfo { export interface UserInfo {
username: string; username: string;
lastSessionSlot: number; lastSessionSlot: number;

View File

@ -15,19 +15,19 @@ import { getBiomeKey } from "#field/arena";
import type { Modifier } from "#modifiers/modifier"; import type { Modifier } from "#modifiers/modifier";
import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#modifiers/modifier-type";
import { vouchers } from "#system/voucher"; import { vouchers } from "#system/voucher";
import type { SessionSaveData } from "#types/save-data";
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler";
import { isLocal, isLocalServerConnected } from "#utils/common"; import { isLocal, isLocalServerConnected } from "#utils/common";
import i18next from "i18next"; import i18next from "i18next";
const NO_SAVE_SLOT = -1;
export class TitlePhase extends Phase { export class TitlePhase extends Phase {
public readonly phaseName = "TitlePhase"; public readonly phaseName = "TitlePhase";
private loaded = false; private loaded = false;
private lastSessionData: SessionSaveData;
public gameMode: GameModes; public gameMode: GameModes;
start(): void { async start(): Promise<void> {
super.start(); super.start();
globalScene.ui.clearText(); globalScene.ui.clearText();
@ -35,30 +35,46 @@ export class TitlePhase extends Phase {
globalScene.playBgm("title", true); globalScene.playBgm("title", true);
globalScene.gameData const lastSlot = await this.checkLastSaveSlot();
.getSession(loggedInUser?.lastSessionSlot ?? -1) await this.showOptions(lastSlot);
.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();
});
} }
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<number> {
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<void> {
const options: OptionSelectItem[] = []; 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({ options.push({
label: i18next.t("continue", { ns: "menu" }), label: i18next.t("continue", { ns: "menu" }),
handler: () => { handler: () => {
this.loadSaveSlot(this.lastSessionData || !loggedInUser ? -1 : loggedInUser.lastSessionSlot); this.loadSaveSlot(lastSessionSlot);
return true; return true;
}, },
}); });
@ -135,8 +151,9 @@ export class TitlePhase extends Phase {
label: i18next.t("menu:loadGame"), label: i18next.t("menu:loadGame"),
handler: () => { handler: () => {
globalScene.ui.setOverlayMode(UiMode.SAVE_SLOT, SaveSlotUiMode.LOAD, (slotId: number) => { globalScene.ui.setOverlayMode(UiMode.SAVE_SLOT, SaveSlotUiMode.LOAD, (slotId: number) => {
if (slotId === -1) { if (slotId === NO_SAVE_SLOT) {
return this.showOptions(); console.warn("Attempted to load save slot of -1 through load game menu!");
return this.showOptions(slotId);
} }
this.loadSaveSlot(slotId); this.loadSaveSlot(slotId);
}); });
@ -165,30 +182,26 @@ export class TitlePhase extends Phase {
noCancel: true, noCancel: true,
yOffset: 47, yOffset: 47,
}; };
globalScene.ui.setMode(UiMode.TITLE, config); await globalScene.ui.setMode(UiMode.TITLE, config);
} }
loadSaveSlot(slotId: number): void { // TODO: Make callers actually wait for the save slot to load
globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; private async loadSaveSlot(slotId: number): Promise<void> {
// TODO: Do we need to `await` this?
globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.resetModeChain(); globalScene.ui.resetModeChain();
globalScene.gameData try {
.loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) const success = await globalScene.gameData.loadSession(slotId);
.then((success: boolean) => { if (success) {
if (success) { this.loaded = true;
this.loaded = true; globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end());
if (loggedInUser) { } else {
loggedInUser.lastSessionSlot = slotId; this.end();
} }
globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); } catch (err) {
} else { console.error(err);
this.end(); globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null);
} }
})
.catch(err => {
console.error(err);
globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null);
});
} }
initDailyRun(): void { initDailyRun(): void {
@ -297,6 +310,7 @@ export class TitlePhase extends Phase {
}); });
} }
// TODO: Refactor this
end(): void { end(): void {
if (!this.loaded && !globalScene.gameMode.isDaily) { if (!this.loaded && !globalScene.gameMode.isDaily) {
globalScene.arena.preloadBgm(); 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)) { for (const achv of Object.keys(globalScene.gameData.achvUnlocks)) {
if (vouchers.hasOwnProperty(achv) && achv !== "CLASSIC_VICTORY") { if (vouchers.hasOwnProperty(achv) && achv !== "CLASSIC_VICTORY") {
globalScene.validateVoucher(vouchers[achv]); globalScene.validateVoucher(vouchers[achv]);

View File

@ -77,6 +77,7 @@ import { applyChallenges } from "#utils/challenge-utils";
import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common"; import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common";
import { decrypt, encrypt } from "#utils/data"; import { decrypt, encrypt } from "#utils/data";
import { getEnumKeys } from "#utils/enums"; import { getEnumKeys } from "#utils/enums";
import { getSaveDataLocalStorageKey } from "#utils/game-data-utils";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import { isBeta } from "#utils/utility-vars"; import { isBeta } from "#utils/utility-vars";
import { AES, enc } from "crypto-js"; import { AES, enc } from "crypto-js";
@ -838,57 +839,45 @@ export class GameData {
} as SessionSaveData; } as SessionSaveData;
} }
async getSession(slotId: number): Promise<SessionSaveData | null> { async getSession(slotId: number): Promise<SessionSaveData | undefined> {
const { promise, resolve, reject } = Promise.withResolvers<SessionSaveData | null>(); // TODO: Do we need this fallback anymore?
if (slotId < 0) { if (slotId < 0) {
resolve(null); return;
return promise;
} }
const handleSessionData = async (sessionDataStr: string) => {
try { // Check local storage for the cached session data
const sessionData = this.parseSessionData(sessionDataStr); if (bypassLogin || localStorage.getItem(getSaveDataLocalStorageKey(slotId))) {
resolve(sessionData); const sessionData = localStorage.getItem(getSaveDataLocalStorageKey(slotId));
} catch (err) { if (!sessionData) {
reject(err); console.error("No session data found!");
return; return;
} }
}; return this.parseSessionData(decrypt(sessionData, bypassLogin));
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;
} }
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
if (sessionData) { // Ask the server API for the save data and store it in localstorage
await handleSessionData(decrypt(sessionData, bypassLogin)); const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
return promise;
// 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<boolean> { async renameSession(slotId: number, newName: string): Promise<boolean> {
if (slotId < 0) { if (slotId < 0) {
return false; return false;
} }
// TODO: Why do we consider renaming to an empty string successful if it does nothing?
if (newName === "") { if (newName === "") {
return true; return true;
} }
const sessionData: SessionSaveData | null = await this.getSession(slotId); const sessionData = await this.getSession(slotId);
if (!sessionData) { if (!sessionData) {
return false; return false;
} }
@ -902,10 +891,7 @@ export class GameData {
const trainerId = this.trainerId; const trainerId = this.trainerId;
if (bypassLogin) { if (bypassLogin) {
localStorage.setItem( localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypt(updatedDataStr, bypassLogin));
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
encrypt(updatedDataStr, bypassLogin),
);
return true; return true;
} }
@ -917,13 +903,20 @@ export class GameData {
if (response) { if (response) {
return false; return false;
} }
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypted);
const success = await updateUserInfo(); const success = await updateUserInfo();
return !(success !== null && !success); return !(success !== null && !success);
} }
async loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> { /**
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<boolean> {
const sessionData = await this.getSession(slotIndex);
if (!sessionData) { if (!sessionData) {
return false; return false;
} }
@ -939,7 +932,7 @@ export class GameData {
this.parseSessionData(JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), this.parseSessionData(JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))),
); );
} catch (err) { } 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<boolean> { deleteSession(slotId: number): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
if (bypassLogin) { if (bypassLogin) {
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(getSaveDataLocalStorageKey(slotId));
return resolve(true); return resolve(true);
} }
@ -1107,7 +1100,7 @@ export class GameData {
loggedInUser.lastSessionSlot = -1; loggedInUser.lastSessionSlot = -1;
} }
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(getSaveDataLocalStorageKey(slotId));
resolve(true); resolve(true);
} }
}); });
@ -1151,7 +1144,7 @@ export class GameData {
let result: [boolean, boolean] = [false, false]; let result: [boolean, boolean] = [false, false];
if (bypassLogin) { if (bypassLogin) {
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(getSaveDataLocalStorageKey(slotId));
result = [true, true]; result = [true, true];
} else { } else {
const sessionData = this.getSessionSaveData(); const sessionData = this.getSessionSaveData();
@ -1166,7 +1159,7 @@ export class GameData {
if (loggedInUser) { if (loggedInUser) {
loggedInUser!.lastSessionSlot = -1; loggedInUser!.lastSessionSlot = -1;
} }
localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(getSaveDataLocalStorageKey(slotId));
} else { } else {
if (jsonResponse?.error?.startsWith("session out of date")) { if (jsonResponse?.error?.startsWith("session out of date")) {
globalScene.phaseManager.clearPhaseQueue(); globalScene.phaseManager.clearPhaseQueue();

View File

@ -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}`;
}

View File

@ -56,7 +56,7 @@ export class ReloadHelper extends GameManagerHelper {
); );
this.game.scene.modifiers = []; 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 this.game.phaseInterceptor.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up
// Run through prompts for switching Pokemon, copied from classicModeHelper.ts // Run through prompts for switching Pokemon, copied from classicModeHelper.ts