From c8d61b134d3ea6972b196f2c544979c9fe276a1f Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Tue, 14 May 2024 22:48:53 -0400 Subject: [PATCH] Implement client session and re-implement hybrid saving (WiP) --- src/account.ts | 3 +- src/phases.ts | 10 +- src/system/game-data.ts | 412 +++++++++++++++++++++------------------- 3 files changed, 230 insertions(+), 195 deletions(-) diff --git a/src/account.ts b/src/account.ts index afb9aca3593..0d79f4d5571 100644 --- a/src/account.ts +++ b/src/account.ts @@ -7,13 +7,14 @@ export interface UserInfo { } export let loggedInUser: UserInfo = null; +export const clientSessionId = Utils.randomString(32); export function updateUserInfo(): Promise<[boolean, integer]> { return new Promise<[boolean, integer]>(resolve => { if (bypassLogin) { let lastSessionSlot = -1; for (let s = 0; s < 2; s++) { - if (localStorage.getItem(`sessionData${s ? s : ''}`)) { + if (localStorage.getItem(`sessionData${s ? s : ''}_${loggedInUser.username}`)) { lastSessionSlot = s; break; } diff --git a/src/phases.ts b/src/phases.ts index 12316902159..d4829235773 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -393,8 +393,12 @@ export class UnavailablePhase extends Phase { } export class ReloadSessionPhase extends Phase { - constructor(scene: BattleScene) { + private systemDataStr: string; + + constructor(scene: BattleScene, systemDataStr?: string) { super(scene); + + this.systemDataStr = systemDataStr; } start(): void { @@ -410,7 +414,7 @@ export class ReloadSessionPhase extends Phase { delayElapsed = true; }); - this.scene.gameData.loadSystem().then(() => { + (this.systemDataStr ? this.scene.gameData.initSystem(this.systemDataStr) : this.scene.gameData.loadSystem()).then(() => { if (delayElapsed) this.end(); else @@ -784,7 +788,7 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - this.scene.gameData.saveAll(this.scene, true).then(success => { + this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 5 === 1).then(success => { this.scene.disableMenu = false; if (!success) return this.scene.reset(true); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 5e942b4fa5b..a1f98f55729 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -20,7 +20,7 @@ import { Egg } from "../data/egg"; import { VoucherType, vouchers } from "./voucher"; import { AES, enc } from "crypto-js"; import { Mode } from "../ui/ui"; -import { loggedInUser, updateUserInfo } from "../account"; +import { clientSessionId, loggedInUser, updateUserInfo } from "../account"; import { Nature } from "../data/nature"; import { GameStats } from "./game-stats"; import { Tutorial } from "../tutorial"; @@ -277,7 +277,13 @@ export class GameData { const maxIntAttrValue = Math.pow(2, 31); const systemData = JSON.stringify(data, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v); - if (!bypassLogin) { + const encFunc = bypassLogin + ? (data: string) => btoa(data) + : (data: string) => AES.encrypt(data, saveKey); + + localStorage.setItem(`data_${loggedInUser.username}`, encFunc(systemData)); + + if (!bypassLogin && !localStorage.getItem(`data_${loggedInUser.username}`)) { Utils.apiPost(`savedata/update?datatype=${GameDataType.SYSTEM}`, systemData, undefined, true) .then(response => response.text()) .then(error => { @@ -296,10 +302,6 @@ export class GameData { resolve(true); }); } else { - localStorage.setItem('data_bak', localStorage.getItem('data')); - - localStorage.setItem('data', btoa(systemData)); - this.scene.ui.savingIcon.hide(); resolve(true); @@ -309,120 +311,13 @@ export class GameData { public loadSystem(): Promise { return new Promise(resolve => { - if (bypassLogin && !localStorage.hasOwnProperty('data')) + console.log('Client Session:', clientSessionId); + + if (bypassLogin && !localStorage.getItem(`data_${loggedInUser.username}`)) return resolve(false); - const handleSystemData = (systemDataStr: string) => { - try { - const systemData = this.parseSystemData(systemDataStr); - - console.debug(systemData); - - /*const versions = [ this.scene.game.config.gameVersion, data.gameVersion || '0.0.0' ]; - - if (versions[0] !== versions[1]) { - const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); - }*/ - - this.trainerId = systemData.trainerId; - this.secretId = systemData.secretId; - - this.gender = systemData.gender; - - this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); - - const initStarterData = !systemData.starterData; - - if (initStarterData) { - this.initStarterData(); - - if (systemData['starterMoveData']) { - const starterMoveData = systemData['starterMoveData']; - for (let s of Object.keys(starterMoveData)) - this.starterData[s].moveset = starterMoveData[s]; - } - - if (systemData['starterEggMoveData']) { - const starterEggMoveData = systemData['starterEggMoveData']; - for (let s of Object.keys(starterEggMoveData)) - this.starterData[s].eggMoves = starterEggMoveData[s]; - } - - this.migrateStarterAbilities(systemData, this.starterData); - } else { - if ([ '1.0.0', '1.0.1' ].includes(systemData.gameVersion)) - this.migrateStarterAbilities(systemData); - //this.fixVariantData(systemData); - this.fixStarterData(systemData); - // Migrate ability starter data if empty for caught species - Object.keys(systemData.starterData).forEach(sd => { - if (systemData.dexData[sd].caughtAttr && !systemData.starterData[sd].abilityAttr) - systemData.starterData[sd].abilityAttr = 1; - }); - this.starterData = systemData.starterData; - } - - if (systemData.gameStats) { - if (systemData.gameStats.legendaryPokemonCaught !== undefined && systemData.gameStats.subLegendaryPokemonCaught === undefined) - this.fixLegendaryStats(systemData); - this.gameStats = systemData.gameStats; - } - - if (systemData.unlocks) { - for (let key of Object.keys(systemData.unlocks)) { - if (this.unlocks.hasOwnProperty(key)) - this.unlocks[key] = systemData.unlocks[key]; - } - } - - if (systemData.achvUnlocks) { - for (let a of Object.keys(systemData.achvUnlocks)) { - if (achvs.hasOwnProperty(a)) - this.achvUnlocks[a] = systemData.achvUnlocks[a]; - } - } - - if (systemData.voucherUnlocks) { - for (let v of Object.keys(systemData.voucherUnlocks)) { - if (vouchers.hasOwnProperty(v)) - this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; - } - } - - if (systemData.voucherCounts) { - Utils.getEnumKeys(VoucherType).forEach(key => { - const index = VoucherType[key]; - this.voucherCounts[index] = systemData.voucherCounts[index] || 0; - }); - } - - this.eggs = systemData.eggs - ? systemData.eggs.map(e => e.toEgg()) - : []; - - this.dexData = Object.assign(this.dexData, systemData.dexData); - this.consolidateDexData(this.dexData); - this.defaultDexData = null; - - if (initStarterData) { - const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species); - for (let s of starterIds) { - this.starterData[s].candyCount += this.dexData[s].caughtCount; - this.starterData[s].candyCount += this.dexData[s].hatchedCount * 2; - if (this.dexData[s].caughtAttr & DexAttr.SHINY) - this.starterData[s].candyCount += 4; - } - } - - resolve(true); - } catch (err) { - console.error(err); - resolve(false); - } - } - if (!bypassLogin) { - Utils.apiFetch(`savedata/get?datatype=${GameDataType.SYSTEM}`, true) + Utils.apiFetch(`savedata/system?clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== '{') { @@ -437,10 +332,139 @@ export class GameData { return resolve(false); } - handleSystemData(response); + const cachedSystem = localStorage.getItem(`data_${loggedInUser.username}`); + this.initSystem(response, cachedSystem ? AES.decrypt(cachedSystem, saveKey).toString(enc.Utf8) : null).then(resolve); }); - } else - handleSystemData(atob(localStorage.getItem('data'))); + } else { + const decFunc = bypassLogin + ? (data: string) => atob(data) + : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8); + this.initSystem(decFunc(localStorage.getItem(`data_${loggedInUser.username}`))).then(resolve); + } + }); + } + + public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise { + return new Promise(resolve => { + try { + let systemData = this.parseSystemData(systemDataStr); + + if (cachedSystemDataStr) { + let cachedSystemData = this.parseSystemData(cachedSystemDataStr); + console.log(cachedSystemData.timestamp, systemData.timestamp) + if (cachedSystemData.timestamp > systemData.timestamp) { + console.debug('Use cached system'); + systemData = cachedSystemData; + } else { + localStorage.removeItem(`data_${loggedInUser.username}`); + for (let s = 0; s < 5; s++) + localStorage.removeItem(`sessionData${s ? s : ''}_${loggedInUser.username}`); + } + } + + console.debug(systemData); + + /*const versions = [ this.scene.game.config.gameVersion, data.gameVersion || '0.0.0' ]; + + if (versions[0] !== versions[1]) { + const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); + }*/ + + this.trainerId = systemData.trainerId; + this.secretId = systemData.secretId; + + this.gender = systemData.gender; + + this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + + const initStarterData = !systemData.starterData; + + if (initStarterData) { + this.initStarterData(); + + if (systemData['starterMoveData']) { + const starterMoveData = systemData['starterMoveData']; + for (let s of Object.keys(starterMoveData)) + this.starterData[s].moveset = starterMoveData[s]; + } + + if (systemData['starterEggMoveData']) { + const starterEggMoveData = systemData['starterEggMoveData']; + for (let s of Object.keys(starterEggMoveData)) + this.starterData[s].eggMoves = starterEggMoveData[s]; + } + + this.migrateStarterAbilities(systemData, this.starterData); + } else { + if ([ '1.0.0', '1.0.1' ].includes(systemData.gameVersion)) + this.migrateStarterAbilities(systemData); + //this.fixVariantData(systemData); + this.fixStarterData(systemData); + // Migrate ability starter data if empty for caught species + Object.keys(systemData.starterData).forEach(sd => { + if (systemData.dexData[sd].caughtAttr && !systemData.starterData[sd].abilityAttr) + systemData.starterData[sd].abilityAttr = 1; + }); + this.starterData = systemData.starterData; + } + + if (systemData.gameStats) { + if (systemData.gameStats.legendaryPokemonCaught !== undefined && systemData.gameStats.subLegendaryPokemonCaught === undefined) + this.fixLegendaryStats(systemData); + this.gameStats = systemData.gameStats; + } + + if (systemData.unlocks) { + for (let key of Object.keys(systemData.unlocks)) { + if (this.unlocks.hasOwnProperty(key)) + this.unlocks[key] = systemData.unlocks[key]; + } + } + + if (systemData.achvUnlocks) { + for (let a of Object.keys(systemData.achvUnlocks)) { + if (achvs.hasOwnProperty(a)) + this.achvUnlocks[a] = systemData.achvUnlocks[a]; + } + } + + if (systemData.voucherUnlocks) { + for (let v of Object.keys(systemData.voucherUnlocks)) { + if (vouchers.hasOwnProperty(v)) + this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; + } + } + + if (systemData.voucherCounts) { + Utils.getEnumKeys(VoucherType).forEach(key => { + const index = VoucherType[key]; + this.voucherCounts[index] = systemData.voucherCounts[index] || 0; + }); + } + + this.eggs = systemData.eggs + ? systemData.eggs.map(e => e.toEgg()) + : []; + + this.dexData = Object.assign(this.dexData, systemData.dexData); + this.consolidateDexData(this.dexData); + this.defaultDexData = null; + + if (initStarterData) { + const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species); + for (let s of starterIds) { + this.starterData[s].candyCount += this.dexData[s].caughtCount; + this.starterData[s].candyCount += this.dexData[s].hatchedCount * 2; + if (this.dexData[s].caughtAttr & DexAttr.SHINY) + this.starterData[s].candyCount += 4; + } + } + + resolve(true); + } catch (err) { + console.error(err); + resolve(false); + } }); } @@ -474,6 +498,26 @@ export class GameData { return dataStr; } + public verify(): Promise { + return new Promise(resolve => { + if (bypassLogin) + return resolve(true); + Utils.apiFetch(`savedata/system/verify?clientSessionId=${clientSessionId}`, true) + .then(response => response.json()) + .then(response => { + if (!response.valid) { + this.scene.clearPhaseQueue(); + this.scene.unshiftPhase(new ReloadSessionPhase(this.scene, JSON.stringify(response.systemData))); + localStorage.removeItem(`data_${loggedInUser.username}`); + for (let s = 0; s < 5; s++) + localStorage.removeItem(`sessionData${s ? s : ''}_${loggedInUser.username}`); + return resolve(false); + } + resolve(true); + }); + }); + } + public saveSetting(setting: Setting, valueIndex: integer): boolean { let settings: object = {}; if (localStorage.hasOwnProperty('settings')) @@ -557,40 +601,6 @@ export class GameData { } as SessionSaveData; } - saveSession(scene: BattleScene, skipVerification?: boolean): Promise { - return new Promise(resolve => { - Utils.executeIf(!skipVerification, updateUserInfo).then(success => { - if (success !== null && !success) - return resolve(false); - - const sessionData = this.getSessionSaveData(scene); - - if (!bypassLogin) { - Utils.apiPost(`savedata/update?datatype=${GameDataType.SESSION}&slot=${scene.sessionSlotId}&trainerId=${this.trainerId}&secretId=${this.secretId}`, JSON.stringify(sessionData), undefined, true) - .then(response => response.text()) - .then(error => { - if (error) { - if (error.startsWith('session out of date')) { - this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new ReloadSessionPhase(this.scene)); - } - console.error(error); - return resolve(false); - } - console.debug('Session data saved'); - resolve(true); - }); - } else { - localStorage.setItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}`, btoa(JSON.stringify(sessionData))); - - console.debug('Session data saved'); - - resolve(true); - } - }); - }); - } - getSession(slotId: integer): Promise { return new Promise(async (resolve, reject) => { if (slotId < 0) @@ -605,8 +615,8 @@ export class GameData { } }; - if (!bypassLogin) { - Utils.apiFetch(`savedata/get?datatype=${GameDataType.SESSION}&slot=${slotId}`, true) + if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`)) { + Utils.apiFetch(`savedata/session?slot=${slotId}&clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(async response => { if (!response.length || response[0] !== '{') { @@ -617,10 +627,13 @@ export class GameData { await handleSessionData(response); }); } else { - const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ''}`); - if (sessionData) - await handleSessionData(atob(sessionData)); - else + const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`); + if (sessionData) { + const decFunc = bypassLogin + ? (data: string) => atob(data) + : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8); + await handleSessionData(decFunc(sessionData)); + } else return resolve(null); } }); @@ -731,16 +744,17 @@ export class GameData { deleteSession(slotId: integer): Promise { return new Promise(resolve => { if (bypassLogin) { - localStorage.removeItem('sessionData'); + localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ''}_${loggedInUser.username}`); return resolve(true); } updateUserInfo().then(success => { if (success !== null && !success) return resolve(false); - Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}`, true).then(response => { + Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { if (response.ok) { loggedInUser.lastSessionSlot = -1; + localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ''}_${loggedInUser.username}`); resolve(true); } return response.text(); @@ -792,7 +806,7 @@ export class GameData { tryClearSession(scene: BattleScene, slotId: integer): Promise<[success: boolean, newClear: boolean]> { return new Promise<[boolean, boolean]>(resolve => { if (bypassLogin) { - localStorage.removeItem(`sessionData${slotId ? slotId : ''}`); + localStorage.removeItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`); return resolve([true, true]); } @@ -800,9 +814,11 @@ export class GameData { if (success !== null && !success) return resolve([false, false]); const sessionData = this.getSessionSaveData(scene); - Utils.apiPost(`savedata/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}`, JSON.stringify(sessionData), undefined, true).then(response => { - if (response.ok) + Utils.apiPost(`savedata/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true).then(response => { + if (response.ok) { loggedInUser.lastSessionSlot = -1; + localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ''}_${loggedInUser.username}`); + } return response.json(); }).then(jsonResponse => { if (!jsonResponse.error) @@ -855,13 +871,13 @@ export class GameData { }) as SessionSaveData; } - saveAll(scene: BattleScene, skipVerification?: boolean): Promise { + saveAll(scene: BattleScene, skipVerification: boolean = false, sync: boolean = false): Promise { return new Promise(resolve => { Utils.executeIf(!skipVerification, updateUserInfo).then(success => { if (success !== null && !success) return resolve(false); - this.scene.ui.savingIcon.show(); - const data = this.getSystemSaveData(); + if (sync) + this.scene.ui.savingIcon.show(); const sessionData = this.getSessionSaveData(scene); const maxIntAttrValue = Math.pow(2, 31); @@ -870,14 +886,26 @@ export class GameData { const request = { system: systemData, session: sessionData, - sessionSlotId: scene.sessionSlotId + sessionSlotId: scene.sessionSlotId, + clientSessionId: clientSessionId }; - if (!bypassLogin) { + const encFunc = bypassLogin + ? (data: string) => btoa(data) + : (data: string) => AES.encrypt(data, saveKey); + + localStorage.setItem(`data_${loggedInUser.username}`, encFunc(JSON.stringify(systemData, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v))); + + localStorage.setItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}_${loggedInUser.username}`, encFunc(JSON.stringify(sessionData))); + + console.debug('Session data saved'); + + if (!bypassLogin && sync) { Utils.apiPost('savedata/updateall', JSON.stringify(request, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v), undefined, true) .then(response => response.text()) .then(error => { - this.scene.ui.savingIcon.hide(); + if (sync) + this.scene.ui.savingIcon.hide(); if (error) { if (error.startsWith('client version out of date')) { this.scene.clearPhaseQueue(); @@ -892,17 +920,10 @@ export class GameData { resolve(true); }); } else { - localStorage.setItem('data_bak', localStorage.getItem('data')); - - localStorage.setItem('data', btoa(JSON.stringify(systemData, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v))); - - localStorage.setItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}`, btoa(JSON.stringify(sessionData))); - - console.debug('Session data saved'); - - this.scene.ui.savingIcon.hide(); - - resolve(true); + this.verify().then(success => { + this.scene.ui.savingIcon.hide(); + resolve(success); + }); } }); }); @@ -910,7 +931,7 @@ export class GameData { public tryExportData(dataType: GameDataType, slotId: integer = 0): Promise { return new Promise(resolve => { - const dataKey: string = getDataTypeKey(dataType, slotId); + const dataKey: string = `${getDataTypeKey(dataType, slotId)}_${loggedInUser.username}`; const handleData = (dataStr: string) => { switch (dataType) { case GameDataType.SYSTEM: @@ -926,7 +947,7 @@ export class GameData { link.remove(); }; if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/get?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}`, true) + Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? 'system' : 'session'}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== '{') { @@ -940,15 +961,19 @@ export class GameData { }); } else { const data = localStorage.getItem(dataKey); - if (data) - handleData(atob(data)); + if (data) { + const decFunc = bypassLogin + ? (data: string) => atob(data) + : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8); + handleData(decFunc(data)); + } resolve(!!data); } }); } public importData(dataType: GameDataType, slotId: integer = 0): void { - const dataKey = getDataTypeKey(dataType, slotId); + const dataKey = `${getDataTypeKey(dataType, slotId)}_${loggedInUser.username}`; let saveFile: any = document.getElementById('saveFile'); if (saveFile) @@ -1009,11 +1034,18 @@ export class GameData { return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500)); this.scene.ui.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => { this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { + const hasLocalData = !!localStorage.getItem(dataKey); + + const encFunc = bypassLogin || dataType === GameDataType.SETTINGS + ? (data: string) => btoa(data) + : (data: string) => AES.encrypt(data, saveKey); + localStorage.setItem(dataKey, encFunc(dataStr)); + if (!bypassLogin && dataType < GameDataType.SETTINGS) { updateUserInfo().then(success => { if (!success) return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`); - Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&trainerId=${this.trainerId}&secretId=${this.secretId}`, dataStr, undefined, true) + Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, dataStr, undefined, true) .then(response => response.text()) .then(error => { if (error) { @@ -1023,10 +1055,8 @@ export class GameData { window.location = window.location; }); }); - } else { - localStorage.setItem(dataKey, btoa(dataStr)); + } else window.location = window.location; - } }, () => { this.scene.ui.revertMode(); this.scene.ui.showText(null, 0);