diff --git a/src/system/game-data.ts b/src/system/game-data.ts index fd3b0fb7caa..0b077d0e3f7 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1353,6 +1353,26 @@ export class GameData { }); } + public getDataToExport(dataType: GameDataType, slotId: integer = 0): Promise { + return new Promise(resolve => { + if (!bypassLogin && dataType < GameDataType.SETTINGS) { + Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}/get?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) + .then(response => response.text()) + .then(response => { + if (!response.length || response[0] !== "{") { + console.error(response); + resolve(null); + } + resolve(response); + }); + } else { + const dataKey: string = getDataTypeKey(dataType, slotId, loggedInUser?.username); + const data = localStorage.getItem(dataKey); + resolve((!data || dataType === GameDataType.SETTINGS) ? data : decrypt(data, bypassLogin)); + } + }); + } + public tryExportData(dataType: GameDataType, slotId: integer = 0): Promise { return new Promise(resolve => { const dataKey: string = getDataTypeKey(dataType, slotId, loggedInUser?.username); @@ -1370,33 +1390,56 @@ export class GameData { link.click(); link.remove(); }; - if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}/get?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) - .then(response => response.text()) - .then(response => { - if (!response.length || response[0] !== "{") { - console.error(response); - resolve(false); - return; - } - - handleData(response); - resolve(true); - }); - } else { - const data = localStorage.getItem(dataKey); - if (data) { - const decryptedData = (dataType === GameDataType.SETTINGS) ? data : decrypt(data, bypassLogin); - handleData(decryptedData); - } - resolve(!!data); - } + this.getDataToExport(dataType, slotId) + .then(data => { + if (data) { + handleData(data); + } + resolve(!!data); + }); }); } - public importData(dataType: GameDataType, slotId: integer = 0): void { - const dataKey = getDataTypeKey(dataType, slotId, loggedInUser?.username); + public validateDataToImport(dataStr: string, dataType: GameDataType): boolean { + try { + switch (dataType) { + case GameDataType.SYSTEM: + dataStr = this.convertSystemDataStr(dataStr); + const systemData = this.parseSystemData(dataStr); + return !!systemData.dexData && !!systemData.timestamp; + case GameDataType.SESSION: + const sessionData = this.parseSessionData(dataStr); + return !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp; + case GameDataType.RUN_HISTORY: + const data = JSON.parse(dataStr); + const keys = Object.keys(data); + return keys.every((key) => { + const entryKeys = Object.keys(data[key]); + return [ "isFavorite", "isVictory", "entry" ].every(v => entryKeys.includes(v)) && entryKeys.length === 3; + }); + case GameDataType.SETTINGS: + return Object.entries(JSON.parse(dataStr)) + .every(([ k, v ]: [string, number]) => { + const index: number = settingIndex(k); + return index === -1 || Setting[index].options.length > v; + }); + case GameDataType.TUTORIALS: + case GameDataType.SEEN_DIALOGUES: + return true; + } + } catch (ex) { + console.error(ex); + return false; + } + } + public setImportedData(dataStr: string, dataType: GameDataType, slotId: integer = 0) { + const dataKey = getDataTypeKey(dataType, slotId, loggedInUser?.username); + const encryptedData = (dataType === GameDataType.SETTINGS) ? dataStr : encrypt(dataStr, bypassLogin); + localStorage.setItem(dataKey, encryptedData); + } + + public importData(dataType: GameDataType, slotId: integer = 0): void { let saveFile: any = document.getElementById("saveFile"); if (saveFile) { saveFile.remove(); @@ -1413,47 +1456,13 @@ export class GameData { reader.onload = (_ => { return e => { - let dataName: string; - let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct? - let valid = false; - try { - dataName = GameDataType[dataType].toLowerCase(); - switch (dataType) { - case GameDataType.SYSTEM: - dataStr = this.convertSystemDataStr(dataStr); - const systemData = this.parseSystemData(dataStr); - valid = !!systemData.dexData && !!systemData.timestamp; - break; - case GameDataType.SESSION: - const sessionData = this.parseSessionData(dataStr); - valid = !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp; - break; - case GameDataType.RUN_HISTORY: - const data = JSON.parse(dataStr); - const keys = Object.keys(data); - dataName = i18next.t("menuUiHandler:RUN_HISTORY").toLowerCase(); - keys.forEach((key) => { - const entryKeys = Object.keys(data[key]); - valid = [ "isFavorite", "isVictory", "entry" ].every(v => entryKeys.includes(v)) && entryKeys.length === 3; - }); - break; - case GameDataType.SETTINGS: - valid = Object.entries(JSON.parse(dataStr)) - .every(([ k, v ]: [string, number]) => { - const index: number = settingIndex(k); - return index === -1 || Setting[index].options.length > v; - }); - break; - case GameDataType.TUTORIALS: - valid = true; - break; - } - } catch (ex) { - console.error(ex); - } + const dataName = (dataType === GameDataType.RUN_HISTORY) + ? i18next.t("menuUiHandler:RUN_HISTORY").toLowerCase() + : GameDataType[dataType].toLowerCase(); + const dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct? + const valid = this.validateDataToImport(dataStr, dataType); const displayError = (error: string) => this.scene.ui.showText(error, null, () => this.scene.ui.showText("", 0), Utils.fixedInt(1500)); - dataName = dataName!; // tell TS compiler that dataName is defined! if (!valid) { return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText("", 0), Utils.fixedInt(1500)); @@ -1461,8 +1470,7 @@ export class GameData { this.scene.ui.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => { this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { - const encryptedData = (dataType === GameDataType.SETTINGS) ? dataStr : encrypt(dataStr, bypassLogin); - localStorage.setItem(dataKey, encryptedData); + this.setImportedData(dataStr, dataType, slotId); if (!bypassLogin && dataType < GameDataType.SETTINGS) { updateUserInfo().then(success => { diff --git a/src/test/system/game_data.test.ts b/src/test/system/game_data.test.ts index 5eb4dea3910..541516982d6 100644 --- a/src/test/system/game_data.test.ts +++ b/src/test/system/game_data.test.ts @@ -1,6 +1,7 @@ import * as BattleScene from "#app/battle-scene"; import { SessionSaveData } from "#app/system/game-data"; import { Abilities } from "#enums/abilities"; +import { GameDataType } from "#enums/game-data-type"; import { Moves } from "#enums/moves"; import GameManager from "#test/utils/gameManager"; import { http, HttpResponse } from "msw"; @@ -86,4 +87,63 @@ describe("System - Game Data", () => { expect(account.updateUserInfo).toHaveBeenCalled(); }); }); + + describe("getDataToExport", () => { + it("should get default settings", async () => { + const defaultSettings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}"; + localStorage.setItem("settings", defaultSettings); + + const result = await game.scene.gameData.getDataToExport(GameDataType.SETTINGS); + + expect(result).toEqual(defaultSettings); + }); + + it("should get undefined when there is no settings", async () => { + const result = await game.scene.gameData.getDataToExport(GameDataType.SETTINGS); + + expect(result).toBeUndefined(); + }); + }); + + describe("setImportedData", () => { + it("should set settings in local storage", () => { + const settings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}"; + game.scene.gameData.setImportedData(settings, GameDataType.SETTINGS); + + expect(localStorage.getItem("settings")).toEqual(settings); + }); + + it("should override default settings", () => { + const defaultSettings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}"; + localStorage.setItem("settings", defaultSettings); + + const newSettings = "{\"PLAYER_GENDER\":1,\"gameVersion\":\"1.0.7\",\"GAME_SPEED\":7}"; + game.scene.gameData.setImportedData(newSettings, GameDataType.SETTINGS); + + expect(localStorage.getItem("settings")).toEqual(newSettings); + }); + }); + + describe("validateDataToImport", () => { + it("should be true when the setting data is valid", async () => { + const settings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}"; + const result = await game.scene.gameData.validateDataToImport(settings, GameDataType.SETTINGS); + + expect(result).toBeTruthy(); + }); + + it("should be false when the setting data is an invalid JSON", async () => { + const settings = ""; + const result = await game.scene.gameData.validateDataToImport(settings, GameDataType.SETTINGS); + + expect(result).toBeFalsy(); + }); + + it("should be false when the setting data contains an unknow value", async () => { + const settings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\",\"GAME_SPEED\":999}"; + const result = await game.scene.gameData.validateDataToImport(settings, GameDataType.SETTINGS); + + expect(result).toBeFalsy(); + }); + }); });