CORP: Robotics industry NaN fix + better exports validation (#578)

This commit is contained in:
Snarling 2023-06-06 23:50:23 -04:00 committed by GitHub
parent 4c4c4a0335
commit cbff2a420b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 107 additions and 58 deletions

@ -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:**

@ -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. |

@ -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:**

@ -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 |

@ -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 {

@ -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(

@ -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 + "");
}

@ -465,29 +465,22 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
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);

@ -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.",
);
}
}

@ -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