SCRIPTS: Script modules are reused when they are imported (#461)

Also corrects some compile race conditions.
This commit is contained in:
Snarling
2023-04-07 00:33:51 -04:00
committed by GitHub
parent f5cddb6984
commit 04d49e3a6d
67 changed files with 428 additions and 561 deletions

16
package-lock.json generated
View File

@ -6068,9 +6068,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001414",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001414.tgz",
"integrity": "sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg==",
"version": "1.0.30001473",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001473.tgz",
"integrity": "sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg==",
"funding": [
{
"type": "opencollective",
@ -6079,6 +6079,10 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
},
@ -20196,9 +20200,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001414",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001414.tgz",
"integrity": "sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg=="
"version": "1.0.30001473",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001473.tgz",
"integrity": "sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg=="
},
"chalk": {
"version": "2.4.2",

View File

@ -6,7 +6,7 @@ import { Factions } from "../Faction/Factions";
import { formatPercent } from "../ui/formatNumber";
import { Money } from "../ui/React/Money";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { FactionNames } from "../Faction/data/FactionNames";
import { Player } from "@player";
import { AugmentationNames } from "./data/AugmentationNames";
@ -607,4 +607,4 @@ export class Augmentation {
}
}
Reviver.constructors.Augmentation = Augmentation;
constructorsForReviver.Augmentation = Augmentation;

View File

@ -1,6 +1,6 @@
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { addOffset } from "../utils/helpers/addOffset";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { BladeburnerConstants } from "./data/Constants";
import { Bladeburner } from "./Bladeburner";
import { Person } from "../PersonObjects/Person";
@ -304,4 +304,4 @@ export class Action {
}
}
Reviver.constructors.Action = Action;
constructorsForReviver.Action = Action;

View File

@ -1,4 +1,4 @@
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
interface IParams {
name?: string;
@ -23,4 +23,4 @@ export class ActionIdentifier {
}
}
Reviver.constructors.ActionIdentifier = ActionIdentifier;
constructorsForReviver.ActionIdentifier = ActionIdentifier;

View File

@ -1,5 +1,5 @@
import { Operation, IOperationParams } from "./Operation";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class BlackOperation extends Operation {
constructor(params: IOperationParams | null = null) {
@ -29,4 +29,4 @@ export class BlackOperation extends Operation {
}
}
Reviver.constructors.BlackOperation = BlackOperation;
constructorsForReviver.BlackOperation = BlackOperation;

View File

@ -1,4 +1,4 @@
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { ActionIdentifier } from "./ActionIdentifier";
import { ActionTypes } from "./data/ActionTypes";
import { Growths } from "./data/Growths";
@ -2390,4 +2390,4 @@ export class Bladeburner {
}
}
Reviver.constructors.Bladeburner = Bladeburner;
constructorsForReviver.Bladeburner = Bladeburner;

View File

@ -1,6 +1,6 @@
import { BladeburnerConstants } from "./data/Constants";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { addOffset } from "../utils/helpers/addOffset";
import { CityName } from "../Enums";
@ -169,4 +169,4 @@ export class City {
}
}
Reviver.constructors.City = City;
constructorsForReviver.City = City;

View File

@ -1,6 +1,6 @@
import { Bladeburner } from "./Bladeburner";
import { Action, IActionParams } from "./Action";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class Contract extends Action {
constructor(params: IActionParams | null = null) {
@ -20,4 +20,4 @@ export class Contract extends Action {
}
}
Reviver.constructors.Contract = Contract;
constructorsForReviver.Contract = Contract;

View File

@ -1,7 +1,7 @@
import { Bladeburner } from "./Bladeburner";
import { BladeburnerConstants } from "./data/Constants";
import { Action, IActionParams } from "./Action";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export interface IOperationParams extends IActionParams {
reqdRank?: number;
@ -53,4 +53,4 @@ export class Operation extends Action {
}
}
Reviver.constructors.Operation = Operation;
constructorsForReviver.Operation = Operation;

View File

@ -1,6 +1,6 @@
import { codingContractTypesMetadata, DescriptionFunc, GeneratorFunc, SolverFunc } from "./data/codingcontracttypes";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "./utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
import { CodingContractEvent } from "./ui/React/CodingContractModal";
/* tslint:disable:no-magic-numbers completed-docs max-classes-per-file no-console */
@ -172,4 +172,4 @@ export class CodingContract {
}
}
Reviver.constructors.CodingContract = CodingContract;
constructorsForReviver.CodingContract = CodingContract;

View File

@ -2,7 +2,7 @@ import { CompanyPosition } from "./CompanyPosition";
import * as posNames from "./data/JobTracks";
import { favorToRep, repToFavor } from "../Faction/formulas/favor";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export interface IConstructorParams {
name: string;
@ -143,4 +143,4 @@ export class Company {
}
}
Reviver.constructors.Company = Company;
constructorsForReviver.Company = Company;

View File

@ -10,7 +10,7 @@ import { LiteratureNames } from "../Literature/data/LiteratureNames";
import { Player } from "@player";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { isString } from "../utils/helpers/isString";
import { CityName } from "../Enums";
import { CorpStateName } from "@nsdefs";
@ -468,4 +468,4 @@ export class Corporation {
}
}
Reviver.constructors.Corporation = Corporation;
constructorsForReviver.Corporation = Corporation;

View File

@ -1,5 +1,5 @@
import { CorpStateName } from "@nsdefs";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { stateNames } from "./data/Constants";
export class CorporationState {
// Number representing what state the Corporation is in. The number
@ -36,4 +36,4 @@ export class CorporationState {
}
}
Reviver.constructors.CorporationState = CorporationState;
constructorsForReviver.CorporationState = CorporationState;

View File

@ -1,4 +1,4 @@
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { CityName } from "../Enums";
import { IndustryResearchTrees, IndustriesData } from "./IndustryData";
import * as corpConstants from "./data/Constants";
@ -1238,4 +1238,4 @@ export class Industry {
}
}
Reviver.constructors.Industry = Industry;
constructorsForReviver.Industry = Industry;

View File

@ -1,5 +1,5 @@
import { CorpMaterialName } from "@nsdefs";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { materialNames } from "./data/Constants";
import { Export } from "./Export";
import { MaterialInfo } from "./MaterialInfo";
@ -140,4 +140,4 @@ export class Material {
}
}
Reviver.constructors.Material = Material;
constructorsForReviver.Material = Material;

View File

@ -1,6 +1,6 @@
import { EmployeePositions } from "./data/Enums";
import * as corpConstants from "./data/Constants";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { Industry } from "./Industry";
import { Corporation } from "./Corporation";
import { getRandomInt } from "../utils/helpers/getRandomInt";
@ -272,4 +272,4 @@ export class OfficeSpace {
}
}
Reviver.constructors.OfficeSpace = OfficeSpace;
constructorsForReviver.OfficeSpace = OfficeSpace;

View File

@ -5,7 +5,7 @@ import { IndustriesData } from "./IndustryData";
import { createCityMap } from "../Locations/createCityMap";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { CityName } from "../Enums";
import { materialNames } from "./data/Constants";
@ -285,4 +285,4 @@ export class Product {
}
}
Reviver.constructors.Product = Product;
constructorsForReviver.Product = Product;

View File

@ -2,7 +2,7 @@ import { Material } from "./Material";
import { Corporation } from "./Corporation";
import { Industry } from "./Industry";
import { MaterialInfo } from "./MaterialInfo";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { CityName } from "../Enums";
import { CorpMaterialName } from "@nsdefs";
@ -107,4 +107,4 @@ export class Warehouse {
}
}
Reviver.constructors.Warehouse = Warehouse;
constructorsForReviver.Warehouse = Warehouse;

