[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
This commit is contained in:
Sirz Benjie 2025-09-07 20:17:12 -05:00 committed by GitHub
parent 8fdd5043c3
commit 43f8b78c35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 230 additions and 206 deletions

View File

@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase {
} }
this.getRunHistoryEntry().then(runHistoryEntry => { this.getRunHistoryEntry().then(runHistoryEntry => {
globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory); globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory);
globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase); globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase);
this.end(); this.end();
}); });
}; };

View File

@ -5,10 +5,11 @@ import type { EndCardPhase } from "#phases/end-card-phase";
export class PostGameOverPhase extends Phase { export class PostGameOverPhase extends Phase {
public readonly phaseName = "PostGameOverPhase"; public readonly phaseName = "PostGameOverPhase";
private endCardPhase?: EndCardPhase; private endCardPhase?: EndCardPhase;
private slotId: number;
constructor(endCardPhase?: EndCardPhase) { constructor(slotId: number, endCardPhase?: EndCardPhase) {
super(); super();
this.slotId = slotId;
this.endCardPhase = endCardPhase; this.endCardPhase = endCardPhase;
} }
@ -20,9 +21,7 @@ export class PostGameOverPhase extends Phase {
if (!success) { if (!success) {
return globalScene.reset(true); return globalScene.reset(true);
} }
globalScene.gameData globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => {
.tryClearSession(globalScene.sessionSlotId)
.then((success: boolean | [boolean, boolean]) => {
if (!success[0]) { if (!success[0]) {
return globalScene.reset(true); return globalScene.reset(true);
} }

View File

@ -177,6 +177,9 @@ export class TitlePhase extends Phase {
.then((success: boolean) => { .then((success: boolean) => {
if (success) { if (success) {
this.loaded = true; this.loaded = true;
if (loggedInUser) {
loggedInUser.lastSessionSlot = slotId;
}
globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end());
} else { } else {
this.end(); this.end();

View File

@ -82,6 +82,7 @@ export class PokerogueSessionSavedataApi extends ApiBase {
try { try {
const urlSearchParams = this.toUrlSearchParams(params); const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`); 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) { if (response.ok) {
return null; return null;

View File

@ -68,6 +68,7 @@ import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } fro
import { decrypt, encrypt } from "#utils/data"; import { decrypt, encrypt } from "#utils/data";
import { getEnumKeys } from "#utils/enums"; import { getEnumKeys } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import { isBeta } from "#utils/utility-vars";
import { AES, enc } from "crypto-js"; import { AES, enc } from "crypto-js";
import i18next from "i18next"; 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)); localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
@ -945,11 +954,11 @@ export class GameData {
} as SessionSaveData; } as SessionSaveData;
} }
getSession(slotId: number): Promise<SessionSaveData | null> { async getSession(slotId: number): Promise<SessionSaveData | null> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this const { promise, resolve, reject } = Promise.withResolvers<SessionSaveData | null>();
return new Promise(async (resolve, reject) => {
if (slotId < 0) { if (slotId < 0) {
return resolve(null); resolve(null);
return promise;
} }
const handleSessionData = async (sessionDataStr: string) => { const handleSessionData = async (sessionDataStr: string) => {
try { try {
@ -962,10 +971,12 @@ export class GameData {
}; };
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => { const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
if (!response || response?.length === 0 || response?.[0] !== "{") { if (!response || response?.length === 0 || response?.[0] !== "{") {
console.error(response); console.error(response);
return resolve(null); resolve(null);
return promise;
} }
localStorage.setItem( localStorage.setItem(
@ -974,16 +985,15 @@ export class GameData {
); );
await handleSessionData(response); await handleSessionData(response);
}); return promise;
} else { }
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
if (sessionData) { if (sessionData) {
await handleSessionData(decrypt(sessionData, bypassLogin)); await handleSessionData(decrypt(sessionData, bypassLogin));
} else { return promise;
return resolve(null);
} }
} resolve(null);
}); return promise;
} }
async renameSession(slotId: number, newName: string): Promise<boolean> { async renameSession(slotId: number, newName: string): Promise<boolean> {
@ -1028,24 +1038,33 @@ export class GameData {
return !(success !== null && !success); return !(success !== null && !success);
} }
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> { async loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this const { promise, resolve, reject } = Promise.withResolvers<boolean>();
return new Promise(async (resolve, reject) => {
try { try {
const initSessionFromData = async (sessionData: SessionSaveData) => { const initSessionFromData = (fromSession: SessionSaveData) => {
console.debug(sessionData); if (isLocal || isBeta) {
try {
globalScene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); console.debug(
if (sessionData.challenges) { this.parseSessionData(
globalScene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); 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.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(); globalScene.resetSeed();
console.log("Seed:", globalScene.seed); console.log("Seed:", globalScene.seed);
globalScene.sessionPlayTime = sessionData.playTime || 0; globalScene.sessionPlayTime = fromSession.playTime || 0;
globalScene.lastSavePlayTime = 0; globalScene.lastSavePlayTime = 0;
const loadPokemonAssets: Promise<void>[] = []; const loadPokemonAssets: Promise<void>[] = [];
@ -1053,7 +1072,7 @@ export class GameData {
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
party.splice(0, party.length); party.splice(0, party.length);
for (const p of sessionData.party) { for (const p of fromSession.party) {
const pokemon = p.toPokemon() as PlayerPokemon; const pokemon = p.toPokemon() as PlayerPokemon;
pokemon.setVisible(false); pokemon.setVisible(false);
loadPokemonAssets.push(pokemon.loadAssets(false)); loadPokemonAssets.push(pokemon.loadAssets(false));
@ -1061,48 +1080,48 @@ export class GameData {
} }
Object.keys(globalScene.pokeballCounts).forEach((key: string) => { Object.keys(globalScene.pokeballCounts).forEach((key: string) => {
globalScene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0; globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0;
}); });
if (Overrides.POKEBALL_OVERRIDE.active) { if (Overrides.POKEBALL_OVERRIDE.active) {
globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
} }
globalScene.money = Math.floor(sessionData.money || 0); globalScene.money = Math.floor(fromSession.money || 0);
globalScene.updateMoneyText(); globalScene.updateMoneyText();
if (globalScene.money > this.gameStats.highestMoney) { if (globalScene.money > this.gameStats.highestMoney) {
this.gameStats.highestMoney = globalScene.money; this.gameStats.highestMoney = globalScene.money;
} }
globalScene.score = sessionData.score; globalScene.score = fromSession.score;
globalScene.updateScoreText(); globalScene.updateScoreText();
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData);
globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints); globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints);
const battleType = sessionData.battleType || 0; const battleType = fromSession.battleType || 0;
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null;
const mysteryEncounterType = const mysteryEncounterType =
sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined;
const battle = globalScene.newBattle( const battle = globalScene.newBattle(
sessionData.waveIndex, fromSession.waveIndex,
battleType, battleType,
sessionData.trainer, fromSession.trainer,
battleType === BattleType.TRAINER battleType === BattleType.TRAINER
? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE
: sessionData.enemyParty.length > 1, : fromSession.enemyParty.length > 1,
mysteryEncounterType, mysteryEncounterType,
); );
battle.enemyLevels = sessionData.enemyParty.map(p => p.level); battle.enemyLevels = fromSession.enemyParty.map(p => p.level);
globalScene.arena.init(); globalScene.arena.init();
sessionData.enemyParty.forEach((enemyData, e) => { fromSession.enemyParty.forEach((enemyData, e) => {
const enemyPokemon = enemyData.toPokemon( const enemyPokemon = enemyData.toPokemon(
battleType, battleType,
e, e,
sessionData.trainer?.variant === TrainerVariant.DOUBLE, fromSession.trainer?.variant === TrainerVariant.DOUBLE,
) as EnemyPokemon; ) as EnemyPokemon;
battle.enemyParty[e] = enemyPokemon; battle.enemyParty[e] = enemyPokemon;
if (battleType === BattleType.WILD) { if (battleType === BattleType.WILD) {
@ -1112,7 +1131,7 @@ export class GameData {
loadPokemonAssets.push(enemyPokemon.loadAssets()); loadPokemonAssets.push(enemyPokemon.loadAssets());
}); });
globalScene.arena.weather = sessionData.arena.weather; globalScene.arena.weather = fromSession.arena.weather;
globalScene.arena.eventTarget.dispatchEvent( globalScene.arena.eventTarget.dispatchEvent(
new WeatherChangedEvent( new WeatherChangedEvent(
WeatherType.NONE, WeatherType.NONE,
@ -1121,7 +1140,7 @@ export class GameData {
), ),
); // TODO: is this bang correct? ); // TODO: is this bang correct?
globalScene.arena.terrain = sessionData.arena.terrain; globalScene.arena.terrain = fromSession.arena.terrain;
globalScene.arena.eventTarget.dispatchEvent( globalScene.arena.eventTarget.dispatchEvent(
new TerrainChangedEvent( new TerrainChangedEvent(
TerrainType.NONE, TerrainType.NONE,
@ -1130,9 +1149,9 @@ export class GameData {
), ),
); // TODO: is this bang correct? ); // TODO: is this bang correct?
globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed; globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed;
globalScene.arena.tags = sessionData.arena.tags; globalScene.arena.tags = fromSession.arena.tags;
if (globalScene.arena.tags) { if (globalScene.arena.tags) {
for (const tag of globalScene.arena.tags) { for (const tag of globalScene.arena.tags) {
if (tag instanceof EntryHazardTag) { if (tag instanceof EntryHazardTag) {
@ -1146,7 +1165,7 @@ export class GameData {
} }
} }
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag => globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag =>
loadPositionalTag(tag), loadPositionalTag(tag),
); );
@ -1154,7 +1173,7 @@ export class GameData {
console.warn("Existing modifiers not cleared on session load, deleting..."); console.warn("Existing modifiers not cleared on session load, deleting...");
globalScene.modifiers = []; globalScene.modifiers = [];
} }
for (const modifierData of sessionData.modifiers) { for (const modifierData of fromSession.modifiers) {
const modifier = modifierData.toModifier(Modifier[modifierData.className]); const modifier = modifierData.toModifier(Modifier[modifierData.className]);
if (modifier) { if (modifier) {
globalScene.addModifier(modifier, true); globalScene.addModifier(modifier, true);
@ -1162,7 +1181,7 @@ export class GameData {
} }
globalScene.updateModifiers(true); globalScene.updateModifiers(true);
for (const enemyModifierData of sessionData.enemyModifiers) { for (const enemyModifierData of fromSession.enemyModifiers) {
const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]);
if (modifier) { if (modifier) {
globalScene.addEnemyModifier(modifier, true); globalScene.addEnemyModifier(modifier, true);
@ -1177,7 +1196,9 @@ export class GameData {
initSessionFromData(sessionData); initSessionFromData(sessionData);
} else { } else {
this.getSession(slotId) this.getSession(slotId)
.then(data => data && initSessionFromData(data)) .then(data => {
return data && initSessionFromData(data);
})
.catch(err => { .catch(err => {
reject(err); reject(err);
return; return;
@ -1185,9 +1206,9 @@ export class GameData {
} }
} catch (err) { } catch (err) {
reject(err); reject(err);
return;
} }
});
return promise;
} }
/** /**