mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 14:28:36 +02:00
BUGFIX: Running scripts may be loaded before main UI (#1726)
This commit is contained in:
@@ -29,6 +29,8 @@ import { ScriptArg } from "@nsdefs";
|
||||
import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers";
|
||||
import { handleUnknownError } from "./utils/ErrorHandler";
|
||||
import { isLegacyScript, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
|
||||
import { Player } from "@player";
|
||||
import { UIEventEmitter, UIEventType } from "./ui/UIEventEmitter";
|
||||
import { getErrorMessageWithStackAndCause } from "./utils/ErrorHelper";
|
||||
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
|
||||
import { Result } from "./types";
|
||||
@@ -215,36 +217,50 @@ function createAutoexec(server: BaseServer): RunningScript | null {
|
||||
*/
|
||||
export function loadAllRunningScripts(): void {
|
||||
/**
|
||||
* Accept all parameters containing "?noscript". The "standard" parameter is "?noScripts", but new players may not
|
||||
* notice the "s" character at the end of "noScripts".
|
||||
* While loading the save data, the game engine calls this function to load all running scripts. With each script, we
|
||||
* calculate the offline data, so we need the current "lastUpdate" and "playtimeSinceLastAug" from the save data.
|
||||
* After the main UI is loaded and the logic of this function starts executing, those info in the Player object might be
|
||||
* overwritten, so we need to save them here and use them later in "scriptCalculateOfflineProduction".
|
||||
*/
|
||||
const skipScriptLoad = window.location.href.toLowerCase().includes("?noscript");
|
||||
if (skipScriptLoad) {
|
||||
Terminal.warn("Skipped loading player scripts during startup");
|
||||
console.info("Skipping the load of any scripts during startup");
|
||||
}
|
||||
for (const server of GetAllServers()) {
|
||||
// Reset each server's RAM usage to 0
|
||||
server.ramUsed = 0;
|
||||
|
||||
const rsList = server.savedScripts;
|
||||
server.savedScripts = undefined;
|
||||
if (skipScriptLoad || !rsList) {
|
||||
// Start game with no scripts
|
||||
continue;
|
||||
const playerLastUpdate = Player.lastUpdate;
|
||||
const playerPlaytimeSinceLastAug = Player.playtimeSinceLastAug;
|
||||
const unsubscribe = UIEventEmitter.subscribe((event) => {
|
||||
if (event !== UIEventType.MainUILoaded) {
|
||||
return;
|
||||
}
|
||||
if (server.hostname === "home") {
|
||||
// Push autoexec script onto the front of the list
|
||||
const runningScript = createAutoexec(server);
|
||||
if (runningScript) {
|
||||
rsList.unshift(runningScript);
|
||||
unsubscribe();
|
||||
/**
|
||||
* Accept all parameters containing "?noscript". The "standard" parameter is "?noScripts", but new players may not
|
||||
* notice the "s" character at the end of "noScripts".
|
||||
*/
|
||||
const skipScriptLoad = window.location.href.toLowerCase().includes("?noscript");
|
||||
if (skipScriptLoad) {
|
||||
Terminal.warn("Skipped loading player scripts during startup");
|
||||
console.info("Skipping the load of any scripts during startup");
|
||||
}
|
||||
for (const server of GetAllServers()) {
|
||||
// Reset each server's RAM usage to 0
|
||||
server.ramUsed = 0;
|
||||
|
||||
const rsList = server.savedScripts;
|
||||
server.savedScripts = undefined;
|
||||
if (skipScriptLoad || !rsList) {
|
||||
// Start game with no scripts
|
||||
continue;
|
||||
}
|
||||
if (server.hostname === "home") {
|
||||
// Push autoexec script onto the front of the list
|
||||
const runningScript = createAutoexec(server);
|
||||
if (runningScript) {
|
||||
rsList.unshift(runningScript);
|
||||
}
|
||||
}
|
||||
for (const runningScript of rsList) {
|
||||
startWorkerScript(runningScript, server);
|
||||
scriptCalculateOfflineProduction(runningScript, playerLastUpdate, playerPlaytimeSinceLastAug);
|
||||
}
|
||||
}
|
||||
for (const runningScript of rsList) {
|
||||
startWorkerScript(runningScript, server);
|
||||
scriptCalculateOfflineProduction(runningScript);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createRunningScriptInstance(
|
||||
|
||||
@@ -11,10 +11,14 @@ import { scriptKey } from "../utils/helpers/scriptKey";
|
||||
|
||||
import type { ScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
|
||||
export function scriptCalculateOfflineProduction(runningScript: RunningScript): void {
|
||||
export function scriptCalculateOfflineProduction(
|
||||
runningScript: RunningScript,
|
||||
playerLastUpdate: number,
|
||||
playerPlaytimeSinceLastAug: number,
|
||||
): void {
|
||||
//The Player object stores the last update time from when we were online
|
||||
const thisUpdate = new Date().getTime();
|
||||
const lastUpdate = Player.lastUpdate;
|
||||
const lastUpdate = playerLastUpdate;
|
||||
const timePassed = Math.max((thisUpdate - lastUpdate) / 1000, 0); //Seconds
|
||||
|
||||
//Calculate the "confidence" rating of the script's true production. This is based
|
||||
@@ -59,7 +63,7 @@ export function scriptCalculateOfflineProduction(runningScript: RunningScript):
|
||||
Player.gainHackingExp(expGain);
|
||||
|
||||
let moneyGain =
|
||||
(runningScript.onlineMoneyMade / Player.playtimeSinceLastAug) * timePassed * CONSTANTS.OfflineHackingIncome;
|
||||
(runningScript.onlineMoneyMade / playerPlaytimeSinceLastAug) * timePassed * CONSTANTS.OfflineHackingIncome;
|
||||
if (!Number.isFinite(moneyGain)) {
|
||||
moneyGain = 0;
|
||||
}
|
||||
|
||||
@@ -262,10 +262,10 @@ const Engine = {
|
||||
offlineHackingIncome = 0;
|
||||
}
|
||||
Player.gainMoney(offlineHackingIncome, "hacking");
|
||||
// Process offline progress
|
||||
|
||||
loadAllRunningScripts(); // This also takes care of offline production for those scripts
|
||||
|
||||
// Process offline progress
|
||||
if (Player.currentWork !== null) {
|
||||
Player.focus = true;
|
||||
Player.processWork(numCyclesOffline);
|
||||
@@ -418,7 +418,7 @@ const Engine = {
|
||||
Engine._lastUpdate = _thisUpdate - offset;
|
||||
Player.lastUpdate = _thisUpdate - offset;
|
||||
Engine.updateGame(diff);
|
||||
if (GameCycleEvents.hasSubscibers()) {
|
||||
if (GameCycleEvents.hasSubscribers()) {
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
GameCycleEvents.emit();
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import { StockMarket } from "../StockMarket/StockMarket";
|
||||
|
||||
import type { ComplexPage } from "./Enums";
|
||||
import type { IRouter, PageContext, PageWithContext } from "./Router";
|
||||
import { Page } from "./Router";
|
||||
import { isSimplePage, Page } from "./Router";
|
||||
import { Overview } from "./React/Overview";
|
||||
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
|
||||
import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot";
|
||||
@@ -76,6 +76,7 @@ import { HistoryProvider } from "./React/Documentation";
|
||||
import { GoRoot } from "../Go/ui/GoRoot";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { isBitNodeFinished } from "../BitNode/BitNodeUtils";
|
||||
import { UIEventEmitter, UIEventType } from "./UIEventEmitter";
|
||||
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
||||
import { SpecialServers } from "../Server/data/SpecialServers";
|
||||
import { ErrorModal } from "../ErrorHandling/ErrorModal";
|
||||
@@ -98,19 +99,54 @@ const useStyles = makeStyles()((theme: Theme) => ({
|
||||
|
||||
const MAX_PAGES_IN_HISTORY = 10;
|
||||
|
||||
type RouterAction = (
|
||||
| {
|
||||
type: "toPage";
|
||||
page: Page;
|
||||
context?: PageContext<ComplexPage>;
|
||||
}
|
||||
| {
|
||||
type: "back";
|
||||
}
|
||||
) & { stackTrace: string | undefined };
|
||||
|
||||
/**
|
||||
* When the main UI is not loaded, all router actions ("toPage" and "back") are stored in this array. After that, we
|
||||
* will run them and show a warning popup. This queue is empty in a normal situation. If it has items, there are bugs
|
||||
* that try to route the main UI when it's not loaded.
|
||||
*/
|
||||
const pendingRouterActions: RouterAction[] = [];
|
||||
|
||||
export let Router: IRouter = {
|
||||
page: () => {
|
||||
return Page.LoadingScreen;
|
||||
},
|
||||
/**
|
||||
* This function is only called in ImportSave.tsx. That component is only used when the main UI shows Page.ImportSave,
|
||||
* so it's impossible for this function to run before the main UI is loaded. If it happens, it's a fatal error. In
|
||||
* that case, throwing an error is the only option.
|
||||
*/
|
||||
allowRouting: () => {
|
||||
throw new Error("Router called before initialization - allowRouting");
|
||||
throw new Error("Router.allowRouting() was called before initialization.");
|
||||
},
|
||||
hidingMessages: () => true,
|
||||
toPage: (page: Page) => {
|
||||
throw new Error(`Router called before initialization - toPage(${page})`);
|
||||
toPage: (page: Page, context?: PageContext<ComplexPage>) => {
|
||||
const stackTrace = new Error().stack;
|
||||
console.error("Router.toPage() was called before initialization.", page, context, stackTrace);
|
||||
pendingRouterActions.push({
|
||||
type: "toPage",
|
||||
page,
|
||||
context,
|
||||
stackTrace,
|
||||
});
|
||||
},
|
||||
back: () => {
|
||||
throw new Error("Router called before initialization - back");
|
||||
const stackTrace = new Error().stack;
|
||||
console.error("Default Router.back() was called before initialization.", stackTrace);
|
||||
pendingRouterActions.push({
|
||||
type: "back",
|
||||
stackTrace,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -141,7 +177,25 @@ export function GameRoot(): React.ReactElement {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const [pages, setPages] = useState<PageWithContext[]>(() => [determineStartPage()]);
|
||||
const pageWithContext = pages[0];
|
||||
let pageWithContext = pages[0];
|
||||
|
||||
/**
|
||||
* Theoretically, this case cannot happen because of the check in Router.back(). Nevertheless, we should still check
|
||||
* it. In the future, if we call "setPages" and remove items in the "pages" array without checking it properly,
|
||||
* this case can still happen.
|
||||
*/
|
||||
if (pageWithContext === undefined) {
|
||||
/**
|
||||
* We have to delay showing the warning popup due to these reasons:
|
||||
* - React will complain: "Warning: Cannot update a component (`AlertManager`) while rendering a different
|
||||
* component (`GameRoot`)".
|
||||
* - There is a potential problem in AlertManager.tsx. Please check the comment there for more information.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
exceptionAlert(new Error(`pageWithContext is undefined`));
|
||||
}, 1000);
|
||||
pageWithContext = { page: Page.Terminal };
|
||||
}
|
||||
|
||||
const setNextPage = (pageWithContext: PageWithContext) =>
|
||||
setPages((prev) => {
|
||||
@@ -208,7 +262,16 @@ export function GameRoot(): React.ReactElement {
|
||||
setNextPage({ page, ...context } as PageWithContext);
|
||||
},
|
||||
back: () => {
|
||||
if (!allowRoutingCalls) return attemptedForbiddenRouting("back");
|
||||
if (!allowRoutingCalls) {
|
||||
return attemptedForbiddenRouting("back");
|
||||
}
|
||||
/**
|
||||
* If something calls Router.back() when the "pages" array has only 1 item, that array will be empty when the UI
|
||||
* is rerendered, and pageWithContext will be undefined. To avoid this problem, we return immediately in that case.
|
||||
*/
|
||||
if (pages.length === 1) {
|
||||
return;
|
||||
}
|
||||
setPages((pages) => pages.slice(1));
|
||||
},
|
||||
};
|
||||
@@ -415,9 +478,37 @@ export function GameRoot(): React.ReactElement {
|
||||
mainPage = <ImportSave saveData={pageWithContext.saveData} automatic={!!pageWithContext.automatic} />;
|
||||
withSidebar = false;
|
||||
bypassGame = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingRouterActions.length > 0) {
|
||||
// Run all pending actions and show a warning popup.
|
||||
for (const action of pendingRouterActions) {
|
||||
if (action.type === "toPage") {
|
||||
if (isSimplePage(action.page)) {
|
||||
Router.toPage(action.page);
|
||||
} else {
|
||||
Router.toPage(action.page, action.context ?? {});
|
||||
}
|
||||
} else {
|
||||
Router.back();
|
||||
}
|
||||
}
|
||||
exceptionAlert(
|
||||
new Error(
|
||||
`Router was used before the main UI is loaded. pendingRouterActions: ${JSON.stringify(
|
||||
pendingRouterActions,
|
||||
)}.`,
|
||||
),
|
||||
);
|
||||
pendingRouterActions.length = 0;
|
||||
}
|
||||
// Emit an event to notify subscribers that the main UI is loaded.
|
||||
UIEventEmitter.emit(UIEventType.MainUILoaded);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MathJaxContext version={3} src={__webpack_public_path__ + "mathjax/tex-chtml.js"}>
|
||||
<ErrorBoundary key={errorBoundaryKey} softReset={softReset}>
|
||||
|
||||
7
src/ui/UIEventEmitter.ts
Normal file
7
src/ui/UIEventEmitter.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
|
||||
export enum UIEventType {
|
||||
MainUILoaded,
|
||||
}
|
||||
|
||||
export const UIEventEmitter = new EventEmitter<UIEventType[]>();
|
||||
@@ -16,7 +16,7 @@ export class EventEmitter<T extends any[]> {
|
||||
}
|
||||
}
|
||||
|
||||
hasSubscibers(): boolean {
|
||||
hasSubscribers(): boolean {
|
||||
return this.subscribers.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { loadAllRunningScripts } from "../../src/NetscriptWorker";
|
||||
import { Settings } from "../../src/Settings/Settings";
|
||||
import { Player, setPlayer } from "../../src/Player";
|
||||
import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
|
||||
import { UIEventEmitter, UIEventType } from "../../src/ui/UIEventEmitter";
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Direct tests of loading and saving.
|
||||
@@ -146,6 +147,7 @@ function loadStandardServers() {
|
||||
}
|
||||
}`); // Fix confused highlighting `
|
||||
loadAllRunningScripts();
|
||||
UIEventEmitter.emit(UIEventType.MainUILoaded);
|
||||
}
|
||||
|
||||
test("load/saveAllServers", () => {
|
||||
|
||||
Reference in New Issue
Block a user