UI: Add option to enable/disable syncing Steam achievements (#2117)

This commit is contained in:
catloversg
2025-05-10 16:13:41 +07:00
committed by GitHub
parent 290557332c
commit 494ef0dff3
13 changed files with 197 additions and 64 deletions
+92 -24
View File
@@ -65,6 +65,7 @@ export interface ImportPlayerData {
bitNodeLevel: number;
sourceFiles: number;
exploits: number;
syncSteamAchievements: boolean;
}
export type BitburnerSaveObjectType = {
@@ -83,6 +84,13 @@ export type BitburnerSaveObjectType = {
GoSave: unknown; // "loadGo" function can process unknown data
};
type ParsedSaveData = {
data: {
PlayerSave: string;
SettingsSave: unknown;
};
};
/**
* This function asserts the unknown saveObject.
*
@@ -136,6 +144,18 @@ function assertBitburnerSaveObjectType(saveObject: unknown): asserts saveObject
}
}
function assertParsedSaveData(parsedSaveData: unknown): asserts parsedSaveData is ParsedSaveData {
if (
!isObject(parsedSaveData) ||
parsedSaveData.ctor !== "BitburnerSaveObject" ||
!isObject(parsedSaveData.data) ||
typeof parsedSaveData.data.PlayerSave !== "string"
) {
console.error("parsedSaveData:", parsedSaveData);
throw new Error("The parsed save data is not valid.");
}
}
class BitburnerSaveObject implements BitburnerSaveObjectType {
PlayerSave = "";
AllServersSave = "";
@@ -230,9 +250,37 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
downloadContentAsFile(saveData, filename);
}
async importGame(saveData: SaveData, reload = true): Promise<void> {
async importGame(
saveData: SaveData,
overrideSettings?: {
SyncSteamAchievements: boolean;
},
): Promise<void> {
if (!saveData || saveData.length === 0) {
throw new Error("Invalid import string");
dialogBoxCreate("Invalid save data");
return;
}
// Modify settings in save data if needed (i.e., toggle SyncSteamAchievements before importing).
if (overrideSettings) {
let parsedSaveData;
try {
parsedSaveData = await this.getParsedSaveData(saveData);
// Validate SettingsSave
if (parsedSaveData.data.SettingsSave && typeof parsedSaveData.data.SettingsSave === "string") {
// Parse settings from data.SettingsSave
const settings: unknown = JSON.parse(parsedSaveData.data.SettingsSave);
assertObject(settings);
// Modify setting
settings.SyncSteamAchievements = overrideSettings.SyncSteamAchievements;
// Save modified data back to saveData
parsedSaveData.data.SettingsSave = JSON.stringify(settings);
saveData = await encodeJsonSaveString(JSON.stringify(parsedSaveData));
}
} catch (error) {
console.error(error);
dialogBoxCreate(`Cannot override settings: ${error}`);
return;
}
}
try {
await save(saveData);
@@ -246,58 +294,62 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
dialogBoxCreate(`Cannot import save data: ${error}`);
return;
}
if (reload) {
setTimeout(() => location.reload(), 1000);
}
setTimeout(() => location.reload(), 1000);
}
async getSaveDataFromFile(files: FileList | null): Promise<SaveData> {
if (files === null) return Promise.reject(new Error("No file selected"));
if (files === null) {
throw new Error("No file selected");
}
const file = files[0];
if (!file) return Promise.reject(new Error("Invalid file selected"));
if (!file) {
throw new Error("Invalid file selected");
}
const rawData = new Uint8Array(await file.arrayBuffer());
if (isBinaryFormat(rawData)) {
return rawData;
} else {
return new TextDecoder().decode(rawData);
}
return new TextDecoder().decode(rawData);
}
async getImportDataFromSaveData(saveData: SaveData): Promise<ImportData> {
if (!saveData || saveData.length === 0) throw new Error("Invalid save data");
async getParsedSaveData(saveData: SaveData): Promise<ParsedSaveData> {
if (!saveData || saveData.length === 0) {
throw new Error("Invalid save data");
}
let decodedSaveData;
try {
decodedSaveData = await decodeSaveData(saveData);
} catch (error) {
console.error(error);
// Rethrow immediately if the error is SaveDataError; otherwise, handle it below.
if (error instanceof SaveDataError) {
return Promise.reject(error);
throw error;
}
}
if (!decodedSaveData || decodedSaveData === "") {
console.error("decodedSaveData:", decodedSaveData);
return Promise.reject(new Error("Save game is invalid. The save data cannot be decoded."));
console.error("saveData:", saveData);
throw new Error("The save data cannot be decoded.");
}
let parsedSaveData: unknown;
try {
parsedSaveData = JSON.parse(decodedSaveData);
} catch (error) {
console.error(error); // We'll handle below
console.error("decodedSaveData:", decodedSaveData);
throw new Error("The decoded save data is not valid.");
}
if (
!isObject(parsedSaveData) ||
parsedSaveData.ctor !== "BitburnerSaveObject" ||
!isObject(parsedSaveData.data) ||
typeof parsedSaveData.data.PlayerSave !== "string"
) {
console.error("decodedSaveData:", decodedSaveData);
return Promise.reject(new Error("Save game is invalid. The decoded save data is not valid."));
}
assertParsedSaveData(parsedSaveData);
return parsedSaveData;
}
async getImportDataFromSaveData(saveData: SaveData): Promise<ImportData> {
const parsedSaveData = await this.getParsedSaveData(saveData);
const data: ImportData = {
saveData: saveData,
@@ -305,6 +357,20 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
const importedPlayer = loadPlayer(parsedSaveData.data.PlayerSave);
let syncSteamAchievements = true;
// Parse data.SettingsSave to get syncSteamAchievements.
if (parsedSaveData.data.SettingsSave && typeof parsedSaveData.data.SettingsSave === "string") {
try {
const settings: unknown = JSON.parse(parsedSaveData.data.SettingsSave);
assertObject(settings);
if (typeof settings.SyncSteamAchievements === "boolean") {
syncSteamAchievements = settings.SyncSteamAchievements;
}
} catch (error) {
console.error(error);
}
}
const playerData: ImportPlayerData = {
identifier: importedPlayer.identifier,
lastSave: importedPlayer.lastSave,
@@ -321,10 +387,12 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
bitNodeLevel: importedPlayer.sourceFileLvl(Player.bitNodeN) + 1,
sourceFiles: [...importedPlayer.sourceFiles].reduce<number>((total, [__bn, lvl]) => (total += lvl), 0),
exploits: importedPlayer.exploits.length,
syncSteamAchievements,
};
data.playerData = playerData;
return Promise.resolve(data);
return data;
}
toJSON(): IReviverValue {