diff --git a/.eslintrc.js b/.eslintrc.js index 2e1d646e3..54203c4ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { "@typescript-eslint/no-explicit-any": "off", "react/no-unescaped-entities": "off", "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/no-unsafe-enum-comparison": "off", }, settings: { react: { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9289bcc2c..670eb9e44 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -49,3 +49,14 @@ declare global { } } } + +module "monaco-vim" { + export const initVimMode: (...args: unknown[]) => { dispose: () => void }; + export const VimMode: { + Vim: { + defineEx: (...args: unknown[]) => void; + mapCommand: (...args: unknown[]) => void; + defineAction: (...args: unknown[]) => void; + }; + }; +} diff --git a/src/Casino/CardDeck/ReactCard.tsx b/src/Casino/CardDeck/ReactCard.tsx index 73345afba..074dc7a42 100644 --- a/src/Casino/CardDeck/ReactCard.tsx +++ b/src/Casino/CardDeck/ReactCard.tsx @@ -3,6 +3,7 @@ import { Card, Suit } from "./Card"; import { makeStyles } from "tss-react/mui"; import Paper from "@mui/material/Paper"; +import { throwIfReachable } from "../../utils/helpers/throwIfReachable"; interface Props { card: Card; @@ -51,7 +52,7 @@ export const ReactCard: FC = ({ card, hidden }) => { suit = ; break; default: - throw new Error(`MissingCaseException: ${card.suit}`); + throwIfReachable(card.suit); } return ( diff --git a/src/Company/GetJobRequirements.ts b/src/Company/GetJobRequirements.ts index 3c9ed2e91..03d8102c5 100644 --- a/src/Company/GetJobRequirements.ts +++ b/src/Company/GetJobRequirements.ts @@ -2,13 +2,15 @@ import { Company } from "./Company"; import { CompanyPosition } from "./CompanyPosition"; import { PlayerCondition, haveSkill, haveCompanyRep } from "../Faction/FactionJoinCondition"; -import type { Skills } from "../PersonObjects/Skills"; +import { getRecordEntries } from "../Types/Record"; export function getJobRequirements(company: Company, pos: CompanyPosition): PlayerCondition[] { const reqSkills = pos.requiredSkills(company.jobStatReqOffset); const reqs = []; - for (const [skillName, value] of Object.entries(reqSkills)) { - if (value > 0) reqs.push(haveSkill(skillName as keyof Skills, value)); + for (const [skillName, value] of getRecordEntries(reqSkills)) { + if (value > 0) { + reqs.push(haveSkill(skillName, value)); + } } if (pos.requiredReputation > 0) { reqs.push(haveCompanyRep(company.name, pos.requiredReputation)); diff --git a/src/Corporation/Corporation.ts b/src/Corporation/Corporation.ts index 800921ae7..f90b0bc1e 100644 --- a/src/Corporation/Corporation.ts +++ b/src/Corporation/Corporation.ts @@ -17,7 +17,7 @@ import { dialogBoxCreate } from "../ui/React/DialogBox"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; import { JSONMap, JSONSet } from "../Types/Jsonable"; import { formatMoney } from "../ui/formatNumber"; -import { isPositiveInteger } from "../types"; +import { isPositiveInteger, type Result } from "../types"; import { createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { getKeyList } from "../utils/helpers/getKeyList"; @@ -372,29 +372,56 @@ export class Corporation { } } - /** Purchasing a one-time unlock - * @returns A string on failure, indicating the reason for failure. */ - purchaseUnlock(unlockName: CorpUnlockName): string | void { - if (this.unlocks.has(unlockName)) return `The corporation has already unlocked ${unlockName}`; + /** + * Purchasing a one-time unlock + */ + purchaseUnlock(unlockName: CorpUnlockName): Result { + if (this.unlocks.has(unlockName)) { + return { + success: false, + message: `${unlockName} has already been unlocked.`, + }; + } const price = CorpUnlocks[unlockName].price; - if (this.funds < price) return `Insufficient funds to purchase ${unlockName}, requires ${formatMoney(price)}`; + if (this.funds < price) { + return { + success: false, + message: `Insufficient funds to purchase ${unlockName}, requires ${formatMoney(price)}.`, + }; + } this.loseFunds(price, "upgrades"); this.unlocks.add(unlockName); // Apply effects for one-time unlocks - if (unlockName === CorpUnlockName.ShadyAccounting) this.dividendTax -= 0.05; - if (unlockName === CorpUnlockName.GovernmentPartnership) this.dividendTax -= 0.1; + if (unlockName === CorpUnlockName.ShadyAccounting) { + this.dividendTax -= 0.05; + } + if (unlockName === CorpUnlockName.GovernmentPartnership) { + this.dividendTax -= 0.1; + } + return { + success: true, + }; } - /** Purchasing a levelable upgrade - * @returns A string on failure, indicating the reason for failure. */ - purchaseUpgrade(upgradeName: CorpUpgradeName, amount = 1): string | void { + /** + * Purchasing a levelable upgrade + */ + purchaseUpgrade(upgradeName: CorpUpgradeName, amount = 1): Result { if (!isPositiveInteger(amount)) { - return `Number of upgrade levels purchased must be a positive integer (attempted: ${amount}).`; + return { + success: false, + message: `Number of upgrade levels purchased must be a positive integer (attempted: ${amount}).`, + }; } const upgrade = CorpUpgrades[upgradeName]; const totalCost = calculateUpgradeCost(this, upgrade, amount); - if (this.funds < totalCost) return `Not enough funds to purchase ${amount} of upgrade ${upgradeName}.`; + if (this.funds < totalCost) { + return { + success: false, + message: `Not enough funds to purchase ${amount} of upgrade ${upgradeName}.`, + }; + } this.loseFunds(totalCost, "upgrades"); this.upgrades[upgradeName].level += amount; this.upgrades[upgradeName].value += upgrade.benefit * amount; @@ -407,6 +434,9 @@ export class Corporation { } } } + return { + success: true, + }; } getProductionMultiplier(): number { diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index 0d521cb97..9ccb54a10 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -16,6 +16,7 @@ import { PartialRecord, getRecordEntries, getRecordKeys, getRecordValues } from import { Material } from "./Material"; import { getKeyList } from "../utils/helpers/getKeyList"; import { calculateMarkupMultiplier } from "./helpers"; +import { throwIfReachable } from "../utils/helpers/throwIfReachable"; interface DivisionParams { name: string; @@ -704,8 +705,7 @@ export class Division { case "START": break; default: - console.error(`Invalid state: ${state}`); - break; + throwIfReachable(state); } //End switch(this.state) this.updateWarehouseSizeUsed(warehouse); } @@ -936,8 +936,7 @@ export class Division { case "EXPORT": break; default: - console.error(`Invalid State: ${state}`); - break; + throwIfReachable(state); } //End switch(this.state) this.updateWarehouseSizeUsed(warehouse); } diff --git a/src/Corporation/OfficeSpace.ts b/src/Corporation/OfficeSpace.ts index 6de7c114f..fce5a93e4 100644 --- a/src/Corporation/OfficeSpace.ts +++ b/src/Corporation/OfficeSpace.ts @@ -5,6 +5,7 @@ import { Division } from "./Division"; import { Corporation } from "./Corporation"; import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive"; import { createEnumKeyedRecord, getRecordKeys } from "../Types/Record"; +import { throwIfReachable } from "../utils/helpers/throwIfReachable"; interface IParams { city: CityName; @@ -165,8 +166,7 @@ export class OfficeSpace { case "total": continue; default: - console.error(`Invalid employee position: ${name}`); - break; + throwIfReachable(name); } this.employeeProductionByJob[name] = this.employeeJobs[name] * prodMult * prodBase; total += this.employeeProductionByJob[name]; diff --git a/src/Corporation/Product.ts b/src/Corporation/Product.ts index 5a551fcd3..2de76168a 100644 --- a/src/Corporation/Product.ts +++ b/src/Corporation/Product.ts @@ -214,7 +214,9 @@ export class Product { calculateRating(industry: Division): void { const weights = IndustriesData[industry.type].product?.ratingWeights; - if (!weights) return console.error(`Could not find product rating weights for: ${industry}`); + if (!weights) { + return console.error(`Could not find product rating weights for: ${industry.name}`); + } this.rating = getRecordEntries(weights).reduce( (total, [statName, weight]) => total + this.stats[statName] * weight, 0, diff --git a/src/Corporation/ui/LevelableUpgrade.tsx b/src/Corporation/ui/LevelableUpgrade.tsx index d625db64e..709a0d436 100644 --- a/src/Corporation/ui/LevelableUpgrade.tsx +++ b/src/Corporation/ui/LevelableUpgrade.tsx @@ -31,8 +31,10 @@ export function LevelableUpgrade({ upgradeName, mult, rerender }: IProps): React const tooltip = data.desc; function onClick(): void { if (corp.funds < cost) return; - const message = corp.purchaseUpgrade(upgradeName, amount); - if (message) dialogBoxCreate(`Could not upgrade ${upgradeName} ${amount} times:\n${message}`); + const result = corp.purchaseUpgrade(upgradeName, amount); + if (!result.success) { + dialogBoxCreate(`Could not upgrade ${upgradeName} ${amount} times:\n${result.message}`); + } rerender(); } diff --git a/src/Corporation/ui/Unlock.tsx b/src/Corporation/ui/Unlock.tsx index 13ef9e9e7..90cdf5812 100644 --- a/src/Corporation/ui/Unlock.tsx +++ b/src/Corporation/ui/Unlock.tsx @@ -25,8 +25,10 @@ export function Unlock(props: UnlockProps): React.ReactElement { function onClick(): void { // corp.unlock handles displaying a dialog on failure - const message = corp.purchaseUnlock(props.name); - if (message) dialogBoxCreate(`Error while attempting to purchase ${props.name}:\n${message}`); + const result = corp.purchaseUnlock(props.name); + if (!result.success) { + dialogBoxCreate(`Error while attempting to purchase ${props.name}:\n${result.message}`); + } // Rerenders the parent, which should remove this item if the purchase was successful props.rerender(); } diff --git a/src/Corporation/ui/modals/SellSharesModal.tsx b/src/Corporation/ui/modals/SellSharesModal.tsx index 9a5b95a2a..7b9aaa3ad 100644 --- a/src/Corporation/ui/modals/SellSharesModal.tsx +++ b/src/Corporation/ui/modals/SellSharesModal.tsx @@ -44,8 +44,8 @@ export function SellSharesModal(props: IProps): React.ReactElement { props.onClose(); props.rerender(); setShares(NaN); - } catch (err) { - dialogBoxCreate(`${err as Error}`); + } catch (error) { + dialogBoxCreate(String(error)); } } diff --git a/src/CotMG/Helper.tsx b/src/CotMG/Helper.tsx index 661c1948b..0c45899d8 100644 --- a/src/CotMG/Helper.tsx +++ b/src/CotMG/Helper.tsx @@ -7,17 +7,17 @@ export let staneksGift = new StaneksGift(); export function loadStaneksGift(saveString: string): void { if (saveString) { - staneksGift = JSON.parse(saveString, Reviver); + staneksGift = JSON.parse(saveString, Reviver) as StaneksGift; } else { staneksGift = new StaneksGift(); } } export function zeros(width: number, height: number): number[][] { - const array = []; + const array: number[][] = []; for (let i = 0; i < width; ++i) { - array.push(Array(height).fill(0)); + array.push(Array(height).fill(0)); } return array; diff --git a/src/CotMG/StaneksGift.ts b/src/CotMG/StaneksGift.ts index 9f4d26b0b..95dc283b4 100644 --- a/src/CotMG/StaneksGift.ts +++ b/src/CotMG/StaneksGift.ts @@ -18,9 +18,6 @@ export class StaneksGift extends BaseGift { isBonusCharging = false; justCharged = true; storedCycles = 0; - constructor() { - super(); - } baseSize(): number { return StanekConstants.BaseSize + currentNodeMults.StaneksGiftExtraSize + Player.activeSourceFileLvl(13); diff --git a/src/GameOptions/ui/GameOptionsSidebar.tsx b/src/GameOptions/ui/GameOptionsSidebar.tsx index a6c3891d5..3c4394b3b 100644 --- a/src/GameOptions/ui/GameOptionsSidebar.tsx +++ b/src/GameOptions/ui/GameOptionsSidebar.tsx @@ -155,13 +155,27 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => { > setImportSaveOpen(false)} - onConfirm={() => confirmedImportGame()} + onConfirm={() => { + confirmedImportGame().catch((error) => { + console.error(error); + }); + }} additionalButton={} confirmationText={ <> diff --git a/src/Gang/ui/GangMemberStats.tsx b/src/Gang/ui/GangMemberStats.tsx index c3b2b73d4..5575bc696 100644 --- a/src/Gang/ui/GangMemberStats.tsx +++ b/src/Gang/ui/GangMemberStats.tsx @@ -15,6 +15,7 @@ import { Settings } from "../../Settings/Settings"; import { MoneyRate } from "../../ui/React/MoneyRate"; import { StatsRow } from "../../ui/React/StatsRow"; import { useStyles } from "../../ui/React/CharacterOverview"; +import { getKeyFromReactElements } from "../../utils/StringHelperFunctions"; interface IProps { member: GangMember; @@ -103,7 +104,7 @@ export function GangMemberStats(props: IProps): React.ReactElement { {data.map(([a, b]) => ( - + {a} diff --git a/src/Go/SaveLoad.ts b/src/Go/SaveLoad.ts index 48f908124..8b76b50d7 100644 --- a/src/Go/SaveLoad.ts +++ b/src/Go/SaveLoad.ts @@ -83,7 +83,11 @@ export function loadGo(data: unknown): boolean { Go.storeCycles(loadStoredCycles(parsedData.storedCycles)); // If it's the AI's turn, initiate their turn, which will populate nextTurn - if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) makeAIMove(currentGame); + if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) { + makeAIMove(currentGame).catch((error) => { + showError(error); + }); + } // If it's not the AI's turn and we're not in gameover status, initialize nextTurn promise based on the previous move/pass else if (currentGame.previousPlayer) { const previousMove = getPreviousMove(); diff --git a/src/Infiltration/ui/MinesweeperGame.tsx b/src/Infiltration/ui/MinesweeperGame.tsx index d9c5edde0..81df2942e 100644 --- a/src/Infiltration/ui/MinesweeperGame.tsx +++ b/src/Infiltration/ui/MinesweeperGame.tsx @@ -135,7 +135,7 @@ export function MinesweeperGame(props: IMinigameProps): React.ReactElement { return ( > { const descriptor = Object.getOwnPropertyDescriptor(this.ns, key); if (!descriptor) return descriptor; - const field = descriptor.value; + const field: unknown = descriptor.value; if (typeof field === "function") { const arrayPath = [...this.tree, key]; @@ -74,7 +74,7 @@ class NSProxyHandler> { const ctx = { workerScript: this.ws, function: key, functionPath }; // Only do the context-binding once, instead of each time the function // is called. - const func: any = field(ctx); + const func = field(ctx) as (...args: unknown[]) => unknown; const wrappedFunction = function (...args: unknown[]): unknown { // What remains *must* be called every time. helpers.checkEnvFlags(ctx); diff --git a/src/Netscript/NetscriptHelpers.tsx b/src/Netscript/NetscriptHelpers.tsx index 9c9eb0441..24c6dac7c 100644 --- a/src/Netscript/NetscriptHelpers.tsx +++ b/src/Netscript/NetscriptHelpers.tsx @@ -692,8 +692,11 @@ function getRunningScript(ctx: NetscriptContext, ident: ScriptIdentifier): Runni return findRunningScriptByPid(ident); } else { const scripts = getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args); - if (scripts === null) return null; - return scripts.values().next().value ?? null; + if (scripts === null) { + return null; + } + const next = scripts.values().next(); + return !next.done ? next.value : null; } } diff --git a/src/NetscriptFunctions/Corporation.ts b/src/NetscriptFunctions/Corporation.ts index 2cd5b2426..5cbad188c 100644 --- a/src/NetscriptFunctions/Corporation.ts +++ b/src/NetscriptFunctions/Corporation.ts @@ -624,15 +624,19 @@ export function NetscriptCorporation(): InternalAPI { checkAccess(ctx); const unlockName = getEnumHelper("CorpUnlockName").nsGetMember(ctx, _unlockName, "unlockName"); const corporation = getCorporation(); - const message = corporation.purchaseUnlock(unlockName); - if (message) throw new Error(`Could not unlock ${unlockName}: ${message}`); + const result = corporation.purchaseUnlock(unlockName); + if (!result.success) { + throw new Error(`Could not unlock ${unlockName}: ${result.message}`); + } }, levelUpgrade: (ctx) => (_upgradeName) => { checkAccess(ctx); const upgradeName = getEnumHelper("CorpUpgradeName").nsGetMember(ctx, _upgradeName, "upgradeName"); const corporation = getCorporation(); - const message = corporation.purchaseUpgrade(upgradeName, 1); - if (message) throw new Error(`Could not upgrade ${upgradeName}: ${message}`); + const result = corporation.purchaseUpgrade(upgradeName, 1); + if (!result.success) { + throw new Error(`Could not upgrade ${upgradeName}: ${result.message}`); + } }, issueDividends: (ctx) => (_rate) => { checkAccess(ctx); diff --git a/src/NetscriptFunctions/Infiltration.ts b/src/NetscriptFunctions/Infiltration.ts index f860c8769..1681b4bbc 100644 --- a/src/NetscriptFunctions/Infiltration.ts +++ b/src/NetscriptFunctions/Infiltration.ts @@ -13,6 +13,7 @@ import { Factions } from "../Faction/Factions"; import { getEnumHelper } from "../utils/EnumHelper"; import { helpers } from "../Netscript/NetscriptHelpers"; import { filterTruthy } from "../utils/helpers/ArrayHelpers"; +import { exceptionAlert } from "../utils/helpers/exceptionAlert"; export function NetscriptInfiltration(): InternalAPI { const getLocationsWithInfiltrations = Object.values(Locations).filter( @@ -21,16 +22,29 @@ export function NetscriptInfiltration(): InternalAPI { const calculateInfiltrationData = (ctx: NetscriptContext, locationName: LocationName): InfiltrationLocation => { const location = Locations[locationName]; - if (location === undefined) throw helpers.errorMessage(ctx, `Location '${location}' does not exists.`); - if (location.infiltrationData === undefined) - throw helpers.errorMessage(ctx, `Location '${location}' does not provide infiltrations.`); + if (location === undefined) { + throw helpers.errorMessage(ctx, `Location "${locationName}" does not exist.`); + } + if (location.infiltrationData === undefined) { + throw helpers.errorMessage(ctx, `Location "${locationName}" does not provide infiltrations.`); + } + const locationCity = location.city; + /** + * location.city is only null when the location is available in all cities. This kind of location does not have + * infiltration data. + */ + if (locationCity === null) { + const errorMessage = `Location "${locationName}" is available in all cities, but it still has infiltration data.`; + exceptionAlert(new Error(errorMessage)); + throw helpers.errorMessage(ctx, errorMessage); + } const startingSecurityLevel = location.infiltrationData.startingSecurityLevel; const difficulty = calculateDifficulty(startingSecurityLevel); const reward = calculateReward(startingSecurityLevel); const maxLevel = location.infiltrationData.maxClearanceLevel; return { location: { - city: location.city!, + city: locationCity, name: location.name, }, reward: { diff --git a/src/NetscriptPort.ts b/src/NetscriptPort.ts index f9dc1dcb1..c26864e0b 100644 --- a/src/NetscriptPort.ts +++ b/src/NetscriptPort.ts @@ -47,7 +47,7 @@ export function portHandle(n: PortNumber): NetscriptPort { }; } -export function writePort(n: PortNumber, value: unknown): any { +export function writePort(n: PortNumber, value: unknown): unknown { const port = getPort(n); // Primitives don't need to be cloned. port.add(isObjectLike(value) ? structuredClone(value) : value); @@ -63,15 +63,15 @@ export function tryWritePort(n: PortNumber, value: unknown): boolean { return true; } -export function readPort(n: PortNumber): any { +export function readPort(n: PortNumber): unknown { const port = NetscriptPorts.get(n); if (!port || !port.data.length) return emptyPortData; - const returnVal = port.data.shift(); + const returnVal: unknown = port.data.shift(); if (!port.data.length && !port.resolver) NetscriptPorts.delete(n); return returnVal; } -export function peekPort(n: PortNumber): any { +export function peekPort(n: PortNumber): unknown { const port = NetscriptPorts.get(n); if (!port || !port.data.length) return emptyPortData; // Needed to avoid exposing internal objects. diff --git a/src/PersonObjects/Sleeve/ui/StatsElement.tsx b/src/PersonObjects/Sleeve/ui/StatsElement.tsx index 80a8d8127..d490bbce0 100644 --- a/src/PersonObjects/Sleeve/ui/StatsElement.tsx +++ b/src/PersonObjects/Sleeve/ui/StatsElement.tsx @@ -26,6 +26,7 @@ import { isSleeveFactionWork } from "../Work/SleeveFactionWork"; import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork"; import { isSleeveCrimeWork } from "../Work/SleeveCrimeWork"; import { canAccessBitNodeFeature } from "../../../BitNode/BitNodeUtils"; +import { getKeyFromReactElements } from "../../../utils/StringHelperFunctions"; const CYCLES_PER_SEC = 1000 / CONSTANTS.MilliPerCycle; @@ -177,7 +178,7 @@ export function EarningsElement(props: IProps): React.ReactElement { {data.map(([a, b]) => ( - + {a} diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index fcc799b06..be6b46a9c 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -231,16 +231,16 @@ function parseOnlyRamCalculate( prefix: string, obj: object, ref: string, - ): { func: () => number | number; refDetail: string } | undefined => { + ): { func: (() => number) | number; refDetail: string } | undefined => { if (!obj) { return; } const elem = Object.entries(obj).find(([key]) => key === ref); if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) { - return { func: elem[1], refDetail: `${prefix}${ref}` }; + return { func: elem[1] as (() => number) | number, refDetail: `${prefix}${ref}` }; } for (const [key, value] of Object.entries(obj)) { - const found = findFunc(`${key}.`, value, ref); + const found = findFunc(`${key}.`, value as object, ref); if (found) { return found; } diff --git a/src/Script/Script.ts b/src/Script/Script.ts index d5ee9d6ce..dcbfd2cec 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -76,7 +76,7 @@ export class Script implements ContentFile { const ramCalc = calculateRamUsage(this.code, this.filename, this.server, otherScripts); if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) { this.ramUsage = roundToTwo(ramCalc.cost); - this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; + this.ramUsageEntries = ramCalc.entries; this.ramCalculationError = null; return; } diff --git a/src/ScriptEditor/ui/useVimEditor.tsx b/src/ScriptEditor/ui/useVimEditor.tsx index 4ad98b809..27f8d4303 100644 --- a/src/ScriptEditor/ui/useVimEditor.tsx +++ b/src/ScriptEditor/ui/useVimEditor.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useState } from "react"; -// @ts-expect-error This library does not have types. import * as MonacoVim from "monaco-vim"; import type { editor } from "monaco-editor"; type IStandaloneCodeEditor = editor.IStandaloneCodeEditor; @@ -18,8 +17,7 @@ interface IProps { } export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, onSave }: IProps) { - // monaco-vim does not have types, so this is an any - const [vimEditor, setVimEditor] = useState(null); + const [vimEditor, setVimEditor] = useState | null>(null); const statusBarRef = useRef(null); const rerender = useRerender(); @@ -30,7 +28,6 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on useEffect(() => { // setup monaco-vim if (vim && editor && !vimEditor) { - // Using try/catch because MonacoVim does not have types. try { setVimEditor(MonacoVim.initVimMode(editor, statusBarRef, StatusBar, rerender)); MonacoVim.VimMode.Vim.defineEx("write", "w", function () { @@ -65,8 +62,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on MonacoVim.VimMode.Vim.mapCommand("gT", "action", "prevTabs", {}, { context: "normal" }); editor.focus(); } catch (e) { - console.error("An error occurred while loading monaco-vim:"); - console.error(e); + console.error("An error occurred while loading monaco-vim:", e); } } else if (!vim) { // When vim mode is disabled diff --git a/src/ScriptEditor/ui/utils.ts b/src/ScriptEditor/ui/utils.ts index c00c9c8c0..d4cde39b4 100644 --- a/src/ScriptEditor/ui/utils.ts +++ b/src/ScriptEditor/ui/utils.ts @@ -2,6 +2,7 @@ import { GetServer } from "../../Server/AllServers"; import { editor, Uri } from "monaco-editor"; import { OpenScript } from "./OpenScript"; import { getFileType, FileType } from "../../utils/ScriptTransformer"; +import { throwIfReachable } from "../../utils/helpers/throwIfReachable"; function getServerCode(scripts: OpenScript[], index: number): string | null { const openScript = scripts[index]; @@ -48,7 +49,7 @@ function makeModel(hostname: string, filename: string, code: string) { language = "javascript"; break; default: - throw new Error(`Invalid file type: ${fileType}. Filename: ${filename}.`); + throwIfReachable(fileType); } //if somehow a model already exist return it return editor.getModel(uri) ?? editor.createModel(code, language, uri); diff --git a/src/Sidebar/ui/SidebarAccordion.tsx b/src/Sidebar/ui/SidebarAccordion.tsx index a56521378..ff4f4a9c8 100644 --- a/src/Sidebar/ui/SidebarAccordion.tsx +++ b/src/Sidebar/ui/SidebarAccordion.tsx @@ -8,8 +8,9 @@ import Typography from "@mui/material/Typography"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { SidebarItem, ICreateProps as IItemProps } from "./SidebarItem"; +import { SidebarItem, ICreateProps as IItemProps, type SidebarItemProps } from "./SidebarItem"; import type { Page } from "../../ui/Router"; +import type { PartialRecord } from "../../Types/Record"; type SidebarAccordionProps = { key_: string; @@ -19,9 +20,12 @@ type SidebarAccordionProps = { items: (IItemProps | boolean)[]; icon: React.ReactElement["type"]; sidebarOpen: boolean; - classes: any; + classes: Record<"listitem" | "active", string>; }; +type ClickFnCacheKeyType = (page: Page) => void; +type ClickFnCacheValueType = PartialRecord; + // We can't useCallback for this, because in the items map it would be // called a changing number of times, and hooks can't be called in loops. So // we set up this explicit cache of function objects instead. @@ -30,7 +34,7 @@ type SidebarAccordionProps = { // WeakMap prevents memory leaks. We won't drop slices of the cache too soon, // because the fn keys are themselves memoized elsewhere, which keeps them // alive and thus keeps the WeakMap entries alive. -const clickFnCache = new WeakMap(); +const clickFnCache = new WeakMap(); function getClickFn(toWrap: (page: Page) => void, page: Page) { let first = clickFnCache.get(toWrap); if (first === undefined) { diff --git a/src/Sidebar/ui/SidebarItem.tsx b/src/Sidebar/ui/SidebarItem.tsx index e360dd1f8..e181a9089 100644 --- a/src/Sidebar/ui/SidebarItem.tsx +++ b/src/Sidebar/ui/SidebarItem.tsx @@ -18,7 +18,7 @@ export interface ICreateProps { export interface SidebarItemProps extends ICreateProps { clickFn: () => void; flash: boolean; - classes: any; + classes: Record<"listitem" | "active", string>; sidebarOpen: boolean; } diff --git a/src/StockMarket/OrderProcessing.tsx b/src/StockMarket/OrderProcessing.tsx index 31f565902..0ded485b1 100644 --- a/src/StockMarket/OrderProcessing.tsx +++ b/src/StockMarket/OrderProcessing.tsx @@ -17,6 +17,7 @@ import { dialogBoxCreate } from "../ui/React/DialogBox"; import { Settings } from "../Settings/Settings"; import * as React from "react"; +import { throwIfReachable } from "../utils/helpers/throwIfReachable"; export interface IProcessOrderRefs { stockMarket: IStockMarket; @@ -50,8 +51,8 @@ export function processOrders( return; // Newly created, so no orders to process } let stockOrders = orderBook[stock.symbol]; - if (stockOrders == null || !(stockOrders.constructor === Array)) { - console.error(`Invalid Order book for ${stock.symbol} in processOrders(): ${stockOrders}`); + if (stockOrders == null || !Array.isArray(stockOrders)) { + console.error(`Invalid Order book for ${stock.symbol} in processOrders(). stockOrders: ${stockOrders}`); stockOrders = []; return; } @@ -88,8 +89,7 @@ export function processOrders( } break; default: - console.warn(`Invalid order type: ${order.type}`); - return; + throwIfReachable(order.type); } } } @@ -137,8 +137,7 @@ function executeOrder(order: Order, refs: IProcessOrderRefs): void { } break; default: - console.warn(`Invalid order type: ${order.type}`); - return; + throwIfReachable(order.type); } // Position type, for logging/message purposes diff --git a/src/Terminal/commands/check.ts b/src/Terminal/commands/check.ts index 0c9553f9c..e79858249 100644 --- a/src/Terminal/commands/check.ts +++ b/src/Terminal/commands/check.ts @@ -21,6 +21,9 @@ export function check(args: (string | number | boolean)[], server: BaseServer): Terminal.error(`No script named ${scriptName} is running on the server`); return; } - runningScripts.values().next().value.displayLog(); + const next = runningScripts.values().next(); + if (!next.done) { + next.value.displayLog(); + } } } diff --git a/src/Terminal/commands/expr.ts b/src/Terminal/commands/expr.ts index a820de2b2..b79bd611b 100644 --- a/src/Terminal/commands/expr.ts +++ b/src/Terminal/commands/expr.ts @@ -9,11 +9,11 @@ export function expr(args: (string | number | boolean)[]): void { // Sanitize the math expression const sanitizedExpr = expr.replace(/[^-()\d/*+.%]/g, ""); - let result; + let result: string; try { - result = eval?.(sanitizedExpr); + result = String(eval?.(sanitizedExpr)); } catch (e) { - Terminal.error(`Could not evaluate expression: ${sanitizedExpr}`); + Terminal.error(`Could not evaluate expression: ${sanitizedExpr}. Error: ${e}.`); return; } Terminal.print(result); diff --git a/src/Terminal/ui/TerminalInput.tsx b/src/Terminal/ui/TerminalInput.tsx index 2a276c789..77de9882c 100644 --- a/src/Terminal/ui/TerminalInput.tsx +++ b/src/Terminal/ui/TerminalInput.tsx @@ -86,7 +86,7 @@ export function TerminalInput(): React.ReactElement { function getSearchSuggestionPrespace() { const currentPrefix = `[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> `; const prefixLength = `${currentPrefix}${value}`.length; - return Array(prefixLength).fill(" "); + return Array(prefixLength).fill(" "); } function modifyInput(mod: Modification): void { @@ -206,7 +206,7 @@ export function TerminalInput(): React.ReactElement { return () => document.removeEventListener("keydown", keyDown); }); - async function onKeyDown(event: React.KeyboardEvent): Promise { + async function onKeyDown(event: React.KeyboardEvent): Promise { const ref = terminalInput.current; // Run command or insert newline @@ -427,7 +427,11 @@ export function TerminalInput(): React.ReactElement { setPossibilities([]); resetSearch(); }, - onKeyDown: onKeyDown, + onKeyDown: (event) => { + onKeyDown(event).catch((error) => { + console.error(error); + }); + }, }} > { - const debounced = _.debounce(async () => rerender(), 25, { maxWait: 50 }); + const debounced = _.debounce(() => rerender(), 25, { maxWait: 50 }); const unsubscribe = TerminalEvents.subscribe(debounced); return () => { debounced.cancel(); @@ -51,7 +51,7 @@ export function TerminalRoot(): React.ReactElement { useEffect(() => { const clear = () => setKey((key) => key + 1); - const debounced = _.debounce(async () => clear(), 25, { maxWait: 50 }); + const debounced = _.debounce(() => clear(), 25, { maxWait: 50 }); const unsubscribe = TerminalClearEvents.subscribe(debounced); return () => { debounced.cancel(); diff --git a/src/data/codingcontracttypes.ts b/src/data/codingcontracttypes.ts index 748039331..41c5c6147 100644 --- a/src/data/codingcontracttypes.ts +++ b/src/data/codingcontracttypes.ts @@ -917,7 +917,7 @@ export const codingContractTypesMetadata: CodingContractType[] = [ desc: (data: number[][]): string => { return [ "You are located in the top-left corner of the following grid:\n\n", - `  [${data.map((line) => "[" + line + "]").join(",\n   ")}]\n\n`, + `  [${data.map((line) => `[${line}]`).join(",\n   ")}]\n\n`, "You are trying to find the shortest path to the bottom-right corner of the grid,", "but there are obstacles on the grid that you cannot move onto.", "These obstacles are denoted by '1', while empty spaces are denoted by 0.\n\n", @@ -945,8 +945,8 @@ export const codingContractTypesMetadata: CodingContractType[] = [ const dstX = width - 1; const minPathLength = dstY + dstX; // Math.abs(dstY - srcY) + Math.abs(dstX - srcX) - const grid: number[][] = new Array(height); - for (let y = 0; y < height; y++) grid[y] = new Array(width).fill(0); + const grid: number[][] = new Array(height); + for (let y = 0; y < height; y++) grid[y] = new Array(width).fill(0); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { @@ -969,12 +969,12 @@ export const codingContractTypesMetadata: CodingContractType[] = [ const dstY = height - 1; const dstX = width - 1; - const distance: [number][] = new Array(height); + const distance: number[][] = new Array(height); //const prev: [[number, number] | undefined][] = new Array(height); const queue: [number, number][] = []; for (let y = 0; y < height; y++) { - distance[y] = new Array(width).fill(Infinity) as [number]; + distance[y] = new Array(width).fill(Infinity); //prev[y] = new Array(width).fill(undefined) as [undefined]; } @@ -1422,7 +1422,7 @@ export const codingContractTypesMetadata: CodingContractType[] = [ //Attempt to construct one to check if this is correct. if (sanitizedPlayerAns === "") { //Verify that there is no solution by attempting to create a proper 2-coloring. - const coloring: (number | undefined)[] = Array(data[0]).fill(undefined); + const coloring: (number | undefined)[] = Array(data[0]).fill(undefined); while (coloring.some((val) => val === undefined)) { //Color a vertex in the graph const initialVertex: number = coloring.findIndex((val) => val === undefined); @@ -1435,9 +1435,7 @@ export const codingContractTypesMetadata: CodingContractType[] = [ const neighbors: number[] = neighbourhood(v); //For each vertex u adjacent to v - for (const id in neighbors) { - const u: number = neighbors[id]; - + for (const u of neighbors) { //Set the color of u to the opposite of v's color if it is new, //then add u to the frontier to continue the algorithm. if (coloring[u] === undefined) { diff --git a/src/ui/MD/a.tsx b/src/ui/MD/a.tsx index 5ee0814a2..a3006e5f1 100644 --- a/src/ui/MD/a.tsx +++ b/src/ui/MD/a.tsx @@ -46,7 +46,7 @@ export const A = (props: React.PropsWithChildren<{ href?: string }>): React.Reac cursor: "pointer", }} > - + ); return ( diff --git a/src/ui/React/CharacterOverview.tsx b/src/ui/React/CharacterOverview.tsx index 2c7b77de2..3bf700569 100644 --- a/src/ui/React/CharacterOverview.tsx +++ b/src/ui/React/CharacterOverview.tsx @@ -38,7 +38,7 @@ type RowName = SkillRowName | "HP" | "Money"; const OverviewEventEmitter = new EventEmitter(); // These values aren't displayed, they're just used for comparison to check if state has changed -const valUpdaters: Record any> = { +const valUpdaters: Record unknown> = { HP: () => Player.hp.current + "|" + Player.hp.max, // This isn't displayed, it's just compared for updates. Money: () => Player.money, Hack: () => Player.skills.hacking, diff --git a/src/ui/React/CinematicLine.tsx b/src/ui/React/CinematicLine.tsx index 757f7cdd1..61573c353 100644 --- a/src/ui/React/CinematicLine.tsx +++ b/src/ui/React/CinematicLine.tsx @@ -26,9 +26,11 @@ export function CinematicLine(props: IProps): React.ReactElement { return; } let cancel = false; - (async () => { - await sleep(10).then(() => !cancel && advance()); - })(); + sleep(10) + .then(() => !cancel && advance()) + .catch((error) => { + console.error(error); + }); return () => { cancel = true; }; diff --git a/src/ui/React/ImportSave/ImportSave.tsx b/src/ui/React/ImportSave/ImportSave.tsx index c55204a91..9bfe818e4 100644 --- a/src/ui/React/ImportSave/ImportSave.tsx +++ b/src/ui/React/ImportSave/ImportSave.tsx @@ -399,7 +399,11 @@ export const ImportSave = (props: { saveData: SaveData; automatic: boolean }): J { + handleImport().catch((error) => { + console.error(error); + }); + }} confirmationText={ <> Importing new save game data will completely wipe the current game data! diff --git a/src/ui/React/LogBoxManager.tsx b/src/ui/React/LogBoxManager.tsx index 6dab2ece7..2ac73898a 100644 --- a/src/ui/React/LogBoxManager.tsx +++ b/src/ui/React/LogBoxManager.tsx @@ -298,7 +298,16 @@ function LogWindow({ hidden, script, onClose }: LogWindowProps): React.ReactElem return !(bounds.right < 0 || bounds.bottom < 0 || bounds.left > innerWidth || bounds.top > outerWidth); }; - const onDrag = (e: DraggableEvent): void | false => { + /** + * The returned type of onDrag is a bit weird here. The Draggable component expects an onDrag that returns "void | false". + * In that component's internal code, it checks for the explicit "false" value. If onDrag returns false, the component + * cancels the dragging. + * + * That's why they use "void | false" as the returned type. However, in TypeScript, "void" is not supposed to be used + * like that. ESLint will complain "void is not valid as a constituent in a union type". Please check its documentation + * for the reason. In order to solve this problem, I changed the returned type to "undefined | false". + */ + const onDrag = (e: DraggableEvent): undefined | false => { e.preventDefault(); // bound to body if ( diff --git a/src/ui/React/RecoveryRoot.tsx b/src/ui/React/RecoveryRoot.tsx index 536c0115a..3d95af1ba 100644 --- a/src/ui/React/RecoveryRoot.tsx +++ b/src/ui/React/RecoveryRoot.tsx @@ -98,7 +98,7 @@ export function RecoveryRoot({ softReset, errorData, resetError }: IProps): Reac {sourceError && ( - Error: {sourceError.toString()} + Error: {String(sourceError)}
diff --git a/src/ui/React/Snackbar.tsx b/src/ui/React/Snackbar.tsx index e7bb63a04..e5072dc50 100644 --- a/src/ui/React/Snackbar.tsx +++ b/src/ui/React/Snackbar.tsx @@ -14,7 +14,7 @@ interface IProps { const useStyles = makeStyles()({ snackbar: { // Log popup z-index increments, so let's add a padding to be well above them. - zIndex: `${logBoxBaseZIndex + 1000} !important` as any, + zIndex: `${logBoxBaseZIndex + 1000} !important`, "& .MuiAlert-icon": { alignSelf: "center", diff --git a/src/utils/CompressionContracts.ts b/src/utils/CompressionContracts.ts index 64040ff68..8c19c2df0 100644 --- a/src/utils/CompressionContracts.ts +++ b/src/utils/CompressionContracts.ts @@ -1,4 +1,4 @@ -// choose random character for generating plaintexts to compress +// choose random characters for generating plaintext to compress export function comprGenChar(): string { const r = Math.random(); if (r < 0.4) { @@ -34,13 +34,13 @@ export function comprLZGenerate(): string { return plain.substring(0, length); } -// compress plaintest string +// compress plaintext string export function comprLZEncode(plain: string): string { // for state[i][j]: // if i is 0, we're adding a literal of length j // else, we're adding a backreference of offset i and length j - let cur_state: (string | null)[][] = Array.from(Array(10), () => Array(10).fill(null)); - let new_state: (string | null)[][] = Array.from(Array(10), () => Array(10)); + let cur_state: (string | null)[][] = Array.from(Array(10), () => Array(10).fill(null)); + let new_state: (string | null)[][] = Array.from(Array(10), () => Array(10)); function set(state: (string | null)[][], i: number, j: number, str: string): void { const current = state[i][j]; diff --git a/src/utils/DeprecationHelper.ts b/src/utils/DeprecationHelper.ts index 593bc3eff..194b3c95b 100644 --- a/src/utils/DeprecationHelper.ts +++ b/src/utils/DeprecationHelper.ts @@ -3,7 +3,7 @@ import { Terminal } from "../Terminal"; const deprecatedWarningsGiven = new Set(); export function setDeprecatedProperties( obj: object, - properties: Record, + properties: Record, ) { for (const [name, info] of Object.entries(properties)) { Object.defineProperty(obj, name, { @@ -11,7 +11,7 @@ export function setDeprecatedProperties( deprecationWarning(info.identifier, info.message); return info.value; }, - set: (value: any) => (info.value = value), + set: (value: unknown) => (info.value = value), enumerable: true, }); } diff --git a/src/utils/EnumHelper.ts b/src/utils/EnumHelper.ts index 350574cac..723b18413 100644 --- a/src/utils/EnumHelper.ts +++ b/src/utils/EnumHelper.ts @@ -5,6 +5,7 @@ import * as allEnums from "../Enums"; import { assertString } from "../Netscript/TypeAssertion"; import { errorMessage } from "../Netscript/ErrorMessages"; import { getRandomIntInclusive } from "./helpers/getRandomIntInclusive"; +import { getRecordValues } from "../Types/Record"; interface GetMemberOptions { /** Whether to use fuzzy matching on the input (case insensitive, ignore spaces and dashes) */ @@ -22,7 +23,7 @@ class EnumHelper & st constructor(obj: EnumObj, name: string) { this.name = name; this.defaultArgName = name.charAt(0).toLowerCase() + name.slice(1); - this.valueArray = Object.values(obj); + this.valueArray = getRecordValues(obj); this.valueSet = new Set(this.valueArray); this.fuzzMap = new Map(this.valueArray.map((val) => [val.toLowerCase().replace(/[ -]+/g, ""), val])); } diff --git a/src/utils/HammingCodeTools.ts b/src/utils/HammingCodeTools.ts index 12a61754b..d55ed7544 100644 --- a/src/utils/HammingCodeTools.ts +++ b/src/utils/HammingCodeTools.ts @@ -1,10 +1,10 @@ export function HammingEncode(data: number): string { const enc: number[] = [0]; - const data_bits: any[] = data.toString(2).split("").reverse(); - - data_bits.forEach((e, i, a) => { - a[i] = parseInt(e); - }); + const data_bits: number[] = data + .toString(2) + .split("") + .reverse() + .map((value) => parseInt(value)); let k = data_bits.length; @@ -18,35 +18,36 @@ export function HammingEncode(data: number): string { } } - let parity: any = 0; + let parityNumber = 0; /* Figure out the subsection parities */ for (let i = 0; i < enc.length; i++) { if (enc[i]) { - parity ^= i; + parityNumber ^= i; } } - parity = parity.toString(2).split("").reverse(); - parity.forEach((e: any, i: any, a: any) => { - a[i] = parseInt(e); - }); + const parityArray = parityNumber + .toString(2) + .split("") + .reverse() + .map((value) => parseInt(value)); /* Set the parity bits accordingly */ - for (let i = 0; i < parity.length; i++) { - enc[2 ** i] = parity[i] ? 1 : 0; + for (let i = 0; i < parityArray.length; i++) { + enc[2 ** i] = parityArray[i] ? 1 : 0; } - parity = 0; + parityNumber = 0; /* Figure out the overall parity for the entire block */ for (let i = 0; i < enc.length; i++) { if (enc[i]) { - parity++; + parityNumber++; } } /* Finally set the overall parity bit */ - enc[0] = parity % 2 == 0 ? 0 : 1; + enc[0] = parityNumber % 2 == 0 ? 0 : 1; return enc.join(""); } @@ -68,11 +69,11 @@ export function HammingEncodeProperly(data: number): string { const k: number = 2 ** m - m - 1; const enc: number[] = [0]; - const data_bits: any[] = data.toString(2).split("").reverse(); - - data_bits.forEach((e, i, a) => { - a[i] = parseInt(e); - }); + const data_bits: number[] = data + .toString(2) + .split("") + .reverse() + .map((value) => parseInt(value)); /* Flip endianness as in the original implementation by Hedrauta * and write the data back to front @@ -83,35 +84,36 @@ export function HammingEncodeProperly(data: number): string { } } - let parity: any = 0; + let parityNumber = 0; /* Figure out the subsection parities */ for (let i = 0; i < n; i++) { if (enc[i]) { - parity ^= i; + parityNumber ^= i; } } - parity = parity.toString(2).split("").reverse(); - parity.forEach((e: any, i: any, a: any) => { - a[i] = parseInt(e); - }); + const parityArray = parityNumber + .toString(2) + .split("") + .reverse() + .map((value) => parseInt(value)); /* Set the parity bits accordingly */ for (let i = 0; i < m; i++) { - enc[2 ** i] = parity[i] ? 1 : 0; + enc[2 ** i] = parityArray[i] ? 1 : 0; } - parity = 0; + parityNumber = 0; /* Figure out the overall parity for the entire block */ for (let i = 0; i < n; i++) { if (enc[i]) { - parity++; + parityNumber++; } } /* Finally set the overall parity bit */ - enc[0] = parity % 2 == 0 ? 0 : 1; + enc[0] = parityNumber % 2 == 0 ? 0 : 1; return enc.join(""); } @@ -121,8 +123,9 @@ export function HammingDecode(data: string): number { const bits: number[] = []; /* TODO why not just work with an array of digits from the start? */ - for (const i in data.split("")) { - const bit = parseInt(data[i]); + const bitStringArray = data.split(""); + for (let i = 0; i < bitStringArray.length; ++i) { + const bit = parseInt(bitStringArray[i]); bits[i] = bit; if (bit) { diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts index f0a4e7b00..0c00766c4 100644 --- a/src/utils/StringHelperFunctions.ts +++ b/src/utils/StringHelperFunctions.ts @@ -125,3 +125,9 @@ export function getNsApiDocumentationUrl(isDevBranch: boolean = CONSTANTS.isDevB isDevBranch ? "dev" : "stable" }/markdown/bitburner.ns.md`; } + +export function getKeyFromReactElements(a: string | React.JSX.Element, b: string | React.JSX.Element): string { + const keyOfA = typeof a === "string" ? a : a.key ?? ""; + const keyOfb = typeof b === "string" ? b : b.key ?? ""; + return keyOfA + keyOfb; +} diff --git a/src/utils/helpers/throwIfReachable.ts b/src/utils/helpers/throwIfReachable.ts new file mode 100644 index 000000000..fe33b1345 --- /dev/null +++ b/src/utils/helpers/throwIfReachable.ts @@ -0,0 +1,3 @@ +export function throwIfReachable(missingCase: never) { + throw new Error(`The case of ${missingCase} was not handled.`); +}