import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"; import { AugmentationName, ToastVariant } from "@enums"; import { initBitNodeMultipliers } from "./BitNode/BitNode"; import { initSourceFiles } from "./SourceFile/SourceFiles"; import { generateRandomContract } from "./CodingContractGenerator"; import { CONSTANTS } from "./Constants"; import { Factions } from "./Faction/Factions"; import { staneksGift } from "./CotMG/Helper"; import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers"; import { Router } from "./ui/GameRoot"; import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors import "./PersonObjects/Player/PlayerObject"; // For side-effect of creating Player import { getHackingWorkRepGain, getFactionSecurityWorkRepGain, getFactionFieldWorkRepGain, } from "./PersonObjects/formulas/reputation"; import { hasHacknetServers, processHacknetEarnings } from "./Hacknet/HacknetHelpers"; import { iTutorialStart } from "./InteractiveTutorial"; import { checkForMessagesToSend } from "./Message/MessageHelpers"; import { loadAllRunningScripts, updateOnlineScriptTimes } from "./NetscriptWorker"; import { Player } from "@player"; import { saveObject, loadGame } from "./SaveObject"; import { initForeignServers } from "./Server/AllServers"; import { Settings } from "./Settings/Settings"; import { FormatsNeedToChange } from "./ui/formatNumber"; import { initSymbolToStockMap, processStockPrices } from "./StockMarket/StockMarket"; import { Terminal } from "./Terminal"; import { Money } from "./ui/React/Money"; import { Hashes } from "./ui/React/Hashes"; import { Reputation } from "./ui/React/Reputation"; import { AlertEvents } from "./ui/React/AlertManager"; import { exceptionAlert } from "./utils/helpers/exceptionAlert"; 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; function showWarningAboutSystemClock(timeDiff: number) { AlertEvents.emit( `Warning: The system clock moved backward: ${convertTimeMsToTimeElapsedString(Math.abs(timeDiff))}.`, ); } export const GameCycleEvents = new EventEmitter<[]>(); /** Game engine. Handles the main game loop. */ const Engine: { _lastUpdate: number; updateGame: (numCycles?: number) => void; Counters: { [key: string]: number | undefined; autoSaveCounter: number; updateSkillLevelsCounter: number; updateDisplays: number; updateDisplaysLong: number; updateActiveScriptsDisplay: number; createProgramNotifications: number; augmentationsNotifications: number; checkFactionInvitations: number; passiveFactionGrowth: number; messages: number; mechanicProcess: number; contractGeneration: number; achievementsCounter: number; }; decrementAllCounters: (numCycles?: number) => void; checkCounters: () => void; load: (saveData: SaveData) => Promise; start: () => void; } = { // Time variables (milliseconds unix epoch time) _lastUpdate: new Date().getTime(), updateGame: function (numCycles = 1) { const time = numCycles * CONSTANTS.MilliPerCycle; if (Player.totalPlaytime == null) { Player.totalPlaytime = 0; } if (Player.playtimeSinceLastAug == null) { Player.playtimeSinceLastAug = 0; } if (Player.playtimeSinceLastBitnode == null) { Player.playtimeSinceLastBitnode = 0; } Player.totalPlaytime += time; Player.playtimeSinceLastAug += time; Player.playtimeSinceLastBitnode += time; Terminal.process(numCycles); Player.processWork(numCycles); // Update stock prices if (Player.hasWseAccount) { processStockPrices(numCycles); } // Gang if (Player.gang) Player.gang.process(numCycles); // Staneks gift staneksGift.process(numCycles); // Corporation if (Player.corporation) { Player.corporation.storeCycles(numCycles); Player.corporation.process(); } // Bladeburner if (Player.bladeburner) Player.bladeburner.storeCycles(numCycles); // Sleeves Player.sleeves.forEach((sleeve) => sleeve.process(numCycles)); // Update the running time of all active scripts updateOnlineScriptTimes(numCycles); // Hacknet Nodes processHacknetEarnings(numCycles); // Counters Engine.decrementAllCounters(numCycles); // This **MUST** be the last call in the function, because checkCounters() // can invoke the autosave, so any work done after here risks not getting saved! Engine.checkCounters(); }, /** * Counters for the main event loop. Represent the number of game cycles that * are required for something to happen. These counters are in game cycles, * which is once every 200ms */ Counters: { autoSaveCounter: 300, updateSkillLevelsCounter: 10, updateDisplays: 3, updateDisplaysLong: 15, updateActiveScriptsDisplay: 5, createProgramNotifications: 10, augmentationsNotifications: 10, checkFactionInvitations: 100, passiveFactionGrowth: 5, messages: 150, mechanicProcess: 5, // Process Bladeburner contractGeneration: 3000, // Generate Coding Contracts achievementsCounter: 60, // Check if we have new achievements }, decrementAllCounters: function (numCycles = 1) { for (const [counterName, counter] of Object.entries(Engine.Counters)) { if (counter === undefined) throw new Error("counter should not be undefined"); Engine.Counters[counterName] = counter - numCycles; } }, /** * Checks if any counters are 0. If they are, executes whatever * is necessary and then resets the counter */ checkCounters: function () { if (Engine.Counters.checkFactionInvitations <= 0) { const invitedFactions = Player.checkForFactionInvitations(); for (const invitedFaction of invitedFactions) { inviteToFaction(invitedFaction); } Engine.Counters.checkFactionInvitations = 100; } if (Engine.Counters.passiveFactionGrowth <= 0) { const adjustedCycles = Math.floor(5 - Engine.Counters.passiveFactionGrowth); processPassiveFactionRepGain(adjustedCycles); Engine.Counters.passiveFactionGrowth = 5; } if (Engine.Counters.messages <= 0) { checkForMessagesToSend(); if (Player.hasAugmentation(AugmentationName.TheRedPill)) { Engine.Counters.messages = 4500; // 15 minutes for Red pill message } else { Engine.Counters.messages = 150; } } if (Engine.Counters.mechanicProcess <= 0) { if (Player.bladeburner) { try { Player.bladeburner.process(); } catch (e) { exceptionAlert("Exception caught in Bladeburner.process(): " + e); } } Engine.Counters.mechanicProcess = 5; } if (Engine.Counters.contractGeneration <= 0) { // X% chance of a contract being generated if (Math.random() <= 0.25) { generateRandomContract(); } Engine.Counters.contractGeneration = 3000; } if (Engine.Counters.achievementsCounter <= 0) { calculateAchievements(); Engine.Counters.achievementsCounter = 300; } // This **MUST** remain the last block in the function! // Otherwise, any work done after this point won't be saved! // Due to the way most of these counters are reset, that would probably be // OK, but it's much simpler to reason about if we can assume that the // entire function has been performed after a save. if (Engine.Counters.autoSaveCounter <= 0) { if (Settings.AutosaveInterval == null) { Settings.AutosaveInterval = 60; } if (Settings.AutosaveInterval === 0) { warnAutosaveDisabled(); Engine.Counters.autoSaveCounter = 60 * 5; // Let's check back in a bit } else { Engine.Counters.autoSaveCounter = Settings.AutosaveInterval * 5; saveObject.saveGame(!Settings.SuppressSavedGameToast); } } }, load: async function (saveData) { startExploits(); setupUncaughtPromiseHandler(); // Source files must be initialized early because save-game translation in // loadGame() needs them sometimes. initSourceFiles(); // Load game from save or create new game if (await loadGame(saveData)) { FormatsNeedToChange.emit(); initBitNodeMultipliers(); if (Player.hasWseAccount) { initSymbolToStockMap(); } // Apply penalty for entropy accumulation Player.applyEntropy(Player.entropy); // Calculate the number of cycles have elapsed while offline Engine._lastUpdate = new Date().getTime(); const lastUpdate = Player.lastUpdate; let timeOffline = Engine._lastUpdate - lastUpdate; if (timeOffline < 0) { if (Math.abs(timeOffline) > thresholdOfTimeDiffForShowingWarningAboutSystemClock) { const timeDiff = timeOffline; setTimeout(() => { showWarningAboutSystemClock(timeDiff); }, 250); } timeOffline = 0; } const numCyclesOffline = Math.floor(timeOffline / CONSTANTS.MilliPerCycle); // Calculate the number of chances for a contract the player had whilst offline const contractChancesWhileOffline = Math.floor(timeOffline / (1000 * 60 * 10)); // Generate coding contracts let numContracts = 0; if (contractChancesWhileOffline > 100) { numContracts += Math.floor(contractChancesWhileOffline * 0.25); } if (contractChancesWhileOffline > 0 && contractChancesWhileOffline <= 100) { for (let i = 0; i < contractChancesWhileOffline; ++i) { if (Math.random() <= 0.25) { numContracts++; } } } for (let i = 0; i < numContracts; i++) { generateRandomContract(); } let offlineReputation = 0; const offlineHackingIncome = (Player.moneySourceA.hacking / Player.playtimeSinceLastAug) * timeOffline * CONSTANTS.OfflineHackingIncome; Player.gainMoney(offlineHackingIncome, "hacking"); // Process offline progress loadAllRunningScripts(); // This also takes care of offline production for those scripts if (Player.currentWork !== null) { Player.focus = true; Player.processWork(numCyclesOffline); } else if (Player.bitNodeN !== 2) { for (let i = 0; i < Player.factions.length; i++) { const facName = Player.factions[i]; if (!Object.hasOwn(Factions, facName)) continue; const faction = Factions[facName]; if (!faction.isMember) continue; // No rep for special factions. const info = faction.getInfo(); if (!info.offersWork()) continue; // No rep for gangs. if (Player.getGangName() === facName) continue; const hRep = getHackingWorkRepGain(Player, faction.favor); const sRep = getFactionSecurityWorkRepGain(Player, faction.favor); const fRep = getFactionFieldWorkRepGain(Player, faction.favor); // can be infinite, doesn't matter. const reputationRate = Math.max(hRep, sRep, fRep) / Player.factions.length; const rep = reputationRate * numCyclesOffline; faction.playerReputation += rep; offlineReputation += rep; } } // Hacknet Nodes offline progress const offlineProductionFromHacknetNodes = processHacknetEarnings(numCyclesOffline); const hacknetProdInfo = hasHacknetServers() ? ( <> hashes ) : ( ); // Passive faction rep gain offline processPassiveFactionRepGain(numCyclesOffline); // Stock Market offline progress if (Player.hasWseAccount) { processStockPrices(numCyclesOffline); } // Gang progress for BitNode 2 if (Player.gang) Player.gang.process(numCyclesOffline); // Corporation offline progress if (Player.corporation) Player.corporation.storeCycles(numCyclesOffline); // Bladeburner offline progress if (Player.bladeburner) Player.bladeburner.storeCycles(numCyclesOffline); Go.storeCycles(numCyclesOffline); staneksGift.process(numCyclesOffline); // Sleeves offline progress Player.sleeves.forEach((sleeve) => sleeve.process(numCyclesOffline)); // Update total playtime const time = numCyclesOffline * CONSTANTS.MilliPerCycle; Player.totalPlaytime ??= 0; Player.playtimeSinceLastAug ??= 0; Player.playtimeSinceLastBitnode ??= 0; Player.totalPlaytime += time; Player.playtimeSinceLastAug += time; Player.playtimeSinceLastBitnode += time; Player.lastUpdate = Engine._lastUpdate; Engine.start(); // Run main game loop and Scripts loop const timeOfflineString = convertTimeMsToTimeElapsedString(time); setTimeout( () => AlertEvents.emit( <> Offline for {timeOfflineString}. While you were offline:
  • Your scripts generated
  • Your Hacknet Nodes generated {hacknetProdInfo}
  • You gained reputation divided amongst your factions
