BUGFIX: Fix additionalMsec overflow issue (#941)

This commit is contained in:
David Walker 2023-12-07 17:34:49 -08:00 committed by GitHub
parent 21c7f56d23
commit 61ffed9b3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 179 deletions

@ -7,7 +7,7 @@ import { Player } from "@player";
import { ScriptDeath } from "./ScriptDeath";
import { formatExp, formatMoney, formatRam, formatThreads } from "../ui/formatNumber";
import { ScriptArg } from "./ScriptArg";
import { BasicHGWOptions, RunningScript as IRunningScript, Person as IPerson, Server as IServer } from "@nsdefs";
import { RunningScript as IRunningScript, Person as IPerson, Server as IServer } from "@nsdefs";
import { Server } from "../Server/Server";
import {
calculateHackingChance,
@ -49,7 +49,7 @@ export const helpers = {
argsToString,
makeBasicErrorMsg,
makeRuntimeErrorMsg,
resolveNetscriptRequestedThreads,
validateHGWOptions,
checkEnvFlags,
checkSingularityAccess,
netscriptDelay,
@ -84,46 +84,18 @@ export interface CompleteRunOptions {
export interface CompleteSpawnOptions extends CompleteRunOptions {
spawnDelay: PositiveInteger;
}
/** HGWOptions with non-optional, type-validated members, for passing between internal functions. */
export interface CompleteHGWOptions {
threads: PositiveInteger;
stock: boolean;
additionalMsec: number;
}
export function assertString(ctx: NetscriptContext, argName: string, v: unknown): asserts v is string {
if (typeof v !== "string")
throw makeRuntimeErrorMsg(ctx, `${argName} expected to be a string. ${debugType(v)}`, "TYPE");
}
/** Will probably remove the below function in favor of a different approach to object type assertion.
* This method cannot be used to handle optional properties. */
export function assertObjectType<T extends object>(
ctx: NetscriptContext,
name: string,
obj: unknown,
desiredObject: T,
): asserts obj is T {
if (typeof obj !== "object" || obj === null) {
throw makeRuntimeErrorMsg(
ctx,
`Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`,
"TYPE",
);
}
for (const [key, val] of Object.entries(desiredObject)) {
if (!Object.hasOwn(obj, key)) {
throw makeRuntimeErrorMsg(
ctx,
`Object provided for argument ${name} is missing required property ${key}.`,
"TYPE",
);
}
const objVal = (obj as Record<string, unknown>)[key];
if (typeof val !== typeof objVal) {
throw makeRuntimeErrorMsg(
ctx,
`Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`,
"TYPE",
);
}
}
}
const userFriendlyString = (v: unknown): string => {
const clip = (s: string): string => {
if (s.length > 15) return s.slice(0, 12) + "...";
@ -266,7 +238,7 @@ function makeBasicErrorMsg(ws: WorkerScript | ScriptDeath, msg: string, type = "
}
/** Creates an error message string with a stack trace. */
function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string {
export function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string {
const errstack = new Error().stack;
if (errstack === undefined) throw new Error("how did we not throw an error?");
const stack = errstack.split("\n").slice(1);
@ -322,23 +294,44 @@ function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME
}
}
/** Validate requested number of threads for h/g/w options */
function resolveNetscriptRequestedThreads(ctx: NetscriptContext, requestedThreads?: number): number {
function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOptions {
const result: CompleteHGWOptions = {
threads: 1 as PositiveInteger,
stock: false,
additionalMsec: 0,
};
if (opts == null) {
return result;
}
if (typeof opts !== "object") {
throw makeRuntimeErrorMsg(ctx, `BasicHGWOptions must be an object if specified, was ${opts}`);
}
// Safe assertion since threadOrOption type has been narrowed to a non-null object
const options = opts as Unknownify<CompleteHGWOptions>;
result.stock = !!options.stock;
result.additionalMsec = number(ctx, "opts.additionalMsec", options.additionalMsec ?? 0);
if (result.additionalMsec < 0) {
throw makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${options.additionalMsec}`);
}
if (result.additionalMsec > 1e9) {
throw makeRuntimeErrorMsg(ctx, `additionalMsec too large (>1e9), got ${options.additionalMsec}`);
}
const requestedThreads = options.threads;
const threads = ctx.workerScript.scriptRef.threads;
if (!requestedThreads) {
return isNaN(threads) || threads < 1 ? 1 : threads;
result.threads = (isNaN(threads) || threads < 1 ? 1 : threads) as PositiveInteger;
} else {
const positiveThreads = positiveInteger(ctx, "opts.threads", requestedThreads);
if (positiveThreads > threads) {
throw makeRuntimeErrorMsg(
ctx,
`Too many threads requested by ${ctx.function}. Requested: ${positiveThreads}. Has: ${threads}.`,
);
}
result.threads = positiveThreads;
}
const requestedThreadsAsInt = requestedThreads | 0;
if (isNaN(requestedThreads) || requestedThreadsAsInt < 1) {
throw makeRuntimeErrorMsg(ctx, `Invalid thread count: ${requestedThreads}. Threads must be a positive number.`);
}
if (requestedThreadsAsInt > threads) {
throw makeRuntimeErrorMsg(
ctx,
`Too many threads requested by ${ctx.function}. Requested: ${requestedThreads}. Has: ${threads}.`,
);
}
return requestedThreadsAsInt;
return result;
}
/** Validate singularity access by throwing an error if the player does not have access. */
@ -472,18 +465,9 @@ function isScriptArgs(args: unknown): args is ScriptArg[] {
return Array.isArray(args) && args.every(isScriptArg);
}
function hack(
ctx: NetscriptContext,
hostname: string,
manual: boolean,
{ threads: requestedThreads, stock, additionalMsec: requestedSec }: BasicHGWOptions = {},
): Promise<number> {
function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: unknown): Promise<number> {
const ws = ctx.workerScript;
const threads = helpers.resolveNetscriptRequestedThreads(ctx, requestedThreads);
const additionalMsec = number(ctx, "opts.additionalMsec", requestedSec ?? 0);
if (additionalMsec < 0) {
throw makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${additionalMsec}`);
}
const { threads, stock, additionalMsec } = validateHGWOptions(ctx, opts);
const server = getServer(ctx, hostname);
if (!(server instanceof Server)) {
throw makeRuntimeErrorMsg(ctx, "Cannot be executed on this server.");

@ -51,7 +51,7 @@ import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScript } from "./Netscript/WorkerScript";
import { helpers, assertObjectType, wrapUserNode } from "./Netscript/NetscriptHelpers";
import { helpers, wrapUserNode } from "./Netscript/NetscriptHelpers";
import {
formatExp,
formatNumberNoSuffix,
@ -78,7 +78,7 @@ import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
import { NetscriptFormulas } from "./NetscriptFunctions/Formulas";
import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket";
import { NetscriptGrafting } from "./NetscriptFunctions/Grafting";
import { NS, RecentScript, BasicHGWOptions, ProcessInfo, NSEnums } from "@nsdefs";
import { NS, RecentScript, ProcessInfo, NSEnums } from "@nsdefs";
import { NetscriptSingularity } from "./NetscriptFunctions/Singularity";
import { dialogBoxCreate } from "./ui/React/DialogBox";
@ -154,15 +154,10 @@ export const ns: InternalAPI<NSFull> = {
return out;
},
hasTorRouter: () => () => Player.hasTorRouter(),
hack:
(ctx) =>
(_hostname, opts = {}) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
// TODO 2.2: better type safety rework for functions using assertObjectType, then remove function.
const optsValidator: BasicHGWOptions = {};
assertObjectType(ctx, "opts", opts, optsValidator);
return helpers.hack(ctx, hostname, false, opts);
},
hack: (ctx) => (_hostname, opts?) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
return helpers.hack(ctx, hostname, false, opts);
},
hackAnalyzeThreads: (ctx) => (_hostname, _hackAmount) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
const hackAmount = helpers.number(ctx, "hackAmount", _hackAmount);
@ -252,66 +247,58 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => `Sleeping for ${convertTimeMsToTimeElapsedString(time, true)}.`);
return new Promise((resolve) => setTimeout(() => resolve(true), time));
},
grow:
(ctx) =>
(_hostname, opts = {}) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
const optsValidator: BasicHGWOptions = {};
assertObjectType(ctx, "opts", opts, optsValidator);
const threads = helpers.resolveNetscriptRequestedThreads(ctx, opts.threads);
const additionalMsec = helpers.number(ctx, "opts.additionalMsec", opts.additionalMsec ?? 0);
if (additionalMsec < 0) {
throw helpers.makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${additionalMsec}`);
}
grow: (ctx) => (_hostname, opts?) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
const { threads, stock, additionalMsec } = helpers.validateHGWOptions(ctx, opts);
const server = helpers.getServer(ctx, hostname);
if (!(server instanceof Server)) {
helpers.log(ctx, () => "Cannot be executed on this server.");
return Promise.resolve(0);
}
const server = helpers.getServer(ctx, hostname);
if (!(server instanceof Server)) {
helpers.log(ctx, () => "Cannot be executed on this server.");
return Promise.resolve(0);
}
const host = GetServer(ctx.workerScript.hostname);
if (host === null) {
throw new Error("Workerscript host is null");
}
const host = GetServer(ctx.workerScript.hostname);
if (host === null) {
throw new Error("Workerscript host is null");
}
// No root access or skill level too low
const canHack = netscriptCanGrow(server);
if (!canHack.res) {
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
}
// No root access or skill level too low
const canHack = netscriptCanGrow(server);
if (!canHack.res) {
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
}
const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0;
const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0;
helpers.log(
ctx,
() =>
`Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
growTime * 1000,
true,
)} (t=${formatThreads(threads)}).`,
);
return helpers.netscriptDelay(ctx, growTime * 1000).then(function () {
const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable;
processSingleServerGrowth(server, threads, host.cpuCores);
const moneyAfter = server.moneyAvailable;
ctx.workerScript.scriptRef.recordGrow(server.hostname, threads);
const expGain = calculateHackingExpGain(server, Player) * threads;
const logGrowPercent = moneyAfter / moneyBefore - 1;
helpers.log(
ctx,
() =>
`Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
growTime * 1000,
true,
)} (t=${formatThreads(threads)}).`,
`Available money on '${server.hostname}' grown by ${formatPercent(logGrowPercent, 6)}. Gained ${formatExp(
expGain,
)} hacking exp (t=${formatThreads(threads)}).`,
);
return helpers.netscriptDelay(ctx, growTime * 1000).then(function () {
const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable;
processSingleServerGrowth(server, threads, host.cpuCores);
const moneyAfter = server.moneyAvailable;
ctx.workerScript.scriptRef.recordGrow(server.hostname, threads);
const expGain = calculateHackingExpGain(server, Player) * threads;
const logGrowPercent = moneyAfter / moneyBefore - 1;
helpers.log(
ctx,
() =>
`Available money on '${server.hostname}' grown by ${formatPercent(logGrowPercent, 6)}. Gained ${formatExp(
expGain,
)} hacking exp (t=${formatThreads(threads)}).`,
);
ctx.workerScript.scriptRef.onlineExpGained += expGain;
Player.gainHackingExp(expGain);
if (opts.stock) {
influenceStockThroughServerGrow(server, moneyAfter - moneyBefore);
}
return Promise.resolve(moneyAfter / moneyBefore);
});
},
ctx.workerScript.scriptRef.onlineExpGained += expGain;
Player.gainHackingExp(expGain);
if (stock) {
influenceStockThroughServerGrow(server, moneyAfter - moneyBefore);
}
return Promise.resolve(moneyAfter / moneyBefore);
});
},
growthAnalyze:
(ctx) =>
(_host, _multiplier, _cores = 1) => {
@ -359,64 +346,56 @@ export const ns: InternalAPI<NSFull> = {
return 2 * CONSTANTS.ServerFortifyAmount * threads;
},
weaken:
(ctx) =>
async (_hostname, opts = {}) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
const optsValidator: BasicHGWOptions = {};
assertObjectType(ctx, "opts", opts, optsValidator);
const threads = helpers.resolveNetscriptRequestedThreads(ctx, opts.threads);
const additionalMsec = helpers.number(ctx, "opts.additionalMsec", opts.additionalMsec ?? 0);
if (additionalMsec < 0) {
throw helpers.makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${additionalMsec}`);
}
weaken: (ctx) => async (_hostname, opts?) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
const { threads, additionalMsec } = helpers.validateHGWOptions(ctx, opts);
const server = helpers.getServer(ctx, hostname);
if (!(server instanceof Server)) {
helpers.log(ctx, () => "Cannot be executed on this server.");
const server = helpers.getServer(ctx, hostname);
if (!(server instanceof Server)) {
helpers.log(ctx, () => "Cannot be executed on this server.");
return Promise.resolve(0);
}
// No root access or skill level too low
const canHack = netscriptCanWeaken(server);
if (!canHack.res) {
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
}
const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0;
helpers.log(
ctx,
() =>
`Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
weakenTime * 1000,
true,
)} (t=${formatThreads(threads)})`,
);
return helpers.netscriptDelay(ctx, weakenTime * 1000).then(function () {
const host = GetServer(ctx.workerScript.hostname);
if (host === null) {
helpers.log(ctx, () => "Server is null, did it die?");
return Promise.resolve(0);
}
// No root access or skill level too low
const canHack = netscriptCanWeaken(server);
if (!canHack.res) {
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
}
const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0;
const cores = host.cpuCores;
const coreBonus = getCoreBonus(cores);
const weakenAmt = CONSTANTS.ServerWeakenAmount * threads * coreBonus;
server.weaken(weakenAmt);
ctx.workerScript.scriptRef.recordWeaken(server.hostname, threads);
const expGain = calculateHackingExpGain(server, Player) * threads;
helpers.log(
ctx,
() =>
`Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
weakenTime * 1000,
true,
)} (t=${formatThreads(threads)})`,
`'${server.hostname}' security level weakened to ${server.hackDifficulty}. Gained ${formatExp(
expGain,
)} hacking exp (t=${formatThreads(threads)})`,
);
return helpers.netscriptDelay(ctx, weakenTime * 1000).then(function () {
const host = GetServer(ctx.workerScript.hostname);
if (host === null) {
helpers.log(ctx, () => "Server is null, did it die?");
return Promise.resolve(0);
}
const cores = helpers.getServer(ctx, ctx.workerScript.hostname).cpuCores;
const coreBonus = getCoreBonus(cores);
const weakenAmt = CONSTANTS.ServerWeakenAmount * threads * coreBonus;
server.weaken(weakenAmt);
ctx.workerScript.scriptRef.recordWeaken(server.hostname, threads);
const expGain = calculateHackingExpGain(server, Player) * threads;
helpers.log(
ctx,
() =>
`'${server.hostname}' security level weakened to ${server.hackDifficulty}. Gained ${formatExp(
expGain,
)} hacking exp (t=${formatThreads(threads)})`,
);
ctx.workerScript.scriptRef.onlineExpGained += expGain;
Player.gainHackingExp(expGain);
// Account for hidden multiplier in Server.weaken()
return Promise.resolve(weakenAmt * currentNodeMults.ServerWeakenRate);
});
},
ctx.workerScript.scriptRef.onlineExpGained += expGain;
Player.gainHackingExp(expGain);
// Account for hidden multiplier in Server.weaken()
return Promise.resolve(weakenAmt * currentNodeMults.ServerWeakenRate);
});
},
weakenAnalyze:
(ctx) =>
(_threads, _cores = 1) => {

@ -530,7 +530,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
manualHack: (ctx) => () => {
helpers.checkSingularityAccess(ctx);
const server = Player.getCurrentServer();
return helpers.hack(ctx, server.hostname, true);
return helpers.hack(ctx, server.hostname, true, null);
},
installBackdoor: (ctx) => async (): Promise<void> => {
helpers.checkSingularityAccess(ctx);

@ -5,9 +5,43 @@ import { defaultTheme } from "../Themes/Themes";
import { defaultStyles } from "../Themes/Styles";
import { CONSTANTS } from "../Constants";
import { hash } from "../hash/hash";
import { InternalAPI } from "../Netscript/APIWrapper";
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import { Terminal } from "../../src/Terminal";
import { helpers, assertObjectType } from "../Netscript/NetscriptHelpers";
import { helpers, makeRuntimeErrorMsg } from "../Netscript/NetscriptHelpers";
/** Will probably remove the below function in favor of a different approach to object type assertion.
* This method cannot be used to handle optional properties. */
export function assertObjectType<T extends object>(
ctx: NetscriptContext,
name: string,
obj: unknown,
desiredObject: T,
): asserts obj is T {
if (typeof obj !== "object" || obj === null) {
throw makeRuntimeErrorMsg(
ctx,
`Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`,
"TYPE",
);
}
for (const [key, val] of Object.entries(desiredObject)) {
if (!Object.hasOwn(obj, key)) {
throw makeRuntimeErrorMsg(
ctx,
`Object provided for argument ${name} is missing required property ${key}.`,
"TYPE",
);
}
const objVal = (obj as Record<string, unknown>)[key];
if (typeof val !== typeof objVal) {
throw makeRuntimeErrorMsg(
ctx,
`Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`,
"TYPE",
);
}
}
}
export function NetscriptUserInterface(): InternalAPI<IUserInterface> {
return {