diff --git a/src/Augmentation/ui/AugmentationsRoot.tsx b/src/Augmentation/ui/AugmentationsRoot.tsx index bab9dda85..476b507f7 100644 --- a/src/Augmentation/ui/AugmentationsRoot.tsx +++ b/src/Augmentation/ui/AugmentationsRoot.tsx @@ -26,7 +26,7 @@ import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { Info } from "@mui/icons-material"; import { Link } from "@mui/material"; import { AlertEvents } from "../../ui/React/AlertManager"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; const NeuroFluxDisplay = (): React.ReactElement => { const level = Player.augmentations.find((e) => e.name === AugmentationName.NeuroFluxGovernor)?.level ?? 0; @@ -85,7 +85,7 @@ interface IProps { export function AugmentationsRoot(props: IProps): React.ReactElement { const [installOpen, setInstallOpen] = useState(false); - const rerender = useRerender(200); + const rerender = useCycleRerender(); function doExport(): void { props.exportGameFn(); diff --git a/src/Bladeburner/ui/BladeburnerRoot.tsx b/src/Bladeburner/ui/BladeburnerRoot.tsx index 650c32f73..a2e31285c 100644 --- a/src/Bladeburner/ui/BladeburnerRoot.tsx +++ b/src/Bladeburner/ui/BladeburnerRoot.tsx @@ -5,10 +5,10 @@ import { AllPages } from "./AllPages"; import { Player } from "@player"; import { Box } from "@mui/material"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; export function BladeburnerRoot(): React.ReactElement { - useRerender(200); + useCycleRerender(); const bladeburner = Player.bladeburner; if (!bladeburner) return <>; return ( diff --git a/src/Corporation/ui/CorporationRoot.tsx b/src/Corporation/ui/CorporationRoot.tsx index 1550155cb..86ebf4d98 100644 --- a/src/Corporation/ui/CorporationRoot.tsx +++ b/src/Corporation/ui/CorporationRoot.tsx @@ -10,10 +10,10 @@ import { Overview } from "./Overview"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; export function CorporationRoot(): React.ReactElement { - const rerender = useRerender(200); + const rerender = useCycleRerender(); const [divisionName, setDivisionName] = useState("Overview"); const corporation = Player.corporation; diff --git a/src/Faction/ui/FactionRoot.tsx b/src/Faction/ui/FactionRoot.tsx index b96c8ca57..226f79a1f 100644 --- a/src/Faction/ui/FactionRoot.tsx +++ b/src/Faction/ui/FactionRoot.tsx @@ -20,7 +20,7 @@ import { CovenantPurchasesRoot } from "../../PersonObjects/Sleeve/ui/CovenantPur import { FactionName, FactionWorkType } from "@enums"; import { GangButton } from "./GangButton"; import { FactionWork } from "../../Work/FactionWork"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; import { repNeededToDonate } from "../formulas/donation"; type FactionRootProps = { @@ -147,7 +147,7 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea } export function FactionRoot({ faction }: FactionRootProps): React.ReactElement { - const rerender = useRerender(200); + const rerender = useCycleRerender(); if (!Player.factions.includes(faction.name)) { return ( diff --git a/src/Faction/ui/FactionsRoot.tsx b/src/Faction/ui/FactionsRoot.tsx index 694f35cee..858e51156 100644 --- a/src/Faction/ui/FactionsRoot.tsx +++ b/src/Faction/ui/FactionsRoot.tsx @@ -9,7 +9,7 @@ import { Settings } from "../../Settings/Settings"; import { formatFavor, formatReputation } from "../../ui/formatNumber"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; import { CorruptableText } from "../../ui/React/CorruptableText"; import { Requirement } from "../../ui/Components/Requirement"; @@ -205,7 +205,7 @@ const FactionElement = (props: FactionElementProps): React.ReactElement => { export function FactionsRoot(): React.ReactElement { const theme = useTheme(); - const rerender = useRerender(200); + const rerender = useCycleRerender(); useEffect(() => { Player.factionInvitations.forEach((factionName) => { InvitationsSeen.add(factionName); diff --git a/src/Faction/ui/Info.tsx b/src/Faction/ui/Info.tsx index 82804f9ef..fb0370e2a 100644 --- a/src/Faction/ui/Info.tsx +++ b/src/Faction/ui/Info.tsx @@ -15,7 +15,7 @@ import { makeStyles } from "tss-react/mui"; import Typography from "@mui/material/Typography"; import Tooltip from "@mui/material/Tooltip"; import Box from "@mui/material/Box"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; import { calculateFavorAfterResetting } from "../formulas/favor"; interface IProps { @@ -42,7 +42,7 @@ function DefaultAssignment(): React.ReactElement { } export function Info(props: IProps): React.ReactElement { - useRerender(200); + useCycleRerender(); const { classes } = useStyles(); const Assignment = props.factionInfo.assignment ?? DefaultAssignment; diff --git a/src/Gang/ui/GangRoot.tsx b/src/Gang/ui/GangRoot.tsx index 21a1d1d61..73deb7ddc 100644 --- a/src/Gang/ui/GangRoot.tsx +++ b/src/Gang/ui/GangRoot.tsx @@ -8,7 +8,7 @@ import { Context } from "./Context"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; /** React Component for all the gang stuff. */ export function GangRoot(): React.ReactElement { @@ -22,7 +22,7 @@ export function GangRoot(): React.ReactElement { setValue(tab); } - useRerender(200); + useCycleRerender(); return ( diff --git a/src/Hacknet/ui/HacknetRoot.tsx b/src/Hacknet/ui/HacknetRoot.tsx index 4ab96b0b7..1d9eeb3d4 100644 --- a/src/Hacknet/ui/HacknetRoot.tsx +++ b/src/Hacknet/ui/HacknetRoot.tsx @@ -25,12 +25,12 @@ import Typography from "@mui/material/Typography"; import Grid from "@mui/material/Grid"; import Button from "@mui/material/Button"; import { Box } from "@mui/material"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; /** Root React Component for the Hacknet Node UI */ export function HacknetRoot(): React.ReactElement { const [open, setOpen] = useState(false); - const rerender = useRerender(200); + const rerender = useCycleRerender(); const [purchaseMultiplier, setPurchaseMultiplier] = useState(PurchaseMultipliers.x1); let totalProduction = 0; diff --git a/src/Hacknet/ui/HashUpgradeModal.tsx b/src/Hacknet/ui/HashUpgradeModal.tsx index 5ba1fe402..46ebf5287 100644 --- a/src/Hacknet/ui/HashUpgradeModal.tsx +++ b/src/Hacknet/ui/HashUpgradeModal.tsx @@ -7,7 +7,7 @@ import { HacknetUpgradeElem } from "./HacknetUpgradeElem"; import { Modal } from "../../ui/React/Modal"; import { Player } from "@player"; import Typography from "@mui/material/Typography"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; interface IProps { open: boolean; @@ -16,7 +16,7 @@ interface IProps { /** Create the pop-up for purchasing upgrades with hashes */ export function HashUpgradeModal(props: IProps): React.ReactElement { - const rerender = useRerender(200); + const rerender = useCycleRerender(); const hashManager = Player.hashManager; if (!hashManager) { diff --git a/src/Locations/ui/CompanyLocation.tsx b/src/Locations/ui/CompanyLocation.tsx index ca8a0b442..e3eb1e525 100644 --- a/src/Locations/ui/CompanyLocation.tsx +++ b/src/Locations/ui/CompanyLocation.tsx @@ -19,7 +19,7 @@ import { Page } from "../../ui/Router"; import { Player } from "@player"; import { QuitJobModal } from "../../Company/ui/QuitJobModal"; import { CompanyWork } from "../../Work/CompanyWork"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; import { companyNameAsLocationName } from "../../Company/utils"; import { JobSummary } from "../../Company/ui/JobSummary"; import { StatsTable } from "../../ui/React/StatsTable"; @@ -32,7 +32,7 @@ interface IProps { export function CompanyLocation(props: IProps): React.ReactElement { const [quitOpen, setQuitOpen] = useState(false); - const rerender = useRerender(200); + const rerender = useCycleRerender(); /** * We'll keep a reference to the Company that this component is being rendered for, diff --git a/src/Locations/ui/HospitalLocation.tsx b/src/Locations/ui/HospitalLocation.tsx index afb5ffe72..a5671c487 100644 --- a/src/Locations/ui/HospitalLocation.tsx +++ b/src/Locations/ui/HospitalLocation.tsx @@ -12,12 +12,12 @@ import { getHospitalizationCost } from "../../Hospital/Hospital"; import { Money } from "../../ui/React/Money"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; export function HospitalLocation(): React.ReactElement { /** Stores button styling that sets them all to block display */ const btnStyle = { display: "block" }; - const rerender = useRerender(200); + const rerender = useCycleRerender(); function getHealed(e: React.MouseEvent): void { if (!e.isTrusted) { diff --git a/src/PersonObjects/Grafting/ui/GraftingRoot.tsx b/src/PersonObjects/Grafting/ui/GraftingRoot.tsx index f608c3aff..f6565a165 100644 --- a/src/PersonObjects/Grafting/ui/GraftingRoot.tsx +++ b/src/PersonObjects/Grafting/ui/GraftingRoot.tsx @@ -21,7 +21,7 @@ import { formatNumberNoSuffix } from "../../../ui/formatNumber"; import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions"; import { GraftableAugmentation } from "../GraftableAugmentation"; import { calculateGraftingTimeWithBonus, getGraftingAvailableAugs } from "../GraftingHelpers"; -import { useRerender } from "../../../ui/React/hooks"; +import { useCycleRerender } from "../../../ui/React/hooks"; export const GraftableAugmentations = (): Record => { const gAugs: Record = {}; @@ -67,7 +67,7 @@ export const GraftingRoot = (): React.ReactElement => { const [selectedAug, setSelectedAug] = useState(getGraftingAvailableAugs()[0]); const [graftOpen, setGraftOpen] = useState(false); const selectedAugmentation = Augmentations[selectedAug]; - const rerender = useRerender(200); + const rerender = useCycleRerender(); const getAugsSorted = (): AugmentationName[] => { const augs = getGraftingAvailableAugs(); diff --git a/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx b/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx index eb924391c..e1f121671 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx @@ -6,11 +6,11 @@ import { Player } from "@player"; import { SleeveElem } from "./SleeveElem"; import { FAQModal } from "./FAQModal"; -import { useRerender } from "../../../ui/React/hooks"; +import { useCycleRerender } from "../../../ui/React/hooks"; export function SleeveRoot(): React.ReactElement { const [FAQOpen, setFAQOpen] = useState(false); - const rerender = useRerender(200); + const rerender = useCycleRerender(); return ( <> diff --git a/src/Programs/ui/ProgramsRoot.tsx b/src/Programs/ui/ProgramsRoot.tsx index abb31eb20..db2475334 100644 --- a/src/Programs/ui/ProgramsRoot.tsx +++ b/src/Programs/ui/ProgramsRoot.tsx @@ -11,12 +11,12 @@ import { Settings } from "../../Settings/Settings"; import { Programs } from "../Programs"; import { CreateProgramWork, isCreateProgramWork } from "../../Work/CreateProgramWork"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; export const ProgramsSeen = new Set(); export function ProgramsRoot(): React.ReactElement { - useRerender(200); + useCycleRerender(); const programs = [...Object.values(Programs)] .filter((prog) => { diff --git a/src/Sidebar/ui/SidebarRoot.tsx b/src/Sidebar/ui/SidebarRoot.tsx index 601eb8e4b..cdf2cff07 100644 --- a/src/Sidebar/ui/SidebarRoot.tsx +++ b/src/Sidebar/ui/SidebarRoot.tsx @@ -54,7 +54,7 @@ import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot"; import { InvitationsSeen } from "../../Faction/ui/FactionsRoot"; import { hash } from "../../hash/hash"; import { Locations } from "../../Locations/Locations"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; import { playerHasDiscoveredGo } from "../../Go/effects/effect"; import { knowAboutBitverse } from "../../BitNode/BitNodeUtils"; @@ -108,7 +108,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); export function SidebarRoot(props: { page: Page }): React.ReactElement { - useRerender(200); + useCycleRerender(); let flash: Page | null = null; switch (ITutorial.currStep) { diff --git a/src/StockMarket/ui/StockMarketRoot.tsx b/src/StockMarket/ui/StockMarketRoot.tsx index 11c9b0494..d0f532307 100644 --- a/src/StockMarket/ui/StockMarketRoot.tsx +++ b/src/StockMarket/ui/StockMarketRoot.tsx @@ -6,7 +6,7 @@ import { StockTickers } from "./StockTickers"; import { IStockMarket } from "../IStockMarket"; import { Player } from "@player"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; interface IProps { stockMarket: IStockMarket; @@ -14,7 +14,7 @@ interface IProps { /** Root React component for the Stock Market UI */ export function StockMarketRoot(props: IProps): React.ReactElement { - const rerender = useRerender(200); + const rerender = useCycleRerender(); return ( <> diff --git a/src/Terminal/ui/TerminalActionTimer.tsx b/src/Terminal/ui/TerminalActionTimer.tsx index 50d82e1f9..f2f7fd6b3 100644 --- a/src/Terminal/ui/TerminalActionTimer.tsx +++ b/src/Terminal/ui/TerminalActionTimer.tsx @@ -1,11 +1,11 @@ import React from "react"; import Typography from "@mui/material/Typography"; -import { useRerender } from "../../ui/React/hooks"; +import { useCycleRerender } from "../../ui/React/hooks"; import { Terminal } from "../../Terminal"; export function TerminalActionTimer(): React.ReactElement { - useRerender(200); + useCycleRerender(); return {Terminal.action && Terminal.getProgressText()}; } diff --git a/src/engine.tsx b/src/engine.tsx index 855d16269..8eb2d5447 100644 --- a/src/engine.tsx +++ b/src/engine.tsx @@ -39,11 +39,13 @@ import { startExploits } from "./Exploits/loops"; import { calculateAchievements } from "./Achievements/Achievements"; import React from "react"; +import ReactDOM from "react-dom"; import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler"; import { Button, Typography } from "@mui/material"; import { SnackbarEvents } from "./ui/React/Snackbar"; import { SaveData } from "./types"; import { Go } from "./Go/Go"; +import { EventEmitter } from "./utils/EventEmitter"; // Only show warning if the time diff is greater than this value. const thresholdOfTimeDiffForShowingWarningAboutSystemClock = CONSTANTS.MillisecondsPerFiveMinutes; @@ -54,6 +56,8 @@ function showWarningAboutSystemClock(timeDiff: number) { ); } +export const GameCycleEvents = new EventEmitter<[]>(); + /** Game engine. Handles the main game loop. */ const Engine: { _lastUpdate: number; @@ -428,6 +432,11 @@ const Engine: { Engine._lastUpdate = _thisUpdate - offset; Player.lastUpdate = _thisUpdate - offset; Engine.updateGame(diff); + if (GameCycleEvents.hasSubscibers()) { + ReactDOM.unstable_batchedUpdates(() => { + GameCycleEvents.emit(); + }); + } } window.setTimeout(Engine.start, CONSTANTS.MilliPerCycle - offset); }, diff --git a/src/ui/CharacterStats.tsx b/src/ui/CharacterStats.tsx index 7c79e0b37..090700089 100644 --- a/src/ui/CharacterStats.tsx +++ b/src/ui/CharacterStats.tsx @@ -15,7 +15,7 @@ import { Modal } from "./React/Modal"; import { Money } from "./React/Money"; import { StatsRow } from "./React/StatsRow"; import { StatsTable } from "./React/StatsTable"; -import { useRerender } from "./React/hooks"; +import { useCycleRerender } from "./React/hooks"; import { getMaxFavor } from "../Go/effects/effect"; import { canAccessBitNodeFeature, knowAboutBitverse } from "../BitNode/BitNodeUtils"; @@ -220,7 +220,7 @@ function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement { export function CharacterStats(): React.ReactElement { const [moneyOpen, setMoneyOpen] = useState(false); const [employersOpen, setEmployersOpen] = useState(false); - useRerender(200); + useCycleRerender(); const timeRows = [ ["Since last Augmentation installation", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastAug)], diff --git a/src/ui/React/hooks.ts b/src/ui/React/hooks.ts index c6baf2500..073c1088a 100644 --- a/src/ui/React/hooks.ts +++ b/src/ui/React/hooks.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { GameCycleEvents } from "../../engine"; /** Hook that returns a function for the component. Optionally set an interval to rerender the component. * @param autoRerenderTime: Optional. If provided and nonzero, used as the ms interval to automatically call the rerender function. @@ -17,6 +18,19 @@ export function useRerender(autoRerenderTime?: number) { return rerender; } +/** Hook that rerenders the component shortly after the game engine processes a cycle. + * @returns a function that will trigger a rerender. + */ +export function useCycleRerender(): () => void { + const rerender = useRerender(); + + useEffect(() => { + const unsubscribe = GameCycleEvents.subscribe(rerender); + return unsubscribe; + }, [rerender]); + return rerender; +} + export function useBoolean(initialValue = false) { const [value, setValue] = useState(initialValue); diff --git a/src/ui/WorkInProgressRoot.tsx b/src/ui/WorkInProgressRoot.tsx index 586d1acc6..cfa7db910 100644 --- a/src/ui/WorkInProgressRoot.tsx +++ b/src/ui/WorkInProgressRoot.tsx @@ -10,7 +10,7 @@ import { ProgressBar } from "./React/Progress"; import { Reputation } from "./React/Reputation"; import { ReputationRate } from "./React/ReputationRate"; import { StatsRow } from "./React/StatsRow"; -import { useRerender } from "./React/hooks"; +import { useCycleRerender } from "./React/hooks"; import { Companies } from "../Company/Companies"; import { CONSTANTS } from "../Constants"; @@ -183,7 +183,7 @@ function CrimeExpRows(rate: WorkStats): React.ReactElement[] { } export function WorkInProgressRoot(): React.ReactElement { - useRerender(CONSTANTS.MilliPerCycle); + useCycleRerender(); let workInfo: IWorkInfo = { buttons: { diff --git a/src/utils/EventEmitter.ts b/src/utils/EventEmitter.ts index c8286cc71..feef01a5e 100644 --- a/src/utils/EventEmitter.ts +++ b/src/utils/EventEmitter.ts @@ -1,31 +1,24 @@ -function uuidv4(): string { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0, - v = c == "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - /** Generic Event Emitter class following a subscribe/publish paradigm. */ export class EventEmitter { - subscribers: Record void | undefined> = {}; + private subscribers: Set<(...args: [...T]) => void | undefined> = new Set(); + + constructor() {} subscribe(s: (...args: [...T]) => void): () => void { - let uuid = uuidv4(); - while (this.subscribers[uuid] !== undefined) uuid = uuidv4(); - this.subscribers[uuid] = s; + this.subscribers.add(s); return () => { - delete this.subscribers[uuid]; + this.subscribers.delete(s); }; } emit(...args: [...T]): void { - for (const s in this.subscribers) { - const sub = this.subscribers[s]; - if (sub === undefined) continue; - + for (const sub of this.subscribers) { sub(...args); } } + + hasSubscibers(): boolean { + return this.subscribers.size > 0; + } }