Printing to the in-game ~ console (and where mod log lines actually go)¶
There are three separate "log" surfaces in a BepInEx-modded Stationeers, and they do not all carry the same content:
| Surface | Fed by | A player typically looks here? |
|---|---|---|
BepInEx\LogOutput.log (disk) |
BepInEx ManualLogSource.LogInfo/LogWarning/LogError (i.e. Logger.Log* in a plugin). StationeersLaunchPad also mirrors mod log lines into Player.log. |
Power users / when sending a log to a mod author. |
Unity Player.log (%USERPROFILE%\AppData\LocalLow\Rocketwerkz\rocketstation\Player.log, rotated to Player-prev.log on each launch) |
UnityEngine.Debug.Log/LogWarning/LogError, plus (via StationeersLaunchPad's mirror) BepInEx mod log lines. |
Occasionally. |
The in-game ~ console (ConsoleWindow) |
Only ConsoleWindow.Print* calls and console-command output. Not UnityEngine.Debug.Log, and not BepInEx Logger.Log*. |
Yes -- this is the one a player sees while playing. |
Key gotcha: UnityEngine.Debug.Log(...) writes to Player.log but does not appear in the in-game ~ console (Stationeers' ConsoleWindow does not subscribe to Application.logMessageReceived). And BepInEx Logger.LogInfo(...) goes to LogOutput.log (and Player.log via the StationeersLaunchPad mirror) but also not the in-game console. So a mod whose only output is Logger.LogInfo / Debug.Log will look "silent" to a player who checks the ~ console. To show up there, call ConsoleWindow.Print*.
ConsoleWindow API¶
public static class Assets.Scripts.ConsoleWindow (Assembly-CSharp.dll). The namespace is Assets.Scripts -- the class sits inside the namespace Assets.Scripts { ... } block (around Assembly-CSharp.decompiled.cs:206094; the enclosing namespace opens at ~184063 and the next sibling namespace Assets.Scripts.Weather does not open until ~207612). The game's own code refers to it everywhere as bare ConsoleWindow.PrintAction(...) / ConsoleWindow.PrintError(...) after using Assets.Scripts;. There is exactly one ConsoleWindow type in Assembly-CSharp and none in any other shipped DLL. Print methods:
public static void Print(string output, ConsoleColor color = ConsoleColor.White, bool clearLine = false, bool aged = true, bool unformatted = false);
public static void PrintAction(string output, bool aged = false); // the "an action happened" style line (Yellow)
public static void PrintError(string output, bool suppressStacktrace = false); // Red, plus a gray Environment.StackTrace line unless suppressed
// plus several Print(GameString, ...) overloads for localised strings, and AsyncPrintError(string, bool)
PrintAction is what the game itself uses for "X happened" notices (input recording started, server paused, an id was reassigned, etc.). PrintError for errors. aged: false keeps the line from fading out quickly.
The buffer accepts lines even before the console UI exists (there is an internal PrematureLog struct that queues early calls), but at very early startup -- e.g. a Prefab.OnPrefabsLoaded handler, which fires before the main menu -- it is safest to wrap the call in try/catch and rely on the Debug.Log / Logger.Log channels as the fallback.
Recommended mod pattern¶
For a message the player should actually see, emit it on all three channels (greppable in the log files, visible in the ~ console). A direct compile-time reference is fine for any mod that already depends on Assembly-CSharp (ConsoleWindow is a vanilla game class). Reference it via a using alias rather than a blanket using Assets.Scripts; -- Assets.Scripts is a large namespace that contains common short names (Settings : UserInterfaceBase at ~Assembly-CSharp.decompiled.cs:248232, Localization, etc.), so a bare using Assets.Scripts; collides with any same-named type in the caller's own namespace (a mod with its own Settings class gets CS0104 'Settings' is an ambiguous reference). The alias dodges that:
using ConsoleWindow = Assets.Scripts.ConsoleWindow; // top of the file, with the other usings
internal static void PlayerLog(string msg) {
Plugin.Log?.LogInfo(msg); // -> LogOutput.log (+ Player.log via StationeersLaunchPad mirror)
UnityEngine.Debug.Log($"[ModName] {msg}"); // -> Player.log
try { ConsoleWindow.PrintAction($"[ModName] {msg}", aged: false); } catch { } // -> in-game ~ console
}
internal static void PlayerError(string msg) {
Plugin.Log?.LogError(msg);
UnityEngine.Debug.LogError($"[ModName] {msg}");
try { ConsoleWindow.PrintError($"[ModName] {msg}", suppressStacktrace: true); } catch { }
}
EquipmentPlus references ConsoleWindow.PrintError(...) exactly this way -- using Assets.Scripts; plus a bare ConsoleWindow.PrintError(...) (it has no Settings class of its own, so the blanket using is safe there): Mods/EquipmentPlus/EquipmentPlus/HelmetBeamPatches.cs:123, ScrollDispatchPatches.cs:271. MaintenanceBureauPlus (Plans/) reflects but already uses the correct asm.GetType("Assets.Scripts.ConsoleWindow") name.
Reflection variant (only if you want zero compile-time dependency on Assembly-CSharp and graceful degradation if the class ever moves):
static MethodInfo _printAction, _printError; static bool _resolved;
static void ResolveConsole() {
if (_resolved) return; _resolved = true;
try {
var t = AccessTools.TypeByName("Assets.Scripts.ConsoleWindow")
?? AccessTools.TypeByName("ConsoleWindow")
?? AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => { try { return a.GetTypes(); } catch { return Type.EmptyTypes; } })
.FirstOrDefault(x => x.Name == "ConsoleWindow" && x.IsClass && x.IsAbstract && x.IsSealed);
if (t == null) return;
_printAction = AccessTools.Method(t, "PrintAction", new[] { typeof(string), typeof(bool) }) ?? AccessTools.Method(t, "PrintAction", new[] { typeof(string) });
_printError = AccessTools.Method(t, "PrintError", new[] { typeof(string), typeof(bool) }) ?? AccessTools.Method(t, "PrintError", new[] { typeof(string) });
} catch { }
}
Do not include "Util.Commands.ConsoleWindow" in the resolver -- Util.Commands is a real namespace (the say / console-command classes live there) but ConsoleWindow is not in it; the lookup always fails and HarmonyX logs a Could not find type named Util.Commands.ConsoleWindow warning at startup.
Verification history¶
- 2026-05-11: page created after a NetworkPuristPlus user reported "I see nothing in the player log" despite the mod's
Logger.LogInfolines being present in bothLogOutput.logandPlayer.log-- they were looking at the in-game~console, which neitherLogger.Log*norDebug.Logreaches.ConsoleWindow.PrintAction/PrintErrorsignatures lifted from.work/decomp/0.2.6228.27061/Assembly-CSharp.decompiled.cs(around line 206094, 206824-206957). Namespace ofConsoleWindowcould not be pinned from the dump at the time (theawk"lastnamespacebefore the class" heuristic saidAssets.Scripts; a brace-depth tracker said global -- unreliable because decompiled$"..."interpolation throws off naive{ }counting; a directusing Util.Commands;failed to compile) -- the page recommended the reflection-based resolver and listed the namespace as an open question. - 2026-05-12: namespace resolved to
Assets.Scripts. Confirmed two ways: (1) in.work/decomp/0.2.6228.27061/Assembly-CSharp.decompiled.csthepublic static class ConsoleWindowat ~206094 sits inside thenamespace Assets.Scriptsblock opened at ~184063 with no interveningnamespacedeclaration before the next siblingnamespace Assets.Scripts.Weatherat ~207612; (2)EquipmentPluscompiles cleanly withusing Assets.Scripts;+ a bareConsoleWindow.PrintError(...)(Mods/EquipmentPlus/EquipmentPlus/HelmetBeamPatches.cs:123,ScrollDispatchPatches.cs:271; builtEquipmentPlus.dllpresent). The earlier "a directusingfailed to compile" was specificallyusing Assets.Scripts;colliding with a same-namedSettingsclass in the caller (NetworkPuristPlus.SettingsvsAssets.Scripts.Settings), not aConsoleWindow-name problem -- the fix is ausing ConsoleWindow = Assets.Scripts.ConsoleWindow;alias.ConsoleWindow APIandRecommended mod patternsections rewritten accordingly;Util.Commands.ConsoleWindowremoved from the suggested reflection chain (it never matched). No fresh-validator pass needed: the contradicted claim ("namespace unknown / ausingfails") is overturned by a strictly stronger source (the decompilednamespaceblock plus a compiling counter-example mod), with no ambiguity to resolve.
Open questions¶
None.