mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 14:59:16 +02:00
211 lines
8.1 KiB
TypeScript
211 lines
8.1 KiB
TypeScript
// 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";
|
|
import { pluralize } from "../I18nUtils";
|
|
import { getTerminalStdIO } from "../../Terminal/StdIO/RedirectIO";
|
|
|
|
// Temporary until fixing alerts manager to store alerts outside of react scope
|
|
const dialogBoxCreate = (text: string) =>
|
|
setTimeout(() => {
|
|
dialogBoxCreateOriginal(text, { html: false, canBeDismissedEasily: false });
|
|
}, 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 VersionBreakingChange {
|
|
additionalText?: string;
|
|
apiBreakingChanges: APIBreakInfo[];
|
|
}
|
|
|
|
type NormalMigration = {
|
|
replaceValue: string;
|
|
migrator?: never;
|
|
};
|
|
|
|
type MigrationWithCustomLogic = {
|
|
replaceValue?: never;
|
|
migrator: (line: string) => string;
|
|
};
|
|
|
|
export interface APIBreakInfo {
|
|
/** The API functions impacted by the API break */
|
|
brokenAPIs: {
|
|
name: string;
|
|
migration?: {
|
|
/** We may need to use a custom search value instead of name */
|
|
searchValue: string | RegExp;
|
|
} & (NormalMigration | MigrationWithCustomLogic);
|
|
}[];
|
|
/** Info that should be shown to the player, alongside the list of impacted scripts */
|
|
info: string;
|
|
/** If broken APIs can be safely migrated, we can skip displaying the warning */
|
|
showWarning: boolean;
|
|
/**
|
|
* With a new version with breaking changes, the "showAPIBreaks" function checks all breaking changes and does 2
|
|
* things with changes that affect the player's scripts:
|
|
* - Write info of changes to a log file.
|
|
* - Show a warning per change.
|
|
* Note that we skip changes that do not affect the player's scripts. This is problematic with some breaking changes.
|
|
*
|
|
* With each breaking change in "brokenAPIs", we try to detect the affected code by using "name" or
|
|
* "migration.searchValue". However, with some breaking changes, we cannot detect the affected code reliably via
|
|
* "brokenAPIs". In this case, instead of skipping them, we always "process" that change (i.e., write info to the log
|
|
* file and optionally show a warning that notifies the player about this change).
|
|
*/
|
|
doNotSkip?: boolean;
|
|
}
|
|
|
|
function detectImpactAndMigrateLines(script: Script, brokenFunctions: APIBreakInfo["brokenAPIs"]): number[] | null {
|
|
const impactedLines: number[] = [];
|
|
const lines = script.content.split("\n");
|
|
for (let i = 0; i < lines.length; ++i) {
|
|
for (const brokenFunction of brokenFunctions) {
|
|
if (
|
|
!lines[i].includes(brokenFunction.name) &&
|
|
(!brokenFunction.migration || !lines[i].match(brokenFunction.migration.searchValue))
|
|
) {
|
|
continue;
|
|
}
|
|
impactedLines.push(i + 1);
|
|
if (!brokenFunction.migration) {
|
|
continue;
|
|
}
|
|
if (!brokenFunction.migration.migrator) {
|
|
lines[i] = lines[i].replaceAll(brokenFunction.migration.searchValue, brokenFunction.migration.replaceValue);
|
|
} else {
|
|
lines[i] = brokenFunction.migration.migrator(lines[i]);
|
|
}
|
|
}
|
|
}
|
|
script.content = lines.join("\n");
|
|
return impactedLines.length ? impactedLines : null;
|
|
}
|
|
|
|
/** Returns a map keyed by hostname */
|
|
function detectImpactAndMigrate(brokenFunctions: APIBreakInfo["brokenAPIs"]): {
|
|
impactMap: ImpactMap;
|
|
totalDetectedLines: number;
|
|
} {
|
|
const returnMap = new Map<string, ScriptImpactMap>();
|
|
let totalDetectedLines = 0;
|
|
for (const server of GetAllServers()) {
|
|
const impactedScripts = new Map<ScriptFilePath, number[]>();
|
|
for (const [filename, script] of server.scripts) {
|
|
const impactedLines = detectImpactAndMigrateLines(script, brokenFunctions);
|
|
if (impactedLines) {
|
|
totalDetectedLines += impactedLines.length;
|
|
impactedScripts.set(filename, impactedLines);
|
|
}
|
|
}
|
|
if (impactedScripts.size) {
|
|
returnMap.set(server.hostname, impactedScripts);
|
|
}
|
|
}
|
|
return { impactMap: returnMap, totalDetectedLines };
|
|
}
|
|
|
|
/** 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, { additionalText, apiBreakingChanges }: VersionBreakingChange) {
|
|
const details: {
|
|
apiBreakInfo: APIBreakInfo;
|
|
text: string;
|
|
totalDetectedLines: number;
|
|
showWarning: boolean;
|
|
}[] = [];
|
|
let numberOfWarnings = 0;
|
|
for (const breakInfo of apiBreakingChanges) {
|
|
const scanResult = detectImpactAndMigrate(breakInfo.brokenAPIs);
|
|
const impactMap = scanResult.impactMap;
|
|
// Skip processing if we don't find any affected code and the breaking change does not enable the "doNotSkip" flag.
|
|
if (impactMap.size === 0 && !breakInfo.doNotSkip) {
|
|
continue;
|
|
}
|
|
let detailText = breakInfo.info;
|
|
if (impactMap.size > 0) {
|
|
detailText +=
|
|
`\n\nUsage of the following functions may have been affected:\n${breakInfo.brokenAPIs
|
|
.map((func) => func.name)
|
|
.join("\n")}\n\n` +
|
|
[...impactMap]
|
|
.map(
|
|
([hostname, scriptImpactMap]) =>
|
|
`Potentially affected files on server ${hostname} (with line numbers):\n` +
|
|
[...scriptImpactMap]
|
|
.map(
|
|
([filename, lineNumbers]) =>
|
|
`${filename}: (${pluralize(lineNumbers.length, "Line number", undefined, true)}: ${lineNumbers.join(
|
|
", ",
|
|
)})`,
|
|
)
|
|
.join("\n"),
|
|
)
|
|
.join("\n\n");
|
|
}
|
|
details.push({
|
|
apiBreakInfo: breakInfo,
|
|
text: detailText,
|
|
totalDetectedLines: scanResult.totalDetectedLines,
|
|
showWarning: breakInfo.showWarning,
|
|
});
|
|
if (breakInfo.showWarning) {
|
|
++numberOfWarnings;
|
|
}
|
|
}
|
|
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.map((detail) => detail.text).join("\n\n\n\n")}`,
|
|
);
|
|
const stdIO = getTerminalStdIO();
|
|
Terminal.warn(`AN API BREAK FROM VERSION ${version} MAY HAVE AFFECTED SOME OF YOUR SCRIPTS.`, stdIO);
|
|
Terminal.warn(
|
|
`INFORMATION ABOUT THIS POTENTIAL IMPACT HAS BEEN LOGGED IN ${textFileName} ON YOUR HOME COMPUTER.`,
|
|
stdIO,
|
|
);
|
|
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}.` +
|
|
(additionalText ? `\n\n${additionalText}` : ""),
|
|
);
|
|
let warningIndex = 0;
|
|
for (const detail of details) {
|
|
if (!detail.showWarning) {
|
|
continue;
|
|
}
|
|
Terminal.warn(
|
|
`\nAPI BREAK VERSION ${version} DETAILS ${warningIndex + 1} of ${numberOfWarnings}\n\n${
|
|
detail.apiBreakInfo.info
|
|
}` +
|
|
/**
|
|
* If we can detect the affected lines via apiBreakInfo.brokenAPIs, we will show the number of affected lines.
|
|
* However, some breaking changes cannot be reliably detected, so we intentionally leave apiBreakInfo.brokenAPIs
|
|
* empty. With these changes, the number of affected lines is always 0, but saying that there are no affected
|
|
* lines is misleading, so we won't say anything about the number of affected lines.
|
|
*/
|
|
(detail.apiBreakInfo.brokenAPIs.length > 0
|
|
? `\n\nWe found ${pluralize(detail.totalDetectedLines, "affected line")}.`
|
|
: ""),
|
|
stdIO,
|
|
);
|
|
++warningIndex;
|
|
}
|
|
stdIO.close();
|
|
}
|