InspectorPlus Usage¶
How to capture live runtime state from a running Stationeers session using the InspectorPlus plugin. Reach for this recipe whenever you need to verify a hypothesis about field or property values at a specific moment instead of adding one-off logging.
When to use¶
- Verifying that a Harmony patch actually updates the field it claims to update (paired before / after snapshots).
- Inspecting the steady-state values of a device, network, or entity after the game has reached a stable condition.
- Confirming the shape of an unfamiliar type (which fields exist, which are non-null, what the
Transform.positionlooks like).
Do not use InspectorPlus as a replacement for logging inside hot Harmony patches. The request / snapshot cycle captures steady state well; short-lived transitions are better logged with Logger.LogInfo(...) from inside the patch.
Setup¶
InspectorPlus is a local-only BepInEx plugin. It runs two triggers:
- Drop a request JSON into the watched request folder to produce a programmatic snapshot. The plugin processes the file, writes a snapshot, then deletes the request.
- Press F8 in-game to dump every MonoBehaviour in the scene to a snapshot file.
Headless dedicated server (no client)¶
On a headless dedicated server with no client connected the simulation is paused, and on a headless build neither MonoBehaviour.Update() nor coroutines fire. The request pump runs off the simulation tick (ElectricityManager.ElectricityTick), so while the world is paused a dropped request file is never processed and no snapshot is written.
To capture snapshots in that case without a player joining, enable the opt-in setting (default off) before starting the server, either in the in-game settings panel under Server - Headless, or by adding to BepInEx/config/net.inspectorplus.cfg:
With it on, InspectorPlus keeps the simulation running on a batch-mode server with no client connected, so the tick, and therefore request processing, runs. The setting is gated on Application.isBatchMode and never fires on a client or single-player session. Added in InspectorPlus v1.1.0; hardened on 2026-07-02 into four cooperating pieces in HeadlessUnpausePatch.cs:
- The original one-shot unpause in a
GameManager.StartGamepostfix. On its own this is NOT sufficient: the dedicated-server assembly'sStartGameends withDelayedStartupPause().Forget(), a server-build-only method that re-pauses the world 5 seconds later wheneverNetworkBase.Clients.Count <= 0, ignoringAutoPauseServer. BecauseStartGameisasync UniTask, the postfix fires at the method's first await, always before the delayed pause lands. Full writeup:../GameSystems/SimulationTickDriverHooks.mdsection "Dedicated-server assembly only: DelayedStartupPause re-pauses 5 s after StartGame". - A guarded prefix that skips
GameManager.DelayedStartupPauseentirely (Prepare()disables the patch class on the client build, where the method does not exist). - A 5-second UniTask watchdog loop that logs
GameState / IsGamePaused / GameTickPaused / RunSimulation / GameTickCount / ClientstoLogOutput.logand re-unpauses whenever the world isRunning, parked, and clientless (it skips whileSaveHelper.IsSaving). This also recovers from any other silent pauser (panel pause paths, consolepause, third-party mods). - Pause tracers: every actual
WorldManager.SetGamePausetransition plus everyPauseGameTick/UnpauseGameTickcall is logged with a stack trace, so the next silent pause names its caller inLogOutput.log([PauseTrace]lines). These are instrumentation, not errors: clusters of[PauseTrace]stack dumps appear around every autosave (the save path pauses and resumes the game tick by design) and around any legitimate pause/unpause. When scanningLogOutput.logfor real failures, do not count[PauseTrace]blocks as exceptions; read the named caller in the trace only when diagnosing an UNEXPECTED park.
When a client is connected (a normal playtest) the server is already running and the toggle is unnecessary; on a client or single-player, Update() drives the pump directly.
Readiness caveat when using a probe request as the "world is ticking" signal: the first ~8 ticks run between StartGame and where DelayedStartupPause would land, so with an unhardened plugin a pre-dropped probe could be consumed once even though the sim parked seconds later. With the hardened plugin, confirm sustained ticking via the repeating [TickWatchdog] lines (rising GameTickCount) rather than a single consumed probe.
Request shape¶
Requests are JSON with five fields. The parser (SnapshotRequest.Parse, Mods/InspectorPlus/InspectorPlus/SnapshotRequest.cs:46-68) is a hand-rolled set of Regex.Match calls with NO IgnoreCase option, so the JSON keys are case-sensitive and must be spelled exactly in lowercase camelCase. A key with any other casing (Types, MaxDepth, IncludePrivate, ...) is silently ignored and its field keeps the default, so a fully capitalized request degrades to an unfiltered full-scene dump without any error.
The exact key spellings:
"types": type-name filter (JSON string array). Narrows the walk to instances of the listed types. Default: empty = all known game types.
Entries must be REAL C# class names, spelled exactly. ObjectWalker.FindType (Mods/InspectorPlus/InspectorPlus/ObjectWalker.cs:510-531) resolves each entry via asm.GetType(typeName) (full name) and then a case-sensitive scan for type.Name == typeName or a FullName ending in "." + typeName; there is no substring, prefab-name, or display-name matching. An entry that is not a real class ("StationBattery" matches nothing: the vanilla station-battery CLASS is Battery and the prefab key is StructureBattery) is silently skipped with only a Type not found: ... warning in LogOutput.log; the snapshot itself shows no error and simply omits those objects. A real class name works even from a mod assembly ("StationBatteryNuclear", MorePowerMod's Battery subclass, resolves fine). Once resolved, lookup goes through UnityEngine.Object.FindObjectsOfType(type), which DOES return subclass instances, so naming a real base class (Battery) captures its subclasses too. When a request comes back emptier than expected, check LogOutput.log for the Type not found line before doubting the game state.
- "fields": per-type field / property filter (JSON string array). Narrows the emitted members to the listed names. Default: empty = all public fields/properties.
- "maxDepth": recursion cap (integer, default 3). Controls how deep the reflection walk descends through nested object graphs.
- "includePrivate": when true, also walk non-public fields and properties (boolean, default false, public members only).
- "maxMonoBehaviours": cap on how many top-level objects a snapshot serializes (integer, default 10000).
Minimal example request:
{
"types": ["Assets.Scripts.Objects.Electrical.PowerTransmitter"],
"fields": ["_linkedReceiver", "_linkedReceiverDistance", "_powerProvided"],
"maxDepth": 2,
"includePrivate": true,
"maxMonoBehaviours": 100
}
Narrow types and fields precisely. A full-scene dump is cheap to produce but noisy to read; targeted requests are the default.
Interpreting output¶
Top level is an object with timestamp, frame, gameTime, and an objects array. Each entry carries _type, _name, and, for components, _gameObject, _active, and _position, plus a fields object of the sampled fields and properties. Unity Vector3 values and Transform positions are emitted as [x, y, z] arrays. A snapshot that hits a size cap gets a _truncated marker.
Use this structure directly:
- To confirm "did field X update," diff a baseline snapshot against a post-action snapshot.
- To confirm "the partner reference is non-null," find the entry by
_typeand read the target member underfields. - To confirm "there are N instances of this type live in the scene," count the
objectsentries with that_type.
Common requests¶
Prepare request files in advance of any in-game test. The moment a test instruction is handed to the developer, the corresponding request files should already exist on disk named for the checkpoints they capture (for example, before_link.json, after_link.json, steady_state.json). The developer drops each file into the request folder at the right moment rather than typing JSON mid-test.
Prefer paired before + after requests over a single after-only snapshot when the question is "did field X change". A single snapshot cannot prove a delta.
Pitfalls¶
FileSystemWatcher.Createdfires on a thread-pool thread, not the Unity main thread. Any Unity API call from the watcher callback crashes. InspectorPlus routes work throughMainThreadDispatcherfor this reason; a request that hits Unity APIs from a non-main thread is a bug in the plugin, not a bug in the request.- Unity fake-null:
obj == nullreturnstrueeven when the managed wrapper is still alive. InspectorPlus checksUnityEngine.Object-derived values via!objbefore dereferencing. FileSystemWatcher.Createdcan fire while the writer still holds the file open. InspectorPlus opens the request file withFileShare.ReadWriteand retries for a short window onIOException.
Cleanup¶
Snapshots are evidence, not durable artifacts. After a session, delete the snapshot files read during debugging and any stray request files still sitting in the request folder. Snapshots have timestamped filenames with no automatic rotation. Stray request files are worse: if the plugin failed to process one, it will be picked up the next time the game launches.
Central pages cite the request pattern, not a snapshot file path. Another developer reading the page months later cannot open a snapshot that was deleted at the end of the original session; the request pattern is reproducible. Write citations in the form:
Verified via InspectorPlus on 2026-04-20 in game version 0.2.6228.27061. Request: types=[PowerTransmitter], fields=[_linkedReceiver, _linkedReceiverDistance, _powerProvided].
Verification history¶
- 2026-04-20: page created from the Research migration; verbatim content lifted from F0003 and the surrounding sections of
Mods/InspectorPlus/RESEARCH.md. - 2026-05-21: corrected the request-field list (added
IncludePrivate,MaxMonoBehaviours) and the output-format description (anobjectsarray with[x, y, z]values, not an object keyed by type name with(x, y, z)) to match the shippedObjectWalker; added the Headless dedicated server section for the InspectorPlus v1.1.0Force Unpause Without Clienttoggle. Verified via InspectorPlus on 2026-05-21 in game version 0.2.6228.27061. Request: maxMonoBehaviours=25, maxDepth=2 on a fresh dedicated-server world. - 2026-07-02: rewrote the "Request shape" section to give the exact JSON key spellings. The previous revision listed the fields by their C# property names (
Types,Fields,MaxDepth,IncludePrivate,MaxMonoBehaviours), butSnapshotRequest.Parse(Mods/InspectorPlus/InspectorPlus/SnapshotRequest.cs:46-68) matches"types","fields","maxDepth","includePrivate","maxMonoBehaviours"viaRegexwith noIgnoreCase, so capitalized keys are silently ignored and the request degrades to a full-scene dump. Added the one-line degradation warning and a minimal example request JSON. Source is the mod's own shipped code (game-version-independent); section restamped at the current game version 0.2.6403.27689 per convention. - 2026-07-02 (later still): two additions from the power rearchitecture session's live InspectorPlus usage. (a) "Request shape" /
types: documented that entries must be real, exactly-spelled C# class names, verified againstObjectWalker.FindType(ObjectWalker.cs:510-531,asm.GetTypethen case-sensitiveName/ dotted-FullName-suffix scan) andWalkRequestedTypes(skips unresolved names with only aType not foundLogWarning); observed in-session as "StationBattery" (not a class) matching nothing while "StationBatteryNuclear" (a real MorePowerMod class) worked; resolved classes go throughFindObjectsOfType, which includes subclass instances. (b) Headless section: noted[PauseTrace]stack dumps around autosaves are by-design instrumentation (the save path pauses/resumes the tick), not errors. Source is the mod's own shipped code plus session logs (game-version-independent); sections restamped at the current game version per convention. - 2026-07-02 (later): updated "Headless dedicated server (no client)" for the hardened force-unpause. The 2026-05-21 revision's one-shot description matched the plugin then, but the one-shot is defeated at 0.2.6403.27689 by the server-assembly-only
GameManager.DelayedStartupPause(5-second delayedSetGamePause(true)when clientless); an earlier note inDedicatedServer/CLAUDE.mdhad recorded the resulting flakiness empirically at 0.2.6228.27061 without a cause. Documented the cause, the DelayedStartupPause skip patch, the 5-second watchdog, and the[PauseTrace]stack tracers, all live-verified on a fresh-new Lunardedicated-server boot on 2026-07-02 at 0.2.6403.27689 (three-boot evidence run: unhardened = exactly 8 ticks then parked; hardened = continuous ticking, ScenarioRunner 10-tick scenario fired, probes consumed).
Open questions¶
None at creation.