View File

@ -1,5 +1,5 @@
import { Fragment, FragmentById } from "./Fragment";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export interface IActiveFragmentParams {
x: number;
@ -82,4 +82,4 @@ export class ActiveFragment {
}
}
Reviver.constructors.ActiveFragment = ActiveFragment;
constructorsForReviver.ActiveFragment = ActiveFragment;

View File

@ -6,7 +6,7 @@ import { BaseGift } from "./BaseGift";
import { Factions } from "../Faction/Factions";
import { CalculateEffect } from "./formulas/effect";
import { StaneksGiftEvents } from "./StaneksGiftEvents";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { StanekConstants } from "./data/Constants";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { Player } from "@player";
@ -255,4 +255,4 @@ export class StaneksGift extends BaseGift {
}
}
Reviver.constructors.StaneksGift = StaneksGift;
constructorsForReviver.StaneksGift = StaneksGift;

View File

@ -1,6 +1,6 @@
import { FactionInfo, FactionInfos } from "./FactionInfo";
import { favorToRep, repToFavor } from "./formulas/favor";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class Faction {
/**
@ -71,4 +71,4 @@ export class Faction {
}
}
Reviver.constructors.Faction = Faction;
constructorsForReviver.Faction = Faction;

View File

@ -7,7 +7,7 @@
import { Factions } from "../Faction/Factions";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { getRandomInt } from "../utils/helpers/getRandomInt";
@ -403,4 +403,4 @@ export class Gang {
}
}
Reviver.constructors.Gang = Gang;
constructorsForReviver.Gang = Gang;

View File

@ -5,7 +5,7 @@ import { GangMemberUpgrades } from "./GangMemberUpgrades";
import { IAscensionResult } from "./IAscensionResult";
import { Player } from "@player";
import { Gang } from "./Gang";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import {
calculateRespectGain,
calculateMoneyGain,
@ -332,4 +332,4 @@ export class GangMember {
}
}
Reviver.constructors.GangMember = GangMember;
constructorsForReviver.GangMember = GangMember;

View File

@ -17,7 +17,7 @@ import {
import { HacknetNodeConstants } from "./data/Constants";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { ObjectValidator, minMax } from "../utils/Validator";
export class HacknetNode implements IHacknetNode {
@ -131,4 +131,4 @@ export class HacknetNode implements IHacknetNode {
}
}
Reviver.constructors.HacknetNode = HacknetNode;
constructorsForReviver.HacknetNode = HacknetNode;

View File

@ -15,7 +15,7 @@ import {
import { createRandomIp } from "../utils/IPAddress";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { Player } from "@player";
interface IConstructorParams {
@ -153,4 +153,4 @@ export class HacknetServer extends BaseServer implements IHacknetNode {
}
}
Reviver.constructors.HacknetServer = HacknetServer;
constructorsForReviver.HacknetServer = HacknetServer;

View File

@ -9,7 +9,7 @@
import { HashUpgrades } from "./HashUpgrades";
import { HashUpgrade } from "./HashUpgrade";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class HashManager {
// Max number of hashes this can hold. Equal to the sum of capacities of
@ -156,4 +156,4 @@ export class HashManager {
}
}
Reviver.constructors.HashManager = HashManager;
constructorsForReviver.HashManager = HashManager;

View File

@ -234,8 +234,8 @@ function argsToString(args: unknown[]): string {
/** Creates an error message string containing hostname, scriptname, and the error message msg */
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);
for (const [scriptUrl, script] of ws.scriptRef.dependencies) {
msg = msg.replace(new RegExp(scriptUrl, "g"), script.filename);
}
}
return `${type} ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n${msg}`;
@ -248,20 +248,19 @@ function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME
const stack = errstack.split("\n").slice(1);
const ws = ctx.workerScript;
const caller = ctx.functionPath;
const scripts = ws.getServer().scripts;
const userstack = [];
for (const stackline of stack) {
let filename;
for (const script of scripts) {
if (script.filename && stackline.includes(script.filename)) {
filename = script.filename;
const filename = (() => {
// Filename is current file if url found
if (ws.scriptRef.url && stackline.includes(ws.scriptRef.url)) return ws.scriptRef.filename;
// Also check urls for dependencies
for (const [url, script] of ws.scriptRef.dependencies) if (stackline.includes(url)) return script.filename;
// Check for filenames directly if no URL found
if (stackline.includes(ws.scriptRef.filename)) return ws.scriptRef.filename;
for (const script of ws.scriptRef.dependencies.values()) {
if (stackline.includes(script.filename)) return script.filename;
}
for (const dependency of script.dependencies) {
if (stackline.includes(dependency.filename)) {
filename = dependency.filename;
}
}
}
})();
if (!filename) continue;
interface ILine {

View File

@ -893,18 +893,15 @@ export const ns: InternalAPI<NSFull> = {
}
destScript.code = sourceScript.code;
// Set ramUsage to null in order to force a recalculation prior to next run.
destScript.ramUsage = null;
destScript.markUpdated();
destScript.invalidateModule();
helpers.log(ctx, () => `WARNING: File '${file}' overwritten on '${destServer?.hostname}'`);
continue;
}
// Create new script if it does not already exist
const newScript = new Script(file);
newScript.code = sourceScript.code;
// Set ramUsage to null in order to force a recalculation prior to next run.
newScript.ramUsage = null;
newScript.server = destServer.hostname;
const newScript = new Script(file, sourceScript.code, destServer.hostname);
// If the script being copied has no dependencies, reuse the module / URL
// The new script will not show up in the correct location in the sources tab because it is just reusing the module from a different server
destServer.scripts.push(newScript);
helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`);
}
@ -1389,7 +1386,7 @@ export const ns: InternalAPI<NSFull> = {
}
mode === "w" ? (script.code = String(data)) : (script.code += data);
// Set ram to null so a recalc is performed the next time ram usage is needed
script.ramUsage = null;
script.invalidateModule();
return;
} else {
// Write to text file
@ -1831,14 +1828,14 @@ export const ns: InternalAPI<NSFull> = {
dest_file.text = source_file.text;
} else if (dest_file instanceof Script && source_file instanceof Script) {
dest_file.code = source_file.code;
dest_file.markUpdated();
dest_file.invalidateModule();
}
destServer.removeFile(source);
} else {
source_file.filename = destination;
if (source_file instanceof Script) {
source_file.markUpdated();
source_file.invalidateModule();
}
}
},

View File

@ -5,8 +5,7 @@
import * as walk from "acorn-walk";
import { parse } from "acorn";
import { ScriptUrl } from "./Script/ScriptUrl";
import { Script } from "./Script/Script";
import { Script, ScriptURL } from "./Script/Script";
import { areImportsEquals } from "./Terminal/DirectoryHelpers";
import { ScriptModule } from "./Script/ScriptModule";
@ -18,6 +17,24 @@ function makeScriptBlob(code: string): Blob {
return new Blob([code], { type: "text/javascript" });
}
const urlsToRevoke: ScriptURL[] = [];
let activeCompilations = 0;
/** Function to queue up revoking of script URLs. If there's no active compilation, just revoke it now. */
export const queueUrlRevoke = (url: ScriptURL) => {
if (!activeCompilations) return URL.revokeObjectURL(url);
urlsToRevoke.push(url);
};
/** Function to revoke any expired urls */
function triggerURLRevokes() {
if (activeCompilations === 0) {
// Revoke all pending revoke URLS
urlsToRevoke.forEach((url) => URL.revokeObjectURL(url));
// Remove all url strings from array
urlsToRevoke.length = 0;
}
}
// Webpack likes to turn the import into a require, which sort of
// but not really behaves like import. So we use a "magic comment"
// to disable that and leave it as a dynamic import.
@ -29,171 +46,113 @@ function makeScriptBlob(code: string): Blob {
// import() is not a function, so it can't be replaced. We need this separate
// config object to provide a hook point.
export const config = {
doImport(url: string): Promise<ScriptModule> {
doImport(url: ScriptURL): Promise<ScriptModule> {
return import(/*webpackIgnore:true*/ url);
},
};
export async function compile(script: Script, scripts: Script[]): Promise<ScriptModule> {
//!shouldCompile ensures that script.module is non-null, hence the "as".
if (!shouldCompile(script, scripts)) return script.module as Promise<ScriptModule>;
script.queueCompile = true;
//If we're already in the middle of compiling (script.module has not resolved yet), wait for the previous compilation to finish
//If script.module is null, this does nothing.
await script.module;
//If multiple compiles were called on the same script before a compilation could be completed this ensures only one compilation is actually performed.
if (!script.queueCompile) return script.module as Promise<ScriptModule>;
script.queueCompile = false;
const uurls = _getScriptUrls(script, scripts, []);
const url = uurls[uurls.length - 1].url;
if (script.url && script.url !== url) URL.revokeObjectURL(script.url);
if (script.dependencies.length > 0) script.dependencies.forEach((dep) => URL.revokeObjectURL(dep.url));
script.url = uurls[uurls.length - 1].url;
// The URL at the top is the one we want to import. It will
// recursively import all the other modules in the urlStack.
script.module = config.doImport(uurls[uurls.length - 1].url);
script.dependencies = uurls;
export function compile(script: Script, scripts: Script[]): Promise<ScriptModule> {
// Return the module if it already exists
if (script.module) return script.module;
// While importing, use an existing url or generate a new one.
if (!script.url) script.url = generateScriptUrl(script, scripts, []);
activeCompilations++;
script.module = config
.doImport(script.url)
.catch((e) => {
script.invalidateModule();
console.error(`Error occurred while attempting to compile ${script.filename} on ${script.server}:`);
console.error(e);
throw e;
})
.finally(() => {
activeCompilations--;
triggerURLRevokes();
});
return script.module;
}
function isDependencyOutOfDate(filename: string, scripts: Script[], scriptModuleSequenceNumber: number): boolean {
const depScript = scripts.find((s) => s.filename == filename);
// If the script is not present on the server, we should recompile, if only to get any necessary
// compilation errors.
if (!depScript) return true;
const depIsMoreRecent = depScript.moduleSequenceNumber > scriptModuleSequenceNumber;
return depIsMoreRecent;
}
/** Returns whether we should compile the script parameter.
/** Add the necessary dependency relationships for a script.
* Dependents are used only for passing invalidation up an import tree, so only direct dependents need to be stored.
* Direct and indirect dependents need to have the current url/script added to their dependency map for error text.
*
* @param {Script} script
* @param {Script[]} scripts
*/
function shouldCompile(script: Script, scripts: Script[]): boolean {
if (!script.module) return true;
if (script.dependencies.some((dep) => isDependencyOutOfDate(dep.filename, scripts, script.moduleSequenceNumber))) {
script.module = null;
return true;
* This should only be called once the script has an assigned URL. */
function addDependencyInfo(script: Script, dependents: Script[]) {
if (!script.url) throw new Error(`addDependencyInfo called without an assigned script URL (${script.filename})`);
if (dependents.length) {
script.dependents.add(dependents[dependents.length - 1]);
for (const dependent of dependents) dependent.dependencies.set(script.url, script);
}
return false;
}
// Gets a stack of blob urls, the top/right-most element being
// the blob url for the named script on the named server.
//
// - script -- the script for whom we are getting a URL.
// - scripts -- all the scripts available on this server
// - seen -- The modules above this one -- to prevent mutual dependency.
//
// TODO 2.3 consider: Possibly reusing modules when imported in different locations. Per previous notes, this may
// require a topo-sort then url-izing from leaf-most to root-most.
/**
* @param {Script} script
* @param {Script[]} scripts
* @param {Script[]} seen
* @returns {ScriptUrl[]} All of the compiled scripts, with the final one
* in the list containing the blob corresponding to
* the script parameter.
* @param script the script that needs a URL assigned
* @param scripts array of other scripts on the server
* @param dependents All scripts that were higher up in the import tree in a recursive call.
*/
// BUG: apparently seen is never consulted. Oops.
function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): ScriptUrl[] {
function generateScriptUrl(script: Script, scripts: Script[], dependents: Script[]): ScriptURL {
// Early return for recursive calls where the script already has a URL
if (script.url) {
addDependencyInfo(script, dependents);
return script.url;
}
// Inspired by: https://stackoverflow.com/a/43834063/91401
const urlStack: ScriptUrl[] = [];
// Seen contains the dependents of the current script. Make sure we include that in the script dependents.
for (const dependent of seen) {
if (!script.dependents.some((s) => s.server === dependent.server && s.filename == dependent.filename)) {
script.dependents.push({ server: dependent.server, filename: dependent.filename });
}
const ast = parse(script.code, { sourceType: "module", ecmaVersion: "latest", ranges: true });
interface importNode {
filename: string;
start: number;
end: number;
}
seen.push(script);
try {
// Replace every import statement with an import to a blob url containing
// the corresponding script. E.g.
//
// import {foo} from "bar.js";
//
// becomes
//
// import {foo} from "blob://<uuid>"
//
// Where the blob URL contains the script content.
const importNodes: importNode[] = [];
// Walk the nodes of this tree and find any import declaration statements.
walk.simple(ast, {
ImportDeclaration(node: Node) {
// Push this import onto the stack to replace
if (!node.source) return;
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1,
});
},
ExportNamedDeclaration(node: Node) {
if (!node.source) return;
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1,
});
},
ExportAllDeclaration(node: Node) {
if (!node.source) return;
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1,
});
},
});
// Sort the nodes from last start index to first. This replaces the last import with a blob first,
// preventing the ranges for other imports from being shifted.
importNodes.sort((a, b) => b.start - a.start);
let newCode = script.code;
// Loop through each node and replace the script name with a blob url.
for (const node of importNodes) {
const filename = node.filename.startsWith("./") ? node.filename.substring(2) : node.filename;
// Parse the code into an ast tree
const ast = parse(script.code, { sourceType: "module", ecmaVersion: "latest", ranges: true });
interface importNode {
filename: string;
start: number;
end: number;
}
const importNodes: importNode[] = [];
// Walk the nodes of this tree and find any import declaration statements.
walk.simple(ast, {
ImportDeclaration(node: Node) {
// Push this import onto the stack to replace
if (!node.source) return;
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1,
});
},
ExportNamedDeclaration(node: Node) {
if (!node.source) return;
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1,
});
},
ExportAllDeclaration(node: Node) {
if (!node.source) return;
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1,
});
},
});
// Sort the nodes from last start index to first. This replaces the last import with a blob first,
// preventing the ranges for other imports from being shifted.
importNodes.sort((a, b) => b.start - a.start);
let transformedCode = script.code;
// Loop through each node and replace the script name with a blob url.
for (const node of importNodes) {
const filename = node.filename.startsWith("./") ? node.filename.substring(2) : node.filename;
// Find the corresponding script.
const importedScript = scripts.find((s) => areImportsEquals(s.filename, filename));
if (!importedScript) continue;
// Find the corresponding script.
const matchingScripts = scripts.filter((s) => areImportsEquals(s.filename, filename));
if (matchingScripts.length === 0) continue;
const [importedScript] = matchingScripts;
const urls = _getScriptUrls(importedScript, scripts, seen);
// The top url in the stack is the replacement import file for this script.
urlStack.push(...urls);
const blob = urls[urls.length - 1].url;
// Replace the blob inside the import statement.
transformedCode = transformedCode.substring(0, node.start) + blob + transformedCode.substring(node.end);
}
// We automatically define a print function() in the NetscriptJS module so that
// accidental calls to window.print() do not bring up the "print screen" dialog
transformedCode += `\n//# sourceURL=${script.server}/${script.filename}`;
const blob = URL.createObjectURL(makeScriptBlob(transformedCode));
// Push the blob URL onto the top of the stack.
urlStack.push(new ScriptUrl(script.filename, blob, script.moduleSequenceNumber));
return urlStack;
} catch (err) {
// If there is an error, we need to clean up the URLs.
for (const url of urlStack) URL.revokeObjectURL(url.url);
throw err;
} finally {
seen.pop();
importedScript.url = generateScriptUrl(importedScript, scripts, [...dependents, script]);
newCode = newCode.substring(0, node.start) + importedScript.url + newCode.substring(node.end);
}
newCode += `\n//# sourceURL=${script.server}/${script.filename}`;
// At this point we have the full code and can construct a new blob / assign the URL.
script.url = URL.createObjectURL(makeScriptBlob(newCode)) as ScriptURL;
addDependencyInfo(script, dependents);
return script.url;
}

