mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2025-03-07 11:04:36 +01:00
SCRIPTS: Script modules are reused when they are imported (#461)
Also corrects some compile race conditions.
This commit is contained in:
16
package-lock.json
generated
16
package-lock.json
generated
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
18
src/utils/helpers/getKeyList.ts
Normal file
18
src/utils/helpers/getKeyList.ts
Normal 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];
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
Reference in New Issue
Block a user