ModLoadSequence¶
When Prefab.OnPrefabsLoaded and OnAllModsLoaded fire relative to Stationpedia.Regenerate, and why Unity API calls from inside OnAllModsLoaded are safe without a main-thread dispatch.
OnPrefabsLoaded / OnAllModsLoaded main-thread timing¶
Prefab.OnPrefabsLoaded fires on the Unity main thread (runs synchronously inside game's main-thread loading sequence around game.cs:59080-59090, before Stationpedia.Regenerate at line 59090). OnAllModsLoaded is therefore main-thread; all Unity API calls from within it are safe without dispatching.
PowerTransmitterPlus has a MainThreadDispatcher singleton MonoBehaviour for enqueuing actions from ThreadPool-run PowerTick contexts to the main thread (used by the distance-cost multiplayer sync, not by Stationpedia integration). The StationpediaPlus library does not need this; all its work happens on main thread during OnAllModsLoaded and during the main-thread-driven Regenerate / ChangeDisplay paths.
OnPrefabsLoaded fires at boot, not at world load¶
This is the trap that matters for anything a mod wants a player to see. Prefab.OnPrefabsLoaded is invoked from Prefab.LoadAll() (Assembly-CSharp.decompiled.cs:322971, guarded by if (PrefabsGameObject != null) at :322926), which is awaited from LoadGameDataAsync() at :59725. That runs during process startup with MainMenuCanvas.enabled = false and the ImGui loading screen up. It completes before the main menu appears, and long before any world exists.
So "prefabs loaded" is not "the player is in a game". A mod that anchors player-facing output to this event is writing to a screen nobody is reading:
- Anything printed to
ConsoleWindowhere has aged off the closed-console overlay (5 seconds ofactiveTime, see ConsoleWindow) long before the player finishes picking a save. It survives only in F3 scrollback. - A coroutine started here that paces itself with
WaitForSecondsruns its whole schedule at the main menu. A six-line, 25-second mod-conflict banner is fully spent before the player loads a world, which is exactly the bug this cost two mods in this repo. - The fix is to announce twice: one line here, which is worth having because it reaches the StationeersLaunchPad boot panel and both log files while someone may be watching the splash, then wait for a world before the banner the player is meant to read. Use
GameManager.GameState, NOTGameManager.RunSimulation; see the two paragraphs below for why the obvious-looking one is wrong.while (GameManager.GameState == GameState.None) yield return null;is the shape, andWaitForSecondsRealtimeis the right pacer afterwards because a running world can be paused.Patterns/Console/PlayerMessage.csexposes this asAtBootOrMenuandWaitForWorld()so a mod does not have to re-derive which gate is correct;Mods/EquipmentPlus/EquipmentPlus/Plugin.csandMods/SprayPaintPlus/SprayPaintPlus/Plugin.csRepeatWarningare the worked examples.
Related timing fact for any coroutine anchored here: WaitForSeconds is timeScale-scaled, and the assembly's only Time.timeScale = 0f assignment is WorldManager.SetGamePause(bool) at :60899, reachable through EnablePause (:60874-60884) which is itself gated on GameManager.RunSimulation. GameManager.StartGame() restores it to 1f at :204588. So a coroutine running at the main menu is never stalled by a pause, but one that has waited for RunSimulation can be, and should use WaitForSecondsRealtime if its cadence matters.
GameManager.GameState is the usable phase signal, and it is public. The enum is GameState : byte { None, Joining, Waiting, Running, Loading, Paused } (:290777). It sits at None through boot and whenever no game is running, moves to Loading / Joining as a world comes up (:268516, :213109), and reaches Running at :204577 and :213235. Returning to the main menu sets it back to None (:60544, :213007, :290834). So GameManager.GameState == GameState.None is a reliable "no game is running, we are at boot or the menu" test, which is what a mod wants for deciding whether a startup surface is still worth writing to.
GameManager.RunSimulation is NOT that signal, despite reading like it. It is public static bool RunSimulation => !Assets.Scripts.Networking.NetworkManager.IsClient; (:203945), i.e. purely "am I not a multiplayer client". It is true at the main menu, true during boot, and true in single-player before any world exists. It is the correct gate for "host-only work" and the wrong gate for "a game is running"; several mods in this repo use it correctly for the former, and it would silently misbehave if used for the latter.
Namespace placements confirmed while tracing the above, all easy to get wrong because the names suggest a different home. KeyManager (:43646) and OnServer (:39504) are declared in the global namespace, before the first namespace block, so neither needs a using at all. Of the types a console or chat helper needs: ConsoleWindow is in Assets.Scripts (:221811), NetworkServer is also in Assets.Scripts (:213543) rather than Assets.Scripts.Networking, while ChatMessage (:278421) and NetworkChannel ARE in Assets.Scripts.Networking, and GameState (:290777) is in Assets.Scripts.GridSystem rather than alongside the GameManager that exposes it. The split is not guessable; verify before writing a using alias.
Verification history¶
- 2026-07-29 (second pass): extended the same section with the phase signal a mod should actually use.
GameManager.GameStateis public, its enum isNone / Joining / Waiting / Running / Loading / Paused, andNonecovers boot and the main menu. Recorded the trap thatGameManager.RunSimulationis only!NetworkManager.IsClientand is therefore true at the main menu, so it answers "am I the host" and not "is a game running". Also recorded the non-guessable namespace placements for the types a console or chat helper needs (NetworkServerinAssets.Scripts,ChatMessageandNetworkChannelinAssets.Scripts.Networking,GameStateinAssets.Scripts.GridSystem), all three found by compiler error while writingPatterns/Console/PlayerMessage.cs. - 2026-07-29: added the "OnPrefabsLoaded fires at boot, not at world load" section against 0.2.6403.27689. Purely additive; the existing main-thread claim is unaffected and was not re-verified against the new decompile, so its 0.2.6228.27061 stamp stands. Found while reviewing a mod-conflict banner that paced itself with
WaitForSecondsfrom this event and therefore played out entirely at the main menu. Also records the singleTime.timeScale = 0fassignment site and itsRunSimulationgate, and the fact thatKeyManagerandOnServerlive in the global namespace rather thanAssets.Scripts. - 2026-04-20: page created from the Research migration; F0219c is the primary source per MigrationMap ยง5.1. F0246 is a duplicate extraction that merges here.
Open questions¶
None at creation.