BUGFIX: Fix "Router called before initialization" race (#1474)

If the game takes long enough to load, certain counters can become
eligible to run as soon as Engine.start() runs. When this happens,
eventually Router.page() is called, which throws an Error since Router
isn't initialized yet. (Dropping a breakpoint before Engine.start() and
waiting at least 30 seconds is enough to reliably repro, but I have seen
this both live and in tests.)

This fixes it so that Router.page() is valid immediately, returning a
value of Page.LoadingScreen. It also removes the isInitialized field,
since this is now redundant. Trying to switch pages is still an error,
but that doesn't happen without user input, whereas checking the current
page is quite common.

This also consolidates a check for "should we show toasts" behind a
function in Router, making the logic central and equal for a few
usecases. This means (for instance) that the "autosave is disabled"
logic won't run during infiltration. (The toast should have already been
suppressed.)
This commit is contained in:
David Walker 2024-07-07 22:13:37 -07:00 committed by GitHub
parent 2b6ec5cd33
commit 06553d9700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 33 additions and 29 deletions

@ -22,6 +22,7 @@ import { Skills } from "./data/Skills";
import { City } from "./City";
import { Player } from "@player";
import { Router } from "../ui/GameRoot";
import { Page } from "../ui/Router";
import { ConsoleHelpText } from "./data/Help";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
@ -1307,7 +1308,7 @@ export class Bladeburner {
process(): void {
// Edge race condition when the engine checks the processing counters and attempts to route before the router is initialized.
if (!Router.isInitialized) return;
if (Router.page() === Page.LoadingScreen) return;
// If the Player starts doing some other actions, set action to idle and alert
if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true) && Player.currentWork) {

@ -3,7 +3,6 @@ import { Message } from "./Message";
import { AugmentationName, CompletedProgramName, FactionName, MessageFilename } from "@enums";
import { Router } from "../ui/GameRoot";
import { Player } from "@player";
import { Page } from "../ui/Router";
import { GetServer } from "../Server/AllServers";
import { SpecialServers } from "../Server/data/SpecialServers";
import { Settings } from "../Settings/Settings";
@ -53,7 +52,7 @@ function recvd(name: MessageFilename): boolean {
//Checks if any of the 'timed' messages should be sent
function checkForMessagesToSend(): void {
if (Router.page() === Page.BitVerse) return;
if (Router.hidingMessages()) return;
if (Player.hasAugmentation(AugmentationName.TheRedPill, true)) {
//Get the world daemon required hacking level

@ -8,7 +8,6 @@ import { Factions } from "./Faction/Factions";
import { staneksGift } from "./CotMG/Helper";
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors
import "./PersonObjects/Player/PlayerObject"; // For side-effect of creating Player
@ -441,8 +440,7 @@ function warnAutosaveDisabled(): void {
// We don't want this warning to show up on certain pages.
// When in recovery or importing we want to keep autosave disabled.
const ignoredPages = [Page.Recovery as Page, Page.ImportSave];
if (ignoredPages.includes(Router.page())) return;
if (Router.hidingMessages()) return;
const warningToast = (
<>

@ -18,7 +18,8 @@ import { dialogBoxCreate } from "./React/DialogBox";
import { GetAllServers } from "../Server/AllServers";
import { StockMarket } from "../StockMarket/StockMarket";
import { Page, PageWithContext, IRouter, ComplexPage, PageContext } from "./Router";
import type { PageWithContext, IRouter, ComplexPage, PageContext } from "./Router";
import { Page } from "./Router";
import { Overview } from "./React/Overview";
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot";
@ -90,20 +91,18 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
}));
const uninitialized = (): void => {
throw new Error("Router called before initialization - uninitialized");
};
const MAX_PAGES_IN_HISTORY = 10;
export let Router: IRouter = {
isInitialized: false,
page: () => {
throw new Error("Router called before initialization - page");
return Page.LoadingScreen;
},
allowRouting: uninitialized,
toPage: () => {
throw new Error("Router called before initialization - toPage");
allowRouting: () => {
throw new Error("Router called before initialization - allowRouting");
},
hidingMessages: () => true,
toPage: (page: Page) => {
throw new Error(`Router called before initialization - toPage(${page})`);
},
back: () => {
throw new Error("Router called before initialization - back");
@ -163,10 +162,18 @@ export function GameRoot(): React.ReactElement {
console.error(`Routing is currently disabled - Attempted router.${name}()`);
}
const hiddenPages = new Set([
Page.Recovery,
Page.ImportSave,
Page.BitVerse,
Page.Infiltration,
Page.BladeburnerCinematic,
]);
Router = {
isInitialized: true,
page: () => pageWithContext.page,
allowRouting: (value: boolean) => setAllowRoutingCalls(value),
hidingMessages: () => hiddenPages.has(pageWithContext.page),
toPage: (page: Page, context?: PageContext<ComplexPage>) => {
if (!allowRoutingCalls) return attemptedForbiddenRouting("toPage");
switch (page) {
@ -199,32 +206,28 @@ export function GameRoot(): React.ReactElement {
let mainPage = <Typography>Cannot load</Typography>;
let withSidebar = true;
let withPopups = true;
const hidePopups = Router.hidingMessages();
let bypassGame = false;
switch (pageWithContext.page) {
case Page.Recovery: {
mainPage = <RecoveryRoot softReset={softReset} />;
withSidebar = false;
withPopups = false;
bypassGame = true;
break;
}
case Page.BitVerse: {
mainPage = <BitverseRoot flume={pageWithContext.flume} quick={pageWithContext.quick} />;
withSidebar = false;
withPopups = false;
break;
}
case Page.Infiltration: {
mainPage = <InfiltrationRoot location={pageWithContext.location} />;
withSidebar = false;
withPopups = false;
break;
}
case Page.BladeburnerCinematic: {
mainPage = <BladeburnerCinematic />;
withSidebar = false;
withPopups = false;
break;
}
case Page.Work: {
@ -377,7 +380,6 @@ export function GameRoot(): React.ReactElement {
case Page.ImportSave: {
mainPage = <ImportSave saveData={pageWithContext.saveData} automatic={!!pageWithContext.automatic} />;
withSidebar = false;
withPopups = false;
bypassGame = true;
}
}
@ -410,11 +412,11 @@ export function GameRoot(): React.ReactElement {
<Box className={classes.root}>{mainPage}</Box>
)}
<Unclickable />
<LogBoxManager hidden={!withPopups} />
<AlertManager hidden={!withPopups} />
<PromptManager hidden={!withPopups} />
<FactionInvitationManager hidden={!withPopups} />
<Snackbar hidden={!withPopups} />
<LogBoxManager hidden={hidePopups} />
<AlertManager hidden={hidePopups} />
<PromptManager hidden={hidePopups} />
<FactionInvitationManager hidden={hidePopups} />
<Snackbar hidden={hidePopups} />
<Apr1 />
</SnackbarProvider>
</HistoryProvider>

@ -49,6 +49,7 @@ export enum ComplexPage {
Location = "Location",
ImportSave = "Import Save",
Documentation = "Documentation",
LoadingScreen = "Loading Screen", // Has no PageContext, and thus toPage() cannot be used
}
// Using the same name as both type and object to mimic enum-like behavior.
@ -86,6 +87,7 @@ export type PageWithContext =
| ({ page: ComplexPage.Location } & PageContext<ComplexPage.Location>)
| ({ page: ComplexPage.ImportSave } & PageContext<ComplexPage.ImportSave>)
| ({ page: ComplexPage.Documentation } & PageContext<ComplexPage.Documentation>)
| { page: ComplexPage.LoadingScreen }
| { page: SimplePage };
export interface ScriptEditorRouteOptions {
@ -95,9 +97,10 @@ export interface ScriptEditorRouteOptions {
/** The router keeps track of player navigation/routing within the game. */
export interface IRouter {
isInitialized: boolean;
page(): Page;
allowRouting(value: boolean): void;
/** If messages/toasts are hidden on this page */
hidingMessages(): boolean;
toPage(page: SimplePage): void;
toPage<T extends ComplexPage>(page: T, context: PageContext<T>): void;
/** go to a preveious page (if any) */

@ -12,6 +12,7 @@ jest.mock("../../../src/ui/GameRoot", () => ({
Router: {
page: () => ({}),
toPage: () => ({}),
hidingMessages: () => false,
},
}));