From 43f8b78c35e7613f519c4d34a1c62fc8f572647d Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:17:12 -0500 Subject: [PATCH] [Bug] Fix sessions clearing the wrong slot on save (#6509) * Store session id in session data to prevent somehow deleting wrong slot * Only log session / system if beta or local; fix promise * Fix serialization/deserialization when logging session/system data * Force loadSaveSlot to set the logged in user's last session * No longer add slotId to session data --- src/phases/game-over-phase.ts | 2 +- src/phases/post-game-over-phase.ts | 23 +- src/phases/title-phase.ts | 3 + .../api/pokerogue-session-savedata-api.ts | 1 + src/system/game-data.ts | 407 +++++++++--------- 5 files changed, 230 insertions(+), 206 deletions(-) diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 89162e591fc..c4548a54d2f 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase { } this.getRunHistoryEntry().then(runHistoryEntry => { globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory); - globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase); + globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase); this.end(); }); }; diff --git a/src/phases/post-game-over-phase.ts b/src/phases/post-game-over-phase.ts index 3ac112a8a8b..a69ec2d468b 100644 --- a/src/phases/post-game-over-phase.ts +++ b/src/phases/post-game-over-phase.ts @@ -5,10 +5,11 @@ import type { EndCardPhase } from "#phases/end-card-phase"; export class PostGameOverPhase extends Phase { public readonly phaseName = "PostGameOverPhase"; private endCardPhase?: EndCardPhase; + private slotId: number; - constructor(endCardPhase?: EndCardPhase) { + constructor(slotId: number, endCardPhase?: EndCardPhase) { super(); - + this.slotId = slotId; this.endCardPhase = endCardPhase; } @@ -20,16 +21,14 @@ export class PostGameOverPhase extends Phase { if (!success) { return globalScene.reset(true); } - globalScene.gameData - .tryClearSession(globalScene.sessionSlotId) - .then((success: boolean | [boolean, boolean]) => { - if (!success[0]) { - return globalScene.reset(true); - } - globalScene.reset(); - globalScene.phaseManager.unshiftNew("TitlePhase"); - this.end(); - }); + globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => { + if (!success[0]) { + return globalScene.reset(true); + } + globalScene.reset(); + globalScene.phaseManager.unshiftNew("TitlePhase"); + this.end(); + }); }); }; diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 15d92ba2812..a7db5b6a6dd 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -177,6 +177,9 @@ export class TitlePhase extends Phase { .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(); diff --git a/src/plugins/api/pokerogue-session-savedata-api.ts b/src/plugins/api/pokerogue-session-savedata-api.ts index 39fa292f9f1..6438178dfda 100644 --- a/src/plugins/api/pokerogue-session-savedata-api.ts +++ b/src/plugins/api/pokerogue-session-savedata-api.ts @@ -82,6 +82,7 @@ export class PokerogueSessionSavedataApi extends ApiBase { try { const urlSearchParams = this.toUrlSearchParams(params); const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`); + console.debug("%cSending a request to delete session in slot %d", "color: blue", params.slot); if (response.ok) { return null; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3a4dafb2de2..a00701f86f5 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -68,6 +68,7 @@ import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } fro import { decrypt, encrypt } from "#utils/data"; import { getEnumKeys } from "#utils/enums"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { isBeta } from "#utils/utility-vars"; import { AES, enc } from "crypto-js"; import i18next from "i18next"; @@ -419,7 +420,15 @@ export class GameData { } } - console.debug(systemData); + if (isLocal || isBeta) { + try { + console.debug( + this.parseSystemData(JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), + ); + } catch (err) { + console.debug("Attempt to log system data failed:", err); + } + } localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); @@ -945,45 +954,46 @@ export class GameData { } as SessionSaveData; } - getSession(slotId: number): Promise { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this - return new Promise(async (resolve, reject) => { - if (slotId < 0) { - return resolve(null); + async getSession(slotId: number): Promise { + const { promise, resolve, reject } = Promise.withResolvers(); + if (slotId < 0) { + resolve(null); + return promise; + } + const handleSessionData = async (sessionDataStr: string) => { + try { + const sessionData = this.parseSessionData(sessionDataStr); + resolve(sessionData); + } catch (err) { + reject(err); + return; } - const handleSessionData = async (sessionDataStr: string) => { - try { - const sessionData = this.parseSessionData(sessionDataStr); - resolve(sessionData); - } catch (err) { - reject(err); - return; - } - }; + }; - if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { - pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => { - if (!response || response?.length === 0 || response?.[0] !== "{") { - console.error(response); - return resolve(null); - } + if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { + const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(response, bypassLogin), - ); - - await handleSessionData(response); - }); - } else { - const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); - if (sessionData) { - await handleSessionData(decrypt(sessionData, bypassLogin)); - } else { - return resolve(null); - } + 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) { + await handleSessionData(decrypt(sessionData, bypassLogin)); + return promise; + } + resolve(null); + return promise; } async renameSession(slotId: number, newName: string): Promise { @@ -1028,166 +1038,177 @@ export class GameData { return !(success !== null && !success); } - loadSession(slotId: number, sessionData?: SessionSaveData): Promise { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this - return new Promise(async (resolve, reject) => { - try { - const initSessionFromData = async (sessionData: SessionSaveData) => { - console.debug(sessionData); - - globalScene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); - if (sessionData.challenges) { - globalScene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); + 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); } - - globalScene.setSeed(sessionData.seed || globalScene.game.config.seed[0]); - globalScene.resetSeed(); - - console.log("Seed:", globalScene.seed); - - globalScene.sessionPlayTime = sessionData.playTime || 0; - globalScene.lastSavePlayTime = 0; - - const loadPokemonAssets: Promise[] = []; - - const party = globalScene.getPlayerParty(); - party.splice(0, party.length); - - for (const p of sessionData.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] = sessionData.pokeballCounts[key] || 0; - }); - if (Overrides.POKEBALL_OVERRIDE.active) { - globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; - } - - globalScene.money = Math.floor(sessionData.money || 0); - globalScene.updateMoneyText(); - - if (globalScene.money > this.gameStats.highestMoney) { - this.gameStats.highestMoney = globalScene.money; - } - - globalScene.score = sessionData.score; - globalScene.updateScoreText(); - - globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); - - globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints); - - const battleType = sessionData.battleType || 0; - const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; - const mysteryEncounterType = - sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; - const battle = globalScene.newBattle( - sessionData.waveIndex, - battleType, - sessionData.trainer, - battleType === BattleType.TRAINER - ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE - : sessionData.enemyParty.length > 1, - mysteryEncounterType, - ); - battle.enemyLevels = sessionData.enemyParty.map(p => p.level); - - globalScene.arena.init(); - - sessionData.enemyParty.forEach((enemyData, e) => { - const enemyPokemon = enemyData.toPokemon( - battleType, - e, - sessionData.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 = sessionData.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 = sessionData.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 = sessionData.arena.playerTerasUsed; - - globalScene.arena.tags = sessionData.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 = sessionData.arena.positionalTags.map(tag => - loadPositionalTag(tag), - ); - - if (globalScene.modifiers.length) { - console.warn("Existing modifiers not cleared on session load, deleting..."); - globalScene.modifiers = []; - } - for (const modifierData of sessionData.modifiers) { - const modifier = modifierData.toModifier(Modifier[modifierData.className]); - if (modifier) { - globalScene.addModifier(modifier, true); - } - } - globalScene.updateModifiers(true); - - for (const enemyModifierData of sessionData.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 => data && initSessionFromData(data)) - .catch(err => { - reject(err); - return; - }); } - } catch (err) { - reject(err); - return; + + 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) { + 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) { + reject(err); + } + + return promise; } /**