View File

@ -51,16 +51,16 @@ export function prestigeWorkerScripts(): void {
async function startNetscript2Script(workerScript: WorkerScript): Promise<void> {
const scripts = workerScript.getServer().scripts;
const script = workerScript.getScript();
if (script === null) throw "workerScript had no associated script. This is a bug.";
if (!script.ramUsage) throw "Attempting to start a script with no calculated ram cost. This is a bug.";
const loadedModule = await compile(script, scripts);
if (!script) throw "workerScript had no associated script. This is a bug.";
const ns = workerScript.env.vars;
if (!ns) throw `${script.filename} cannot be run because the NS object hasn't been constructed properly.`;
const loadedModule = await compile(script, scripts);
if (!loadedModule) throw `${script.filename} cannot be run because the script module won't load`;
// TODO unplanned: Better error for "unexpected reserved word" when using await in non-async function?
if (typeof loadedModule.main !== "function")
throw `${script.filename} cannot be run because it does not have a main function.`;
if (!ns) throw `${script.filename} cannot be run because the NS object hasn't been constructed properly.`;
await loadedModule.main(ns);
}
@ -295,6 +295,7 @@ export function startWorkerScript(runningScript: RunningScript, server: BaseServ
function createAndAddWorkerScript(runningScriptObj: RunningScript, server: BaseServer, parent?: WorkerScript): boolean {
const ramUsage = roundToTwo(runningScriptObj.ramUsage * runningScriptObj.threads);
const ramAvailable = server.maxRam - server.ramUsed;
// Check failure conditions before generating the workersScript and return false
if (ramUsage > ramAvailable + 0.001) {
dialogBoxCreate(
`Not enough RAM to run script ${runningScriptObj.filename} with args ${arrayToString(runningScriptObj.args)}.\n` +
@ -304,17 +305,18 @@ function createAndAddWorkerScript(runningScriptObj: RunningScript, server: BaseS
return false;
}
server.updateRamUsed(roundToTwo(server.ramUsed + ramUsage));
// Get the pid
const pid = generateNextPid();
if (pid === -1) {
throw new Error(
dialogBoxCreate(
`Failed to start script because could not find available PID. This is most ` +
`because you have too many scripts running.`,
);
return false;
}
server.updateRamUsed(roundToTwo(server.ramUsed + ramUsage));
// Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying
// RunningScript's PID as well
const workerScript = new WorkerScript(runningScriptObj, pid, NetscriptFunctions);
@ -366,28 +368,18 @@ export function loadAllRunningScripts(): void {
// Reset each server's RAM usage to 0
server.ramUsed = 0;
// Reset modules on all scripts
for (let i = 0; i < server.scripts.length; ++i) {
server.scripts[i].markUpdated();
}
if (skipScriptLoad) {
// Start game with no scripts
server.runningScripts.length = 0;
} else {
for (let j = 0; j < server.runningScripts.length; ++j) {
const fileName = server.runningScripts[j].filename;
createAndAddWorkerScript(server.runningScripts[j], server);
if (!server.runningScripts[j]) {
// createAndAddWorkerScript can modify the server.runningScripts array if a script is invalid
console.error(`createAndAddWorkerScript removed ${fileName} from ${server}`);
continue;
}
// Offline production
scriptCalculateOfflineProduction(server.runningScripts[j]);
}
continue;
}
// Using backwards index iteration to avoid complications when removing elements during iteration.
for (let i = server.runningScripts.length - 1; i >= 0; i--) {
const runningScript = server.runningScripts[i];
const success = createAndAddWorkerScript(runningScript, server);
scriptCalculateOfflineProduction(runningScript);
// Remove the RunningScript if the WorkerScript failed to start.
if (!success) server.runningScripts.splice(i, 1);
}
}
}

View File

@ -20,7 +20,7 @@ import { HacknetNode } from "../../Hacknet/HacknetNode";
import { HashManager } from "../../Hacknet/HashManager";
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver";
import { PlayerAchievement } from "../../Achievements/Achievements";
import { cyrb53 } from "../../utils/StringHelperFunctions";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
@ -175,4 +175,4 @@ export class PlayerObject extends Person implements IPlayer {
setPlayer(new PlayerObject());
Reviver.constructors.PlayerObject = PlayerObject;
constructorsForReviver.PlayerObject = PlayerObject;

View File

@ -22,7 +22,7 @@ import { CityName, CrimeType, GymType, LocationName, UniversityClassType } from
import { Factions } from "../../Faction/Factions";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver";
import { formatPercent } from "../../ui/formatNumber";
import { FactionWorkType } from "../../Enums";
import { SleeveWork } from "./Work/Work";
@ -480,4 +480,4 @@ export class Sleeve extends Person implements SleevePerson {
}
}
Reviver.constructors.Sleeve = Sleeve;
constructorsForReviver.Sleeve = Sleeve;

View File

@ -1,5 +1,5 @@
import { Player } from "@player";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { applySleeveGains, Work, WorkType } from "./Work";
import { CONSTANTS } from "../../../Constants";
@ -84,4 +84,4 @@ export class SleeveBladeburnerWork extends Work {
}
}
Reviver.constructors.SleeveBladeburnerWork = SleeveBladeburnerWork;
constructorsForReviver.SleeveBladeburnerWork = SleeveBladeburnerWork;

View File

@ -1,4 +1,4 @@
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { applySleeveGains, Work, WorkType } from "./Work";
import { Classes, ClassType } from "../../../Work/ClassWork";
import { LocationName } from "../../../Enums";
@ -60,4 +60,4 @@ export class SleeveClassWork extends Work {
}
}
Reviver.constructors.SleeveClassWork = SleeveClassWork;
constructorsForReviver.SleeveClassWork = SleeveClassWork;

View File

@ -1,4 +1,4 @@
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { applySleeveGains, Work, WorkType } from "./Work";
import { LocationName } from "../../../Enums";
@ -63,4 +63,4 @@ export class SleeveCompanyWork extends Work {
}
}
Reviver.constructors.SleeveCompanyWork = SleeveCompanyWork;
constructorsForReviver.SleeveCompanyWork = SleeveCompanyWork;

View File

@ -1,5 +1,5 @@
import { Player } from "@player";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { applySleeveGains, Work, WorkType } from "./Work";
import { CrimeType } from "../../../Enums";
@ -72,4 +72,4 @@ export class SleeveCrimeWork extends Work {
}
}
Reviver.constructors.SleeveCrimeWork = SleeveCrimeWork;
constructorsForReviver.SleeveCrimeWork = SleeveCrimeWork;

View File

@ -1,5 +1,5 @@
import { Player } from "@player";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { applySleeveGains, Work, WorkType } from "./Work";
import { FactionWorkType } from "../../../Enums";
@ -74,4 +74,4 @@ export class SleeveFactionWork extends Work {
}
}
Reviver.constructors.SleeveFactionWork = SleeveFactionWork;
constructorsForReviver.SleeveFactionWork = SleeveFactionWork;

