mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 23:08: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 { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers";
|
||||||
import { handleUnknownError } from "./utils/ErrorHandler";
|
import { handleUnknownError } from "./utils/ErrorHandler";
|
||||||
import { isLegacyScript, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
|
import { isLegacyScript, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
|
||||||
|
import { Player } from "@player";
|
||||||
|
import { UIEventEmitter, UIEventType } from "./ui/UIEventEmitter";
|
||||||
import { getErrorMessageWithStackAndCause } from "./utils/ErrorHelper";
|
import { getErrorMessageWithStackAndCause } from "./utils/ErrorHelper";
|
||||||
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
|
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
|
||||||
import { Result } from "./types";
|
import { Result } from "./types";
|
||||||
@@ -215,36 +217,50 @@ function createAutoexec(server: BaseServer): RunningScript | null {
|
|||||||
*/
|
*/
|
||||||
export function loadAllRunningScripts(): void {
|
export function loadAllRunningScripts(): void {
|
||||||
/**
|
/**
|
||||||
* Accept all parameters containing "?noscript". The "standard" parameter is "?noScripts", but new players may not
|
* While loading the save data, the game engine calls this function to load all running scripts. With each script, we
|
||||||
* notice the "s" character at the end of "noScripts".
|
* 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");
|
const playerLastUpdate = Player.lastUpdate;
|
||||||
if (skipScriptLoad) {
|
const playerPlaytimeSinceLastAug = Player.playtimeSinceLastAug;
|
||||||
Terminal.warn("Skipped loading player scripts during startup");
|
const unsubscribe = UIEventEmitter.subscribe((event) => {
|
||||||
console.info("Skipping the load of any scripts during startup");
|
if (event !== UIEventType.MainUILoaded) {
|
||||||
}
|
return;
|
||||||
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") {
|
unsubscribe();
|
||||||
// Push autoexec script onto the front of the list
|
/**
|
||||||
const runningScript = createAutoexec(server);
|
* Accept all parameters containing "?noscript". The "standard" parameter is "?noScripts", but new players may not
|
||||||
if (runningScript) {
|
* notice the "s" character at the end of "noScripts".
|
||||||
rsList.unshift(runningScript);
|
*/
|
||||||
|
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(
|
export function createRunningScriptInstance(
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ import { scriptKey } from "../utils/helpers/scriptKey";
|
|||||||
|
|
||||||
import type { ScriptFilePath } from "../Paths/ScriptFilePath";
|
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
|
//The Player object stores the last update time from when we were online
|
||||||
const thisUpdate = new Date().getTime();
|
const thisUpdate = new Date().getTime();
|
||||||
const lastUpdate = Player.lastUpdate;
|
const lastUpdate = playerLastUpdate;
|
||||||
const timePassed = Math.max((thisUpdate - lastUpdate) / 1000, 0); //Seconds
|
const timePassed = Math.max((thisUpdate - lastUpdate) / 1000, 0); //Seconds
|
||||||
|
|
||||||
//Calculate the "confidence" rating of the script's true production. This is based
|
//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);
|
Player.gainHackingExp(expGain);
|
||||||
|
|
||||||
let moneyGain =
|
let moneyGain =
|
||||||
(runningScript.onlineMoneyMade / Player.playtimeSinceLastAug) * timePassed * CONSTANTS.OfflineHackingIncome;
|
(runningScript.onlineMoneyMade / playerPlaytimeSinceLastAug) * timePassed * CONSTANTS.OfflineHackingIncome;
|
||||||
if (!Number.isFinite(moneyGain)) {
|
if (!Number.isFinite(moneyGain)) {
|
||||||
moneyGain = 0;
|
moneyGain = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,10 +262,10 @@ const Engine = {
|
|||||||
offlineHackingIncome = 0;
|
offlineHackingIncome = 0;
|
||||||
}
|
}
|
||||||
Player.gainMoney(offlineHackingIncome, "hacking");
|
Player.gainMoney(offlineHackingIncome, "hacking");
|
||||||
// Process offline progress
|
|
||||||
|
|
||||||
loadAllRunningScripts(); // This also takes care of offline production for those scripts
|
loadAllRunningScripts(); // This also takes care of offline production for those scripts
|
||||||
|
|
||||||
|
// Process offline progress
|
||||||
if (Player.currentWork !== null) {
|
if (Player.currentWork !== null) {
|
||||||
Player.focus = true;
|
Player.focus = true;
|
||||||
Player.processWork(numCyclesOffline);
|
Player.processWork(numCyclesOffline);
|
||||||
@@ -418,7 +418,7 @@ const Engine = {
|
|||||||
Engine._lastUpdate = _thisUpdate - offset;
|
Engine._lastUpdate = _thisUpdate - offset;
|
||||||
Player.lastUpdate = _thisUpdate - offset;
|
Player.lastUpdate = _thisUpdate - offset;
|
||||||
Engine.updateGame(diff);
|
Engine.updateGame(diff);
|
||||||
if (GameCycleEvents.hasSubscibers()) {
|
if (GameCycleEvents.hasSubscribers()) {
|
||||||
ReactDOM.unstable_batchedUpdates(() => {
|
ReactDOM.unstable_batchedUpdates(() => {
|
||||||
GameCycleEvents.emit();
|
GameCycleEvents.emit();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { StockMarket } from "../StockMarket/StockMarket";
|
|||||||
|
|
||||||
import type { ComplexPage } from "./Enums";
|
import type { ComplexPage } from "./Enums";
|
||||||
import type { IRouter, PageContext, PageWithContext } from "./Router";
|
import type { IRouter, PageContext, PageWithContext } from "./Router";
|
||||||
import { Page } from "./Router";
|
import { isSimplePage, Page } from "./Router";
|
||||||
import { Overview } from "./React/Overview";
|
import { Overview } from "./React/Overview";
|
||||||
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
|
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
|
||||||
import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot";
|
import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot";
|
||||||
@@ -76,6 +76,7 @@ import { HistoryProvider } from "./React/Documentation";
|
|||||||
import { GoRoot } from "../Go/ui/GoRoot";
|
import { GoRoot } from "../Go/ui/GoRoot";
|
||||||
import { Settings } from "../Settings/Settings";
|
import { Settings } from "../Settings/Settings";
|
||||||
import { isBitNodeFinished } from "../BitNode/BitNodeUtils";
|
import { isBitNodeFinished } from "../BitNode/BitNodeUtils";
|
||||||
|
import { UIEventEmitter, UIEventType } from "./UIEventEmitter";
|
||||||
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
||||||
import { SpecialServers } from "../Server/data/SpecialServers";
|
import { SpecialServers } from "../Server/data/SpecialServers";
|
||||||
import { ErrorModal } from "../ErrorHandling/ErrorModal";
|
import { ErrorModal } from "../ErrorHandling/ErrorModal";
|
||||||
@@ -98,19 +99,54 @@ const useStyles = makeStyles()((theme: Theme) => ({
|
|||||||
|
|
||||||
const MAX_PAGES_IN_HISTORY = 10;
|
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 = {
|
export let Router: IRouter = {
|
||||||
page: () => {
|
page: () => {
|
||||||
return Page.LoadingScreen;
|
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: () => {
|
allowRouting: () => {
|
||||||
throw new Error("Router called before initialization - allowRouting");
|
throw new Error("Router.allowRouting() was called before initialization.");
|
||||||
},
|
},
|
||||||
hidingMessages: () => true,
|
hidingMessages: () => true,
|
||||||
toPage: (page: Page) => {
|
toPage: (page: Page, context?: PageContext<ComplexPage>) => {
|
||||||
throw new Error(`Router called before initialization - toPage(${page})`);
|
const stackTrace = new Error().stack;
|
||||||
|
console.error("Router.toPage() was called before initialization.", page, context, stackTrace);
|
||||||
|
pendingRouterActions.push({
|
||||||
|
type: "toPage",
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
stackTrace,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
back: () => {
|
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 { classes } = useStyles();
|
||||||
|
|
||||||
const [pages, setPages] = useState<PageWithContext[]>(() => [determineStartPage()]);
|
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) =>
|
const setNextPage = (pageWithContext: PageWithContext) =>
|
||||||
setPages((prev) => {
|
setPages((prev) => {
|
||||||
@@ -208,7 +262,16 @@ export function GameRoot(): React.ReactElement {
|
|||||||
setNextPage({ page, ...context } as PageWithContext);
|
setNextPage({ page, ...context } as PageWithContext);
|
||||||
},
|
},
|
||||||
back: () => {
|
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));
|
setPages((pages) => pages.slice(1));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -415,9 +478,37 @@ export function GameRoot(): React.ReactElement {
|
|||||||
mainPage = <ImportSave saveData={pageWithContext.saveData} automatic={!!pageWithContext.automatic} />;
|
mainPage = <ImportSave saveData={pageWithContext.saveData} automatic={!!pageWithContext.automatic} />;
|
||||||
withSidebar = false;
|
withSidebar = false;
|
||||||
bypassGame = true;
|
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 (
|
return (
|
||||||
<MathJaxContext version={3} src={__webpack_public_path__ + "mathjax/tex-chtml.js"}>
|
<MathJaxContext version={3} src={__webpack_public_path__ + "mathjax/tex-chtml.js"}>
|
||||||
<ErrorBoundary key={errorBoundaryKey} softReset={softReset}>
|
<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;
|
return this.subscribers.size > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { loadAllRunningScripts } from "../../src/NetscriptWorker";
|
|||||||
import { Settings } from "../../src/Settings/Settings";
|
import { Settings } from "../../src/Settings/Settings";
|
||||||
import { Player, setPlayer } from "../../src/Player";
|
import { Player, setPlayer } from "../../src/Player";
|
||||||
import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
|
import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
|
||||||
|
import { UIEventEmitter, UIEventType } from "../../src/ui/UIEventEmitter";
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
// Direct tests of loading and saving.
|
// Direct tests of loading and saving.
|
||||||
@@ -146,6 +147,7 @@ function loadStandardServers() {
|
|||||||
}
|
}
|
||||||
}`); // Fix confused highlighting `
|
}`); // Fix confused highlighting `
|
||||||
loadAllRunningScripts();
|
loadAllRunningScripts();
|
||||||
|
UIEventEmitter.emit(UIEventType.MainUILoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("load/saveAllServers", () => {
|
test("load/saveAllServers", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user