2022-08-08 19:43:41 +02:00
|
|
|
import { NetscriptContext } from "./APIWrapper";
|
|
|
|
import { WorkerScript } from "./WorkerScript";
|
2022-08-10 00:08:04 +02:00
|
|
|
import { GetAllServers, GetServer } from "../Server/AllServers";
|
2022-10-10 00:42:14 +02:00
|
|
|
import { Player } from "@player";
|
2022-08-08 19:43:41 +02:00
|
|
|
import { ScriptDeath } from "./ScriptDeath";
|
2023-02-11 19:18:50 +01:00
|
|
|
import { formatExp, formatMoney, formatRam, formatThreads } from "../ui/formatNumber";
|
2022-08-08 19:43:41 +02:00
|
|
|
import { ScriptArg } from "./ScriptArg";
|
2022-12-30 02:28:53 +01:00
|
|
|
import { CityName } from "../Enums";
|
|
|
|
import { BasicHGWOptions, RunningScript as IRunningScript, Person as IPerson } from "@nsdefs";
|
2022-08-08 19:43:41 +02:00
|
|
|
import { Server } from "../Server/Server";
|
|
|
|
import {
|
|
|
|
calculateHackingChance,
|
|
|
|
calculateHackingExpGain,
|
|
|
|
calculateHackingTime,
|
|
|
|
calculatePercentMoneyHacked,
|
|
|
|
} from "../Hacking";
|
|
|
|
import { netscriptCanHack } from "../Hacking/netscriptCanHack";
|
|
|
|
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
|
|
|
|
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
|
|
|
|
import { CONSTANTS } from "../Constants";
|
|
|
|
import { influenceStockThroughServerHack } from "../StockMarket/PlayerInfluencing";
|
2022-09-01 16:13:10 +02:00
|
|
|
import { IPort, NetscriptPort } from "../NetscriptPort";
|
2022-08-08 19:43:41 +02:00
|
|
|
import { NetscriptPorts } from "../NetscriptWorker";
|
|
|
|
import { FormulaGang } from "../Gang/formulas/formulas";
|
|
|
|
import { GangMember } from "../Gang/GangMember";
|
|
|
|
import { GangMemberTask } from "../Gang/GangMemberTask";
|
2022-08-09 21:41:47 +02:00
|
|
|
import { RunningScript } from "../Script/RunningScript";
|
|
|
|
import { toNative } from "../NetscriptFunctions/toNative";
|
2022-08-10 00:08:04 +02:00
|
|
|
import { ScriptIdentifier } from "./ScriptIdentifier";
|
|
|
|
import { findRunningScript, findRunningScriptByPid } from "../Script/ScriptHelpers";
|
|
|
|
import { arrayToString } from "../utils/helpers/arrayToString";
|
|
|
|
import { HacknetServer } from "../Hacknet/HacknetServer";
|
|
|
|
import { BaseServer } from "../Server/BaseServer";
|
2022-09-05 15:55:57 +02:00
|
|
|
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
2022-11-20 14:37:11 +01:00
|
|
|
import { checkEnum } from "../utils/helpers/enum";
|
2022-12-30 02:28:53 +01:00
|
|
|
import { RamCostConstants } from "./RamCostGenerator";
|
2022-08-08 19:43:41 +02:00
|
|
|
|
|
|
|
export const helpers = {
|
|
|
|
string,
|
|
|
|
number,
|
2023-02-14 07:32:01 +01:00
|
|
|
positiveInteger,
|
2022-08-08 19:43:41 +02:00
|
|
|
scriptArgs,
|
2022-08-09 21:41:47 +02:00
|
|
|
argsToString,
|
2022-08-28 11:33:38 +02:00
|
|
|
makeBasicErrorMsg,
|
2022-08-08 19:43:41 +02:00
|
|
|
makeRuntimeErrorMsg,
|
|
|
|
resolveNetscriptRequestedThreads,
|
|
|
|
checkEnvFlags,
|
|
|
|
checkSingularityAccess,
|
|
|
|
netscriptDelay,
|
|
|
|
updateDynamicRam,
|
|
|
|
city,
|
|
|
|
getServer,
|
|
|
|
scriptIdentifier,
|
|
|
|
hack,
|
|
|
|
getValidPort,
|
2022-11-09 13:26:26 +01:00
|
|
|
person,
|
2022-08-08 19:43:41 +02:00
|
|
|
server,
|
|
|
|
gang,
|
|
|
|
gangMember,
|
|
|
|
gangTask,
|
2022-08-08 21:51:50 +02:00
|
|
|
log,
|
2022-08-10 00:08:04 +02:00
|
|
|
getRunningScript,
|
2022-08-09 21:41:47 +02:00
|
|
|
getRunningScriptByArgs,
|
2022-08-10 00:08:04 +02:00
|
|
|
getCannotFindRunningScriptErrorMessage,
|
|
|
|
createPublicRunningScript,
|
|
|
|
failOnHacknetServer,
|
2022-08-08 19:43:41 +02:00
|
|
|
};
|
|
|
|
|
2022-12-30 02:28:53 +01:00
|
|
|
export function assertMember<T extends string>(
|
2022-10-25 03:54:54 +02:00
|
|
|
ctx: NetscriptContext,
|
2022-12-30 02:28:53 +01:00
|
|
|
obj: Record<string, T> | T[],
|
|
|
|
typeName: string,
|
2022-10-25 03:54:54 +02:00
|
|
|
argName: string,
|
|
|
|
v: unknown,
|
|
|
|
): asserts v is T {
|
|
|
|
assertString(ctx, argName, v);
|
2022-12-30 02:28:53 +01:00
|
|
|
if (!checkEnum(obj, v)) throw makeRuntimeErrorMsg(ctx, `${argName}: ${v} is not a valid ${typeName}.`, "TYPE");
|
2022-10-25 03:54:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2022-10-14 17:00:29 +02:00
|
|
|
/** Will probably remove the below function in favor of a different approach to object type assertion.
|
2022-10-14 17:03:50 +02:00
|
|
|
* This method cannot be used to handle optional properties. */
|
2022-10-12 14:49:27 +02:00
|
|
|
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",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const objHas = Object.prototype.hasOwnProperty.bind(obj);
|
|
|
|
for (const [key, val] of Object.entries(desiredObject)) {
|
|
|
|
if (!objHas(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",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 00:07:17 +02:00
|
|
|
const userFriendlyString = (v: unknown): string => {
|
|
|
|
const clip = (s: string): string => {
|
|
|
|
if (s.length > 15) return s.slice(0, 12) + "...";
|
|
|
|
return s;
|
|
|
|
};
|
|
|
|
if (typeof v === "number") return String(v);
|
|
|
|
if (typeof v === "string") {
|
|
|
|
if (v === "") return "empty string";
|
|
|
|
return `'${clip(v)}'`;
|
|
|
|
}
|
|
|
|
const json = JSON.stringify(v);
|
|
|
|
if (!json) return "???";
|
|
|
|
return `'${clip(json)}'`;
|
|
|
|
};
|
|
|
|
|
|
|
|
const debugType = (v: unknown): string => {
|
|
|
|
if (v === null) return `Is null.`;
|
|
|
|
if (v === undefined) return "Is undefined.";
|
|
|
|
if (typeof v === "function") return "Is a function.";
|
|
|
|
return `Is of type '${typeof v}', value: ${userFriendlyString(v)}`;
|
|
|
|
};
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Convert a provided value v for argument argName to string. If it wasn't originally a string or number, throw. */
|
2022-08-08 19:43:41 +02:00
|
|
|
function string(ctx: NetscriptContext, argName: string, v: unknown): string {
|
2022-10-25 03:54:54 +02:00
|
|
|
if (typeof v === "number") v = v + ""; // cast to string;
|
|
|
|
assertString(ctx, argName, v);
|
|
|
|
return v;
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Convert provided value v for argument argName to number. Throw if could not convert to a non-NaN number. */
|
2022-08-08 19:43:41 +02:00
|
|
|
function number(ctx: NetscriptContext, argName: string, v: unknown): number {
|
|
|
|
if (typeof v === "string") {
|
|
|
|
const x = parseFloat(v);
|
|
|
|
if (!isNaN(x)) return x; // otherwise it wasn't even a string representing a number.
|
|
|
|
} else if (typeof v === "number") {
|
|
|
|
if (isNaN(v)) throw makeRuntimeErrorMsg(ctx, `'${argName}' is NaN.`);
|
|
|
|
return v;
|
|
|
|
}
|
2022-09-05 20:12:48 +02:00
|
|
|
throw makeRuntimeErrorMsg(ctx, `'${argName}' should be a number. ${debugType(v)}`, "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
2023-02-14 07:32:01 +01:00
|
|
|
/** Convert provided value v for argument argName to a positive integer, throwing if it looks like something else. */
|
|
|
|
function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): number {
|
|
|
|
const n = number(ctx, argName, v);
|
|
|
|
if (n < 1 || !Number.isInteger(n)) {
|
|
|
|
throw makeRuntimeErrorMsg(ctx, `${argName} should be a positive integer, was ${n}`, "TYPE");
|
|
|
|
}
|
|
|
|
return n;
|
|
|
|
}
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */
|
2022-08-08 19:43:41 +02:00
|
|
|
function scriptArgs(ctx: NetscriptContext, args: unknown) {
|
2022-09-05 20:12:48 +02:00
|
|
|
if (!isScriptArgs(args)) throw makeRuntimeErrorMsg(ctx, "'args' is not an array of script args", "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
return args;
|
|
|
|
}
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Convert multiple arguments for tprint or print into a single string. */
|
2022-08-09 21:41:47 +02:00
|
|
|
function argsToString(args: unknown[]): string {
|
|
|
|
let out = "";
|
|
|
|
for (let arg of args) {
|
|
|
|
if (arg === null) {
|
|
|
|
out += "null";
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (arg === undefined) {
|
|
|
|
out += "undefined";
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
arg = toNative(arg);
|
|
|
|
out += typeof arg === "object" ? JSON.stringify(arg) : `${arg}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Creates an error message string containing hostname, scriptname, and the error message msg */
|
2022-09-05 15:55:57 +02:00
|
|
|
function makeBasicErrorMsg(ws: WorkerScript | ScriptDeath, msg: string, type = "RUNTIME"): string {
|
|
|
|
if (ws instanceof WorkerScript) {
|
|
|
|
for (const scriptUrl of ws.scriptRef.dependencies) {
|
|
|
|
msg = msg.replace(new RegExp(scriptUrl.url, "g"), scriptUrl.filename);
|
|
|
|
}
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
2022-09-05 15:55:57 +02:00
|
|
|
return `${type} ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n${msg}`;
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
2022-08-09 21:41:47 +02:00
|
|
|
/** Creates an error message string with a stack trace. */
|
2022-09-05 20:12:48 +02:00
|
|
|
function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string {
|
2022-08-08 19:43:41 +02:00
|
|
|
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);
|
2022-08-28 11:33:38 +02:00
|
|
|
const ws = ctx.workerScript;
|
|
|
|
const caller = ctx.functionPath;
|
|
|
|
const scripts = ws.getServer().scripts;
|
2022-08-08 19:43:41 +02:00
|
|
|
const userstack = [];
|
|
|
|
for (const stackline of stack) {
|
|
|
|
let filename;
|
|
|
|
for (const script of scripts) {
|
2022-08-30 00:14:54 +02:00
|
|
|
if (script.filename && stackline.includes(script.filename)) {
|
2022-08-08 19:43:41 +02:00
|
|
|
filename = script.filename;
|
|
|
|
}
|
|
|
|
for (const dependency of script.dependencies) {
|
2022-08-30 00:14:54 +02:00
|
|
|
if (stackline.includes(dependency.filename)) {
|
2022-08-08 19:43:41 +02:00
|
|
|
filename = dependency.filename;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!filename) continue;
|
|
|
|
|
|
|
|
interface ILine {
|
|
|
|
line: string;
|
|
|
|
func: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseChromeStackline(line: string): ILine | null {
|
|
|
|
const lineRe = /.*:(\d+):\d+.*/;
|
|
|
|
const funcRe = /.*at (.+) \(.*/;
|
|
|
|
|
|
|
|
const lineMatch = line.match(lineRe);
|
|
|
|
const funcMatch = line.match(funcRe);
|
|
|
|
if (lineMatch && funcMatch) {
|
|
|
|
return { line: lineMatch[1], func: funcMatch[1] };
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
let call = { line: "-1", func: "unknown" };
|
|
|
|
const chromeCall = parseChromeStackline(stackline);
|
|
|
|
if (chromeCall) {
|
|
|
|
call = chromeCall;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseFirefoxStackline(line: string): ILine | null {
|
|
|
|
const lineRe = /.*:(\d+):\d+$/;
|
|
|
|
const lineMatch = line.match(lineRe);
|
|
|
|
|
|
|
|
const lio = line.lastIndexOf("@");
|
|
|
|
|
|
|
|
if (lineMatch && lio !== -1) {
|
|
|
|
return { line: lineMatch[1], func: line.slice(0, lio) };
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const firefoxCall = parseFirefoxStackline(stackline);
|
|
|
|
if (firefoxCall) {
|
|
|
|
call = firefoxCall;
|
|
|
|
}
|
|
|
|
|
|
|
|
userstack.push(`${filename}:L${call.line}@${call.func}`);
|
|
|
|
}
|
|
|
|
|
2022-08-28 11:33:38 +02:00
|
|
|
log(ctx, () => msg);
|
2022-09-05 14:56:39 +02:00
|
|
|
let rejectMsg = `${caller}: ${msg}`;
|
2022-08-29 08:41:17 +02:00
|
|
|
if (userstack.length !== 0) rejectMsg += `\n\nStack:\n${userstack.join("\n")}`;
|
2022-09-05 20:12:48 +02:00
|
|
|
return makeBasicErrorMsg(ws, rejectMsg, type);
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
2022-08-09 21:41:47 +02:00
|
|
|
/** Validate requested number of threads for h/g/w options */
|
2022-08-08 19:43:41 +02:00
|
|
|
function resolveNetscriptRequestedThreads(ctx: NetscriptContext, requestedThreads?: number): number {
|
|
|
|
const threads = ctx.workerScript.scriptRef.threads;
|
|
|
|
if (!requestedThreads) {
|
|
|
|
return isNaN(threads) || threads < 1 ? 1 : threads;
|
|
|
|
}
|
|
|
|
const requestedThreadsAsInt = requestedThreads | 0;
|
|
|
|
if (isNaN(requestedThreads) || requestedThreadsAsInt < 1) {
|
2022-10-12 14:49:27 +02:00
|
|
|
throw makeRuntimeErrorMsg(ctx, `Invalid thread count: ${requestedThreads}. Threads must be a positive number.`);
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
if (requestedThreadsAsInt > threads) {
|
2022-08-28 11:33:38 +02:00
|
|
|
throw makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
2022-08-08 19:43:41 +02:00
|
|
|
`Too many threads requested by ${ctx.function}. Requested: ${requestedThreads}. Has: ${threads}.`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return requestedThreadsAsInt;
|
|
|
|
}
|
|
|
|
|
2022-08-09 21:41:47 +02:00
|
|
|
/** Validate singularity access by throwing an error if the player does not have access. */
|
2022-08-08 19:43:41 +02:00
|
|
|
function checkSingularityAccess(ctx: NetscriptContext): void {
|
|
|
|
if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) {
|
|
|
|
throw makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
|
|
|
`This singularity function requires Source-File 4 to run. A power up you obtain later in the game.
|
|
|
|
It will be very obvious when and how you can obtain it.`,
|
2022-09-05 20:12:48 +02:00
|
|
|
"API ACCESS",
|
2022-08-08 19:43:41 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-09 21:41:47 +02:00
|
|
|
/** Create an error if a script is dead or if concurrent ns function calls are made */
|
2022-08-08 19:43:41 +02:00
|
|
|
function checkEnvFlags(ctx: NetscriptContext): void {
|
|
|
|
const ws = ctx.workerScript;
|
2022-08-28 11:33:38 +02:00
|
|
|
if (ws.env.stopFlag) {
|
|
|
|
log(ctx, () => "Failed to run due to script being killed.");
|
|
|
|
throw new ScriptDeath(ws);
|
|
|
|
}
|
2022-08-08 19:43:41 +02:00
|
|
|
if (ws.env.runningFn && ctx.function !== "asleep") {
|
2022-09-05 15:55:57 +02:00
|
|
|
ws.delayReject?.(new ScriptDeath(ws));
|
2022-08-29 08:41:17 +02:00
|
|
|
ws.env.stopFlag = true;
|
|
|
|
log(ctx, () => "Failed to run due to failed concurrency check.");
|
|
|
|
throw makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
2022-08-08 19:43:41 +02:00
|
|
|
`Concurrent calls to Netscript functions are not allowed!
|
|
|
|
Did you forget to await hack(), grow(), or some other
|
|
|
|
promise-returning function?
|
|
|
|
Currently running: ${ws.env.runningFn} tried to run: ${ctx.function}`,
|
2022-09-05 20:12:48 +02:00
|
|
|
"CONCURRENCY",
|
2022-08-08 19:43:41 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Set a timeout for performing a task, mark the script as busy in the meantime. */
|
2022-08-08 19:43:41 +02:00
|
|
|
function netscriptDelay(ctx: NetscriptContext, time: number): Promise<void> {
|
|
|
|
const ws = ctx.workerScript;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
ws.delay = window.setTimeout(() => {
|
|
|
|
ws.delay = null;
|
|
|
|
ws.delayReject = undefined;
|
|
|
|
ws.env.runningFn = "";
|
|
|
|
if (ws.env.stopFlag) reject(new ScriptDeath(ws));
|
|
|
|
else resolve();
|
|
|
|
}, time);
|
|
|
|
ws.delayReject = reject;
|
|
|
|
ws.env.runningFn = ctx.function;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Adds to dynamic ram cost when calling new ns functions from a script */
|
2022-08-08 19:43:41 +02:00
|
|
|
function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
|
|
|
|
const ws = ctx.workerScript;
|
|
|
|
const fnName = ctx.function;
|
|
|
|
if (ws.dynamicLoadedFns[fnName]) return;
|
|
|
|
ws.dynamicLoadedFns[fnName] = true;
|
|
|
|
|
|
|
|
let threads = ws.scriptRef.threads;
|
|
|
|
if (typeof threads !== "number") {
|
2022-10-09 07:25:31 +02:00
|
|
|
console.warn(`WorkerScript detected NaN for thread count for ${ws.name} on ${ws.hostname}`);
|
2022-08-08 19:43:41 +02:00
|
|
|
threads = 1;
|
|
|
|
}
|
2022-12-30 02:28:53 +01:00
|
|
|
ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max);
|
2022-08-08 19:43:41 +02:00
|
|
|
if (ws.dynamicRamUsage > 1.01 * ws.ramUsage) {
|
2022-08-28 11:33:38 +02:00
|
|
|
log(ctx, () => "Insufficient static ram available.");
|
2022-08-29 08:41:17 +02:00
|
|
|
ws.env.stopFlag = true;
|
|
|
|
throw makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
|
|
|
`Dynamic RAM usage calculated to be greater than initial RAM usage.
|
2022-08-08 19:43:41 +02:00
|
|
|
This is probably because you somehow circumvented the static RAM calculation.
|
|
|
|
|
|
|
|
Threads: ${threads}
|
2023-02-11 19:18:50 +01:00
|
|
|
Dynamic RAM Usage: ${formatRam(ws.dynamicRamUsage)} per thread
|
|
|
|
Static RAM Usage: ${formatRam(ws.ramUsage)} per thread
|
2022-08-08 19:43:41 +02:00
|
|
|
|
|
|
|
One of these could be the reason:
|
|
|
|
* Using eval() to get a reference to a ns function
|
2022-08-29 08:41:17 +02:00
|
|
|
\u00a0\u00a0const myScan = eval('ns.scan');
|
2022-08-08 19:43:41 +02:00
|
|
|
|
|
|
|
* Using map access to do the same
|
2022-08-29 08:41:17 +02:00
|
|
|
\u00a0\u00a0const myScan = ns['scan'];
|
2022-08-08 19:43:41 +02:00
|
|
|
|
|
|
|
Sorry :(`,
|
2022-09-05 20:12:48 +02:00
|
|
|
"RAM USAGE",
|
2022-08-08 19:43:41 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-10 00:08:04 +02:00
|
|
|
/** Validates the input v as being a CityName. Throws an error if it is not. */
|
2022-08-08 19:43:41 +02:00
|
|
|
function city(ctx: NetscriptContext, argName: string, v: unknown): CityName {
|
2022-12-30 02:28:53 +01:00
|
|
|
if (typeof v !== "string" || !checkEnum(CityName, v))
|
|
|
|
throw makeRuntimeErrorMsg(ctx, `${argName} should be a city name.`);
|
2022-10-25 03:54:54 +02:00
|
|
|
return v;
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function scriptIdentifier(
|
|
|
|
ctx: NetscriptContext,
|
|
|
|
scriptID: unknown,
|
|
|
|
_hostname: unknown,
|
|
|
|
_args: unknown,
|
|
|
|
): ScriptIdentifier {
|
|
|
|
const ws = ctx.workerScript;
|
|
|
|
// Provide the pid for the current script if no identifier provided
|
|
|
|
if (scriptID === undefined) return ws.pid;
|
|
|
|
if (typeof scriptID === "number") return scriptID;
|
|
|
|
if (typeof scriptID === "string") {
|
|
|
|
const hostname = _hostname === undefined ? ctx.workerScript.hostname : string(ctx, "hostname", _hostname);
|
|
|
|
const args = _args === undefined ? [] : scriptArgs(ctx, _args);
|
|
|
|
return {
|
|
|
|
scriptname: scriptID,
|
|
|
|
hostname,
|
|
|
|
args,
|
|
|
|
};
|
|
|
|
}
|
2022-09-05 20:12:48 +02:00
|
|
|
throw makeRuntimeErrorMsg(ctx, "An unknown type of input was provided as a script identifier.", "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the Server for a specific hostname/ip, throwing an error
|
|
|
|
* if the server doesn't exist.
|
|
|
|
* @param {NetscriptContext} ctx - Context from which getServer is being called. For logging purposes.
|
|
|
|
* @param {string} hostname - Hostname of the server
|
|
|
|
* @returns {BaseServer} The specified server as a BaseServer
|
|
|
|
*/
|
|
|
|
function getServer(ctx: NetscriptContext, hostname: string) {
|
|
|
|
const server = GetServer(hostname);
|
|
|
|
if (server == null) {
|
|
|
|
const str = hostname === "" ? "'' (empty string)" : "'" + hostname + "'";
|
|
|
|
throw makeRuntimeErrorMsg(ctx, `Invalid hostname: ${str}`);
|
|
|
|
}
|
|
|
|
return server;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isScriptArgs(args: unknown): args is ScriptArg[] {
|
|
|
|
const isScriptArg = (arg: unknown) => typeof arg === "string" || typeof arg === "number" || typeof arg === "boolean";
|
|
|
|
return Array.isArray(args) && args.every(isScriptArg);
|
|
|
|
}
|
|
|
|
|
2022-08-29 08:41:17 +02:00
|
|
|
function hack(
|
2022-08-08 19:43:41 +02:00
|
|
|
ctx: NetscriptContext,
|
|
|
|
hostname: string,
|
|
|
|
manual: boolean,
|
|
|
|
{ threads: requestedThreads, stock }: BasicHGWOptions = {},
|
|
|
|
): Promise<number> {
|
|
|
|
const ws = ctx.workerScript;
|
|
|
|
const threads = helpers.resolveNetscriptRequestedThreads(ctx, requestedThreads);
|
|
|
|
const server = getServer(ctx, hostname);
|
|
|
|
if (!(server instanceof Server)) {
|
|
|
|
throw makeRuntimeErrorMsg(ctx, "Cannot be executed on this server.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate the hacking time
|
|
|
|
const hackingTime = calculateHackingTime(server, Player); // This is in seconds
|
|
|
|
|
|
|
|
// No root access or skill level too low
|
2022-09-06 15:07:12 +02:00
|
|
|
const canHack = netscriptCanHack(server);
|
2022-08-08 19:43:41 +02:00
|
|
|
if (!canHack.res) {
|
|
|
|
throw makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
|
|
|
}
|
|
|
|
|
2022-08-08 21:51:50 +02:00
|
|
|
log(
|
|
|
|
ctx,
|
2022-08-08 19:43:41 +02:00
|
|
|
() =>
|
|
|
|
`Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
|
|
|
|
hackingTime * 1000,
|
|
|
|
true,
|
2023-02-11 19:18:50 +01:00
|
|
|
)} (t=${formatThreads(threads)})`,
|
2022-08-08 19:43:41 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
return helpers.netscriptDelay(ctx, hackingTime * 1000).then(function () {
|
|
|
|
const hackChance = calculateHackingChance(server, Player);
|
|
|
|
const rand = Math.random();
|
|
|
|
let expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads;
|
|
|
|
const expGainedOnFailure = expGainedOnSuccess / 4;
|
|
|
|
if (rand < hackChance) {
|
|
|
|
// Success!
|
|
|
|
const percentHacked = calculatePercentMoneyHacked(server, Player);
|
|
|
|
let maxThreadNeeded = Math.ceil(1 / percentHacked);
|
|
|
|
if (isNaN(maxThreadNeeded)) {
|
|
|
|
// Server has a 'max money' of 0 (probably). We'll set this to an arbitrarily large value
|
|
|
|
maxThreadNeeded = 1e6;
|
|
|
|
}
|
|
|
|
|
|
|
|
let moneyDrained = Math.floor(server.moneyAvailable * percentHacked) * threads;
|
|
|
|
|
|
|
|
// Over-the-top safety checks
|
|
|
|
if (moneyDrained <= 0) {
|
|
|
|
moneyDrained = 0;
|
|
|
|
expGainedOnSuccess = expGainedOnFailure;
|
|
|
|
}
|
|
|
|
if (moneyDrained > server.moneyAvailable) {
|
|
|
|
moneyDrained = server.moneyAvailable;
|
|
|
|
}
|
|
|
|
server.moneyAvailable -= moneyDrained;
|
|
|
|
if (server.moneyAvailable < 0) {
|
|
|
|
server.moneyAvailable = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
let moneyGained = moneyDrained * BitNodeMultipliers.ScriptHackMoneyGain;
|
|
|
|
if (manual) {
|
|
|
|
moneyGained = moneyDrained * BitNodeMultipliers.ManualHackMoney;
|
|
|
|
}
|
|
|
|
|
|
|
|
Player.gainMoney(moneyGained, "hacking");
|
|
|
|
ws.scriptRef.onlineMoneyMade += moneyGained;
|
|
|
|
Player.scriptProdSinceLastAug += moneyGained;
|
|
|
|
ws.scriptRef.recordHack(server.hostname, moneyGained, threads);
|
|
|
|
Player.gainHackingExp(expGainedOnSuccess);
|
|
|
|
if (manual) Player.gainIntelligenceExp(0.005);
|
|
|
|
ws.scriptRef.onlineExpGained += expGainedOnSuccess;
|
2022-08-08 21:51:50 +02:00
|
|
|
log(
|
|
|
|
ctx,
|
2022-08-08 19:43:41 +02:00
|
|
|
() =>
|
2023-02-11 19:18:50 +01:00
|
|
|
`Successfully hacked '${server.hostname}' for ${formatMoney(moneyGained)} and ${formatExp(
|
|
|
|
expGainedOnSuccess,
|
|
|
|
)} exp (t=${formatThreads(threads)})`,
|
2022-08-08 19:43:41 +02:00
|
|
|
);
|
|
|
|
server.fortify(CONSTANTS.ServerFortifyAmount * Math.min(threads, maxThreadNeeded));
|
|
|
|
if (stock) {
|
|
|
|
influenceStockThroughServerHack(server, moneyDrained);
|
|
|
|
}
|
|
|
|
if (manual) {
|
|
|
|
server.backdoorInstalled = true;
|
|
|
|
}
|
2022-08-29 08:41:17 +02:00
|
|
|
return moneyGained;
|
2022-08-08 19:43:41 +02:00
|
|
|
} else {
|
|
|
|
// Player only gains 25% exp for failure?
|
|
|
|
Player.gainHackingExp(expGainedOnFailure);
|
|
|
|
ws.scriptRef.onlineExpGained += expGainedOnFailure;
|
2022-08-08 21:51:50 +02:00
|
|
|
log(
|
|
|
|
ctx,
|
2022-08-08 19:43:41 +02:00
|
|
|
() =>
|
2023-02-11 19:18:50 +01:00
|
|
|
`Failed to hack '${server.hostname}'. Gained ${formatExp(expGainedOnFailure)} exp (t=${formatThreads(
|
|
|
|
threads,
|
|
|
|
)})`,
|
2022-08-08 19:43:41 +02:00
|
|
|
);
|
2022-08-29 08:41:17 +02:00
|
|
|
return 0;
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getValidPort(ctx: NetscriptContext, port: number): IPort {
|
|
|
|
if (isNaN(port)) {
|
|
|
|
throw makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
|
|
|
`Invalid argument. Must be a port number between 1 and ${CONSTANTS.NumNetscriptPorts}, is ${port}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
port = Math.round(port);
|
|
|
|
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
|
|
|
|
throw makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
|
|
|
`Trying to use an invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
|
|
|
|
);
|
|
|
|
}
|
2022-09-01 16:13:10 +02:00
|
|
|
let iport = NetscriptPorts.get(port);
|
2022-09-23 05:55:16 +02:00
|
|
|
if (!iport) {
|
|
|
|
iport = NetscriptPort();
|
|
|
|
NetscriptPorts.set(port, iport);
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
return iport;
|
|
|
|
}
|
|
|
|
|
2022-11-09 13:26:26 +01:00
|
|
|
function person(ctx: NetscriptContext, p: unknown): IPerson {
|
|
|
|
const fakePerson = {
|
2022-08-08 19:43:41 +02:00
|
|
|
hp: undefined,
|
2022-11-09 13:26:26 +01:00
|
|
|
exp: undefined,
|
2022-08-08 19:43:41 +02:00
|
|
|
mults: undefined,
|
|
|
|
city: undefined,
|
|
|
|
};
|
2022-11-09 13:26:26 +01:00
|
|
|
if (!roughlyIs(fakePerson, p)) throw makeRuntimeErrorMsg(ctx, `person should be a Person.`, "TYPE");
|
|
|
|
return p as IPerson;
|
2022-08-08 19:43:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function server(ctx: NetscriptContext, s: unknown): Server {
|
2022-08-23 22:01:34 +02:00
|
|
|
const fakeServer = {
|
|
|
|
cpuCores: undefined,
|
|
|
|
ftpPortOpen: undefined,
|
|
|
|
hasAdminRights: undefined,
|
|
|
|
hostname: undefined,
|
|
|
|
httpPortOpen: undefined,
|
|
|
|
ip: undefined,
|
|
|
|
isConnectedTo: undefined,
|
|
|
|
maxRam: undefined,
|
|
|
|
organizationName: undefined,
|
|
|
|
ramUsed: undefined,
|
|
|
|
smtpPortOpen: undefined,
|
|
|
|
sqlPortOpen: undefined,
|
|
|
|
sshPortOpen: undefined,
|
|
|
|
purchasedByPlayer: undefined,
|
|
|
|
backdoorInstalled: undefined,
|
|
|
|
baseDifficulty: undefined,
|
|
|
|
hackDifficulty: undefined,
|
|
|
|
minDifficulty: undefined,
|
|
|
|
moneyAvailable: undefined,
|
|
|
|
moneyMax: undefined,
|
|
|
|
numOpenPortsRequired: undefined,
|
|
|
|
openPortCount: undefined,
|
|
|
|
requiredHackingSkill: undefined,
|
|
|
|
serverGrowth: undefined,
|
|
|
|
};
|
2022-09-05 20:12:48 +02:00
|
|
|
if (!roughlyIs(fakeServer, s)) throw makeRuntimeErrorMsg(ctx, `server should be a Server.`, "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
return s as Server;
|
|
|
|
}
|
|
|
|
|
|
|
|
function roughlyIs(expect: object, actual: unknown): boolean {
|
|
|
|
if (typeof actual !== "object" || actual == null) return false;
|
2022-08-23 22:01:34 +02:00
|
|
|
|
2022-08-08 19:43:41 +02:00
|
|
|
const expects = Object.keys(expect);
|
|
|
|
const actuals = Object.keys(actual);
|
|
|
|
for (const expect of expects)
|
|
|
|
if (!actuals.includes(expect)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function gang(ctx: NetscriptContext, g: unknown): FormulaGang {
|
|
|
|
if (!roughlyIs({ respect: 0, territory: 0, wantedLevel: 0 }, g))
|
2022-09-05 20:12:48 +02:00
|
|
|
throw makeRuntimeErrorMsg(ctx, `gang should be a Gang.`, "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
return g as FormulaGang;
|
|
|
|
}
|
|
|
|
|
|
|
|
function gangMember(ctx: NetscriptContext, m: unknown): GangMember {
|
2022-09-05 20:12:48 +02:00
|
|
|
if (!roughlyIs(new GangMember(), m)) throw makeRuntimeErrorMsg(ctx, `member should be a GangMember.`, "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
return m as GangMember;
|
|
|
|
}
|
|
|
|
|
|
|
|
function gangTask(ctx: NetscriptContext, t: unknown): GangMemberTask {
|
2022-08-25 11:08:33 +02:00
|
|
|
if (!roughlyIs(new GangMemberTask("", "", false, false, { hackWeight: 100 }), t))
|
2022-09-05 20:12:48 +02:00
|
|
|
throw makeRuntimeErrorMsg(ctx, `task should be a GangMemberTask.`, "TYPE");
|
2022-08-08 19:43:41 +02:00
|
|
|
return t as GangMemberTask;
|
|
|
|
}
|
2022-08-08 22:13:04 +02:00
|
|
|
|
2022-08-08 21:51:50 +02:00
|
|
|
function log(ctx: NetscriptContext, message: () => string) {
|
|
|
|
ctx.workerScript.log(ctx.functionPath, message);
|
|
|
|
}
|
2022-08-09 21:41:47 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Searches for and returns the RunningScript object for the specified script.
|
|
|
|
* If the 'fn' argument is not specified, this returns the current RunningScript.
|
|
|
|
* @param {string} fn - Filename of script
|
|
|
|
* @param {string} hostname - Hostname/ip of the server on which the script resides
|
|
|
|
* @param {any[]} scriptArgs - Running script's arguments
|
|
|
|
* @returns {RunningScript}
|
|
|
|
* Running script identified by the parameters, or null if no such script
|
|
|
|
* exists, or the current running script if the first argument 'fn'
|
|
|
|
* is not specified.
|
|
|
|
*/
|
|
|
|
function getRunningScriptByArgs(
|
|
|
|
ctx: NetscriptContext,
|
|
|
|
fn: string,
|
|
|
|
hostname: string,
|
|
|
|
scriptArgs: ScriptArg[],
|
|
|
|
): RunningScript | null {
|
|
|
|
if (!Array.isArray(scriptArgs)) {
|
2022-08-29 08:41:17 +02:00
|
|
|
throw helpers.makeRuntimeErrorMsg(
|
|
|
|
ctx,
|
|
|
|
"Invalid scriptArgs argument passed into getRunningScriptByArgs().\n" +
|
|
|
|
"This is probably a bug. Please report to game developer",
|
2022-08-09 21:41:47 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fn != null && typeof fn === "string") {
|
|
|
|
// Get Logs of another script
|
|
|
|
if (hostname == null) {
|
|
|
|
hostname = ctx.workerScript.hostname;
|
|
|
|
}
|
|
|
|
const server = helpers.getServer(ctx, hostname);
|
|
|
|
|
|
|
|
return findRunningScript(fn, scriptArgs, server);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no arguments are specified, return the current RunningScript
|
|
|
|
return ctx.workerScript.scriptRef;
|
|
|
|
}
|
2022-08-10 00:08:04 +02:00
|
|
|
|
|
|
|
function getRunningScriptByPid(pid: number): RunningScript | null {
|
|
|
|
for (const server of GetAllServers()) {
|
|
|
|
const runningScript = findRunningScriptByPid(pid, server);
|
|
|
|
if (runningScript) return runningScript;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRunningScript(ctx: NetscriptContext, ident: ScriptIdentifier): RunningScript | null {
|
|
|
|
if (typeof ident === "number") {
|
|
|
|
return getRunningScriptByPid(ident);
|
|
|
|
} else {
|
|
|
|
return getRunningScriptByArgs(ctx, ident.scriptname, ident.hostname, ident.args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function for getting the error log message when the user specifies
|
|
|
|
* a nonexistent running script
|
|
|
|
* @param {string} fn - Filename of script
|
|
|
|
* @param {string} hostname - Hostname/ip of the server on which the script resides
|
|
|
|
* @param {any[]} scriptArgs - Running script's arguments
|
|
|
|
* @returns {string} Error message to print to logs
|
|
|
|
*/
|
|
|
|
function getCannotFindRunningScriptErrorMessage(ident: ScriptIdentifier): string {
|
|
|
|
if (typeof ident === "number") return `Cannot find running script with pid: ${ident}`;
|
|
|
|
|
|
|
|
return `Cannot find running script ${ident.scriptname} on server ${ident.hostname} with args: ${arrayToString(
|
|
|
|
ident.args,
|
|
|
|
)}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sanitizes a `RunningScript` to remove sensitive information, making it suitable for
|
|
|
|
* return through an NS function.
|
|
|
|
* @see NS.getRecentScripts
|
|
|
|
* @see NS.getRunningScript
|
|
|
|
* @param runningScript Existing, internal RunningScript
|
|
|
|
* @returns A sanitized, NS-facing copy of the RunningScript
|
|
|
|
*/
|
|
|
|
function createPublicRunningScript(runningScript: RunningScript): IRunningScript {
|
|
|
|
return {
|
|
|
|
args: runningScript.args.slice(),
|
|
|
|
filename: runningScript.filename,
|
2023-01-06 02:30:34 +01:00
|
|
|
logs: runningScript.logs.map((x) => "" + x),
|
2022-08-10 00:08:04 +02:00
|
|
|
offlineExpGained: runningScript.offlineExpGained,
|
|
|
|
offlineMoneyMade: runningScript.offlineMoneyMade,
|
|
|
|
offlineRunningTime: runningScript.offlineRunningTime,
|
|
|
|
onlineExpGained: runningScript.onlineExpGained,
|
|
|
|
onlineMoneyMade: runningScript.onlineMoneyMade,
|
|
|
|
onlineRunningTime: runningScript.onlineRunningTime,
|
|
|
|
pid: runningScript.pid,
|
|
|
|
ramUsage: runningScript.ramUsage,
|
|
|
|
server: runningScript.server,
|
|
|
|
threads: runningScript.threads,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to fail a function if the function's target is a Hacknet Server.
|
|
|
|
* This is used for functions that should run on normal Servers, but not Hacknet Servers
|
|
|
|
* @param {Server} server - Target server
|
|
|
|
* @param {string} callingFn - Name of calling function. For logging purposes
|
|
|
|
* @returns {boolean} True if the server is a Hacknet Server, false otherwise
|
|
|
|
*/
|
2022-09-05 16:35:12 +02:00
|
|
|
function failOnHacknetServer(ctx: NetscriptContext, server: BaseServer): boolean {
|
2022-08-10 00:08:04 +02:00
|
|
|
if (server instanceof HacknetServer) {
|
2022-09-05 16:35:12 +02:00
|
|
|
log(ctx, () => `Does not work on Hacknet Servers`);
|
2022-08-10 00:08:04 +02:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2022-09-05 15:55:57 +02:00
|
|
|
|
|
|
|
/** Generate an error dialog when workerscript is known */
|
|
|
|
export function handleUnknownError(e: unknown, ws: WorkerScript | ScriptDeath | null = null, initialText = "") {
|
|
|
|
if (e instanceof ScriptDeath) {
|
|
|
|
//No dialog for an empty ScriptDeath
|
|
|
|
if (e.errorMessage === "") return;
|
|
|
|
if (!ws) {
|
|
|
|
ws = e;
|
|
|
|
e = ws.errorMessage;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (ws && typeof e === "string") {
|
|
|
|
const headerText = makeBasicErrorMsg(ws, "", "");
|
|
|
|
if (!e.includes(headerText)) e = makeBasicErrorMsg(ws, e);
|
|
|
|
} else if (e instanceof SyntaxError) {
|
|
|
|
const msg = `${e.message} (sorry we can't be more helpful)`;
|
|
|
|
e = ws ? makeBasicErrorMsg(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`;
|
|
|
|
} else if (e instanceof Error) {
|
2022-11-10 16:08:59 +01:00
|
|
|
// Ignore any cancellation errors from Monaco that get here
|
|
|
|
if (e.name === "Canceled" && e.message === "Canceled") return;
|
2022-09-05 15:55:57 +02:00
|
|
|
const msg = `${e.message}${e.stack ? `\nstack:\n${e.stack.toString()}` : ""}`;
|
|
|
|
e = ws ? makeBasicErrorMsg(ws, msg) : `RUNTIME ERROR:\n\n${msg}`;
|
|
|
|
}
|
|
|
|
if (typeof e !== "string") {
|
2022-10-19 22:32:39 +02:00
|
|
|
console.error("Unexpected error:", e);
|
|
|
|
const msg = `Unexpected type of error thrown. This error was likely thrown manually within a script.
|
|
|
|
Error has been logged to the console.\n\nType of error: ${typeof e}\nValue of error: ${e}`;
|
|
|
|
e = ws ? makeBasicErrorMsg(ws, msg, "UNKNOWN") : msg;
|
2022-09-05 15:55:57 +02:00
|
|
|
}
|
|
|
|
dialogBoxCreate(initialText + e);
|
|
|
|
}
|