View File

@ -1,5 +1,5 @@
import { Player } from "@player";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { Work, WorkType } from "./Work";
import { CONSTANTS } from "../../../Constants";
@ -45,4 +45,4 @@ export class SleeveInfiltrateWork extends Work {
}
}
Reviver.constructors.SleeveInfiltrateWork = SleeveInfiltrateWork;
constructorsForReviver.SleeveInfiltrateWork = SleeveInfiltrateWork;

View File

@ -1,4 +1,4 @@
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { Work, WorkType } from "./Work";
import { calculateIntelligenceBonus } from "../../formulas/intelligence";
@ -32,4 +32,4 @@ export class SleeveRecoveryWork extends Work {
}
}
Reviver.constructors.SleeveRecoveryWork = SleeveRecoveryWork;
constructorsForReviver.SleeveRecoveryWork = SleeveRecoveryWork;

View File

@ -1,5 +1,5 @@
import { Player } from "@player";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Work, WorkType } from "./Work";
export const isSleeveSupportWork = (w: Work | null): w is SleeveSupportWork =>
@ -35,4 +35,4 @@ export class SleeveSupportWork extends Work {
}
}
Reviver.constructors.SleeveSupportWork = SleeveSupportWork;
constructorsForReviver.SleeveSupportWork = SleeveSupportWork;

