BUGFIX: More savegame loading fixes (#543)

* Fix loading issues back to pre-1.0
* Be more robust about issues with files not being maps
* Avoid non-fatal error when there's no LastExportBonus
This commit is contained in:
David Walker 2023-05-29 04:10:26 -07:00 committed by GitHub
parent 5f2a1c3f27
commit e51527aa86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 71 deletions

@ -26,6 +26,7 @@ import { save } from "./db";
import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak"; import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
import { initAugmentations } from "./Augmentation/AugmentationHelpers";
import { LocationName } from "./Enums"; import { LocationName } from "./Enums";
import { pushGameSaved } from "./Electron"; import { pushGameSaved } from "./Electron";
import { defaultMonacoTheme } from "./ScriptEditor/ui/themes"; import { defaultMonacoTheme } from "./ScriptEditor/ui/themes";
@ -34,12 +35,6 @@ import { Faction } from "./Faction/Faction";
import { safelyCreateUniqueServer } from "./Server/ServerHelpers"; import { safelyCreateUniqueServer } from "./Server/ServerHelpers";
import { SpecialServers } from "./Server/data/SpecialServers"; import { SpecialServers } from "./Server/data/SpecialServers";
import { v2APIBreak } from "./utils/v2APIBreak"; import { v2APIBreak } from "./utils/v2APIBreak";
import { Script } from "./Script/Script";
import { JSONMap } from "./Types/Jsonable";
import { TextFile } from "./TextFile";
import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath";
import { Directory, resolveDirectory } from "./Paths/Directory";
import { TextFilePath, resolveTextFilePath } from "./Paths/TextFilePath";
import { Corporation } from "./Corporation/Corporation"; import { Corporation } from "./Corporation/Corporation";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
@ -87,7 +82,7 @@ class BitburnerSaveObject {
SettingsSave = ""; SettingsSave = "";
VersionSave = ""; VersionSave = "";
AllGangsSave = ""; AllGangsSave = "";
LastExportBonus = ""; LastExportBonus = "0";
StaneksGiftSave = ""; StaneksGiftSave = "";
getSaveString(forceExcludeRunningScripts = false): string { getSaveString(forceExcludeRunningScripts = false): string {
@ -355,8 +350,8 @@ function evaluateVersionCompatibility(ver: string | number): void {
[/purchase4SMarketData/g, "stock.purchase4SMarketData"], [/purchase4SMarketData/g, "stock.purchase4SMarketData"],
[/purchase4SMarketDataTixApi/g, "stock.purchase4SMarketDataTixApi"], [/purchase4SMarketDataTixApi/g, "stock.purchase4SMarketDataTixApi"],
]; ];
for (const server of GetAllServers() as unknown as { scripts: Script[] }[]) { for (const server of GetAllServers()) {
for (const script of server.scripts) { for (const script of server.scripts.values()) {
script.content = convert(script.code, changes); script.content = convert(script.code, changes);
} }
} }
@ -367,7 +362,7 @@ function evaluateVersionCompatibility(ver: string | number): void {
if (typeof ver !== "number") return; if (typeof ver !== "number") return;
if (ver < 2) { if (ver < 2) {
AwardNFG(10); AwardNFG(10);
Player.reapplyAllAugmentations(true); initAugmentations();
Player.reapplyAllSourceFiles(); Player.reapplyAllSourceFiles();
} }
if (ver < 3) { if (ver < 3) {
@ -450,7 +445,7 @@ function evaluateVersionCompatibility(ver: string | number): void {
]; ];
v22PlayerBreak(); v22PlayerBreak();
Player.reapplyAllAugmentations(true); initAugmentations();
Player.reapplyAllSourceFiles(); Player.reapplyAllSourceFiles();
} }
@ -478,51 +473,6 @@ function evaluateVersionCompatibility(ver: string | number): void {
const graft = anyPlayer.graftAugmentationName; const graft = anyPlayer.graftAugmentationName;
if (graft) Player.augmentations.push({ name: graft, level: 1 }); if (graft) Player.augmentations.push({ name: graft, level: 1 });
} }
if (ver < 31) {
// This section of 2.3.0 changes is intentionally OUT-OF-ORDER.
// This is because it upgrades how files and paths are handled, and other
// (old) upgrade sections have come to depend on the new format, via
// standard helper functions.
// Other 2.3.0 changes are in their regular place.
Terminal.warn("Migrating to 2.3.0, loading with no scripts.");
const newDirectory = resolveDirectory("v2.3FileChanges/") as Directory;
for (const server of GetAllServers()) {
// Do not load saved scripts on migration
server.savedScripts = [];
let invalidScriptCount = 0;
// There was a brief dev window where Server.scripts was already a map but the filepath changes weren't in yet.
const oldScripts = Array.isArray(server.scripts) ? (server.scripts as Script[]) : [...server.scripts.values()];
server.scripts = new JSONMap();
// In case somehow there are previously valid filenames that can't be sanitized, they will go in a new directory with a note.
for (const script of oldScripts) {
let newFilePath = resolveScriptFilePath(script.filename);
if (!newFilePath) {
newFilePath = `${newDirectory}script${++invalidScriptCount}.js` as ScriptFilePath;
script.content = `// Original path: ${script.filename}. Path was no longer valid\n` + script.content;
}
script.filename = newFilePath;
server.scripts.set(newFilePath, script);
}
// Handle changing textFiles to a map as well as FilePath changes at the same time.
if (Array.isArray(server.textFiles)) {
const oldTextFiles = server.textFiles as (TextFile & { fn?: string })[];
server.textFiles = new JSONMap();
let invalidTextCount = 0;
for (const textFile of oldTextFiles) {
const oldName = textFile.fn ?? textFile.filename;
delete textFile.fn;
let newFilePath = resolveTextFilePath(oldName);
if (!newFilePath) {
newFilePath = `${newDirectory}text${++invalidTextCount}.txt` as TextFilePath;
textFile.content = `// Original path: ${textFile.filename}. Path was no longer valid\n` + textFile.content;
}
textFile.filename = newFilePath;
server.textFiles.set(newFilePath, textFile);
}
}
}
}
if (ver < 22) { if (ver < 22) {
v22PlayerBreak(); v22PlayerBreak();
v2APIBreak(); v2APIBreak();
@ -703,6 +653,11 @@ function evaluateVersionCompatibility(ver: string | number): void {
for (const sleeve of Player.sleeves) sleeve.shock = 100 - sleeve.shock; for (const sleeve of Player.sleeves) sleeve.shock = 100 - sleeve.shock;
} }
if (ver < 31) { if (ver < 31) {
Terminal.warn("Migrating to 2.3.0, loading with no scripts.");
for (const server of GetAllServers()) {
// Do not load any saved scripts on migration
server.savedScripts = [];
}
if (anyPlayer.hashManager?.upgrades) { if (anyPlayer.hashManager?.upgrades) {
anyPlayer.hashManager.upgrades["Company Favor"] ??= 0; anyPlayer.hashManager.upgrades["Company Favor"] ??= 0;
} }

@ -5,8 +5,9 @@ import { Script } from "../Script/Script";
import { TextFile } from "../TextFile"; import { TextFile } from "../TextFile";
import { IReturnStatus } from "../types"; import { IReturnStatus } from "../types";
import { ScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath"; import { ScriptFilePath, resolveScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath";
import { TextFilePath, hasTextExtension } from "../Paths/TextFilePath"; import { Directory, resolveDirectory } from "../Paths/Directory";
import { TextFilePath, resolveTextFilePath, hasTextExtension } from "../Paths/TextFilePath";
import { Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; import { Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { matchScriptPathExact } from "../utils/helpers/scriptKey"; import { matchScriptPathExact } from "../utils/helpers/scriptKey";
@ -313,9 +314,54 @@ export abstract class BaseServer implements IServer {
// Initializes a Server Object from a JSON save state // Initializes a Server Object from a JSON save state
// Called by subclasses, not Reviver. // Called by subclasses, not Reviver.
static fromJSONBase<T extends BaseServer>(value: IReviverValue, ctor: new () => T, keys: readonly (keyof T)[]): T { static fromJSONBase<T extends BaseServer>(value: IReviverValue, ctor: new () => T, keys: readonly (keyof T)[]): T {
const result = Generic_fromJSON(ctor, value.data, keys); const server = Generic_fromJSON(ctor, value.data, keys);
result.savedScripts = value.data.runningScripts; server.savedScripts = value.data.runningScripts;
return result; // If textFiles is not an array, we've already done the 2.3 migration to textFiles and scripts as maps + path changes.
if (!Array.isArray(server.textFiles)) return server;
// Migrate to using maps for scripts and textfiles. This is done here, directly at load, instead of the
// usual upgrade logic, for two reasons:
// 1) Our utility functions depend on it, so the upgrade logic itself needs the data to be in maps, even the logic
// written earlier than 2.3!
// 2) If the upgrade logic throws, and then you soft-reset at the recovery screen (or maybe don't even see the
// recovery screen), you can end up with a "migrated" save that still has arrays.
const newDirectory = resolveDirectory("v2.3FileChanges/") as Directory;
let invalidScriptCount = 0;
// There was a brief dev window where Server.scripts was already a map but the filepath changes weren't in yet.
// Thus, we can't skip this logic just because it's already a map.
const oldScripts = Array.isArray(server.scripts) ? (server.scripts as Script[]) : [...server.scripts.values()];
server.scripts = new JSONMap();
// In case somehow there are previously valid filenames that can't be sanitized, they will go in a new directory with a note.
for (const script of oldScripts) {
let newFilePath = resolveScriptFilePath(script.filename);
if (!newFilePath) {
newFilePath = `${newDirectory}script${++invalidScriptCount}.js` as ScriptFilePath;
script.content = `// Original path: ${script.filename}. Path was no longer valid\n` + script.content;
}
script.filename = newFilePath;
server.scripts.set(newFilePath, script);
}
let invalidTextCount = 0;
const oldTextFiles = server.textFiles as (TextFile & { fn?: string })[];
server.textFiles = new JSONMap();
for (const textFile of oldTextFiles) {
const oldName = textFile.fn ?? textFile.filename;
delete textFile.fn;
let newFilePath = resolveTextFilePath(oldName);
if (!newFilePath) {
newFilePath = `${newDirectory}text${++invalidTextCount}.txt` as TextFilePath;
textFile.content = `// Original path: ${textFile.filename}. Path was no longer valid\n` + textFile.content;
}
textFile.filename = newFilePath;
server.textFiles.set(newFilePath, textFile);
}
if (invalidScriptCount || invalidTextCount) {
// If we had to migrate names, don't run scripts for this server.
server.savedScripts = [];
}
return server;
} }
// Customize a prune list for a subclass. // Customize a prune list for a subclass.

@ -98,11 +98,8 @@ export function v1APIBreak(): void {
for (const server of GetAllServers()) { for (const server of GetAllServers()) {
for (const change of detect) { for (const change of detect) {
const s: IFileLine[] = []; const s: IFileLine[] = [];
const scriptsArray: Script[] = Array.isArray(server.scripts)
? (server.scripts as Script[])
: [...server.scripts.values()];
for (const script of scriptsArray) { for (const script of server.scripts.values()) {
const lines = script.code.split("\n"); const lines = script.code.split("\n");
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(change[0])) { if (lines[i].includes(change[0])) {
@ -130,10 +127,8 @@ export function v1APIBreak(): void {
home.writeToTextFile(textPath, txt); home.writeToTextFile(textPath, txt);
} }
// API break function is called before version31 / 2.3.0 changes - scripts is still an array for (const server of GetAllServers()) {
for (const server of GetAllServers() as unknown as { scripts: Script[] }[]) { for (const script of server.scripts.values()) {
const backups: Script[] = [];
for (const script of server.scripts) {
if (!hasChanges(script.code)) continue; if (!hasChanges(script.code)) continue;
// Sanitize first before combining // Sanitize first before combining
const oldFilename = resolveScriptFilePath(script.filename); const oldFilename = resolveScriptFilePath(script.filename);
@ -142,9 +137,8 @@ export function v1APIBreak(): void {
console.error(`Unexpected error resolving backup path for ${script.filename}`); console.error(`Unexpected error resolving backup path for ${script.filename}`);
continue; continue;
} }
backups.push(new Script(filename, script.code, script.server)); server.scripts.set(filename, new Script(filename, script.code, script.server));
script.code = convert(script.code); script.code = convert(script.code);
} }
server.scripts = server.scripts.concat(backups);
} }
} }