bitburner-src/src/SaveObject.tsx
Martin Fournier 855a4e622d Add Electron preload script to allow communication
Adds a channel to communicate between the main process & the renderer
process, so that the game can easily ship data back to the main process.

It uses the Electron contextBridge & ipcRenderer/ipcMain.

Connects those events to various save functions. Adds triggered events
on game save, game load, and imported game. Adds way for the Electron
app to ask for certain actions or data.

Hook handlers to disable automatic restore

Allows to temporarily disable restore when the game just did an import
or deleted a save game. Prevents looping screens.
2022-01-26 03:56:19 -05:00

574 lines
18 KiB
TypeScript
Executable File

import { loadAliases, loadGlobalAliases, Aliases, GlobalAliases } from "./Alias";
import { Companies, loadCompanies } from "./Company/Companies";
import { CONSTANTS } from "./Constants";
import { Factions, loadFactions } from "./Faction/Factions";
import { loadAllGangs, AllGangs } from "./Gang/AllGangs";
import { loadMessages, initMessages, Messages } from "./Message/MessageHelpers";
import { Player, loadPlayer } from "./Player";
import { saveAllServers, loadAllServers, GetAllServers } from "./Server/AllServers";
import { Settings } from "./Settings/Settings";
import { SourceFileFlags } from "./SourceFile/SourceFileFlags";
import { loadStockMarket, StockMarket } from "./StockMarket/StockMarket";
import { staneksGift, loadStaneksGift } from "./CotMG/Helper";
import { SnackbarEvents } from "./ui/React/Snackbar";
import * as ExportBonus from "./ExportBonus";
import { dialogBoxCreate } from "./ui/React/DialogBox";
import { Reviver, Generic_toJSON, Generic_fromJSON } from "./utils/JSONReviver";
import { save } from "./db";
import { v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
import { LocationName } from "./Locations/data/LocationNames";
import { SxProps } from "@mui/system";
import { PlayerObject } from "./PersonObjects/Player/PlayerObject";
import { pushGameSaved } from "./Electron";
/* SaveObject.js
* Defines the object used to save/load games
*/
export interface SaveData {
playerIdentifier: string;
fileName: string;
save: string;
savedOn: number;
}
export interface ImportData {
base64: string;
parsed: any;
playerData?: ImportPlayerData;
}
export interface ImportPlayerData {
identifier: string;
lastSave: number;
totalPlaytime: number;
money: number;
hacking: number;
augmentations: number;
factions: number;
achievements: number;
bitNode: number;
bitNodeLevel: number;
sourceFiles: number;
}
class BitburnerSaveObject {
PlayerSave = "";
AllServersSave = "";
CompaniesSave = "";
FactionsSave = "";
AliasesSave = "";
GlobalAliasesSave = "";
MessagesSave = "";
StockMarketSave = "";
SettingsSave = "";
VersionSave = "";
AllGangsSave = "";
LastExportBonus = "";
StaneksGiftSave = "";
getSaveString(excludeRunningScripts = false): string {
this.PlayerSave = JSON.stringify(Player);
this.AllServersSave = saveAllServers(excludeRunningScripts);
this.CompaniesSave = JSON.stringify(Companies);
this.FactionsSave = JSON.stringify(Factions);
this.AliasesSave = JSON.stringify(Aliases);
this.GlobalAliasesSave = JSON.stringify(GlobalAliases);
this.MessagesSave = JSON.stringify(Messages);
this.StockMarketSave = JSON.stringify(StockMarket);
this.SettingsSave = JSON.stringify(Settings);
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
this.StaneksGiftSave = JSON.stringify(staneksGift);
if (Player.inGang()) {
this.AllGangsSave = JSON.stringify(AllGangs);
}
const saveString = btoa(unescape(encodeURIComponent(JSON.stringify(this))));
return saveString;
}
saveGame(emitToastEvent = true): Promise<void> {
const savedOn = new Date().getTime();
Player.lastSave = savedOn;
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
return new Promise((resolve, reject) => {
save(saveString)
.then(() => {
const saveData: SaveData = {
playerIdentifier: Player.identifier,
fileName: this.getSaveFileName(),
save: saveString,
savedOn,
};
pushGameSaved(saveData);
if (emitToastEvent) {
SnackbarEvents.emit("Game Saved!", "info", 2000);
}
return resolve();
})
.catch((err) => {
console.error(err);
return reject();
});
});
}
getSaveFileName(isRecovery = false): string {
// Save file name is based on current timestamp and BitNode
const epochTime = Math.round(Date.now() / 1000);
const bn = Player.bitNodeN;
let filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
if (isRecovery) filename = "RECOVERY" + filename;
return filename;
}
exportGame(): void {
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
const filename = this.getSaveFileName();
download(filename, saveString);
}
importGame(base64Save: string, reload = true): Promise<void> {
if (!base64Save || base64Save === "") throw new Error("Invalid import string");
return save(base64Save).then(() => {
if (reload) setTimeout(() => location.reload(), 1000);
return Promise.resolve();
});
}
getImportStringFromFile(files: FileList | null): Promise<string> {
if (files === null) return Promise.reject(new Error("No file selected"));
const file = files[0];
if (!file) return Promise.reject(new Error("Invalid file selected"));
const reader = new FileReader();
const promise: Promise<string> = new Promise((resolve, reject) => {
reader.onload = function (this: FileReader, e: ProgressEvent<FileReader>) {
const target = e.target;
if (target === null) {
return reject(new Error("Error importing file"));
}
const result = target.result;
if (typeof result !== "string" || result === null) {
return reject(new Error("FileReader event was not type string"));
}
const contents = result;
resolve(contents);
};
});
reader.readAsText(file);
return promise;
}
async getImportDataFromString(base64Save: string): Promise<ImportData> {
if (!base64Save || base64Save === "") throw new Error("Invalid import string");
let newSave;
try {
newSave = window.atob(base64Save);
newSave = newSave.trim();
} catch (error) {
console.error(error); // We'll handle below
}
if (!newSave || newSave === "") {
return Promise.reject(new Error("Save game had not content or was not base64 encoded"));
}
let parsedSave;
try {
parsedSave = JSON.parse(newSave);
} catch (error) {
console.log(error); // We'll handle below
}
if (!parsedSave || parsedSave.ctor !== "BitburnerSaveObject" || !parsedSave.data) {
return Promise.reject(new Error("Save game did not seem valid"));
}
const data: ImportData = {
base64: base64Save,
parsed: parsedSave,
};
const importedPlayer = PlayerObject.fromJSON(JSON.parse(parsedSave.data.PlayerSave));
const playerData: ImportPlayerData = {
identifier: importedPlayer.identifier,
lastSave: importedPlayer.lastSave,
totalPlaytime: importedPlayer.totalPlaytime,
money: importedPlayer.money,
hacking: importedPlayer.hacking,
augmentations: importedPlayer.augmentations?.reduce<number>((total, current) => (total += current.level), 0) ?? 0,
factions: importedPlayer.factions?.length ?? 0,
achievements: importedPlayer.achievements?.length ?? 0,
bitNode: importedPlayer.bitNodeN,
bitNodeLevel: importedPlayer.sourceFileLvl(Player.bitNodeN) + 1,
sourceFiles: importedPlayer.sourceFiles?.reduce<number>((total, current) => (total += current.lvl), 0) ?? 0,
};
data.playerData = playerData;
return Promise.resolve(data);
}
toJSON(): any {
return Generic_toJSON("BitburnerSaveObject", this);
}
static fromJSON(value: { data: any }): BitburnerSaveObject {
return Generic_fromJSON(BitburnerSaveObject, value.data);
}
}
// Makes necessary changes to the loaded/imported data to ensure
// the game stills works with new versions
function evaluateVersionCompatibility(ver: string | number): void {
// We have to do this because ts won't let us otherwise
const anyPlayer = Player as any;
if (typeof ver === "string") {
// This version refactored the Company/job-related code
if (ver <= "0.41.2") {
// Player's company position is now a string
if (anyPlayer.companyPosition != null && typeof anyPlayer.companyPosition !== "string") {
anyPlayer.companyPosition = anyPlayer.companyPosition.data.positionName;
if (anyPlayer.companyPosition == null) {
anyPlayer.companyPosition = "";
}
}
// The "companyName" property of all Companies is renamed to "name"
for (const companyName of Object.keys(Companies)) {
const company: any = Companies[companyName];
if (company.name == 0 && company.companyName != null) {
company.name = company.companyName;
}
if (company.companyPositions instanceof Array) {
const pos: any = {};
for (let i = 0; i < company.companyPositions.length; ++i) {
pos[company.companyPositions[i]] = true;
}
company.companyPositions = pos;
}
}
}
// This version allowed players to hold multiple jobs
if (ver < "0.43.0") {
if (anyPlayer.companyName !== "" && anyPlayer.companyPosition != null && anyPlayer.companyPosition !== "") {
anyPlayer.jobs[anyPlayer.companyName] = anyPlayer.companyPosition;
}
delete anyPlayer.companyPosition;
}
if (ver < "0.56.0") {
for (const q of anyPlayer.queuedAugmentations) {
if (q.name === "Graphene BranchiBlades Upgrade") {
q.name = "Graphene BrachiBlades Upgrade";
}
}
for (const q of anyPlayer.augmentations) {
if (q.name === "Graphene BranchiBlades Upgrade") {
q.name = "Graphene BrachiBlades Upgrade";
}
}
}
if (ver < "0.56.1") {
if (anyPlayer.bladeburner === 0) {
anyPlayer.bladeburner = null;
}
if (anyPlayer.gang === 0) {
anyPlayer.gang = null;
}
if (anyPlayer.corporation === 0) {
anyPlayer.corporation = null;
}
// convert all Messages to just filename to save space.
const home = anyPlayer.getHomeComputer();
for (let i = 0; i < home.messages.length; i++) {
if (home.messages[i].filename) {
home.messages[i] = home.messages[i].filename;
}
}
}
if (ver < "0.58.0") {
const changes: [RegExp, string][] = [
[/getStockSymbols/g, "stock.getSymbols"],
[/getStockPrice/g, "stock.getPrice"],
[/getStockAskPrice/g, "stock.getAskPrice"],
[/getStockBidPrice/g, "stock.getBidPrice"],
[/getStockPosition/g, "stock.getPosition"],
[/getStockMaxShares/g, "stock.getMaxShares"],
[/getStockPurchaseCost/g, "stock.getPurchaseCost"],
[/getStockSaleGain/g, "stock.getSaleGain"],
[/buyStock/g, "stock.buy"],
[/sellStock/g, "stock.sell"],
[/shortStock/g, "stock.short"],
[/sellShort/g, "stock.sellShort"],
[/placeOrder/g, "stock.placeOrder"],
[/cancelOrder/g, "stock.cancelOrder"],
[/getOrders/g, "stock.getOrders"],
[/getStockVolatility/g, "stock.getVolatility"],
[/getStockForecast/g, "stock.getForecast"],
[/purchase4SMarketData/g, "stock.purchase4SMarketData"],
[/purchase4SMarketDataTixApi/g, "stock.purchase4SMarketDataTixApi"],
];
function convert(code: string): string {
for (const change of changes) {
code = code.replace(change[0], change[1]);
}
return code;
}
for (const server of GetAllServers()) {
for (const script of server.scripts) {
script.code = convert(script.code);
}
}
}
v1APIBreak();
ver = 1;
}
if (typeof ver === "number") {
if (ver < 2) {
// Give 10 neuroflux because v1 API break.
const nf = Player.augmentations.find((a) => a.name === AugmentationNames.NeuroFluxGovernor);
if (nf) {
nf.level += 10;
} else {
const nf = new PlayerOwnedAugmentation(AugmentationNames.NeuroFluxGovernor);
nf.level = 10;
Player.augmentations.push(nf);
}
Player.reapplyAllAugmentations(true);
Player.reapplyAllSourceFiles();
}
if (ver < 3) {
anyPlayer.money = parseFloat(anyPlayer.money);
if (anyPlayer.corporation) {
anyPlayer.corporation.funds = parseFloat(anyPlayer.corporation.funds);
anyPlayer.corporation.revenue = parseFloat(anyPlayer.corporation.revenue);
anyPlayer.corporation.expenses = parseFloat(anyPlayer.corporation.expenses);
for (let i = 0; i < anyPlayer.corporation.divisions.length; ++i) {
const ind = anyPlayer.corporation.divisions[i];
ind.lastCycleRevenue = parseFloat(ind.lastCycleRevenue);
ind.lastCycleExpenses = parseFloat(ind.lastCycleExpenses);
ind.thisCycleRevenue = parseFloat(ind.thisCycleRevenue);
ind.thisCycleExpenses = parseFloat(ind.thisCycleExpenses);
}
}
}
if (ver < 9) {
if (StockMarket.hasOwnProperty("Joes Guns")) {
const s = StockMarket["Joes Guns"];
delete StockMarket["Joes Guns"];
StockMarket[LocationName.Sector12JoesGuns] = s;
}
}
if (ver < 10) {
// Augmentation name was changed in 0.56.0 but sleeves aug list was missed.
if (anyPlayer.sleeves && anyPlayer.sleeves.length > 0) {
for (const sleeve of anyPlayer.sleeves) {
if (!sleeve.augmentations || sleeve.augmentations.length === 0) continue;
for (const augmentation of sleeve.augmentations) {
if (augmentation.name !== "Graphene BranchiBlades Upgrade") continue;
augmentation.name = "Graphene BrachiBlades Upgrade";
}
}
}
}
}
}
function loadGame(saveString: string): boolean {
createScamUpdateText();
if (!saveString) return false;
saveString = decodeURIComponent(escape(atob(saveString)));
const saveObj = JSON.parse(saveString, Reviver);
loadPlayer(saveObj.PlayerSave);
loadAllServers(saveObj.AllServersSave);
loadCompanies(saveObj.CompaniesSave);
loadFactions(saveObj.FactionsSave);
if (saveObj.hasOwnProperty("StaneksGiftSave")) {
loadStaneksGift(saveObj.StaneksGiftSave);
} else {
console.warn(`Could not load Staneks Gift from save`);
loadStaneksGift("");
}
if (saveObj.hasOwnProperty("AliasesSave")) {
try {
loadAliases(saveObj.AliasesSave);
} catch (e) {
console.warn(`Could not load Aliases from save`);
loadAliases("");
}
} else {
console.warn(`Save file did not contain an Aliases property`);
loadAliases("");
}
if (saveObj.hasOwnProperty("GlobalAliasesSave")) {
try {
loadGlobalAliases(saveObj.GlobalAliasesSave);
} catch (e) {
console.warn(`Could not load GlobalAliases from save`);
loadGlobalAliases("");
}
} else {
console.warn(`Save file did not contain a GlobalAliases property`);
loadGlobalAliases("");
}
if (saveObj.hasOwnProperty("MessagesSave")) {
try {
loadMessages(saveObj.MessagesSave);
} catch (e) {
console.warn(`Could not load Messages from save`);
initMessages();
}
} else {
console.warn(`Save file did not contain a Messages property`);
initMessages();
}
if (saveObj.hasOwnProperty("StockMarketSave")) {
try {
loadStockMarket(saveObj.StockMarketSave);
} catch (e) {
loadStockMarket("");
}
} else {
loadStockMarket("");
}
if (saveObj.hasOwnProperty("SettingsSave")) {
try {
Settings.load(saveObj.SettingsSave);
} catch (e) {
console.error("ERROR: Failed to parse Settings. Re-initing default values");
Settings.init();
}
} else {
Settings.init();
}
if (saveObj.hasOwnProperty("LastExportBonus")) {
try {
ExportBonus.setLastExportBonus(JSON.parse(saveObj.LastExportBonus));
} catch (err) {
ExportBonus.setLastExportBonus(new Date().getTime());
console.error("ERROR: Failed to parse last export bonus Settings " + err);
}
}
if (Player.inGang() && saveObj.hasOwnProperty("AllGangsSave")) {
try {
loadAllGangs(saveObj.AllGangsSave);
} catch (e) {
console.error("ERROR: Failed to parse AllGangsSave: " + e);
}
}
if (saveObj.hasOwnProperty("VersionSave")) {
try {
const ver = JSON.parse(saveObj.VersionSave, Reviver);
evaluateVersionCompatibility(ver);
if (window.location.href.toLowerCase().includes("bitburner-beta")) {
// Beta branch, always show changes
createBetaUpdateText();
} else if (ver !== CONSTANTS.VersionNumber) {
createNewUpdateText();
}
} catch (e) {
createNewUpdateText();
}
} else {
createNewUpdateText();
}
return true;
}
function createScamUpdateText(): void {
if (navigator.userAgent.indexOf("wv") !== -1 && navigator.userAgent.indexOf("Chrome/") !== -1) {
setInterval(() => {
dialogBoxCreate("SCAM ALERT. This app is not official and you should uninstall it.");
}, 1000);
}
}
const resets: SxProps = {
"& h1, & h2, & h3, & h4, & p, & a, & ul": {
margin: 0,
color: Settings.theme.primary,
whiteSpace: "initial",
},
"& ul": {
paddingLeft: "1.5em",
lineHeight: 1.5,
},
};
function createNewUpdateText(): void {
setTimeout(
() =>
dialogBoxCreate(
"New update!<br>" +
"Please report any bugs/issues through the github repository " +
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
CONSTANTS.LatestUpdate,
resets,
),
1000,
);
}
function createBetaUpdateText(): void {
dialogBoxCreate(
"You are playing on the beta environment! This branch of the game " +
"features the latest developments in the game. This version may be unstable.<br>" +
"Please report any bugs/issues through the github repository (https://github.com/danielyxie/bitburner/issues) " +
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
CONSTANTS.LatestUpdate,
resets,
);
}
function download(filename: string, content: string): void {
const file = new Blob([content], { type: "text/plain" });
const navigator = window.navigator as any;
if (navigator.msSaveOrOpenBlob) {
// IE10+
navigator.msSaveOrOpenBlob(file, filename);
} else {
// Others
const a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
}
Reviver.constructors.BitburnerSaveObject = BitburnerSaveObject;
export { saveObject, loadGame, download };
const saveObject = new BitburnerSaveObject();