mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-22 22:22:26 +01:00
Move import save functionality into SaveObject
Adds a lot of metadata to be able to easily compare the new save to the current player. - Adds lastSaved & identifier to the PlayerObject - Includes a hash method used to generate an identifier
This commit is contained in:
parent
588c3e42d7
commit
974344545d
@ -35,7 +35,9 @@ import { CityName } from "../../Locations/data/CityNames";
|
|||||||
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
||||||
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
||||||
import { ISkillProgress } from "../formulas/skill";
|
import { ISkillProgress } from "../formulas/skill";
|
||||||
import { PlayerAchievement } from '../../Achievements/Achievements';
|
import { PlayerAchievement } from "../../Achievements/Achievements";
|
||||||
|
import { cyrb53 } from "../../utils/StringHelperFunctions";
|
||||||
|
import { getRandomInt } from "../../utils/helpers/getRandomInt";
|
||||||
|
|
||||||
export class PlayerObject implements IPlayer {
|
export class PlayerObject implements IPlayer {
|
||||||
// Class members
|
// Class members
|
||||||
@ -78,7 +80,9 @@ export class PlayerObject implements IPlayer {
|
|||||||
exploits: Exploit[];
|
exploits: Exploit[];
|
||||||
achievements: PlayerAchievement[];
|
achievements: PlayerAchievement[];
|
||||||
terminalCommandHistory: string[];
|
terminalCommandHistory: string[];
|
||||||
|
identifier: string;
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
|
lastSave: number;
|
||||||
totalPlaytime: number;
|
totalPlaytime: number;
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
@ -460,7 +464,9 @@ export class PlayerObject implements IPlayer {
|
|||||||
|
|
||||||
//Used to store the last update time.
|
//Used to store the last update time.
|
||||||
this.lastUpdate = 0;
|
this.lastUpdate = 0;
|
||||||
|
this.lastSave = 0;
|
||||||
this.totalPlaytime = 0;
|
this.totalPlaytime = 0;
|
||||||
|
|
||||||
this.playtimeSinceLastAug = 0;
|
this.playtimeSinceLastAug = 0;
|
||||||
this.playtimeSinceLastBitnode = 0;
|
this.playtimeSinceLastBitnode = 0;
|
||||||
|
|
||||||
@ -474,6 +480,16 @@ export class PlayerObject implements IPlayer {
|
|||||||
this.achievements = [];
|
this.achievements = [];
|
||||||
this.terminalCommandHistory = [];
|
this.terminalCommandHistory = [];
|
||||||
|
|
||||||
|
// Let's get a hash of some semi-random stuff so we have something unique.
|
||||||
|
this.identifier = cyrb53(
|
||||||
|
"I-" +
|
||||||
|
new Date().getTime() +
|
||||||
|
navigator.userAgent +
|
||||||
|
window.innerWidth +
|
||||||
|
window.innerHeight +
|
||||||
|
getRandomInt(100, 999),
|
||||||
|
);
|
||||||
|
|
||||||
this.init = generalMethods.init;
|
this.init = generalMethods.init;
|
||||||
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
||||||
this.prestigeSourceFile = generalMethods.prestigeSourceFile;
|
this.prestigeSourceFile = generalMethods.prestigeSourceFile;
|
||||||
|
@ -23,11 +23,35 @@ import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
|||||||
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
|
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
|
||||||
import { LocationName } from "./Locations/data/LocationNames";
|
import { LocationName } from "./Locations/data/LocationNames";
|
||||||
import { SxProps } from "@mui/system";
|
import { SxProps } from "@mui/system";
|
||||||
|
import { PlayerObject } from "./PersonObjects/Player/PlayerObject";
|
||||||
|
|
||||||
/* SaveObject.js
|
/* SaveObject.js
|
||||||
* Defines the object used to save/load games
|
* Defines the object used to save/load games
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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 {
|
class BitburnerSaveObject {
|
||||||
PlayerSave = "";
|
PlayerSave = "";
|
||||||
AllServersSave = "";
|
AllServersSave = "";
|
||||||
@ -42,7 +66,6 @@ class BitburnerSaveObject {
|
|||||||
AllGangsSave = "";
|
AllGangsSave = "";
|
||||||
LastExportBonus = "";
|
LastExportBonus = "";
|
||||||
StaneksGiftSave = "";
|
StaneksGiftSave = "";
|
||||||
SaveTimestamp = "";
|
|
||||||
|
|
||||||
getSaveString(excludeRunningScripts = false): string {
|
getSaveString(excludeRunningScripts = false): string {
|
||||||
this.PlayerSave = JSON.stringify(Player);
|
this.PlayerSave = JSON.stringify(Player);
|
||||||
@ -58,7 +81,6 @@ class BitburnerSaveObject {
|
|||||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||||
this.SaveTimestamp = new Date().getTime().toString();
|
|
||||||
|
|
||||||
if (Player.inGang()) {
|
if (Player.inGang()) {
|
||||||
this.AllGangsSave = JSON.stringify(AllGangs);
|
this.AllGangsSave = JSON.stringify(AllGangs);
|
||||||
@ -68,28 +90,125 @@ class BitburnerSaveObject {
|
|||||||
return saveString;
|
return saveString;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveGame(emitToastEvent = true): void {
|
saveGame(emitToastEvent = true): Promise<void> {
|
||||||
|
Player.lastSave = new Date().getTime();
|
||||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
save(saveString)
|
||||||
|
.then(() => {
|
||||||
|
if (emitToastEvent) {
|
||||||
|
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
save(saveString)
|
getSaveFileName(isRecovery = false): string {
|
||||||
.then(() => {
|
// Save file name is based on current timestamp and BitNode
|
||||||
if (emitToastEvent) {
|
const epochTime = Math.round(Date.now() / 1000);
|
||||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
const bn = Player.bitNodeN;
|
||||||
}
|
let filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
|
||||||
})
|
if (isRecovery) filename = "RECOVERY" + filename;
|
||||||
.catch((err) => console.error(err));
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportGame(): void {
|
exportGame(): void {
|
||||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||||
|
const filename = this.getSaveFileName();
|
||||||
// Save file name is based on current timestamp and BitNode
|
|
||||||
const epochTime = Math.round(Date.now() / 1000);
|
|
||||||
const bn = Player.bitNodeN;
|
|
||||||
const filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
|
|
||||||
download(filename, saveString);
|
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 {
|
toJSON(): any {
|
||||||
return Generic_toJSON("BitburnerSaveObject", this);
|
return Generic_toJSON("BitburnerSaveObject", this);
|
||||||
}
|
}
|
||||||
|
@ -25,20 +25,19 @@ import SaveIcon from "@mui/icons-material/Save";
|
|||||||
import PaletteIcon from '@mui/icons-material/Palette';
|
import PaletteIcon from '@mui/icons-material/Palette';
|
||||||
|
|
||||||
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
||||||
import { dialogBoxCreate } from "./DialogBox";
|
|
||||||
import { ConfirmationModal } from "./ConfirmationModal";
|
import { ConfirmationModal } from "./ConfirmationModal";
|
||||||
|
|
||||||
import { SnackbarEvents } from "./Snackbar";
|
import { SnackbarEvents } from "./Snackbar";
|
||||||
|
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
import { save } from "../../db";
|
|
||||||
import { formatTime } from "../../utils/helpers/formatTime";
|
|
||||||
import { OptionSwitch } from "./OptionSwitch";
|
|
||||||
import { DeleteGameButton } from "./DeleteGameButton";
|
import { DeleteGameButton } from "./DeleteGameButton";
|
||||||
import { SoftResetButton } from "./SoftResetButton";
|
import { SoftResetButton } from "./SoftResetButton";
|
||||||
import { IRouter } from "../Router";
|
import { IRouter } from "../Router";
|
||||||
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
|
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
|
||||||
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
|
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
|
||||||
|
import { formatTime } from "../../utils/helpers/formatTime";
|
||||||
|
import { OptionSwitch } from "./OptionSwitch";
|
||||||
|
import { saveObject } from "../../SaveObject";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@ -122,78 +121,28 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
|||||||
ii.click();
|
ii.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImport(event: React.ChangeEvent<HTMLInputElement>): void {
|
async function onImport(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
||||||
const files = event.target.files;
|
try {
|
||||||
if (files === null) return;
|
const base64Save = await saveObject.getImportStringFromFile(event.target.files);
|
||||||
const file = files[0];
|
const data = await saveObject.getImportDataFromString(base64Save);
|
||||||
if (!file) {
|
setImportData(data)
|
||||||
dialogBoxCreate("Invalid file selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (this: FileReader, e: ProgressEvent<FileReader>) {
|
|
||||||
const target = e.target;
|
|
||||||
if (target === null) {
|
|
||||||
console.error("error importing file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = target.result;
|
|
||||||
if (typeof result !== "string" || result === null) {
|
|
||||||
console.error("FileReader event was not type string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const contents = result;
|
|
||||||
|
|
||||||
let newSave;
|
|
||||||
try {
|
|
||||||
newSave = window.atob(contents);
|
|
||||||
newSave = newSave.trim();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error); // We'll handle below
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newSave || newSave === "") {
|
|
||||||
SnackbarEvents.emit("Save game had not content or was not base64 encoded", "error", 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsedSave;
|
|
||||||
try {
|
|
||||||
parsedSave = JSON.parse(newSave);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error); // We'll handle below
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedSave || parsedSave.ctor !== "BitburnerSaveObject" || !parsedSave.data) {
|
|
||||||
SnackbarEvents.emit("Save game did not seem valid", "error", 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: ImportData = {
|
|
||||||
base64: contents,
|
|
||||||
parsed: parsedSave,
|
|
||||||
};
|
|
||||||
|
|
||||||
const timestamp = parsedSave.data.SaveTimestamp;
|
|
||||||
if (timestamp && timestamp !== "0") {
|
|
||||||
data.exportDate = new Date(parseInt(timestamp, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
setImportData(data);
|
|
||||||
setImportSaveOpen(true);
|
setImportSaveOpen(true);
|
||||||
};
|
} catch (ex: any) {
|
||||||
reader.readAsText(file);
|
SnackbarEvents.emit(ex.toString(), "error", 5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmedImportGame(): void {
|
async function confirmedImportGame(): Promise<void> {
|
||||||
if (!importData) return;
|
if (!importData) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveObject.importGame(importData.base64);
|
||||||
|
} catch (ex: any) {
|
||||||
|
SnackbarEvents.emit(ex.toString(), "error", 5000);
|
||||||
|
}
|
||||||
|
|
||||||
setImportSaveOpen(false);
|
setImportSaveOpen(false);
|
||||||
save(importData.base64).then(() => {
|
setImportData(null);
|
||||||
setImportData(null);
|
|
||||||
setTimeout(() => location.reload(), 1000);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -97,4 +97,31 @@ function generateRandomString(n: number): string {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { convertTimeMsToTimeElapsedString, longestCommonStart, containsAllStrings, formatNumber, generateRandomString };
|
/**
|
||||||
|
* Hashes the input string. This is a fast hash, so NOT good for cryptography.
|
||||||
|
* This has been ripped off here: https://stackoverflow.com/a/52171480
|
||||||
|
* @param str The string that is to be hashed
|
||||||
|
* @param seed A seed to randomize the result
|
||||||
|
* @returns An hexadecimal string representation of the hashed input
|
||||||
|
*/
|
||||||
|
function cyrb53(str: string, seed = 0): string {
|
||||||
|
let h1 = 0xdeadbeef ^ seed;
|
||||||
|
let h2 = 0x41c6ce57 ^ seed;
|
||||||
|
for (let i = 0, ch; i < str.length; i++) {
|
||||||
|
ch = str.charCodeAt(i);
|
||||||
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||||
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||||
|
}
|
||||||
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||||
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||||
|
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
convertTimeMsToTimeElapsedString,
|
||||||
|
longestCommonStart,
|
||||||
|
containsAllStrings,
|
||||||
|
formatNumber,
|
||||||
|
generateRandomString,
|
||||||
|
cyrb53,
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user