, ), 250, ); } else { // No save found, start new game FormatsNeedToChange.emit(); initBitNodeMultipliers(); Engine.start(); // Run main game loop and Scripts loop Player.init(); initForeignServers(Player.getHomeComputer()); Player.reapplyAllAugmentations(); // Start interactive tutorial iTutorialStart(); } }, start: function () { // Get time difference const _thisUpdate = new Date().getTime(); let diff = _thisUpdate - Engine._lastUpdate; if (diff < 0) { if (Math.abs(diff) > thresholdOfTimeDiffForShowingWarningAboutSystemClock) { showWarningAboutSystemClock(diff); } diff = 0; Engine._lastUpdate = _thisUpdate; Player.lastUpdate = _thisUpdate; } const offset = diff % CONSTANTS.MilliPerCycle; // Divide this by cycle time to determine how many cycles have elapsed since last update diff = Math.floor(diff / CONSTANTS.MilliPerCycle); if (diff > 0) { // Update the game engine by the calculated number of cycles 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); }, }; /** Shows a toast warning that lets the player know that auto-saves are disabled, with an button to re-enable them. */ function warnAutosaveDisabled(): void { // If the player has suppressed those warnings let's just exit right away. if (Settings.SuppressAutosaveDisabledWarnings) return; // We don't want this warning to show up on certain pages. // When in recovery or importing we want to keep autosave disabled. if (Router.hidingMessages()) return; const warningToast = ( <> Auto-saves are disabled! ); SnackbarEvents.emit(warningToast, ToastVariant.WARNING, 5000); } export { Engine };