diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3e3a6ce8f8b..824519a46ac 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -14,7 +14,7 @@ import { GameModes, getGameMode } from "#app/game-mode"; import { BattleType } from "#app/battle"; import TrainerData from "#app/system/trainer-data"; import { trainerConfigs } from "#app/data/trainer-config"; -import { resetSettings, setSetting, SettingKeys } from "#app/system/settings/settings"; +import { resetSettings, setSetting, Setting, settingIndex, SettingKeys } from "#app/system/settings/settings"; import { achvs } from "#app/system/achv"; import EggData from "#app/system/egg-data"; import { Egg } from "#app/data/egg"; @@ -65,16 +65,12 @@ export const defaultStarterSpecies: Species[] = [ const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary -export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): string { +export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0, username?: string): string { switch (dataType) { case GameDataType.SYSTEM: - return "data"; + return `data_${username}`; case GameDataType.SESSION: - let ret = "sessionData"; - if (slotId) { - ret += slotId; - } - return ret; + return `sessionData${slotId || ""}_${username}`; case GameDataType.SETTINGS: return "settings"; case GameDataType.TUTORIALS: @@ -82,7 +78,7 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str case GameDataType.SEEN_DIALOGUES: return "seenDialogues"; case GameDataType.RUN_HISTORY: - return "runHistoryData"; + return `runHistoryData_${username}`; } } @@ -1364,9 +1360,35 @@ export class GameData { }); } + public getDataToExport(dataType: GameDataType, slotId: integer = 0): Promise { + return new Promise(resolve => { + if (!bypassLogin && dataType < GameDataType.SETTINGS) { + let promise: Promise = Promise.resolve(null); + + if (dataType === GameDataType.SYSTEM) { + promise = pokerogueApi.savedata.system.get({ clientSessionId }); + } else if (dataType === GameDataType.SESSION) { + promise = pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); + } + + promise.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}`; + const dataKey: string = getDataTypeKey(dataType, slotId, loggedInUser?.username); const handleData = (dataStr: string) => { switch (dataType) { case GameDataType.SYSTEM: @@ -1381,38 +1403,56 @@ export class GameData { link.click(); link.remove(); }; - if (!bypassLogin && dataType < GameDataType.SETTINGS) { - let promise: Promise = Promise.resolve(null); - - if (dataType === GameDataType.SYSTEM) { - promise = pokerogueApi.savedata.system.get({ clientSessionId }); - } else if (dataType === GameDataType.SESSION) { - promise = pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); - } - - promise.then(response => { - if (!response?.length || response[0] !== "{") { - console.error(response); - resolve(false); - return; + this.getDataToExport(dataType, slotId) + .then(data => { + if (data) { + handleData(data); } - - handleData(response); - resolve(true); + resolve(!!data); }); - } else { - const data = localStorage.getItem(dataKey); - if (data) { - handleData(decrypt(data, bypassLogin)); - } - 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(); @@ -1429,41 +1469,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: - 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)); @@ -1471,7 +1483,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, () => { - localStorage.setItem(dataKey, encrypt(dataStr, bypassLogin)); + 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 1e349470302..7b1ddaa8470 100644 --- a/src/test/system/game_data.test.ts +++ b/src/test/system/game_data.test.ts @@ -2,6 +2,7 @@ import * as BattleScene from "#app/battle-scene"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; 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 Phaser from "phaser"; @@ -73,4 +74,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(); + }); + }); }); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 3ce3f3b7cf0..badeda4aab3 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -246,6 +246,22 @@ export default class MenuUiHandler extends MessageUiHandler { }, keepOpen: true }); + manageDataOptions.push({ + label: i18next.t("menuUiHandler:importSettings"), + handler: () => { + this.scene.gameData.importData(GameDataType.SETTINGS); + return true; + }, + keepOpen: true + }); + manageDataOptions.push({ + label: i18next.t("menuUiHandler:exportSettings"), + handler: () => { + this.scene.gameData.tryExportData(GameDataType.SETTINGS); + return true; + }, + keepOpen: true + }); if (Utils.isLocal || Utils.isBeta) { manageDataOptions.push({ label: i18next.t("menuUiHandler:importData"),