[Dev] Save Data Version Migration (#4080)

* Initial Draft

* Successfuly Migration with Vitamins and White Herb

* Apply Session Data Patches Earlier

* Remove Stray `console.log`
This commit is contained in:
Amani H. 2024-09-07 21:40:45 -04:00 committed by GitHub
parent b36823af4a
commit ffcedfd9a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 165 additions and 120 deletions

View File

@ -1,7 +1,7 @@
import i18next from "i18next"; import i18next from "i18next";
import BattleScene, { PokeballCounts, bypassLogin } from "../battle-scene"; import BattleScene, { PokeballCounts, bypassLogin } from "../battle-scene";
import Pokemon, { EnemyPokemon, PlayerPokemon } from "../field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "../field/pokemon";
import { pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions"; import { pokemonPrevolutions } from "../data/pokemon-evolutions";
import PokemonSpecies, { allSpecies, getPokemonSpecies, noStarterFormKeys, speciesStarters } from "../data/pokemon-species"; import PokemonSpecies, { allSpecies, getPokemonSpecies, noStarterFormKeys, speciesStarters } from "../data/pokemon-species";
import * as Utils from "../utils"; import * as Utils from "../utils";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
@ -27,7 +27,7 @@ import { Tutorial } from "../tutorial";
import { speciesEggMoves } from "../data/egg-moves"; import { speciesEggMoves } from "../data/egg-moves";
import { allMoves } from "../data/move"; import { allMoves } from "../data/move";
import { TrainerVariant } from "../field/trainer"; import { TrainerVariant } from "../field/trainer";
import { Variant, variantData } from "#app/data/variant"; import { Variant } from "#app/data/variant";
import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings/settings-gamepad"; import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings/settings-gamepad";
import {setSettingKeyboard, SettingKeyboard} from "#app/system/settings/settings-keyboard"; import {setSettingKeyboard, SettingKeyboard} from "#app/system/settings/settings-keyboard";
import { TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena.js"; import { TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena.js";
@ -45,6 +45,7 @@ import { TerrainType } from "#app/data/terrain.js";
import { OutdatedPhase } from "#app/phases/outdated-phase.js"; import { OutdatedPhase } from "#app/phases/outdated-phase.js";
import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js";
import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler";
import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "./version-converter";
export const defaultStarterSpecies: Species[] = [ export const defaultStarterSpecies: Species[] = [
Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE,
@ -93,7 +94,7 @@ export function decrypt(data: string, bypassLogin: boolean): string {
: (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data); : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data);
} }
interface SystemSaveData { export interface SystemSaveData {
trainerId: integer; trainerId: integer;
secretId: integer; secretId: integer;
gender: PlayerGender; gender: PlayerGender;
@ -456,17 +457,14 @@ export class GameData {
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
/*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)));
}*/
const lsItemKey = `runHistoryData_${loggedInUser?.username}`; const lsItemKey = `runHistoryData_${loggedInUser?.username}`;
const lsItem = localStorage.getItem(lsItemKey); const lsItem = localStorage.getItem(lsItemKey);
if (!lsItem) { if (!lsItem) {
localStorage.setItem(lsItemKey, ""); localStorage.setItem(lsItemKey, "");
} }
applySystemDataPatches(systemData);
this.trainerId = systemData.trainerId; this.trainerId = systemData.trainerId;
this.secretId = systemData.secretId; this.secretId = systemData.secretId;
@ -474,9 +472,7 @@ export class GameData {
this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
const initStarterData = !systemData.starterData; if (!systemData.starterData) {
if (initStarterData) {
this.initStarterData(); this.initStarterData();
if (systemData["starterMoveData"]) { if (systemData["starterMoveData"]) {
@ -494,25 +490,20 @@ export class GameData {
} }
this.migrateStarterAbilities(systemData, this.starterData); this.migrateStarterAbilities(systemData, this.starterData);
} else {
if ([ "1.0.0", "1.0.1" ].includes(systemData.gameVersion)) { const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species);
this.migrateStarterAbilities(systemData); for (const s of starterIds) {
} this.starterData[s].candyCount += this.dexData[s].caughtCount;
//this.fixVariantData(systemData); this.starterData[s].candyCount += this.dexData[s].hatchedCount * 2;
this.fixStarterData(systemData); if (this.dexData[s].caughtAttr & DexAttr.SHINY) {
// Migrate ability starter data if empty for caught species this.starterData[s].candyCount += 4;
Object.keys(systemData.starterData).forEach(sd => {
if (systemData.dexData[sd].caughtAttr && !systemData.starterData[sd].abilityAttr) {
systemData.starterData[sd].abilityAttr = 1;
} }
}); }
} else {
this.starterData = systemData.starterData; this.starterData = systemData.starterData;
} }
if (systemData.gameStats) { if (systemData.gameStats) {
if (systemData.gameStats.legendaryPokemonCaught !== undefined && systemData.gameStats.subLegendaryPokemonCaught === undefined) {
this.fixLegendaryStats(systemData);
}
this.gameStats = systemData.gameStats; this.gameStats = systemData.gameStats;
} }
@ -558,17 +549,6 @@ export class GameData {
this.consolidateDexData(this.dexData); this.consolidateDexData(this.dexData);
this.defaultDexData = null; this.defaultDexData = null;
if (initStarterData) {
const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species);
for (const 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); resolve(true);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -747,6 +727,7 @@ export class GameData {
setSetting(this.scene, setting, valueIndex); setSetting(this.scene, setting, valueIndex);
settings[setting] = valueIndex; settings[setting] = valueIndex;
settings["gameVersion"] = this.scene.game.config.gameVersion;
localStorage.setItem("settings", JSON.stringify(settings)); localStorage.setItem("settings", JSON.stringify(settings));
@ -857,13 +838,7 @@ export class GameData {
const settings = JSON.parse(localStorage.getItem("settings")!); // TODO: is this bang correct? const settings = JSON.parse(localStorage.getItem("settings")!); // TODO: is this bang correct?
// TODO: Remove this block after save migration is implemented applySettingsDataPatches(settings);
if (settings.hasOwnProperty("REROLL_TARGET") && !settings.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
settings[SettingKeys.Shop_Cursor_Target] = settings["REROLL_TARGET"];
delete settings["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(settings));
}
// End of block to remove
for (const setting of Object.keys(settings)) { for (const setting of Object.keys(settings)) {
setSetting(this.scene, setting, settings[setting]); setSetting(this.scene, setting, settings[setting]);
@ -1226,7 +1201,7 @@ export class GameData {
} }
parseSessionData(dataStr: string): SessionSaveData { parseSessionData(dataStr: string): SessionSaveData {
return JSON.parse(dataStr, (k: string, v: any) => { const sessionData = JSON.parse(dataStr, (k: string, v: any) => {
/*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ]; /*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ];
if (versions[0] !== versions[1]) { if (versions[0] !== versions[1]) {
@ -1283,6 +1258,10 @@ export class GameData {
return v; return v;
}) as SessionSaveData; }) as SessionSaveData;
applySessionDataPatches(sessionData);
return sessionData;
} }
saveAll(scene: BattleScene, skipVerification: boolean = false, sync: boolean = false, useCachedSession: boolean = false, useCachedSystem: boolean = false): Promise<boolean> { saveAll(scene: BattleScene, skipVerification: boolean = false, sync: boolean = false, useCachedSession: boolean = false, useCachedSystem: boolean = false): Promise<boolean> {
@ -1885,75 +1864,4 @@ export class GameData {
} }
} }
} }
fixVariantData(systemData: SystemSaveData): void {
const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species);
const starterData = systemData.starterData;
const dexData = systemData.dexData;
if (starterIds.find(id => (dexData[id].caughtAttr & DexAttr.VARIANT_2 || dexData[id].caughtAttr & DexAttr.VARIANT_3) && !variantData[id])) {
for (const s of starterIds) {
const species = getPokemonSpecies(s);
if (variantData[s]) {
const tempCaughtAttr = dexData[s].caughtAttr;
let seenVariant2 = false;
let seenVariant3 = false;
const checkEvoSpecies = (es: Species) => {
seenVariant2 ||= !!(dexData[es].seenAttr & DexAttr.VARIANT_2);
seenVariant3 ||= !!(dexData[es].seenAttr & DexAttr.VARIANT_3);
if (pokemonEvolutions.hasOwnProperty(es)) {
for (const pe of pokemonEvolutions[es]) {
checkEvoSpecies(pe.speciesId);
}
}
};
checkEvoSpecies(s);
if (dexData[s].caughtAttr & DexAttr.VARIANT_2 && !seenVariant2) {
dexData[s].caughtAttr ^= DexAttr.VARIANT_2;
}
if (dexData[s].caughtAttr & DexAttr.VARIANT_3 && !seenVariant3) {
dexData[s].caughtAttr ^= DexAttr.VARIANT_3;
}
starterData[s].abilityAttr = (tempCaughtAttr & DexAttr.DEFAULT_VARIANT ? AbilityAttr.ABILITY_1 : 0)
| (tempCaughtAttr & DexAttr.VARIANT_2 && species.ability2 ? AbilityAttr.ABILITY_2 : 0)
| (tempCaughtAttr & DexAttr.VARIANT_3 && species.abilityHidden ? AbilityAttr.ABILITY_HIDDEN : 0);
} else {
const tempCaughtAttr = dexData[s].caughtAttr;
if (dexData[s].caughtAttr & DexAttr.VARIANT_2) {
dexData[s].caughtAttr ^= DexAttr.VARIANT_2;
}
if (dexData[s].caughtAttr & DexAttr.VARIANT_3) {
dexData[s].caughtAttr ^= DexAttr.VARIANT_3;
}
starterData[s].abilityAttr = (tempCaughtAttr & DexAttr.DEFAULT_VARIANT ? AbilityAttr.ABILITY_1 : 0)
| (tempCaughtAttr & DexAttr.VARIANT_2 && species.ability2 ? AbilityAttr.ABILITY_2 : 0)
| (tempCaughtAttr & DexAttr.VARIANT_3 && species.abilityHidden ? AbilityAttr.ABILITY_HIDDEN : 0);
}
}
}
}
fixStarterData(systemData: SystemSaveData): void {
for (const starterId of defaultStarterSpecies) {
systemData.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
systemData.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
fixLegendaryStats(systemData: SystemSaveData): void {
systemData.gameStats.subLegendaryPokemonSeen = 0;
systemData.gameStats.subLegendaryPokemonCaught = 0;
systemData.gameStats.subLegendaryPokemonHatched = 0;
allSpecies.filter(s => s.subLegendary).forEach(s => {
const dexEntry = systemData.dexData[s.speciesId];
systemData.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount;
systemData.gameStats.legendaryPokemonSeen = Math.max(systemData.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0);
systemData.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount;
systemData.gameStats.legendaryPokemonCaught = Math.max(systemData.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0);
systemData.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount;
systemData.gameStats.legendaryPokemonHatched = Math.max(systemData.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0);
});
systemData.gameStats.subLegendaryPokemonSeen = Math.max(systemData.gameStats.subLegendaryPokemonSeen, systemData.gameStats.subLegendaryPokemonCaught);
systemData.gameStats.legendaryPokemonSeen = Math.max(systemData.gameStats.legendaryPokemonSeen, systemData.gameStats.legendaryPokemonCaught);
systemData.gameStats.mythicalPokemonSeen = Math.max(systemData.gameStats.mythicalPokemonSeen, systemData.gameStats.mythicalPokemonCaught);
}
} }

View File

@ -3,11 +3,11 @@ import { PersistentModifier } from "../modifier/modifier";
import { GeneratedPersistentModifierType, ModifierType, ModifierTypeGenerator, getModifierTypeFuncById } from "../modifier/modifier-type"; import { GeneratedPersistentModifierType, ModifierType, ModifierTypeGenerator, getModifierTypeFuncById } from "../modifier/modifier-type";
export default class ModifierData { export default class ModifierData {
private player: boolean; public player: boolean;
private typeId: string; public typeId: string;
private typePregenArgs: any[]; public typePregenArgs: any[];
private args: any[]; public args: any[];
private stackCount: integer; public stackCount: integer;
public className: string; public className: string;

View File

@ -0,0 +1,137 @@
import { allSpecies } from "#app/data/pokemon-species.js";
import { AbilityAttr, defaultStarterSpecies, DexAttr, SessionSaveData, SystemSaveData } from "./game-data";
import { SettingKeys } from "./settings/settings";
const LATEST_VERSION = "1.0.5";
export function applySessionDataPatches(data: SessionSaveData) {
const curVersion = data.gameVersion;
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- PATCHES ---
// Fix Battle Items, Vitamins, and Lures
data.modifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
} else if (m.className === "TempBattleStatBoosterModifier") {
m.className = "TempStatStageBoosterModifier";
m.typeId = "TEMP_STAT_STAGE_BOOSTER";
// Migration from TempBattleStat to Stat
const newStat = m.typePregenArgs[0] + 1;
m.typePregenArgs[0] = newStat;
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, 5, m.args[1] ];
} else if (m.className === "DoubleBattleChanceBoosterModifier") {
let maxBattles: number;
switch (m.typeId) {
case "MAX_LURE":
maxBattles = 30;
break;
case "SUPER_LURE":
maxBattles = 15;
break;
default:
maxBattles = 10;
break;
}
// From [ battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, m.args[0] ];
}
});
data.enemyModifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
}
});
}
data.gameVersion = LATEST_VERSION;
}
}
export function applySystemDataPatches(data: SystemSaveData) {
const curVersion = data.gameVersion;
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- LEGACY PATCHES ---
if (data.starterData) {
// Migrate ability starter data if empty for caught species
Object.keys(data.starterData).forEach(sd => {
if (data.dexData[sd].caughtAttr && !data.starterData[sd].abilityAttr) {
data.starterData[sd].abilityAttr = 1;
}
});
}
// Fix Legendary Stats
if (data.gameStats && (data.gameStats.legendaryPokemonCaught !== undefined && data.gameStats.subLegendaryPokemonCaught === undefined)) {
data.gameStats.subLegendaryPokemonSeen = 0;
data.gameStats.subLegendaryPokemonCaught = 0;
data.gameStats.subLegendaryPokemonHatched = 0;
allSpecies.filter(s => s.subLegendary).forEach(s => {
const dexEntry = data.dexData[s.speciesId];
data.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount;
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0);
data.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount;
data.gameStats.legendaryPokemonCaught = Math.max(data.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0);
data.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount;
data.gameStats.legendaryPokemonHatched = Math.max(data.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0);
});
data.gameStats.subLegendaryPokemonSeen = Math.max(data.gameStats.subLegendaryPokemonSeen, data.gameStats.subLegendaryPokemonCaught);
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen, data.gameStats.legendaryPokemonCaught);
data.gameStats.mythicalPokemonSeen = Math.max(data.gameStats.mythicalPokemonSeen, data.gameStats.mythicalPokemonCaught);
}
// --- PATCHES ---
// Fix Starter Data
if (data.gameVersion) {
for (const starterId of defaultStarterSpecies) {
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
}
data.gameVersion = LATEST_VERSION;
}
}
export function applySettingsDataPatches(settings: Object) {
const curVersion = settings.hasOwnProperty("gameVersion") ? settings["gameVersion"] : "1.0.0";
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- PATCHES ---
// Fix Reward Cursor Target
if (settings.hasOwnProperty("REROLL_TARGET") && !settings.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
settings[SettingKeys.Shop_Cursor_Target] = settings["REROLL_TARGET"];
delete settings["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(settings));
}
}
// Note that the current game version will be written at `saveSettings`
}
}