mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-22 14:12:27 +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 { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
||||
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 {
|
||||
// Class members
|
||||
@ -78,7 +80,9 @@ export class PlayerObject implements IPlayer {
|
||||
exploits: Exploit[];
|
||||
achievements: PlayerAchievement[];
|
||||
terminalCommandHistory: string[];
|
||||
identifier: string;
|
||||
lastUpdate: number;
|
||||
lastSave: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
// Stats
|
||||
@ -460,7 +464,9 @@ export class PlayerObject implements IPlayer {
|
||||
|
||||
//Used to store the last update time.
|
||||
this.lastUpdate = 0;
|
||||
this.lastSave = 0;
|
||||
this.totalPlaytime = 0;
|
||||
|
||||
this.playtimeSinceLastAug = 0;
|
||||
this.playtimeSinceLastBitnode = 0;
|
||||
|
||||
@ -474,6 +480,16 @@ export class PlayerObject implements IPlayer {
|
||||
this.achievements = [];
|
||||
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.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
||||
this.prestigeSourceFile = generalMethods.prestigeSourceFile;
|
||||
|
@ -23,11 +23,35 @@ 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";
|
||||
|
||||
/* SaveObject.js
|
||||
* 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 {
|
||||
PlayerSave = "";
|
||||
AllServersSave = "";
|
||||
@ -42,7 +66,6 @@ class BitburnerSaveObject {
|
||||
AllGangsSave = "";
|
||||
LastExportBonus = "";
|
||||
StaneksGiftSave = "";
|
||||
SaveTimestamp = "";
|
||||
|
||||
getSaveString(excludeRunningScripts = false): string {
|
||||
this.PlayerSave = JSON.stringify(Player);
|
||||
@ -58,7 +81,6 @@ class BitburnerSaveObject {
|
||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||
this.SaveTimestamp = new Date().getTime().toString();
|
||||
|
||||
if (Player.inGang()) {
|
||||
this.AllGangsSave = JSON.stringify(AllGangs);
|
||||
@ -68,28 +90,125 @@ class BitburnerSaveObject {
|
||||
return saveString;
|
||||
}
|
||||
|
||||
saveGame(emitToastEvent = true): void {
|
||||
saveGame(emitToastEvent = true): Promise<void> {
|
||||
Player.lastSave = new Date().getTime();
|
||||
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)
|
||||
.then(() => {
|
||||
if (emitToastEvent) {
|
||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
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);
|
||||
|
||||
// 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`;
|
||||
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);
|
||||
}
|
||||
|
@ -25,20 +25,19 @@ import SaveIcon from "@mui/icons-material/Save";
|
||||
import PaletteIcon from '@mui/icons-material/Palette';
|
||||
|
||||
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
||||
import { dialogBoxCreate } from "./DialogBox";
|
||||
import { ConfirmationModal } from "./ConfirmationModal";
|
||||
|
||||
import { SnackbarEvents } from "./Snackbar";
|
||||
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { save } from "../../db";
|
||||
import { formatTime } from "../../utils/helpers/formatTime";
|
||||
import { OptionSwitch } from "./OptionSwitch";
|
||||
import { DeleteGameButton } from "./DeleteGameButton";
|
||||
import { SoftResetButton } from "./SoftResetButton";
|
||||
import { IRouter } from "../Router";
|
||||
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
|
||||
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) =>
|
||||
createStyles({
|
||||
@ -122,78 +121,28 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
ii.click();
|
||||
}
|
||||
|
||||
function onImport(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const files = event.target.files;
|
||||
if (files === null) return;
|
||||
const file = files[0];
|
||||
if (!file) {
|
||||
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);
|
||||
async function onImport(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
||||
try {
|
||||
const base64Save = await saveObject.getImportStringFromFile(event.target.files);
|
||||
const data = await saveObject.getImportDataFromString(base64Save);
|
||||
setImportData(data)
|
||||
setImportSaveOpen(true);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} catch (ex: any) {
|
||||
SnackbarEvents.emit(ex.toString(), "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmedImportGame(): void {
|
||||
async function confirmedImportGame(): Promise<void> {
|
||||
if (!importData) return;
|
||||
|
||||
try {
|
||||
await saveObject.importGame(importData.base64);
|
||||
} catch (ex: any) {
|
||||
SnackbarEvents.emit(ex.toString(), "error", 5000);
|
||||
}
|
||||
|
||||
setImportSaveOpen(false);
|
||||
save(importData.base64).then(() => {
|
||||
setImportData(null);
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
});
|
||||
setImportData(null);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -97,4 +97,31 @@ function generateRandomString(n: number): string {
|
||||
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