From cbff2a420b8623b247c31f55ace729ced677a854 Mon Sep 17 00:00:00 2001 From: Snarling <84951833+Snarling@users.noreply.github.com> Date: Tue, 6 Jun 2023 23:50:23 -0400 Subject: [PATCH] CORP: Robotics industry NaN fix + better exports validation (#578) --- ...itburner.basichgwoptions.additionalmsec.md | 2 +- markdown/bitburner.basichgwoptions.md | 2 +- ...urner.warehouseapi.cancelexportmaterial.md | 2 - markdown/bitburner.warehouseapi.md | 2 +- src/Corporation/Actions.ts | 95 +++++++++++-------- src/Corporation/Division.ts | 9 +- src/Corporation/ui/modals/ExportModal.tsx | 2 +- src/NetscriptFunctions/Corporation.ts | 15 +-- src/SaveObject.tsx | 34 +++++++ src/ScriptEditor/NetscriptDefinitions.d.ts | 2 - 10 files changed, 107 insertions(+), 58 deletions(-) diff --git a/markdown/bitburner.basichgwoptions.additionalmsec.md b/markdown/bitburner.basichgwoptions.additionalmsec.md index 498c72d31..f831a35e2 100644 --- a/markdown/bitburner.basichgwoptions.additionalmsec.md +++ b/markdown/bitburner.basichgwoptions.additionalmsec.md @@ -4,7 +4,7 @@ ## BasicHGWOptions.additionalMsec property -Number of additional milliseconds that will be spent waiting between the start of the function and when it completes. Experimental in 2.2.2, may be removed in 2.3. +Number of additional milliseconds that will be spent waiting between the start of the function and when it completes. **Signature:** diff --git a/markdown/bitburner.basichgwoptions.md b/markdown/bitburner.basichgwoptions.md index eb29d67ef..3e96423ec 100644 --- a/markdown/bitburner.basichgwoptions.md +++ b/markdown/bitburner.basichgwoptions.md @@ -16,7 +16,7 @@ interface BasicHGWOptions | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [additionalMsec?](./bitburner.basichgwoptions.additionalmsec.md) | | number | _(Optional)_ Number of additional milliseconds that will be spent waiting between the start of the function and when it completes. Experimental in 2.2.2, may be removed in 2.3. | +| [additionalMsec?](./bitburner.basichgwoptions.additionalmsec.md) | | number | _(Optional)_ Number of additional milliseconds that will be spent waiting between the start of the function and when it completes. | | [stock?](./bitburner.basichgwoptions.stock.md) | | boolean | _(Optional)_ Set to true this action will affect the stock market. | | [threads?](./bitburner.basichgwoptions.threads.md) | | number | _(Optional)_ Number of threads to use for this function. Must be less than or equal to the number of threads the script is running with. | diff --git a/markdown/bitburner.warehouseapi.cancelexportmaterial.md b/markdown/bitburner.warehouseapi.cancelexportmaterial.md index d55022857..4ccf464d6 100644 --- a/markdown/bitburner.warehouseapi.cancelexportmaterial.md +++ b/markdown/bitburner.warehouseapi.cancelexportmaterial.md @@ -15,7 +15,6 @@ cancelExportMaterial( targetDivision: string, targetCity: CityName | `${CityName}`, materialName: string, - amt: number, ): void; ``` @@ -28,7 +27,6 @@ cancelExportMaterial( | targetDivision | string | Target division | | targetCity | [CityName](./bitburner.cityname.md) \| \`${[CityName](./bitburner.cityname.md)}\` | Target city | | materialName | string | Name of the material | -| amt | number | Amount of material to export. | **Returns:** diff --git a/markdown/bitburner.warehouseapi.md b/markdown/bitburner.warehouseapi.md index 12c460505..947b47d33 100644 --- a/markdown/bitburner.warehouseapi.md +++ b/markdown/bitburner.warehouseapi.md @@ -22,7 +22,7 @@ Requires the Warehouse API upgrade from your corporation. | --- | --- | | [bulkPurchase(divisionName, city, materialName, amt)](./bitburner.warehouseapi.bulkpurchase.md) | Set material to bulk buy | | [buyMaterial(divisionName, city, materialName, amt)](./bitburner.warehouseapi.buymaterial.md) | Set material buy data | -| [cancelExportMaterial(sourceDivision, sourceCity, targetDivision, targetCity, materialName, amt)](./bitburner.warehouseapi.cancelexportmaterial.md) | Cancel material export | +| [cancelExportMaterial(sourceDivision, sourceCity, targetDivision, targetCity, materialName)](./bitburner.warehouseapi.cancelexportmaterial.md) | Cancel material export | | [discontinueProduct(divisionName, productName)](./bitburner.warehouseapi.discontinueproduct.md) | Discontinue a product. | | [exportMaterial(sourceDivision, sourceCity, targetDivision, targetCity, materialName, amt)](./bitburner.warehouseapi.exportmaterial.md) | Set material export data | | [getMaterial(divisionName, city, materialName)](./bitburner.warehouseapi.getmaterial.md) | Get material data | diff --git a/src/Corporation/Actions.ts b/src/Corporation/Actions.ts index 722dc2709..b028d6b9d 100644 --- a/src/Corporation/Actions.ts +++ b/src/Corporation/Actions.ts @@ -48,6 +48,17 @@ export function NewDivision(corporation: Corporation, industry: IndustryType, na export function removeDivision(corporation: Corporation, name: string) { if (!corporation.divisions.has(name)) throw new Error("There is no division called " + name); corporation.divisions.delete(name); + // We also need to remove any exports that were pointing to the old division + for (const otherDivision of corporation.divisions.values()) { + for (const warehouse of getRecordValues(otherDivision.warehouses)) { + for (const material of getRecordValues(warehouse.materials)) { + // Work backwards through exports array so splicing doesn't affect the loop + for (let i = material.exports.length - 1; i >= 0; i--) { + if (material.exports[i].division === name) material.exports.splice(i, 1); + } + } + } + } } export function purchaseOffice(corporation: Corporation, division: Division, city: CityName): void { @@ -443,52 +454,62 @@ export function Research(researchingDivision: Division, researchName: CorpResear } } +/** Set a new export for a material. Throw on any invalid input. */ export function ExportMaterial( - divisionName: string, - cityName: CityName, + targetDivision: Division, + targetCity: CityName, material: Material, - amt: string, - division?: Division, + amount: string, ): void { - // Sanitize amt - let sanitizedAmt = amt.replace(/\s+/g, "").toUpperCase(); + if (!isRelevantMaterial(material.name, targetDivision)) { + throw new Error(`You cannot export material: ${material.name} to division: ${targetDivision.name}!`); + } + if (!targetDivision.warehouses[targetCity]) { + throw new Error(`Cannot export to ${targetCity} in division ${targetDivision.name} because there is no warehouse.`); + } + if (material === targetDivision.warehouses[targetCity]?.materials[material.name]) { + throw new Error(`Source and target division/city cannot be the same.`); + } + for (const existingExport of material.exports) { + if (existingExport.division === targetDivision.name && existingExport.city === targetCity) { + throw new Error(`Tried to initialize an export to a duplicate warehouse. +Target warehouse (division / city): ${existingExport.division} / ${existingExport.city} +Existing export amount: ${existingExport.amount} +Attempted export amount: ${amount}`); + } + } + + // Perform sanitization and tests + let sanitizedAmt = amount.replace(/\s+/g, "").toUpperCase(); sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAXEPRODINV]/g, ""); - let temp = sanitizedAmt.replace(/MAX/g, "1"); - temp = temp.replace(/IPROD/g, "1"); - temp = temp.replace(/EPROD/g, "1"); - temp = temp.replace(/IINV/g, "1"); - temp = temp.replace(/EINV/g, "1"); - try { - temp = eval(temp); - } catch (e) { - throw new Error("Invalid expression entered for export amount: " + e); + for (const testReplacement of ["(1.23)", "(-1.23)"]) { + const replaced = sanitizedAmt.replace(/(IPROD|EPROD|IINV|EINV)/g, testReplacement); + let evaluated, error; + try { + evaluated = eval(replaced); + } catch (e) { + error = e; + } + if (!error && isNaN(evaluated)) error = "evaluated value is NaN"; + if (error) { + throw new Error(`Error while trying to set the exported amount of ${material.name}. +Error occurred while testing keyword replacement with ${testReplacement}. +Your input: ${amount} +Sanitized input: ${sanitizedAmt} +Input after replacement: ${replaced} +Evaluated value: ${evaluated} +Error encountered: ${error}`); + } } - const n = parseFloat(temp); - - if (n == null || isNaN(n)) { - throw new Error("Invalid amount entered for export"); - } - - if (!division || !isRelevantMaterial(material.name, division)) { - throw new Error(`You cannot export material: ${material.name} to division: ${divisionName}!`); - } - - const exportObj = { division: divisionName, city: cityName, amount: sanitizedAmt }; + const exportObj = { division: targetDivision.name, city: targetCity, amount: sanitizedAmt }; material.exports.push(exportObj); } -export function CancelExportMaterial(divisionName: string, cityName: string, material: Material, amt: string): void { - for (let i = 0; i < material.exports.length; ++i) { - if ( - material.exports[i].division !== divisionName || - material.exports[i].city !== cityName || - material.exports[i].amount !== amt - ) - continue; - material.exports.splice(i, 1); - break; - } +export function CancelExportMaterial(divisionName: string, cityName: CityName, material: Material): void { + const index = material.exports.findIndex((exp) => exp.division === divisionName && exp.city === cityName); + if (index === -1) return; + material.exports.splice(index, 1); } export function LimitProductProduction(product: Product, cityName: CityName, quantity: number): void { diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index 3efee0bdb..e6b0faa70 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -434,7 +434,12 @@ export class Division { const divider = requiredMatsEntries.length; for (const [reqMatName, reqMat] of requiredMatsEntries) { const reqMatQtyNeeded = reqMat * prod * producableFrac; - warehouse.materials[reqMatName].stored -= reqMatQtyNeeded; + // producableFrac already takes into account that we have enough stored + // Math.max is used here to avoid stored becoming negative (which can lead to NaNs) + warehouse.materials[reqMatName].stored = Math.max( + 0, + warehouse.materials[reqMatName].stored - reqMatQtyNeeded, + ); warehouse.materials[reqMatName].productionAmount = 0; warehouse.materials[reqMatName].productionAmount -= reqMatQtyNeeded / (corpConstants.secondsPerMarketCycle * marketCycles); @@ -446,7 +451,7 @@ export class Division { let tempQlt = office.employeeProductionByJob[CorpEmployeeJob.Engineer] / 90 + Math.pow(this.researchPoints, this.researchFactor) + - Math.pow(warehouse.materials["AI Cores"].stored, this.aiCoreFactor) / 10e3; + Math.pow(Math.max(0, warehouse.materials["AI Cores"].stored), this.aiCoreFactor) / 10e3; const logQlt = Math.max(Math.pow(tempQlt, 0.5), 1); tempQlt = Math.min(tempQlt, avgQlt * logQlt); warehouse.materials[this.producedMaterials[j]].quality = Math.max( diff --git a/src/Corporation/ui/modals/ExportModal.tsx b/src/Corporation/ui/modals/ExportModal.tsx index 530e9031c..2235b6641 100644 --- a/src/Corporation/ui/modals/ExportModal.tsx +++ b/src/Corporation/ui/modals/ExportModal.tsx @@ -54,7 +54,7 @@ export function ExportModal(props: IProps): React.ReactElement { function exportMaterial(): void { try { - ExportMaterial(targetDivision.name, targetCity, props.mat, exportAmount, targetDivision); + ExportMaterial(targetDivision, targetCity, props.mat, exportAmount); } catch (err) { dialogBoxCreate(err + ""); } diff --git a/src/NetscriptFunctions/Corporation.ts b/src/NetscriptFunctions/Corporation.ts index 26320426e..c82a2b68d 100644 --- a/src/NetscriptFunctions/Corporation.ts +++ b/src/NetscriptFunctions/Corporation.ts @@ -465,29 +465,22 @@ export function NetscriptCorporation(): InternalAPI { checkAccess(ctx, CorpUnlockName.WarehouseAPI); const sourceDivision = helpers.string(ctx, "sourceDivision", _sourceDivision); assertMember(ctx, CityName, "City", "sourceCity", sourceCity); - const targetDivision = helpers.string(ctx, "targetDivision", _targetDivision); + const targetDivision = getDivision(helpers.string(ctx, "targetDivision", _targetDivision)); assertMember(ctx, CityName, "City", "targetCity", targetCity); assertMember(ctx, corpConstants.materialNames, "Material Name", "materialName", materialName); const amt = helpers.string(ctx, "amt", _amt); - ExportMaterial( - targetDivision, - targetCity, - getMaterial(sourceDivision, sourceCity, materialName), - amt + "", - getDivision(targetDivision), - ); + ExportMaterial(targetDivision, targetCity, getMaterial(sourceDivision, sourceCity, materialName), amt); }, cancelExportMaterial: (ctx) => - (_sourceDivision, sourceCity, _targetDivision, targetCity, materialName, _amt): void => { + (_sourceDivision, sourceCity, _targetDivision, targetCity, materialName): void => { checkAccess(ctx, CorpUnlockName.WarehouseAPI); const sourceDivision = helpers.string(ctx, "sourceDivision", _sourceDivision); assertMember(ctx, CityName, "City Name", "sourceCity", sourceCity); const targetDivision = helpers.string(ctx, "targetDivision", _targetDivision); assertMember(ctx, CityName, "City Name", "targetCity", targetCity); assertMember(ctx, corpConstants.materialNames, "Material Name", "materialName", materialName); - const amt = helpers.string(ctx, "amt", _amt); - CancelExportMaterial(targetDivision, targetCity, getMaterial(sourceDivision, sourceCity, materialName), amt); + CancelExportMaterial(targetDivision, targetCity, getMaterial(sourceDivision, sourceCity, materialName)); }, limitMaterialProduction: (ctx) => (_divisionName, cityName, materialName, _qty) => { checkAccess(ctx, CorpUnlockName.WarehouseAPI); diff --git a/src/SaveObject.tsx b/src/SaveObject.tsx index daa4a5b0e..874769606 100755 --- a/src/SaveObject.tsx +++ b/src/SaveObject.tsx @@ -37,6 +37,8 @@ import { SpecialServers } from "./Server/data/SpecialServers"; import { v2APIBreak } from "./utils/v2APIBreak"; import { Corporation } from "./Corporation/Corporation"; import { Terminal } from "./Terminal"; +import { getRecordValues } from "./Types/Record"; +import { ExportMaterial } from "./Corporation/Actions"; /* SaveObject.js * Defines the object used to save/load games @@ -688,6 +690,38 @@ function evaluateVersionCompatibility(ver: string | number): void { } } } + // Sanitize corporation exports + let anyExportsFailed = false; + if (Player.corporation) { + for (const division of Player.corporation.divisions.values()) { + for (const warehouse of getRecordValues(division.warehouses)) { + for (const material of getRecordValues(warehouse.materials)) { + const originalExports = material.exports; + // Clear all exports for the material + material.exports = []; + for (const originalExport of originalExports) { + // Throw if there was a failure re-establishing an export + try { + const targetDivision = Player.corporation.divisions.get(originalExport.division); + if (!targetDivision) throw new Error(`Target division ${originalExport.division} did not exist`); + // Set the export again. ExportMaterial throws on failure + ExportMaterial(targetDivision, originalExport.city, material, originalExport.amount); + } catch (e) { + anyExportsFailed = true; + // We just need the text error, not a full stack trace + console.error(`Failed to load export of material ${material.name} (${division.name} ${warehouse.city}) +Original export details: ${JSON.stringify(originalExport)} +Error: ${e}`); + } + } + } + } + } + } + if (anyExportsFailed) + Terminal.error( + "Some material exports failed to validate while loading and have been removed. See console for more info.", + ); } } diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index 71ba969f0..6707d0307 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -7093,7 +7093,6 @@ export interface WarehouseAPI { * @param targetDivision - Target division * @param targetCity - Target city * @param materialName - Name of the material - * @param amt - Amount of material to export. */ cancelExportMaterial( sourceDivision: string, @@ -7101,7 +7100,6 @@ export interface WarehouseAPI { targetDivision: string, targetCity: CityName | `${CityName}`, materialName: string, - amt: number, ): void; /** * Purchase warehouse for a new city