View File

@ -1,5 +1,5 @@
import { Player } from "@player";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { Work, WorkType } from "./Work";
import { calculateIntelligenceBonus } from "../../formulas/intelligence";
@ -33,4 +33,4 @@ export class SleeveSynchroWork extends Work {
}
}
Reviver.constructors.SleeveSynchroWork = SleeveSynchroWork;
constructorsForReviver.SleeveSynchroWork = SleeveSynchroWork;

View File

@ -191,6 +191,8 @@ export function prestigeSourceFile(flume: boolean): void {
// Reset home computer (only the programs) and add to AllServers
AddToAllServers(homeComp);
prestigeHomeComputer(homeComp);
// Ram usage needs to be cleared for bitnode-level resets, due to possible change in singularity cost.
for (const script of homeComp.scripts) script.ramUsage = undefined;
// Re-create foreign servers
initForeignServers(Player.getHomeComputer());

View File

@ -21,7 +21,7 @@ import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar";
import * as ExportBonus from "./ExportBonus";
import { dialogBoxCreate } from "./ui/React/DialogBox";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "./utils/JSONReviver";
import { Reviver, constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "./utils/JSONReviver";
import { save } from "./db";
import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
@ -798,7 +798,7 @@ function download(filename: string, content: string): void {
}, 0);
}
Reviver.constructors.BitburnerSaveObject = BitburnerSaveObject;
constructorsForReviver.BitburnerSaveObject = BitburnerSaveObject;
export { saveObject, loadGame, download };

View File

