mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
MISC: Support importing Steam Cloud save file manually (#2583)
This commit is contained in:
3
electron/saveDataBinaryFormat.d.ts
vendored
3
electron/saveDataBinaryFormat.d.ts
vendored
@@ -1 +1,4 @@
|
|||||||
|
export declare const encodeBytesToBase64String: (bytes: Uint8Array<ArrayBuffer>) => string;
|
||||||
|
export declare const decodeBase64BytesToBytes: (bytes: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>;
|
||||||
export declare const isBinaryFormat: (saveData: string | Uint8Array<ArrayBuffer>) => boolean;
|
export declare const isBinaryFormat: (saveData: string | Uint8Array<ArrayBuffer>) => boolean;
|
||||||
|
export declare const isSteamCloudFormat: (saveData: string | Uint8Array<ArrayBuffer>) => boolean;
|
||||||
|
|||||||
@@ -1,13 +1,68 @@
|
|||||||
// The 2 magic bytes of the gzip header plus the mandatory compression type of DEFLATE
|
// The 2 magic bytes of the gzip header plus the mandatory compression type of DEFLATE
|
||||||
const magicBytes = [0x1f, 0x8b, 0x08];
|
const magicBytesOfDeflateGzip = new Uint8Array([0x1f, 0x8b, 0x08]);
|
||||||
|
// Base64-encoded string of magicBytesOfDeflateGzip
|
||||||
|
const base64EncodingOfMagicBytes = encodeBytesToBase64String(magicBytesOfDeflateGzip);
|
||||||
|
// Convert the base64-encoded string to a byte array
|
||||||
|
const byteArrayOfBase64EncodingOfMagicBytes = Uint8Array.from(base64EncodingOfMagicBytes, (c) => c.charCodeAt(0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function encodeBytesToBase64String(bytes) {
|
||||||
|
let binaryString = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binaryString += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binaryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function decodeBase64BytesToBytes(bytes) {
|
||||||
|
let base64String = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
base64String += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
const decodedBinaryString = atob(base64String);
|
||||||
|
const result = new Uint8Array(decodedBinaryString.length);
|
||||||
|
for (let i = 0; i < decodedBinaryString.length; i++) {
|
||||||
|
result[i] = decodedBinaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | Uint8Array} rawData
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isBinaryFormat(rawData) {
|
function isBinaryFormat(rawData) {
|
||||||
for (let i = 0; i < magicBytes.length; ++i) {
|
for (let i = 0; i < magicBytesOfDeflateGzip.length; ++i) {
|
||||||
if (magicBytes[i] !== rawData[i]) {
|
if (magicBytesOfDeflateGzip[i] !== rawData[i]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { isBinaryFormat };
|
/**
|
||||||
|
* The Steam Cloud save file is a base64-encoded gz file.
|
||||||
|
*
|
||||||
|
* @param {string | Uint8Array} rawData
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isSteamCloudFormat(rawData) {
|
||||||
|
if (typeof rawData === "string") {
|
||||||
|
return rawData.startsWith(base64EncodingOfMagicBytes);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < byteArrayOfBase64EncodingOfMagicBytes.length; ++i) {
|
||||||
|
if (byteArrayOfBase64EncodingOfMagicBytes[i] !== rawData[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { encodeBytesToBase64String, decodeBase64BytesToBytes, isBinaryFormat, isSteamCloudFormat };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const fs = require("fs/promises");
|
|||||||
const log = require("electron-log");
|
const log = require("electron-log");
|
||||||
const flatten = require("lodash/flatten");
|
const flatten = require("lodash/flatten");
|
||||||
const Store = require("electron-store");
|
const Store = require("electron-store");
|
||||||
const { isBinaryFormat } = require("./saveDataBinaryFormat");
|
const { decodeBase64BytesToBytes, isBinaryFormat, isSteamCloudFormat } = require("./saveDataBinaryFormat");
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
const { steamworksClient } = require("./steamworksUtils");
|
const { steamworksClient } = require("./steamworksUtils");
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ async function getSteamCloudSaveData() {
|
|||||||
async function saveGameToDisk(window, electronGameData) {
|
async function saveGameToDisk(window, electronGameData) {
|
||||||
const currentFolder = getSaveFolder(window);
|
const currentFolder = getSaveFolder(window);
|
||||||
let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
|
let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
|
||||||
const maxFolderSizeBytes = store.get("autosave-quota", 1e8); // 100Mb per playerIndentifier
|
const maxFolderSizeBytes = store.get("autosave-quota", 1e8); // 100Mb
|
||||||
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
|
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
|
||||||
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
|
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
|
||||||
log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
|
log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
|
||||||
@@ -282,6 +282,9 @@ async function loadFileFromDisk(path) {
|
|||||||
if (isBinaryFormat(buffer)) {
|
if (isBinaryFormat(buffer)) {
|
||||||
// Save file is in the binary format.
|
// Save file is in the binary format.
|
||||||
content = buffer;
|
content = buffer;
|
||||||
|
} else if (isSteamCloudFormat(buffer)) {
|
||||||
|
// Save file is in the Steam Cloud format.
|
||||||
|
content = decodeBase64BytesToBytes(buffer);
|
||||||
} else {
|
} else {
|
||||||
// Save file is in the base64 format.
|
// Save file is in the base64 format.
|
||||||
content = buffer.toString("utf8");
|
content = buffer.toString("utf8");
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { pushGameSaved, pushImportResult } from "./Electron";
|
|||||||
import { getGoSave, loadGo } from "./Go/SaveLoad";
|
import { getGoSave, loadGo } from "./Go/SaveLoad";
|
||||||
import { SaveData } from "./types";
|
import { SaveData } from "./types";
|
||||||
import { SaveDataError, canUseBinaryFormat, decodeSaveData, encodeJsonSaveString } from "./utils/SaveDataUtils";
|
import { SaveDataError, canUseBinaryFormat, decodeSaveData, encodeJsonSaveString } from "./utils/SaveDataUtils";
|
||||||
import { isBinaryFormat } from "../electron/saveDataBinaryFormat";
|
import { decodeBase64BytesToBytes, isBinaryFormat, isSteamCloudFormat } from "../electron/saveDataBinaryFormat";
|
||||||
import { downloadContentAsFile } from "./utils/FileUtils";
|
import { downloadContentAsFile } from "./utils/FileUtils";
|
||||||
import { handleGetSaveDataInfoError } from "./utils/ErrorHandler";
|
import { handleGetSaveDataInfoError } from "./utils/ErrorHandler";
|
||||||
import { isObject, assertObject } from "./utils/TypeAssertion";
|
import { isObject, assertObject } from "./utils/TypeAssertion";
|
||||||
@@ -346,6 +346,9 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
|
|||||||
if (isBinaryFormat(rawData)) {
|
if (isBinaryFormat(rawData)) {
|
||||||
return rawData;
|
return rawData;
|
||||||
}
|
}
|
||||||
|
if (isSteamCloudFormat(rawData)) {
|
||||||
|
return decodeBase64BytesToBytes(rawData);
|
||||||
|
}
|
||||||
return new TextDecoder().decode(rawData);
|
return new TextDecoder().decode(rawData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const fs = require("fs").promises;
|
const fs = require("fs").promises;
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { isBinaryFormat } = require("../electron/saveDataBinaryFormat");
|
const { decodeBase64BytesToBytes, isBinaryFormat, isSteamCloudFormat } = require("../electron/saveDataBinaryFormat");
|
||||||
|
|
||||||
|
async function decompress(data) {
|
||||||
|
const decompressedReadableStream = new Blob([data]).stream().pipeThrough(new DecompressionStream("gzip"));
|
||||||
|
return await new Response(decompressedReadableStream).text();
|
||||||
|
}
|
||||||
|
|
||||||
async function getSave(file) {
|
async function getSave(file) {
|
||||||
const data = await fs.readFile(file);
|
const data = await fs.readFile(file);
|
||||||
|
|
||||||
let jsonSaveString;
|
let jsonSaveString;
|
||||||
if (isBinaryFormat(data)) {
|
if (isBinaryFormat(data)) {
|
||||||
const decompressedReadableStream = new Blob([data]).stream().pipeThrough(new DecompressionStream("gzip"));
|
jsonSaveString = await decompress(data);
|
||||||
jsonSaveString = await new Response(decompressedReadableStream).text();
|
} else if (isSteamCloudFormat(data)) {
|
||||||
|
jsonSaveString = await decompress(decodeBase64BytesToBytes(data));
|
||||||
} else {
|
} else {
|
||||||
jsonSaveString = decodeURIComponent(escape(atob(data)));
|
jsonSaveString = decodeURIComponent(escape(atob(data)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user