diff --git a/src/phases.ts b/src/phases.ts index 25b0c3b758f..107fe61b9c4 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -116,13 +116,26 @@ export class LoginPhase extends Phase { } return null; } else { - this.scene.gameData.loadSystem().then(success => { - if (success || bypassLogin) - this.end(); - else { - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t('menu:failedToLoadSaveData')); + this.scene.gameData.getLastSystemRemoteSave().then(timestamp => { + let localLastSave = this.scene.gameData.getLastSystemLocalSave(); + let loadSource = undefined + + if (localLastSave > timestamp) { + console.log("LocalSave is the most recent, loading from it.") + loadSource = () => this.scene.gameData.loadSystemLocal() + } else { + console.log("LocalSave is outdated, loading from remote.") + loadSource = () => this.scene.gameData.loadSystem() } + + loadSource().then(success => { + if (success || bypassLogin) + this.end(); + else { + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(i18next.t('menu:failedToLoadSaveData')); + } + }); }); } }); @@ -400,7 +413,7 @@ export class ReloadSessionPhase extends Phase { else delayElapsed = true; }); - + // This is called when previous sync with the server failed, no need to fetch local data this.scene.gameData.loadSystem().then(() => { if (delayElapsed) this.end(); @@ -436,7 +449,8 @@ export class SelectGenderPhase extends Phase { handler: () => { this.scene.gameData.gender = PlayerGender.MALE; this.scene.gameData.saveSetting(Setting.Player_Gender, 0); - this.scene.gameData.saveSystem().then(() => this.end()); + this.scene.gameData.saveSystem().then(() => console.log("System data synced")); + this.scene.gameData.saveSystemLocal().then(() => this.end()); return true; } }, @@ -445,7 +459,8 @@ export class SelectGenderPhase extends Phase { handler: () => { this.scene.gameData.gender = PlayerGender.FEMALE; this.scene.gameData.saveSetting(Setting.Player_Gender, 1); - this.scene.gameData.saveSystem().then(() => this.end()); + this.scene.gameData.saveSystem().then(() => console.log("System data synced")); + this.scene.gameData.saveSystemLocal().then(() => this.end()); return true; } } @@ -771,12 +786,23 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - this.scene.gameData.saveSystem().then(success => { + this.scene.gameData.saveSystemLocal().then(success => { this.scene.disableMenu = false; if (!success) return this.scene.reset(true); this.scene.gameData.saveSession(this.scene, true).then(() => this.doEncounter()); }); + + // Syncing with the server every 3 waves + if (battle.waveIndex % 5 == 0) { + this.scene.gameData.saveSystem().then(success => { + if (!success) { + console.log("Failed to sync system with server") + } else { + console.log("Synced system with server.") + } + }) + } } else this.doEncounter(); }); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 30cc7c96dfb..9ad9fc11ecf 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -247,6 +247,39 @@ export class GameData { this.initStarterData(); } + public saveSystemLocal(): Promise { + return new Promise(resolve => { + this.scene.ui.savingIcon.show(); + const data: SystemSaveData = { + trainerId: this.trainerId, + secretId: this.secretId, + gender: this.gender, + dexData: this.dexData, + starterData: this.starterData, + gameStats: this.gameStats, + unlocks: this.unlocks, + achvUnlocks: this.achvUnlocks, + voucherUnlocks: this.voucherUnlocks, + voucherCounts: this.voucherCounts, + eggs: this.eggs.map(e => new EggData(e)), + gameVersion: this.scene.game.config.gameVersion, + timestamp: new Date().getTime() + }; + + console.log(data) + + 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); + + localStorage.setItem('data_bak', localStorage.getItem('data')); + + localStorage.setItem('data', btoa(systemData)); + + this.scene.ui.savingIcon.hide(); + return resolve(true); + }); + } + public saveSystem(): Promise { return new Promise(resolve => { this.scene.ui.savingIcon.show(); @@ -299,6 +332,153 @@ export class GameData { }); } + public getLastSystemLocalSave(): int { + if (!localStorage.hasOwnProperty('data')) + return -1 + + return this.parseSystemData(atob(localStorage.getItem('data'))).timestamp; + } + + public getLastSystemRemoteSave(): Promise { + return new Promise(resolve => { + Utils.apiFetch(`savedata/get?datatype=${GameDataType.SYSTEM}`, true) + .then(response => response.text()) + .then(response => { + if (!response.length || response[0] !== '{') { + if (response.startsWith('failed to open save file')) { + this.scene.queueMessage('Save data could not be found. If this is a new account, you can safely ignore this message.', null, true); + return resolve(-1); + } else if (response.indexOf('Too many connections') > -1) { + this.scene.queueMessage('Too many people are trying to connect and the server is overloaded. Please try again later.', null, true); + return resolve(-1); + } + console.error(response); + return resolve(-1); + } + return resolve(this.parseSystemData(response).timestamp) + }); + }); + } + + public loadSystemLocal(): Promise { + return new Promise(resolve => { + if (!localStorage.hasOwnProperty('data')) + 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); + } + } + + handleSystemData(atob(localStorage.getItem('data'))); + return resolve(true) + }); + } + public loadSystem(): Promise { return new Promise(resolve => { if (bypassLogin && !localStorage.hasOwnProperty('data')) diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index bf032667610..5cef378852b 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -310,6 +310,10 @@ export default class MenuUiHandler extends MessageUiHandler { error = true; break; case MenuOptions.LOG_OUT: + // Syncing system to the server before logout + // this way we can safely overwrite local save later + this.scene.gameData.saveSystem().then(() => console.log("System data synced.")); + success = true; const doLogout = () => { Utils.apiFetch('account/logout', true).then(res => {