@ -3,16 +3,16 @@
* A Script can have multiple active instances
*/
import type React from "react";
import { Script } from "./Script";
import { ScriptUrl } from "./ScriptUrl";
import { Script, ScriptURL } from "./Script";
import { Settings } from "../Settings/Settings";
import { Terminal } from "../Terminal";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { formatTime } from "../utils/helpers/formatTime";
import { ScriptArg } from "@nsdefs";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
import { PositiveInteger } from "../types";
import { getKeyList } from "../utils/helpers/getKeyList";
export class RunningScript {
// Script arguments
@ -23,7 +23,7 @@ export class RunningScript {
dataMap: Record<string, number[]> = {};
// Script filename
filename = "";
filename = "default.js";
// This script's logs. An array of log entries
logs: React.ReactNode[] = [];
@ -66,7 +66,8 @@ export class RunningScript {
temporary = false;
// Script urls for the current running script for translating urls back to file names in errors
dependencies: ScriptUrl[] = [];
dependencies: Map<ScriptURL, Script> = new Map();
url?: ScriptURL;
constructor(script?: Script, ramUsage?: number, args: ScriptArg[] = []) {
if (!script) return;
@ -75,7 +76,7 @@ export class RunningScript {
this.args = args;
this.server = script.server;
this.ramUsage = ramUsage;
this.dependencies = script.dependencies;
this.dependencies = new Map(script.dependencies);
}
log(txt: React.ReactNode): void {
@ -133,13 +134,14 @@ export class RunningScript {
// Serialize the current object to a JSON save state
toJSON(): IReviverValue {
return Generic_toJSON("RunningScript", this);
return Generic_toJSON("RunningScript", this, includedProperties);
}
// Initializes a RunningScript Object from a JSON save state
static fromJSON(value: IReviverValue): RunningScript {
return Generic_fromJSON(RunningScript, value.data);
return Generic_fromJSON(RunningScript, value.data, includedProperties);
}
}
const includedProperties = getKeyList(RunningScript, { removedKeys: ["logs", "dependencies", "logUpd", "pid"] });
Reviver.constructors.RunningScript = RunningScript;
constructorsForReviver.RunningScript = RunningScript;

View File

@ -5,58 +5,38 @@
* being evaluated. See RunningScript for that
*/
import { calculateRamUsage, RamUsageEntry } from "./RamCalculations";
import { ScriptUrl } from "./ScriptUrl";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { roundToTwo } from "../utils/helpers/roundToTwo";
import { ScriptModule } from "./ScriptModule";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
import { queueUrlRevoke } from "../NetscriptJSEvaluator";
let globalModuleSequenceNumber = 0;
interface ScriptReference {
filename: string;
server: string;
}
// The object portion of this type is not runtime information, it's only to ensure type validation
// And make it harder to overwrite a url with a random non-url string.
export type ScriptURL = string & { __type: "ScriptURL" };
export class Script {
// Code for this script
code = "";
filename = "default.js";
server = "home";
// Filename for the script file
filename = "";
// url of the script if any, only for NS2.
url = "";
// The dynamic module generated for this script when it is run.
// This is only applicable for NetscriptJS
module: Promise<ScriptModule> | null = null;
// The timestamp when when the script was last updated.
moduleSequenceNumber: number;
// Only used with NS2 scripts; the list of dependency script filenames. This is constructed
// whenever the script is first evaluated, and therefore may be out of date if the script
// has been updated since it was last run.
dependencies: ScriptUrl[] = [];
dependents: ScriptReference[] = [];
// Amount of RAM this Script requires to run. null indicates an error in calculating ram.
ramUsage: number | null = null;
// Ram calculation, only exists after first poll of ram cost after updating
ramUsage?: number;
ramUsageEntries?: RamUsageEntry[];
// Used to deconflict multiple simultaneous compilations.
queueCompile = false;
// hostname of server that this script is on.
server = "";
// Runtime data that only exists when the script has been initiated. Cleared when script or a dependency script is updated.
module?: Promise<ScriptModule>;
url?: ScriptURL;
/** Scripts that directly import this one. Stored so we can invalidate these dependent scripts when this one is invalidated. */
dependents: Set<Script> = new Set();
/** Scripts that are imported by this one, either directly or through an import chain */
dependencies: Map<ScriptURL, Script> = new Map();
constructor(fn = "", code = "", server = "") {
this.filename = fn;
this.code = code;
this.server = server; // hostname of server this script is on
this.moduleSequenceNumber = ++globalModuleSequenceNumber;
}
/** Download the script as a file */
@ -75,13 +55,19 @@ export class Script {
}, 0);
}
/**
* Marks this script as having been updated. It will be recompiled next time something tries
* to exec it.
*/
markUpdated(): void {
this.module = null;
this.moduleSequenceNumber = ++globalModuleSequenceNumber;
/** Invalidates the current script module and related data, e.g. when modifying the file. */
invalidateModule(): void {
// Always clear ram usage
this.ramUsage = undefined;
this.ramUsageEntries = undefined;
// Early return if there's already no URL
if (!this.url) return;
this.module = undefined;
queueUrlRevoke(this.url);
this.url = undefined;
for (const dependency of this.dependencies.values()) dependency.dependents.delete(this);
this.dependencies.clear();
for (const dependent of this.dependents) dependent.invalidateModule();
}
/**
@ -89,30 +75,18 @@ export class Script {
* @param {string} code - The new contents of the script
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/
saveScript(filename: string, code: string, hostname: string, otherScripts: Script[]): void {
// Update code and filename
saveScript(filename: string, code: string, hostname: string): void {
this.invalidateModule();
this.code = Script.formatCode(code);
this.filename = filename;
this.server = hostname;
// Null ramUsage forces a recalc next time ramUsage is needed
this.ramUsage = null;
this.markUpdated();
this.dependents.forEach((dependent) => {
const scriptToUpdate = otherScripts.find(
(otherScript) => otherScript.filename === dependent.filename && otherScript.server === dependent.server,
);
if (!scriptToUpdate) return;
scriptToUpdate.ramUsage = null;
scriptToUpdate.markUpdated();
});
}
/** Gets the ram usage, while also attempting to update it if it's currently null */
getRamUsage(otherScripts: Script[]): number | null {
if (this.ramUsage) return this.ramUsage;
this.updateRamUsage(otherScripts);
return this.ramUsage;
return this.ramUsage ?? null;
}
/**
@ -124,26 +98,24 @@ export class Script {
if (ramCalc.cost >= RamCostConstants.Base) {
this.ramUsage = roundToTwo(ramCalc.cost);
this.ramUsageEntries = ramCalc.entries;
} else this.ramUsage = null;
this.markUpdated();
} else delete this.ramUsage;
}
imports(): string[] {
return [];
}
/** The keys that are relevant in a save file */
static savedKeys = ["code", "filename", "server"] as const;
// Serialize the current object to a JSON save state
toJSON(): IReviverValue {
return Generic_toJSON("Script", this);
return Generic_toJSON("Script", this, Script.savedKeys);
}
// Initializes a Script Object from a JSON save state
static fromJSON(value: IReviverValue): Script {
const s = Generic_fromJSON(Script, value.data);
// Force the url to blank from the save data. Urls are not valid outside the current browser page load.
s.url = "";
s.dependents = [];
return s;
return Generic_fromJSON(Script, value.data, Script.savedKeys);
}
/**
@ -156,4 +128,4 @@ export class Script {
}
}
Reviver.constructors.Script = Script;
constructorsForReviver.Script = Script;

View File

@ -1,11 +0,0 @@
export class ScriptUrl {
filename: string;
url: string;
moduleSequenceNumber: number;
constructor(filename: string, url: string, moduleSequenceNumber: number) {
this.filename = filename;
this.url = url;
this.moduleSequenceNumber = moduleSequenceNumber;
}
}

View File

@ -434,7 +434,7 @@ export function Root(props: IProps): React.ReactElement {
//If the current script already exists on the server, overwrite it
for (let i = 0; i < server.scripts.length; i++) {
if (scriptToSave.fileName == server.scripts[i].filename) {
server.scripts[i].saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer, server.scripts);
server.scripts[i].saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
Router.toPage(Page.Terminal);
return;
@ -443,7 +443,7 @@ export function Root(props: IProps): React.ReactElement {
//If the current script does NOT exist, create a new one
const script = new Script();
script.saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer, server.scripts);
script.saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer);
server.scripts.push(script);
} else if (scriptToSave.isTxt) {
for (let i = 0; i < server.textFiles.length; ++i) {
@ -511,12 +511,7 @@ export function Root(props: IProps): React.ReactElement {
//If the current script already exists on the server, overwrite it
for (let i = 0; i < server.scripts.length; i++) {
if (currentScript.fileName == server.scripts[i].filename) {
server.scripts[i].saveScript(
currentScript.fileName,
currentScript.code,
Player.currentServer,
server.scripts,
);
server.scripts[i].saveScript(currentScript.fileName, currentScript.code, Player.currentServer);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
rerender();
return;
@ -525,7 +520,7 @@ export function Root(props: IProps): React.ReactElement {
//If the current script does NOT exist, create a new one
const script = new Script();
script.saveScript(currentScript.fileName, currentScript.code, Player.currentServer, server.scripts);
script.saveScript(currentScript.fileName, currentScript.code, Player.currentServer);
server.scripts.push(script);
} else if (currentScript.isTxt) {
for (let i = 0; i < server.textFiles.length; ++i) {

View File

@ -214,9 +214,6 @@ function scriptFilter(script: RunningScript): boolean {
}
function includeReplacer(key: string, value: any): any {
if (key === "logs") {
return [];
}
if (key === "runningScripts") {
return value.filter(scriptFilter);
}

View File

@ -186,19 +186,14 @@ export abstract class BaseServer {
}
}
} else if (isScriptFilename(fn)) {
for (let i = 0; i < this.scripts.length; ++i) {
if (this.scripts[i].filename === fn) {
if (this.isRunning(fn)) {
return {
res: false,
msg: "Cannot delete a script that is currently running!",
};
}
this.scripts.splice(i, 1);
return { res: true };
}
const scriptIndex = this.scripts.findIndex((script) => script.filename === fn);
if (scriptIndex === -1) return { res: false, msg: `script ${fn} not found.` };
if (this.isRunning(fn)) {
return { res: false, msg: "Cannot delete a script that is currently running!" };
}
this.scripts[i].invalidateModule();
this.scripts.splice(i, 1);
return { res: true };
} else if (fn.endsWith(".lit")) {
for (let i = 0; i < this.messages.length; ++i) {
const f = this.messages[i];
@ -273,8 +268,7 @@ export abstract class BaseServer {
const script = this.scripts[i];
script.code = code;
// Set ramUsage to null in order to force recalculation on next run
script.ramUsage = null;
script.markUpdated();
script.invalidateModule();
ret.overwritten = true;
ret.success = true;
return ret;

View File

@ -5,7 +5,7 @@ import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { createRandomString } from "../utils/helpers/createRandomString";
import { createRandomIp } from "../utils/IPAddress";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export interface IConstructorParams {
adminRights?: boolean;
@ -155,4 +155,4 @@ export class Server extends BaseServer {
}
}
Reviver.constructors.Server = Server;
constructorsForReviver.Server = Server;

View File

@ -254,11 +254,6 @@ export function prestigeHomeComputer(homeComp: Server): void {
homeComp.programs.push(Programs.BitFlume.name);
}
//Reset RAM usage calculation for all scripts
homeComp.scripts.forEach(function (script) {
script.ramUsage = null;
});
homeComp.messages.length = 0; //Remove .lit and .msg files
homeComp.messages.push(LiteratureNames.HackersStartingHandbook);
}

View File

@ -5,7 +5,7 @@
import { OrderTypes } from "./data/OrderTypes";
import { PositionTypes } from "./data/PositionTypes";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class Order {
readonly pos: PositionTypes;
@ -54,4 +54,4 @@ export class Order {
}
}
Reviver.constructors.Order = Order;
constructorsForReviver.Order = Order;

View File

@ -1,5 +1,5 @@
import { IMinMaxRange } from "../types";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { getRandomInt } from "../utils/helpers/getRandomInt";
export const StockForecastInfluenceLimit = 5;
@ -275,4 +275,4 @@ export class Stock {
}
}
Reviver.constructors.Stock = Stock;
constructorsForReviver.Stock = Stock;

View File

@ -4,9 +4,7 @@ import { killWorkerScript } from "../../Netscript/killWorkerScript";
import { WorkerScriptStartStopEventEmitter } from "../../Netscript/WorkerScriptStartStopEventEmitter";
export function killall(_args: (string | number | boolean)[], server: BaseServer): void {
for (let i = server.runningScripts.length - 1; i >= 0; --i) {
killWorkerScript({ runningScript: server.runningScripts[i], hostname: server.hostname });
}
WorkerScriptStartStopEventEmitter.emit();
Terminal.print("Killing all running scripts");
for (const runningScript of server.runningScripts) killWorkerScript(runningScript.pid);
WorkerScriptStartStopEventEmitter.emit();
}

View File

@ -1,6 +1,6 @@
import { dialogBoxCreate } from "./ui/React/DialogBox";
import { BaseServer } from "./Server/BaseServer";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "./utils/JSONReviver";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
import { removeLeadingSlash, isInRootDirectory } from "./Terminal/DirectoryHelpers";
/** Represents a plain text file that is typically stored on a server. */
@ -73,7 +73,7 @@ export class TextFile {
}
}
Reviver.constructors.TextFile = TextFile;
constructorsForReviver.TextFile = TextFile;
/**
* Retrieve the file object for the filename on the specified server.

View File

@ -1,5 +1,5 @@
import React from "react";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { CONSTANTS } from "../Constants";
import { LocationName } from "../Enums";
import { formatExp } from "../ui/formatNumber";
@ -159,4 +159,4 @@ export class ClassWork extends Work {
}
}
Reviver.constructors.ClassWork = ClassWork;
constructorsForReviver.ClassWork = ClassWork;

View File

@ -1,5 +1,5 @@
import React from "react";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { Player } from "@player";
import { Work, WorkType } from "./Work";
import { influenceStockThroughCompanyWork } from "../StockMarket/PlayerInfluencing";
@ -86,4 +86,4 @@ export class CompanyWork extends Work {
}
}
Reviver.constructors.CompanyWork = CompanyWork;
constructorsForReviver.CompanyWork = CompanyWork;

View File

@ -1,5 +1,5 @@
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { CONSTANTS } from "../Constants";
import { Player } from "@player";
@ -119,4 +119,4 @@ export class CreateProgramWork extends Work {
}
}
Reviver.constructors.CreateProgramWork = CreateProgramWork;
constructorsForReviver.CreateProgramWork = CreateProgramWork;

View File

@ -1,4 +1,4 @@
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { Crime } from "../Crime/Crime";
import { CONSTANTS } from "../Constants";
import { determineCrimeSuccess, findCrime } from "../Crime/CrimeHelpers";
@ -106,4 +106,4 @@ export class CrimeWork extends Work {
}
}
Reviver.constructors.CrimeWork = CrimeWork;
constructorsForReviver.CrimeWork = CrimeWork;

View File

@ -1,6 +1,6 @@
import React from "react";
import { Work, WorkType } from "./Work";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { Player } from "@player";
import { FactionNames } from "../Faction/data/FactionNames";
import { Factions } from "../Faction/Factions";
@ -100,4 +100,4 @@ export class FactionWork extends Work {
}
}
Reviver.constructors.FactionWork = FactionWork;
constructorsForReviver.FactionWork = FactionWork;

View File

@ -7,7 +7,7 @@ import { Work, WorkType } from "./Work";
import { graftingIntBonus } from "../PersonObjects/Grafting/GraftingHelpers";
import { applyAugmentation } from "../Augmentation/AugmentationHelpers";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { GraftableAugmentation } from "../PersonObjects/Grafting/GraftableAugmentation";
import { StaticAugmentations } from "../Augmentation/StaticAugmentations";
@ -102,4 +102,4 @@ export class GraftingWork extends Work {
}
}
Reviver.constructors.GraftingWork = GraftingWork;
constructorsForReviver.GraftingWork = GraftingWork;

View File

@ -1,85 +1,101 @@
/* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */
import { validateObject } from "./Validator";
import { ObjectValidator, validateObject } from "./Validator";
export interface IReviverValue {
ctor: string;
data: any;
}
function isReviverValue(value: unknown): value is IReviverValue {
return (
typeof value === "object" && value !== null && "ctor" in value && typeof value.ctor === "string" && "data" in value
);
}
// A generic "smart reviver" function.
// Looks for object values with a `ctor` property and
// a `data` property. If it finds them, and finds a matching
// constructor that has a `fromJSON` property on it, it hands
// off to that `fromJSON` function, passing in the value.
export function Reviver(key: string, value: IReviverValue | null): any {
if (value == null) {
return null;
}
if (typeof value === "object" && typeof value.ctor === "string" && typeof value.data !== "undefined") {
// Compatibility for version v0.43.1
// TODO Remove this eventually
if (value.ctor === "AllServersMap") {
console.warn("Converting AllServersMap for v0.43.1");
return value.data;
/**
* A generic "smart reviver" function.
* Looks for object values with a `ctor` property and a `data` property.
* If it finds them, and finds a matching constructor, it hands
* off to that `fromJSON` function, passing in the value. */
export function Reviver(_key: string, value: unknown): any {
if (!isReviverValue(value)) return value;
const ctor = constructorsForReviver[value.ctor];
if (!ctor) {
// Known missing constructors with special handling.
switch (value.ctor) {
case "AllServersMap":
console.warn("Converting AllServersMap for v0.43.1");
return value.data;
}
const ctor = Reviver.constructors[value.ctor];
if (typeof ctor === "function" && typeof ctor.fromJSON === "function") {
const obj = ctor.fromJSON(value);
if (ctor.validationData !== undefined) {
validateObject(obj, ctor.validationData);
}
return obj;
}
}
return value;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Reviver {
export const constructors: { [key: string]: any } = {};
}
// A generic "toJSON" function that creates the data expected
// by Reviver.
// `ctorName` The name of the constructor to use to revive it
// `obj` The object being serialized
// `keys` (Optional) Array of the properties to serialize,
// if not given then all of the objects "own" properties
// that don't have function values will be serialized.
// (Note: If you list a property in `keys`, it will be serialized
// regardless of whether it's an "own" property.)
// Returns: The structure (which will then be turned into a string
// as part of the JSON.stringify algorithm)
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function Generic_toJSON(ctorName: string, obj: Record<string, any>, keys?: string[]): IReviverValue {
if (!keys) {
keys = Object.keys(obj); // Only "own" properties are included
// Missing constructor with no special handling. Throw error.
throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`);
}
const data: Record<string, unknown> = {};
for (let index = 0; index < keys.length; ++index) {
const key = keys[index];
data[key] = obj[key];
}
return { ctor: ctorName, data: data };
}
// A generic "fromJSON" function for use with Reviver: Just calls the
// constructor function with no arguments, then applies all of the
// key/value pairs from the raw data to the instance. Only useful for
// constructors that can be reasonably called without arguments!
// `ctor` The constructor to call
// `data` The data to apply
// Returns: The object
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function Generic_fromJSON<T>(ctor: new () => T, data: any): T {
const obj: any = new ctor();
for (const name in data) {
obj[name] = data[name];
const obj = ctor.fromJSON(value);
if (ctor.validationData !== undefined) {
validateObject(obj, ctor.validationData);
}
return obj;
}
export const constructorsForReviver: Partial<
Record<
string,
(new () => object) & {
fromJSON: (value: IReviverValue) => any;
validationData?: ObjectValidator<any>;
}
>
> = {};
/**
* A generic "toJSON" function that creates the data expected by Reviver.
*
* @param ctorName String name of the constructor, part of the reviver JSON.
* @param obj The object to convert to stringified data in the reviver JSON.
* @param keys If provided, only these keys will be saved to the reviver JSON data. */
export function Generic_toJSON<T extends Record<string, any>>(
ctorName: string,
obj: T,
keys?: readonly (keyof T)[],
): IReviverValue {
const data = {} as T;
// keys provided: only save data for the provided keys
if (keys) {
for (const key of keys) data[key] = obj[key];
return { ctor: ctorName, data: data };
}
// no keys provided: save all own keys of the object
for (const [key, val] of Object.entries(obj) as [keyof T, T[keyof T]][]) data[key] = val;
return { ctor: ctorName, data: data };
}
/**
* A generic "fromJSON" function for use with Reviver: Just calls the
* constructor function with no arguments, then applies all of the
* key/value pairs from the raw data to the instance. Only useful for
* constructors that can be reasonably called without arguments!
*
* @param ctor The constructor to call
* @param data The saved data to restore to the constructed object
* @param keys If provided, only these keys will be restored from data.
* @returns The object */
export function Generic_fromJSON<T extends Record<string, any>>(
ctor: new () => T,
// data can actually be anything. We're just pretending it has the right keys for T. Save data is not type validated.
data: Record<keyof T, any>,
keys?: readonly (keyof T)[],
): T {
const obj = new ctor();
// If keys were provided, just load the provided keys (if they are in the data)
if (keys) {
for (const key of keys) {
const val = data[key];
if (val) obj[key] = val;
}
return obj;
}
// No keys provided: load every key in data
for (const [key, val] of Object.entries(data) as [keyof T, T[keyof T]][]) obj[key] = val;
return obj;
}

View File

@ -2,7 +2,7 @@
* This is an object that is used to keep track of where all of the player's
* money is coming from (or going to)
*/
import { Generic_fromJSON, Generic_toJSON, Reviver, IReviverValue } from "./JSONReviver";
import { Generic_fromJSON, Generic_toJSON, constructorsForReviver, IReviverValue } from "./JSONReviver";
export class MoneySourceTracker {
// eslint-disable-next-line @typescript-eslint/ban-types
@ -60,4 +60,4 @@ export class MoneySourceTracker {
}
}
Reviver.constructors.MoneySourceTracker = MoneySourceTracker;
constructorsForReviver.MoneySourceTracker = MoneySourceTracker;

View File

@ -0,0 +1,18 @@
/** Function for getting a list of keys to use for saving an object
* @param ctor the class constructor
*
* @param removedKeys Keys that exist on a default constructed member, but should not be saved.
* These keys will just revert to default values on load.
*
* @param addedKeys Optional keys that do not exist on a default constructed member, but should be saved when present.
*/
export function getKeyList<T extends object>(
ctor: new () => T,
modifications?: { removedKeys?: readonly (keyof T)[]; addedKeys?: readonly (keyof T)[] },
): readonly (keyof T)[] {
const newObj = new ctor();
const keySet = new Set(Object.getOwnPropertyNames(newObj)) as Set<keyof T>;
modifications?.removedKeys?.forEach((key) => keySet.delete(key));
modifications?.addedKeys?.forEach((key) => keySet.add(key));
return [...keySet];
}

View File

@ -7,16 +7,6 @@ import { loadAllServers, saveAllServers } from "../../src/Server/AllServers";
// without requiring burdensome levels of maintenance when legitimate changes
// are made.
// Get a stable clock so we don't have time-based diffs.
function freezeTime() {
// You're about to hack time, are you sure? YES/NO
const RealDate = Date;
Date = function () {
return new RealDate(1678834800000);
};
Date.now = () => 1678834800000;
}
// Savegame generated from dev on 2023-03-12, mostly empty game with a few
// tweaks. A RunningScript was added in-game to test the one bit of
// non-trivial machinery involved in save/load.
@ -147,7 +137,6 @@ test("load/saveAllServers", () => {
// Feed a JSON object through loadAllServers/saveAllServers.
// The object is a pruned set of servers that was extracted from a real (dev) game.
freezeTime();
loadStandardServers();
// Re-stringify with indenting for nicer diffs
@ -159,7 +148,6 @@ test("load/saveAllServers pruning RunningScripts", () => {
// Feed a JSON object through loadAllServers/saveAllServers.
// The object is a pruned set of servers that was extracted from a real (dev) game.
freezeTime();
loadStandardServers();
// Re-stringify with indenting for nicer diffs

View File

@ -10,7 +10,7 @@ describe("Validate Save Script Works", function () {
const server = "home";
const filename = "test.js";
const script = new Script();
script.saveScript(filename, code, server, []);
script.saveScript(filename, code, server);
expect(script.filename).toEqual(filename);
expect(script.code).toEqual(code);

View File

@ -29,26 +29,16 @@ exports[`load/saveAllServers 1`] = `
"args": [],
"dataMap": {},
"filename": "script.js",
"logs": [],
"logUpd": true,
"offlineExpGained": 0,
"offlineMoneyMade": 0,
"offlineRunningTime": 0.01,
"onlineExpGained": 0,
"onlineMoneyMade": 0,
"onlineRunningTime": 7.210000000000004,
"pid": 2,
"ramUsage": 1.6,
"server": "home",
"threads": 1,
"temporary": false,
"dependencies": [
{
"filename": "script.js",
"url": "blob:http://localhost/302fe9e5-2ec3-4ed7-bb5a-4f8f4a85f46d",
"moduleSequenceNumber": 2
}
]
"temporary": false
}
}
],
@ -58,27 +48,7 @@ exports[`load/saveAllServers 1`] = `
"data": {
"code": "/** @param {NS} ns */\\\\nexport async function main(ns) {\\\\n return ns.asleep(1000000);\\\\n}",
"filename": "script.js",
"url": "",
"module": {},
"dependencies": [
{
"filename": "script.js",
"url": "blob:http://localhost/e0abfafd-2c73-42fc-9eea-288c03820c47",
"moduleSequenceNumber": 5
}
],
"dependents": [],
"ramUsage": 1.6,
"queueCompile": false,
"server": "home",
"moduleSequenceNumber": 5,
"ramUsageEntries": [
{
"type": "misc",
"name": "baseCost",
"cost": 1.6
}
]
"server": "home"
}
}
],
@ -172,27 +142,7 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"data": {
"code": "/** @param {NS} ns */\\\\nexport async function main(ns) {\\\\n return ns.asleep(1000000);\\\\n}",
"filename": "script.js",
"url": "",
"module": {},
"dependencies": [
{
"filename": "script.js",
"url": "blob:http://localhost/e0abfafd-2c73-42fc-9eea-288c03820c47",
"moduleSequenceNumber": 5
}
],
"dependents": [],
"ramUsage": 1.6,
"queueCompile": false,
"server": "home",
"moduleSequenceNumber": 5,
"ramUsageEntries": [
{
"type": "misc",
"name": "baseCost",
"cost": 1.6
}
]
"server": "home"
}
}
],