MISC: Rework intelligence override (#2575)

This commit is contained in:
catloversg
2026-03-20 10:09:37 +07:00
committed by GitHub
parent 6a9abd9544
commit 3813d03fb6
16 changed files with 295 additions and 77 deletions

View File

@@ -1,46 +1,34 @@
import { Player } from "@player";
import fs from "node:fs";
import type { ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
import { loadGame } from "../../../src/SaveObject";
import { loadGame, saveObject } from "../../../src/SaveObject";
import * as db from "../../../src/db";
import * as FileUtils from "../../../src/utils/FileUtils";
import type { SaveData } from "../../../src/types";
import { calculateExp } from "../../../src/PersonObjects/formulas/skill";
async function loadGameFromSaveData(saveData: SaveData) {
// Simulate loading the data in IndexedDB
const mockedLoad = jest.spyOn(db, "load");
// We must use structuredClone(saveData) instead of saveData; otherwise, the check of mockedDownload won't catch wrong
// changes in evaluateVersionCompatibility (e.g., unexpectedly mutating saveData before passing it to
// downloadContentAsFile).
mockedLoad.mockReturnValue(Promise.resolve(structuredClone(saveData)));
// Simulate saving the data in IndexedDB
jest.spyOn(db, "save").mockImplementation(() => Promise.resolve());
const mockedDownload = jest.spyOn(FileUtils, "downloadContentAsFile").mockImplementation(() => {});
await loadGame(saveData);
return mockedDownload;
}
describe("v3", () => {
test("v2.8.1 to v3.0.0", async () => {
const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v2.8.1.gz"));
// Simulate loading the data in IndexedDB
const mockedLoad = jest.spyOn(db, "load");
/**
* We must use structuredClone(saveData) instead of saveData; otherwise, the check of mockedDownload won't catch
* wrong changes in evaluateVersionCompatibility (e.g., unexpectedly mutating saveData before passing it to
* downloadContentAsFile).
*/
mockedLoad.mockReturnValue(Promise.resolve(structuredClone(saveData)));
const mockedDownload = jest.spyOn(FileUtils, "downloadContentAsFile");
const originalConsoleError = console.error;
const originalConsoleWarning = console.warn;
const consoleError = jest.spyOn(console, "error").mockImplementation((...data: unknown[]) => {
if (Array.isArray(data) && data.length > 0 && (data[0] === "There was no Darknet savedata" || data[0] === "")) {
return;
}
originalConsoleError(...data);
});
const consoleWarning = jest.spyOn(console, "warn").mockImplementation((...data: unknown[]) => {
if (
Array.isArray(data) &&
data.length > 0 &&
(data[0] === "Encountered the following issue while loading Darknet savedata:" || data[0] === "Savedata:")
) {
return;
}
originalConsoleWarning(...data);
});
await loadGame(await db.load());
consoleError.mockRestore();
consoleWarning.mockRestore();
const mockedDownload = await loadGameFromSaveData(saveData);
// Check if auto-migration works
expect(
@@ -57,4 +45,64 @@ describe("v3", () => {
// Check if evaluateVersionCompatibility correctly loads the data in IndexedDB and passes it to downloadContentAsFile
expect(mockedDownload).toHaveBeenCalledWith(saveData, "bitburnerSave_backup_2.8.1_1756913326.json.gz");
});
test.each([
["test/jest/Migration/save-files/v2.8.1_500int.gz", 1773597870229, 1773597871370, undefined, 500, 300],
["test/jest/Migration/save-files/v2.8.1_500int_override_100int.gz", 1773597926723, 1773597928370, 100, 500, 300],
["test/jest/Migration/save-files/v2.8.1_500int_override_1000int.gz", 1773597951205, 1773597953370, 1000, 500, 300],
])("%s", async (path, lastSave, lastUpdate, intelligenceOverride, playerInt, sleeveInt) => {
const saveData = new Uint8Array(fs.readFileSync(path));
const mockedDownload = await loadGameFromSaveData(saveData);
expect(Player.lastSave).toStrictEqual(lastSave);
const playerIntExp =
intelligenceOverride !== undefined && intelligenceOverride < playerInt
? calculateExp(intelligenceOverride, 1)
: calculateExp(playerInt, 1);
const sleeveIntExp =
intelligenceOverride !== undefined && intelligenceOverride < sleeveInt
? calculateExp(intelligenceOverride, 1)
: calculateExp(sleeveInt, 1);
const persistentPlayerIntExp = calculateExp(playerInt, 1);
const persistentSleeveIntExp = calculateExp(sleeveInt, 1);
// Check if Player.exp.intelligence and Player.persistentIntelligenceData.exp are migrated.
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(intelligenceOverride);
expect(Player.exp.intelligence).toStrictEqual(playerIntExp);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(persistentPlayerIntExp);
for (const sleeve of Player.sleeves) {
expect(sleeve.exp.intelligence).toStrictEqual(sleeveIntExp);
expect(sleeve.persistentIntelligenceData.exp).toStrictEqual(persistentSleeveIntExp);
}
const expGain = 1e9;
// Gain exp and check if it is accumulated correctly.
Player.gainIntelligenceExp(expGain);
expect(Player.exp.intelligence).toStrictEqual(playerIntExp + expGain);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(persistentPlayerIntExp + expGain);
for (const sleeve of Player.sleeves) {
sleeve.gainIntelligenceExp(expGain);
expect(sleeve.exp.intelligence).toStrictEqual(sleeveIntExp + expGain);
expect(sleeve.persistentIntelligenceData.exp).toStrictEqual(persistentSleeveIntExp + expGain);
}
// Save and reload.
await saveObject.saveGame();
await loadGameFromSaveData(await saveObject.getSaveData());
expect(Player.lastSave).not.toStrictEqual(lastSave);
// Check if gained exp is saved correctly.
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(intelligenceOverride);
expect(Player.exp.intelligence).toStrictEqual(playerIntExp + expGain);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(persistentPlayerIntExp + expGain);
for (const sleeve of Player.sleeves) {
expect(sleeve.exp.intelligence).toStrictEqual(sleeveIntExp + expGain);
expect(sleeve.persistentIntelligenceData.exp).toStrictEqual(persistentSleeveIntExp + expGain);
}
expect(mockedDownload).toHaveBeenCalledWith(
saveData,
`bitburnerSave_backup_2.8.1_${Math.round(lastUpdate / 1000)}.json.gz`,
);
});
});

Binary file not shown.

View File

@@ -15,7 +15,7 @@ import { CompanyPositions } from "../../../src/Company/CompanyPositions";
import { getTorRouter } from "../../../src/Server/ServerHelpers";
import * as exceptionAlertModule from "../../../src/utils/helpers/exceptionAlert";
const nextBN = 3;
const nextBN = 4;
function setNumBlackOpsComplete(value: number): void {
if (!Player.bladeburner) {
@@ -48,19 +48,139 @@ beforeAll(() => {
initGameEnvironment();
});
function testIntelligenceOverride(
ns: NSFull,
prestigeAPI: "b1tflum3" | "destroyW0r1dD43m0n",
expectSuccessPrestige: () => void,
setUpBeforePrestige = () => {},
): void {
Player.sourceFiles.set(5, 1);
// Start without exp.
expect(Player.exp.intelligence).toStrictEqual(0);
expect(Player.skills.intelligence).toStrictEqual(1);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(0);
// Gain 1e6 exp (skill = 242).
Player.gainIntelligenceExp(1e6);
expect(Player.exp.intelligence).toStrictEqual(1e6);
expect(Player.skills.intelligence).toStrictEqual(242);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6);
// Prestige and check if intelligenceOverride works (exp is set to 11255, skill = 100, and
// persistentIntelligenceData.exp is still 1e6).
const intelligenceExpGainOnPrestige = prestigeAPI === "destroyW0r1dD43m0n" ? 300 : 0;
setUpBeforePrestige();
ns.singularity[prestigeAPI](nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: 100,
});
expectSuccessPrestige();
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(100);
expect(Player.exp.intelligence).toStrictEqual(11255.317546552918 + intelligenceExpGainOnPrestige);
expect(Player.skills.intelligence).toStrictEqual(100);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige);
// Gain 500e3 exp.
const intExpGain = 500e3;
Player.gainIntelligenceExp(intExpGain);
// Check if int gain is accumulated correctly in both Player.exp.intelligence and
// Player.persistentIntelligenceData.exp.
expect(Player.exp.intelligence).toStrictEqual(11255.317546552918 + intelligenceExpGainOnPrestige + intExpGain);
expect(Player.skills.intelligence).toStrictEqual(220);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige + intExpGain);
// Prestige and check if int gain is still retained correctly.
setUpBeforePrestige();
ns.singularity[prestigeAPI](nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: undefined,
});
expectSuccessPrestige();
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(undefined);
expect(Player.exp.intelligence).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 2 + intExpGain);
expect(Player.skills.intelligence).toStrictEqual(255);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 2 + intExpGain);
// Prestige with intelligenceOverride set higher than the persistent int skill and check if the int skill is
// incorrectly set to that value.
setUpBeforePrestige();
ns.singularity[prestigeAPI](nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: 1000,
});
expectSuccessPrestige();
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(1000);
expect(Player.exp.intelligence).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 3 + intExpGain);
expect(Player.skills.intelligence).toStrictEqual(255);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 3 + intExpGain);
// Start testing another scenario.
// Set the initial state (int exp = 1e6, skill = 242) and bitflume.
Player.exp.intelligence = 1e6;
Player.skills.intelligence = 242;
Player.persistentIntelligenceData.exp = 1e6;
ns.singularity.b1tflum3(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: undefined,
});
// Double-check the initial state.
expect(Player.exp.intelligence).toStrictEqual(1e6);
expect(Player.skills.intelligence).toStrictEqual(242);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6);
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(undefined);
// Limit int skill to 100.
setUpBeforePrestige();
ns.singularity[prestigeAPI](nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: 100,
});
expectSuccessPrestige();
// Check if int is overridden correctly.
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(100);
expect(Player.exp.intelligence).toStrictEqual(11255.317546552918 + intelligenceExpGainOnPrestige);
expect(Player.skills.intelligence).toStrictEqual(100);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige);
// Limit int skill to 1000.
setUpBeforePrestige();
ns.singularity[prestigeAPI](nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: 1000,
});
expectSuccessPrestige();
// The limit is higher than the persistent int skill, so it's not applied. Exp and skill are reset back to the initial
// state, plus the int exp gained from prestige.
expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(1000);
expect(Player.exp.intelligence).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 2);
expect(Player.skills.intelligence).toStrictEqual(242);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 2);
}
function setUpBeforeDestroyingWD(): void {
Player.queueAugmentation(AugmentationName.TheRedPill);
installAugmentations();
Player.gainHackingExp(1e100);
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
wdServer.hasAdminRights = true;
Player.startBladeburner();
setNumBlackOpsComplete(blackOpsArray.length);
}
describe("b1tflum3", () => {
beforeEach(() => {
setupBasicTestingEnvironment();
Player.queueAugmentation(AugmentationName.TheRedPill);
Player.queueAugmentation(AugmentationName.Targeting1);
installAugmentations();
Player.gainHackingExp(1e100);
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
wdServer.hasAdminRights = true;
});
// Make sure that the player is in the next BN without SF rewards.
const expectSucceedInB1tflum3 = () => {
expect(Player.bitNodeN).toStrictEqual(nextBN);
expect(Player.augmentations.length).toStrictEqual(0);
expect(Player.exp.hacking).toStrictEqual(0);
expect(Player.sourceFileLvl(1)).toStrictEqual(0);
};
@@ -79,15 +199,20 @@ describe("b1tflum3", () => {
});
expectSucceedInB1tflum3();
});
test("intelligenceOverride", () => {
testIntelligenceOverride(getNS(), "b1tflum3", expectSucceedInB1tflum3);
});
});
// Make sure that the player is still in the same BN without SF rewards.
const expectFailToB1tflum3 = () => {
expect(Player.bitNodeN).toStrictEqual(1);
expect(Player.augmentations.length).toStrictEqual(1);
expect(Player.augmentations[0].name).toStrictEqual(AugmentationName.Targeting1);
expect(Player.exp.hacking).toStrictEqual(1e100);
expect(Player.sourceFileLvl(1)).toStrictEqual(0);
};
describe("Failure", () => {
// Make sure that the player is still in the same BN without SF rewards.
const expectFailToB1tflum3 = () => {
expect(Player.bitNodeN).toStrictEqual(1);
expect(Player.augmentations.length).toStrictEqual(1);
expect(Player.sourceFileLvl(1)).toStrictEqual(0);
};
test("Invalid intelligenceOverride", () => {
const ns = getNS();
expect(() => {
@@ -114,13 +239,7 @@ describe("b1tflum3", () => {
describe("destroyW0r1dD43m0n", () => {
beforeEach(() => {
setupBasicTestingEnvironment();
Player.queueAugmentation(AugmentationName.TheRedPill);
installAugmentations();
Player.gainHackingExp(1e100);
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
wdServer.hasAdminRights = true;
Player.startBladeburner();
setNumBlackOpsComplete(blackOpsArray.length);
setUpBeforeDestroyingWD();
});
describe("Success", () => {
@@ -162,6 +281,9 @@ describe("destroyW0r1dD43m0n", () => {
});
expectSucceedInDestroyingWD();
});
test("intelligenceOverride", () => {
testIntelligenceOverride(getNS(), "destroyW0r1dD43m0n", expectSucceedInDestroyingWD, setUpBeforeDestroyingWD);
});
});
describe("Failure", () => {
@@ -488,6 +610,7 @@ describe("purchaseProgram", () => {
const spiedExceptionAlert = jest.spyOn(exceptionAlertModule, "exceptionAlert");
const ns = getNS();
expect(Player.hasProgram(CompletedProgramName.darkscape)).toStrictEqual(false);
// @ts-expect-error - Intentionally use lowercase program name
expect(ns.singularity.purchaseProgram(CompletedProgramName.darkscape.toLowerCase())).toStrictEqual(true);
expect(Player.hasProgram(CompletedProgramName.darkscape)).toStrictEqual(true);
expect(spiedExceptionAlert).not.toHaveBeenCalled();
@@ -507,6 +630,7 @@ describe("purchaseProgram", () => {
});
test("Invalid program name", () => {
const ns = getNS();
// @ts-expect-error - Intentionally use invalid program name
expect(ns.singularity.purchaseProgram("InvalidProgram.exe")).toStrictEqual(false);
});
test("Not enough money", () => {

View File

@@ -534,6 +534,9 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"work_money": 1,
},
"numPeopleKilled": 0,
"persistentIntelligenceData": {
"exp": 0,
},
"playtimeSinceLastAug": 0,
"playtimeSinceLastBitnode": 0,
"purchasedServers": [],
@@ -614,6 +617,9 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"strength_exp": 1,
"work_money": 1,
},
"persistentIntelligenceData": {
"exp": 0,
},
"queuedAugmentations": [],
"shock": 100,
"skills": {
@@ -686,6 +692,9 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"strength_exp": 1,
"work_money": 1,
},
"persistentIntelligenceData": {
"exp": 0,
},
"queuedAugmentations": [],
"shock": 100,
"skills": {