API: Add API break utilities, and add an API break for bladeburner.getCurrentAction (#1248)

This commit is contained in:
Snarling 2024-05-14 19:24:03 -04:00 committed by GitHub
parent 9dc3b22919
commit 574c284321
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 131 additions and 12 deletions

@ -9,17 +9,15 @@ Get current action.
**Signature:** **Signature:**
```typescript ```typescript
getCurrentAction(): BladeburnerCurAction; getCurrentAction(): BladeburnerCurAction | null;
``` ```
**Returns:** **Returns:**
[BladeburnerCurAction](./bitburner.bladeburnercuraction.md) [BladeburnerCurAction](./bitburner.bladeburnercuraction.md) \| null
Object that represents the players current Bladeburner action. Object that represents the players current Bladeburner action, or null if no action is being performed.
## Remarks ## Remarks
RAM cost: 1 GB RAM cost: 1 GB
Returns an object that represents the players current Bladeburner action. If the player is not performing an action, the function will return an object with the type property set to “Idle”.

@ -101,8 +101,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
}, },
getCurrentAction: (ctx) => () => { getCurrentAction: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
// Temporary bad return type to not be an API break (idle should just return null) if (!bladeburner.action) return null;
if (!bladeburner.action) return { type: "Idle", name: "Idle" };
return { ...bladeburner.action }; return { ...bladeburner.action };
}, },
getActionTime: (ctx) => (type, name) => { getActionTime: (ctx) => (type, name) => {

@ -42,6 +42,8 @@ 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 { isBinaryFormat } from "../electron/saveDataBinaryFormat";
import { downloadContentAsFile } from "./utils/FileUtils"; import { downloadContentAsFile } from "./utils/FileUtils";
import { showAPIBreaks } from "./utils/APIBreaks/APIBreak";
import { breakInfos261 } from "./utils/APIBreaks/2.6.1";
import { handleGetSaveDataError } from "./Netscript/ErrorMessages"; import { handleGetSaveDataError } from "./Netscript/ErrorMessages";
/* SaveObject.js /* SaveObject.js
@ -735,6 +737,9 @@ Error: ${e}`);
Object.assign(freshSaveData.stats, stats); Object.assign(freshSaveData.stats, stats);
loadGo(JSON.stringify(freshSaveData)); loadGo(JSON.stringify(freshSaveData));
} }
if (ver < 39) {
showAPIBreaks("2.6.1", ...breakInfos261);
}
} }
async function loadGame(saveData: SaveData): Promise<boolean> { async function loadGame(saveData: SaveData): Promise<boolean> {

@ -3070,12 +3070,9 @@ export interface Bladeburner {
* @remarks * @remarks
* RAM cost: 1 GB * RAM cost: 1 GB
* *
* Returns an object that represents the players current Bladeburner action. * @returns Object that represents the players current Bladeburner action, or null if no action is being performed.
* If the player is not performing an action, the function will return an object with the type property set to Idle.
*
* @returns Object that represents the players current Bladeburner action.
*/ */
getCurrentAction(): BladeburnerCurAction; getCurrentAction(): BladeburnerCurAction | null;
/** /**
* Get the time to complete an action. * Get the time to complete an action.

@ -0,0 +1,29 @@
import { APIBreakInfo } from "./APIBreak";
export const breakInfos261: APIBreakInfo[] = [
{
brokenFunctions: ["ns.bladeburner.getCurrentAction"],
info:
"ns.bladeburner.getCurrentAction:\n" +
'When not performing a bladeburner action, previously returned {type: "Idle", name: ""}, now returns null.\n' +
"Because of this change, the null case now needs to be dealt with prior to accessing properties on the return of getCurrentAction, including destructuring.\n" +
"Additionally, any existing code for filtering out the Idle case will need to be adjusted.\n\n" +
"See https://github.com/bitburner-official/bitburner-src/issues/1249 or PR https://github.com/bitburner-official/bitburner-src/pull/1248 for more details.",
},
{
brokenFunctions: [
"ns.bladeburner.getActionCountRemaining",
"ns.bladeburner.getActionEstimatedSuccessChance",
"ns.bladeburner.getActionTime",
],
info:
"ns.bladeburner.getActionCountRemaining:\n" +
'Previously returned -1 when called with type "Idle" and name "". This is no longer valid usage and will result in an error.\n\n' +
"ns.bladeburner.getActionEstimatedSuccessChance:\n" +
'Previously returned [-1, -1] when called with type "Idle" and name "". This is no longer valid usage and will result in an error.\n\n' +
"ns.bladeburner.getActionTime:\n" +
'Previously returned -1 when called with type "Idle" and name "". This is no longer valid usage and will result in an error.\n\n' +
"See the related changes for ns.bladeburner.getCurrentAction, which were shown earlier in these API break details.\n" +
"In most cases, the fixes for ns.bladeburner.getCurrentAction will fix this group of isses as well.",
},
];

@ -0,0 +1,91 @@
// General reusable tools for API breaks
import type { ScriptFilePath } from "../../Paths/ScriptFilePath";
import type { Script } from "../../Script/Script";
import { Player } from "@player";
import { GetAllServers } from "../../Server/AllServers";
import { resolveTextFilePath } from "../../Paths/TextFilePath";
import { dialogBoxCreate as dialogBoxCreateOriginal } from "../../ui/React/DialogBox";
import { Terminal } from "../../Terminal";
// Temporary until fixing alerts manager to store alerts outside of react scope
const dialogBoxCreate = (text: string) => setTimeout(() => dialogBoxCreateOriginal(text), 2000);
/** For a single server, map of script filepath to an array of line numbers where impacted functions were detected */
type ScriptImpactMap = Map<ScriptFilePath, number[]>;
/** For an overall API break, map of server hostnames to an array of impacted scripts */
type ImpactMap = Map<string, ScriptImpactMap>;
export interface APIBreakInfo {
/** The API functions impacted by the API break */
brokenFunctions: string[];
/** Info that should be shown to the player, alongside the list of impacted scripts */
info: string;
}
function getImpactedLines(script: Script, brokenFunctions: string[]): number[] | null {
const impactedLines: number[] = [];
script.content.split("\n").forEach((line, i) => {
for (const brokenFunction of brokenFunctions) {
if (line.includes(brokenFunction)) return impactedLines.push(i + 1);
}
});
return impactedLines.length ? impactedLines : null;
}
/** Returns a map keyed by all ser */
function getImpactMap(brokenFunctions: string[]): ImpactMap | null {
const returnMap = new Map<string, ScriptImpactMap>();
for (const server of GetAllServers()) {
const impactedScripts = new Map<ScriptFilePath, number[]>();
for (const [filename, script] of server.scripts) {
const impactedLines = getImpactedLines(script, brokenFunctions);
if (impactedLines) impactedScripts.set(filename, impactedLines);
}
if (impactedScripts.size) returnMap.set(server.hostname, impactedScripts);
}
return returnMap.size ? returnMap : null;
}
/** Show the player a dialog for their API breaks, and save an info file for the player to review later */
export function showAPIBreaks(version: string, ...breakInfos: APIBreakInfo[]) {
const details = [];
for (const breakInfo of breakInfos) {
const impactMap = getImpactMap(breakInfo.brokenFunctions);
if (!impactMap) continue;
details.push(
breakInfo.info +
`\n\nUsage of the following functions may have been affected:\n${breakInfo.brokenFunctions.join("\n")}\n\n` +
[...impactMap]
.map(
([hostname, scriptImpactMap]) =>
`Potentially affected files on server ${hostname} (with line numbers):\n` +
[...scriptImpactMap]
.map(
([filename, lineNumbers]) =>
`${filename}: (Line number${lineNumbers.length > 1 ? "s" : ""}: ${lineNumbers.join(", ")})`,
)
.join("\n"),
)
.join("\n\n"),
);
}
if (!details.length) return;
const textFileName = resolveTextFilePath(`APIBreakInfo-${version}.txt`);
if (!textFileName) throw new Error("Version string created an invalid API break file name");
Player.getHomeComputer().writeToTextFile(
textFileName,
`API BREAK INFO FOR ${version}\n\n${details.join("\n\n\n\n")}`,
);
Terminal.warn(`AN API BREAK FROM VERSION ${version} MAY HAVE AFFECTED SOME OF YOUR SCRIPTS.`);
Terminal.warn(`INFORMATION ABOUT THIS POTENTIAL IMPACT HAS BEEN LOGGED IN ${textFileName} ON YOUR HOME COMPUTER.`);
dialogBoxCreate(
`SOME OF YOUR SCRIPTS HAVE POTENTIALLY BEEN IMPACTED BY AN API BREAK, DUE TO CHANGES IN VERSION ${version}\n\n` +
"The following dialog boxes will provide details of the potential impact to your scripts.\n" +
`A file with these details has also been saved on your home computer under filename ${textFileName}.`,
);
details.forEach((detail, i) => {
dialogBoxCreate(`API BREAK VERSION ${version} DETAILS ${i + 1} of ${details.length}\n\n${detail}`);
});
}