Skip to content

Display resolution and Unity PlayerPrefs

Anyone running more than one instance of a Unity game on one Windows account hits this, and Stationeers is no exception: the game's display preference lives in two places that are not the same scope. setting.xml is per install (and per -settingspath), so it is safe to give a driven instance its own. The Unity Screenmanager * values are per Windows USER and per product name, so every instance on the machine writes the same ones, including the developer's own client.

The practical consequence, measured on this machine at 0.2.6428.27798 on 2026-09-21: the test rig launches its clients at 800x600 windowed, and afterwards

  • the developer's own setting.xml still reads <ScreenWidth>3840</ScreenWidth> <ScreenHeight>2160</ScreenHeight> <RefreshRate>120</RefreshRate> <FullScreen>true</FullScreen>, untouched, and
  • HKCU\Software\Rocketwerkz\rocketstation reads Screenmanager Resolution Width = 800, Screenmanager Resolution Height = 600, Screenmanager Resolution Window Width = 800, Screenmanager Resolution Window Height = 600, Screenmanager Resolution Use Native = 0, Screenmanager Fullscreen mode = 3.

That is the whole problem in one pair of observations. There is no per-install and no per-instance scope for the second list, and nothing in the game or in Unity redirects it (see "Nothing redirects PlayerPrefs" below).

The rig side of the same problem was measured separately and agrees: TestRig/RESEARCH.md records the transition happening on three runs in one night (Screenmanager Fullscreen mode 1 to 3, Resolution Width 3840 to 800, Resolution Height 2160 to 600, Resolution Use Native 1 to 0, plus both Resolution Window values), each time needing a manual restore. So this is a per-run write, not a one-off.

What is shared, and at what scope

State Physical location Scope Can it be moved?
ScreenWidth / ScreenHeight / RefreshRate / FullScreen / Vsync / Monitor <SavePath>/setting.xml per file, and -settingspath gives an instance its own yes, per instance, no side effects
Screenmanager *, UnitySelectMonitor, UnityGraphicsQuality, unity.player_* HKCU\Software\<companyName>\<productName> per Windows user, per product name only by changing companyName / productName in globalgamemanagers
PlayerCookie-v2.xml, Player.log, Blueprints/ Application.persistentDataPath, which is %USERPROFILE%\AppData\LocalLow\<companyName>\<productName> per Windows user, per product name same lever, same edit

Both shared rows are keyed on the same two strings, so one edit moves both. That is the entire content of the second half of this page.

Two directions of damage are NOT symmetric:

  • The game re-asserts resolution from setting.xml on every boot (four call sites below), so a poisoned Screenmanager Resolution * self-corrects once managed code runs. What the user sees is the pre-managed-code window: Unity's native player sizes and places the window from the registry before a single line of Assembly-CSharp executes.
  • The game never re-asserts window POSITION. Screenmanager Window Position X / Y (observed at 1519 / 760) have no counterpart in setting.xml and no game-side writer: Assembly-CSharp contains zero OS-level window-position calls. Every SetWindowPos occurrence in the assembly (21 of them, at 57617, 96076, 108988, 109054, 109761, 109954, 112367, 112605, 130439, 156986, 211645, 229046, 229229, 244465, 262749, 263044, 263280, 263314, 263634, 309320, 455460) is ImGui.SetWindowPos on an in-game debug panel. The only user32.dll imports are SetWindowText and FindWindow (210483-210487). So a moved window stays moved. The dedicated server moves it too, from a window nobody sees: a server-only session left the pair at 475 / 468 (see "A server-only session writes the key: measured" below).

Where the resolution comes from: four Screen.SetResolution sites

A whole-file grep for Screen.SetResolution returns exactly four lines. All four read Settings.CurrentData, never Screen.currentResolution, so the flow is one-way out of setting.xml.

Line Enclosing method Reached from
272252 Settings.ApplyDisplaySettings() (272241) No caller in the assembly. Wired to a settings-panel Apply button in the scene.
272283 Settings.LoadSettings() (272270) WorldManager.ManagerAwake() at 59747.
272328 Settings.ApplyVideoSettings() (272308), last statement GameManager.Start() at 210582, and Settings.Initialize() at 271531.
272942 Settings.OnValueChanged(SettingType) (272543), case SettingType.RefreshRate Settings-panel refresh-rate dropdown.

LoadSettings is the guarded-parse one:

if (int.TryParse(CurrentData.ScreenWidth, out var result) && int.TryParse(CurrentData.ScreenHeight, out var result2))
{
    Screen.SetResolution(result, result2, CurrentData.FullScreen, CurrentData.RefreshRate);
}
else
{
    ConsoleWindow.Print("Cannot parse resolution leaving at default");
}

The other three use a bare int.Parse on the same string fields:

Screen.SetResolution(int.Parse(CurrentData.ScreenWidth), int.Parse(CurrentData.ScreenHeight), CurrentData.FullScreen, CurrentData.RefreshRate);

The two that fire on every boot, at the main menu

Both boot-time writes happen inside GameManager.Start() (private async void Start(), 210523), before any world is loaded, so this is main-menu behaviour and not something a driven instance can dodge by never loading a save.

WorldManager.ManagerAwake() is driven from the manager loop inside Start() (manager.ManagerAwake(); at 210535) and calls Settings.LoadSettings() at 59747. That call sits ABOVE the method's own if (!GameManager.IsBatchMode) block (59754), so it is not batch-guarded:

public override void ManagerAwake()
{
    CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
    CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
    base.ManagerAwake();
    Instance = this;
    StringManager.Initialize();
    Localization.GetLanguages();
    GameStrings.Initialize();
    Settings.LoadSettings();
    UpdateFrameLimiter();
    ...
    if (!GameManager.IsBatchMode)
    {

Later in the same Start(), after the launch-flag dispatcher has run:

    Settings.Initialize();
    Stationpedia.Initialize();
    PanelToolTip.Initialize();
    MoleHelper.Initialize();
    await WorldManager.Initialize();
    StatusUpdates.Initialize();
    StringReferenceInt.Initialize();
    Logicable.Initialize();
    DifficultySetting.SetCurrent();
    CommandLine.ExecutePostLaunchCommands();
    Settings.ApplyVideoSettings();          // :210582

Settings there is the public Settings Settings; field on GameManager (209225), so both are instance calls. The ApplyVideoSettings() at 210582 is NOT batch-guarded, which is why a -batchmode instance still calls Screen.SetResolution.

The batch guard that does exist is one level in, on the OTHER caller. Settings.Initialize() (271499) returns before reaching its own tail call:

public void Initialize()
{
    Instance = this;
    if (GameManager.IsBatchMode)
    {
        return;
    }
    SetVisible(isVisble: true);
    ...
    InitVideoPageValues();
    InitAdvancedPageValues();
    ApplyVideoSettings();           // :271531
}

So of the three boot-time routes, two run unconditionally (LoadSettings, and ApplyVideoSettings from GameManager.Start) and one is skipped in batch mode (ApplyVideoSettings from Settings.Initialize). Net effect: launch flags lose in every mode.

Settings.OnInitComplete() is NOT part of this chain, despite being the method Settings.Awake() hooks to WorldManager.OnGameDataLoaded. Its whole body is the save-path check (271546):

public void Awake()
{
    WorldManager.OnGameDataLoaded = (Action)Delegate.Combine(WorldManager.OnGameDataLoaded, new Action(OnInitComplete));
}

public static void OnInitComplete()
{
    if (ValidateSavePath())
    {
        Instance.ShowWarningSavePathNotWritable();
        GetInputField(SettingType.SavePath).text = CurrentData.SavePath;
    }
}

See SettingsPersistence for what ValidateSavePath does and when it writes.

The defaults a fresh instance comes up with

Settings.SettingData field initialisers, verbatim (271036-271057). These are what an instance with no setting.xml uses, which is every freshly created rig instance:

[XmlElement]
public int Monitor = 1;

[XmlElement]
public string ScreenWidth = "1920";

[XmlElement]
public string ScreenHeight = "1080";

[XmlElement]
public int RefreshRate = 60;
...
[XmlElement]
public bool FullScreen = true;

[XmlElement]
public bool Vsync;

FullScreen defaulting to true is why a driven instance comes up fullscreen on the developer's desktop no matter what the launch line said. CurrentData.Monitor has zero consumers in the assembly at this version; the display is actually selected by Unity's own UnitySelectMonitor registry value.

Screen.fullScreenMode and the FullScreenMode enum have zero occurrences in the assembly. The game only ever uses the legacy SetResolution(int, int, bool, int) overload.

setting.xml cannot be poisoned back from the registry

The settings panel builds its resolution list from the display but selects from CurrentData, so a wrong registry value cannot round-trip into the XML through the UI.

Settings.StoreAvailableResolutions() (271805) fills availResolutions from Screen.resolutions, deduplicated and sorted by width then height. Settings.RefreshResolutionAndRefreshRateDropdown() (271582) then does:

private void RefreshResolutionAndRefreshRateDropdown()
{
    List<string> list = availResolutions.Select((Resolution x) => $"{x.width} x {x.height}").Distinct().ToList();
    TMP_Dropdown dropdown = GetDropdown(SettingType.Resolution);
    dropdown.ClearOptions();
    dropdown.AddOptions(list);
    dropdown.value = list.IndexOf(CurrentData.ScreenWidth + " x " + CurrentData.ScreenHeight);
    List<int> list2 = availResolutions.Select((Resolution x) => x.refreshRate).Distinct().ToList();
    List<string> options = (from x in list2
        orderby x
        select $"{x}Hz").ToList();
    TMP_Dropdown dropdown2 = GetDropdown(SettingType.RefreshRate);
    dropdown2.ClearOptions();
    dropdown2.AddOptions(options);
    dropdown2.value = list2.IndexOf(CurrentData.RefreshRate);
}

The options come from the hardware, the SELECTION comes from CurrentData. Screen.currentResolution is read in exactly two places in the whole assembly (103140 and 103141) and both are the systeminfo console command's debug tree, which prints and discards.

Caveat worth knowing before relying on this: List.IndexOf returns -1 when CurrentData's resolution is not among the display's modes, and ApplyDisplaySettings() (272241) later parses GetDropdown(SettingType.Resolution).captionText.text.Split('x') back into CurrentData.ScreenWidth / ScreenHeight. So the panel is a one-way street only as long as nobody presses Apply with an out-of-range saved resolution.

What writes the registry: UnityPlayer.dll, not game code

The string Screenmanager has zero occurrences in Assembly-CSharp.decompiled.cs, and it appears in UnityPlayer.dll as 17 distinct key names:

Screenmanager Fullscreen mode
Screenmanager Fullscreen mode Default
Screenmanager Native RefreshRate Denominator
Screenmanager Native RefreshRate Numerator
Screenmanager RefreshRate Denominator
Screenmanager RefreshRate Numerator
Screenmanager Resolution Height
Screenmanager Resolution Height Default
Screenmanager Resolution Use Native
Screenmanager Resolution Use Native Default
Screenmanager Resolution Width
Screenmanager Resolution Width Default
Screenmanager Resolution Window Height
Screenmanager Resolution Window Width
Screenmanager Stereo 3D
Screenmanager Window Position X
Screenmanager Window Position Y

UnityPlayer.dll also carries the registry path prefixes Software\ and Software\AppDataLow\Software\, and the UnityEngine.PlayerPrefs::* internal-call registrations. rocketstation.exe (666,624 bytes) carries none of them; it is a launcher stub.

The game's own PlayerPrefs use is nine call sites, none of them display-related:

46489:  PlayerPrefs.SetFloat("ChatSize", Target.sizeDelta.y);
47628:  FilterVersion.isOn  = PlayerPrefs.GetInt("FILTER_VERSION", 1) == 1;
47629:  FilterPassword.isOn = PlayerPrefs.GetInt("FILTER_PASSWORD", 0) == 1;
47630:  FilterEmpty.isOn    = PlayerPrefs.GetInt("FILTER_EMPTY", 0) == 1;
47656:  PlayerPrefs.SetInt("FILTER_VERSION",  toggle.isOn ? 1 : 0);
47660:  PlayerPrefs.SetInt("FILTER_PASSWORD", toggle.isOn ? 1 : 0);
47664:  PlayerPrefs.SetInt("FILTER_EMPTY",    toggle.isOn ? 1 : 0);
47671:  int value = PlayerPrefs.GetInt("JoinSortBy", 0);
47691:  PlayerPrefs.SetInt("JoinSortBy", (int)SortType);

So the display values in that key are written natively by the player, as a side effect of Screen.SetResolution and of window moves, and no Harmony patch on game code can intercept them.

The live key, 2026-09-21

HKCU\Software\Rocketwerkz\rocketstation, read directly. Unity appends _h<hash> to every value name; the hash is part of the name on disk, not a separate field.

Value Type Observed
Screenmanager Fullscreen mode Default DWord 1
Screenmanager Fullscreen mode DWord 3
Screenmanager Native RefreshRate Denominator DWord 1
Screenmanager Native RefreshRate Numerator DWord 120
Screenmanager Resolution Height Default DWord 768
Screenmanager Resolution Height DWord 600
Screenmanager Resolution Use Native Default DWord 1
Screenmanager Resolution Use Native DWord 0
Screenmanager Resolution Width Default DWord 1024
Screenmanager Resolution Width DWord 800
Screenmanager Resolution Window Height DWord 600
Screenmanager Resolution Window Width DWord 800
Screenmanager Stereo 3D DWord 1
Screenmanager Window Position X DWord 1519
Screenmanager Window Position Y DWord 760
UnityGraphicsQuality DWord 5
UnitySelectMonitor DWord 0
unity.player_session_count Binary (Unity session telemetry)
unity.player_sessionid Binary (Unity session telemetry)
FILTER_EMPTY DWord 0
FILTER_PASSWORD DWord 0
FILTER_VERSION DWord 0
Multiplayer_WIP DWord 1
updated-mod-config DWord 1
BetterServerList_RecentServers Binary (third-party mod)

Three things this table settles. First, the Resolution * values are the rig's 800x600 while Native RefreshRate Numerator is the developer's 120 Hz, so the two sources are genuinely interleaved in one key. Second, Fullscreen mode = 3 against Fullscreen mode Default = 1: those index Unity's public UnityEngine.FullScreenMode enum (ExclusiveFullScreen = 0, FullScreenWindow = 1, MaximizedWindow = 2, Windowed = 3, taken from the published Unity API rather than read out of a binary on this pass), so the build's own default is a fullscreen window and the stored state is windowed. Third, Multiplayer_WIP, updated-mod-config and BetterServerList_RecentServers match none of the nine game call sites and none in StationeersLaunchPad.decompiled.cs (which has zero PlayerPrefs. calls), so third-party mods write into the same shared key too.

Nothing redirects PlayerPrefs at runtime

Checked three ways, all negative.

rocketstation_Data/boot.config is six lines and contains no prefs, registry or data-path key:

gfx-enable-gfx-jobs=1
gfx-enable-native-gfx-jobs=1
wait-for-native-debugger=0
hdr-display-enabled=0
gc-max-time-slice=3
build-guid=08555964053640b690cd7fb3fe7eae84

Command-line switches: the search method is validated by the fact that real Unity switches are present as bare strings in UnityPlayer.dll without their leading dash (screen-width, screen-height, screen-fullscreen, screen-quality, popupwindow, single-instance, nolog, force-d3d11 and its variants all return exactly one hit each). No string matching pref, regist, company or product in a switch-shaped form exists in either UnityPlayer.dll or rocketstation.exe, over both ASCII and UTF-16 extraction. The only prefs hits are the ten UnityEngine.PlayerPrefs::* internal-call registrations plus the bare word prefs.

Environment variables: the UNITY_*-shaped strings in UnityPlayer.dll are exactly UNITY_DEVICE_SUPPORTS_NATIVE_16BIT, UNITY_DEVICE_SUPPORTS_WAVE_128, UNITY_DEVICE_SUPPORTS_WAVE_16, UNITY_DEVICE_SUPPORTS_WAVE_32, UNITY_DEVICE_SUPPORTS_WAVE_64, UNITY_DEVICE_SUPPORTS_WAVE_8, UNITY_DEVICE_SUPPORTS_WAVE_ANY, UNITY_DISABLE_ETW_PROFILING, UNITY_EXT_LOGGING, UNITY_GIVE_CHANCE_TO_ATTACH_DEBUGGER, UNITY_HDR_ON, UNITY_LOG_FILE, UNITY_NOPROXY, UNITY_PROXYSERVER, UNITY_RUNTIME_INSTANCING_ARRAY_SIZE, UNITY_SINGLE_PASS_STEREO, UNITY_UI_CLIP_RECT. None relates to prefs or to the registry path.

The only lever is the pair of strings the key name is built from.

The identity behind both: two strings in globalgamemanagers

rocketstation_Data/globalgamemanagers (6,212,900 bytes) carries Unity's serialized PlayerSettings object, and with it companyName and productName. Each appears exactly ONCE in the whole file, so there is no second copy to keep in sync.

Unity serialises a string as int32 length, then the bytes, then align-4 padding. Read out of the file:

Field Length int32 at Bytes at Value Length Pad Next field at Slot
companyName 1284 1288 Rocketwerkz 11 1 1300 16 bytes
productName 1300 1304 rocketstation 13 3 1320 20 bytes

Hex, offsets 1270 to 1333:

000004f6: 0000 0200 0000 0000 0000 3c00 0000 0b00  ..........<.....
00000506: 0000 526f 636b 6574 7765 726b 7a00 0d00  ..Rocketwerkz...
00000516: 0000 726f 636b 6574 7374 6174 696f 6e00  ..rocketstation.
00000526: 0000 0100 0000 0400 0000 0000 0000 0000  ................

Because the align-4 padding absorbs the difference, a replacement name keeps every later offset in the file identical as long as length + pad is unchanged:

  • companyName needs length + pad == 12, so 9 to 12 characters are free substitutions. 8 or fewer shortens the slot and shifts the rest of the file; 13 or more lengthens it.
  • productName needs length + pad == 16, so 13 to 16 characters are free substitutions.

Changing those two strings moves the PlayerPrefs registry key and Application.persistentDataPath together, because both are derived from them. Verified on the live install: the key is HKCU\Software\Rocketwerkz\rocketstation (there is no Rocketwerkz Limited key anywhere under HKCU\Software), and persistentDataPath resolves to %USERPROFILE%\AppData\LocalLow\Rocketwerkz\rocketstation, which is where PlayerCookie-v2.xml, Player.log, Player-prev.log and Blueprints/ sit.

rocketstation_Data/app.info is the cheap corroboration of the same two strings without a hex dump. Unity writes it as exactly two lines, companyName then productName:

Rocketwerkz
rocketstation

The decoy: Rocketwerkz Limited is a real string, in a different field

The install ships FOUR identity strings, in two pairs that do not match each other. Only one pair builds the registry key and persistentDataPath. Taking the wrong pair is the mistake this subsection exists to prevent, and it has already been made once in this knowledge base (see Verification history, 2026-09-21).

Field Where it lives Value
Unity PlayerSettings.companyName globalgamemanagers byte 1288; app.info line 1 Rocketwerkz
Unity PlayerSettings.productName globalgamemanagers byte 1304; app.info line 2 rocketstation
Win32 version-resource CompanyName rocketstation_Data\Managed\Assembly-CSharp.dll Rocketwerkz Limited
Win32 version-resource ProductName same DLL Space Station Online

Read with [System.Diagnostics.FileVersionInfo]::GetVersionInfo() on the DLL at 0.2.6428.27798, the version resource returns CompanyName = 'Rocketwerkz Limited', ProductName = 'Space Station Online', FileVersion = 0.2.6428.27798. That is the assembly's own metadata, set from AssemblyCompany / AssemblyProduct at compile time. Unity's native player never consults it.

So Rocketwerkz Limited is genuinely present in the game folder, which is what makes it a convincing wrong answer: it is not a typo and not an invention. Substituting it predicts HKCU\Software\Rocketwerkz Limited\Space Station Online, and both halves are wrong. Rocketwerkz Limited does not occur as a byte string anywhere in globalgamemanagers, in either install.

The dedicated server has its own copy of the same identity

The Dedicated Server build ships its own globalgamemanagers, at rocketstation_DedicatedServer_Data/globalgamemanagers. It is a genuinely different file from the client's (6,205,532 bytes against 6,212,900, different sha256), but bytes 1270 to 1339 are byte-identical, so the identity pair is the same:

Install companyName at 1288 productName at 1304
$(StationeersPath)\rocketstation_Data\ Rocketwerkz rocketstation
TestRig/DedicatedServer/install/rocketstation_DedicatedServer_Data/ Rocketwerkz rocketstation

Both app.info files agree. Both installs report UPDATEVERSION=Update 0.2.6428.27798 in StreamingAssets/version.ini, so this is a same-version comparison.

Consequence: the dedicated server does NOT get a private PlayerPrefs key or a private persistentDataPath. It shares HKCU\Software\Rocketwerkz\rocketstation and %USERPROFILE%\AppData\LocalLow\Rocketwerkz\rocketstation with the client and with every rig instance on the same Windows account, exactly as TestRig/CLAUDE.md assumes. The server half of the rig is inside the same shared-state problem as the client half, not outside it.

MEASURED here: the two identity strings are identical across the two installs. MEASURED since: the server writes that same key. This paragraph first recorded the write as INFERRED, from Unity's HKCU\Software\<companyName>\<productName> rule and the live key on the client side, because no server launch had been observed; a server-only session on 2026-09-21 observed it, and the next subsection has the numbers. Still INFERRED: that the server's persistentDataPath is the shared LocalLow folder. No server-side read of it exists, and that session changed nothing under the folder.

A server-only session writes the key: measured

Batch mode is not windowless, and the dedicated server writes its window's state into the shared key. Measured on 2026-09-21 in a test-rig session that ran only the dedicated server: 22 launches between 15:32 and 15:54, one of which crashed and 21 of which were stopped with the rig's control-plane quit request.

The window. The last launch was queried four times while it booted, read-only: EnumWindows filtered on the server's PID, then GetClassName, GetWindowText, IsWindowVisible, IsIconic, GetWindowRect, and a comparison with GetForegroundWindow. The process was up at 15:53:03, and the reads at 15:53:10.680, 15:53:19.530, 15:53:28.377 and 15:53:37.266 each returned the same two windows:

hwnd=0x22130C class='Unity.BatchModeWindow' title='rocketstation' visible=False minimised=False rect=(468,468)-(3348,2031) size=2880x1563 foreground=False
hwnd=0x3E14A8 class='IME' title='Default IME' visible=False minimised=False rect=(0,0)-(0,0) size=0x0 foreground=False

The one change across the four was the title, which read Stationeers v0.2.6428.27798 at the fourth. That rename is the game's own. The server build's Assets.Scripts.GameManager.Awake() does

GameName = $"Stationeers v{Version}";
SetApplicationWindowName(GameName);

and SetApplicationWindowName is SetWindowText(FindWindow(null, Application.productName), name); (Assembly-CSharp.DedicatedServer.decompiled.cs 198446-198458), the same product-name lookup as the client's at 210491 (next subsection). It finds the invisible window and renames it.

The key. HKCU\Software\Rocketwerkz\rocketstation, read with reg query before the session, during it, and on each side of the last two launches. The eight values that moved:

Value 15:31:59, before the session 15:48:58, a launch 15 s in 15:52:16, after it quit 15:53:37, the last launch 34 s in 15:54:21, after it quit
Screenmanager Fullscreen mode 1 3 3 3 3
Screenmanager Resolution Width 3840 2864 2864 2864 2864
Screenmanager Resolution Height 2160 1524 1524 1524 1524
Screenmanager Resolution Use Native 1 0 0 0 0
Screenmanager Resolution Window Width 1920 2864 2864 2864 2864
Screenmanager Resolution Window Height 1080 1524 1524 1524 1524
Screenmanager Window Position X 1519 397 449 449 475
Screenmanager Window Position Y 760 390 442 442 468

The other eight display values read the same at all five points: Screenmanager Stereo 3D 1, Screenmanager Resolution Width Default 1024, Screenmanager Resolution Height Default 768, Screenmanager Resolution Use Native Default 1, Screenmanager Fullscreen mode Default 1, UnitySelectMonitor 0, Screenmanager Native RefreshRate Numerator 120, Screenmanager Native RefreshRate Denominator 1.

Two other readers agree on the transition. The rig's own release report for the session, which compares the key with a snapshot taken when the lock was acquired, lists the same eight moves (1519 -> 475, 760 -> 468 and the six above). Reads taken outside the session, at 15:06:43 and 15:58:18, show the position going from 1519 / 760 to 475 / 468 across it, the other six having been put back by the rig in between.

Why it is the server.

  • No client ran. Both rig client instances were stopped at status reads at 15:14:59 and 15:55:21, the rig lock was held from 15:32 to 15:55 by a session whose recorded purpose was dedicated server only, and the developer's own client had last written %USERPROFILE%\AppData\LocalLow\Rocketwerkz\rocketstation\Player.log at 02:53:53 that day. No file under that folder has a modification time between 15:31 and 15:56.
  • Twice, a read taken while a launch was running still held the previous values and a read after that launch quit held new ones: 15:48:58 to 15:52:16 (that launch was up at 15:48:43 and stopped at 15:50:03), and 15:53:37 to 15:54:21 (up at 15:53:03, stopped at 15:54:20).
  • The last launch left the position at 475 / 468, against its own window rectangle's top-left of (468, 468): Y equal, X seven greater. The size it left, 2864 x 1524, sits against the 2880x1563 outer rectangle; the client rectangle was not read. The window was not placed from the key either: before and during that launch the key held 449 / 442, and the window sat at (468, 468).

Measured, and not.

  • MEASURED: the server owns an invisible, non-minimised Unity.BatchModeWindow that was not the foreground window at any of four reads (one launch was queried). A server-only session wrote the eight values in the table into the shared key. On both launches that were bracketed individually, the stored position changed between a read during the run and a read after the quit, and the second time it changed to that launch's own window position.
  • NOT MEASURED: that the write happens AT quit. Both brackets also contain the world load (inWorld was reached at 15:50:01 and 15:54:17, a few seconds before each stop), so a write during loading is not excluded.
  • NOT MEASURED: that every quit writes. Two of two bracketed stops did. The other 20 launches were not bracketed one at a time, and the launch that crashed was not checked on its own.
  • NOT MEASURED: whether the eight unchanged values are written again with the same numbers or not written at all. Equal before and after, the reads cannot tell.

Of the eight that moved, the position pair is the one the game never re-asserts (see "What is shared, and at what scope"), so it is the pair a server-only session leaves moved unless something puts it back.

What in the game reads that identity

Four sites in Assembly-CSharp, all of them harmless to a rename, plus whatever Unity does natively:

Line Use
103119 TreeString.Node("name: " + Application.productName, myParent); in the systeminfo console command's debug tree. Display only.
194894 private static string PATH_VERSIONED => string.Format("{0}/{1}-v{2}.xml", Application.persistentDataPath, "PlayerCookie", (ushort)2);
194896 private static string PATH_NONVERSIONED => Application.persistentDataPath + "/PlayerCookie.xml";
210491 SetWindowText(FindWindow(null, Application.productName), name); inside GameManager.SetApplicationWindowName(string). Finds its own window by title, so it keeps working as long as the title matches the product name.

Application.companyName has zero occurrences in the assembly; only Unity's native side consumes it. StationeersLaunchPad.decompiled.cs has zero occurrences of persistentDataPath, Application.productName and Application.companyName, so the loader does not depend on the identity either.

Renaming productName therefore relocates PlayerCookie-v2.xml (the per-Windows-user identity file named in the repo's rig rules) along with the registry key, in one edit. See PlayerIdentityAcrossRejoin for what that file carries.

The trap: the file is hard-linked across instances

The client rig builds each instance tree from hard links into the Steam install, so globalgamemanagers is ONE inode under three names. Measured with fsutil hardlink list on 2026-09-21, link count 3 (install and rig roots shown as placeholders):

$(StationeersPath)\rocketstation_Data\globalgamemanagers
%STATIONEERS_CLIENTRIG_ROOT%\hostie\rocketstation_Data\globalgamemanagers
%STATIONEERS_CLIENTRIG_ROOT%\joiner\rocketstation_Data\globalgamemanagers

An in-place byte edit of any one of those three paths edits all three, including the developer's Steam install. Giving one instance its own identity means deleting the link and writing a fresh independent copy in its place, never editing through it. The same applies to any other file in an instance tree.

What can be done about it

Four directions, ordered by how much they actually isolate:

  1. Per-instance setting.xml via -settingspath. Fixes which resolution the game asks for, so the instance comes up windowed at the size you chose. Does NOT protect the shared registry: the Screen.SetResolution(800, 600, false, ...) that results is exactly what writes Screenmanager Resolution Width = 800. Cheap, already in use, insufficient on its own.
  2. Rewrite Settings.CurrentData in memory before the game applies it. This is what the rig's WindowMode does, and it is strictly better than calling Screen.SetResolution afterwards (which would be overwritten by the game's own call). Same limitation as 1: it changes what gets written to the shared key, not whether.
  3. Save and restore the Screenmanager * values around a session. Bounded and reversible, but it is a per-Windows-user global, so it races any other instance and the developer's own client.
  4. Give the rig its own companyName / productName. The only option that actually separates the scopes: a different key and a different persistentDataPath, so PlayerCookie-v2.xml separates too. Costs a real, non-hard-linked copy of the install per identity (see the hard-link trap above), a name inside the 9-12 / 13-16 character windows, and it takes the tree outside anything Steam will validate.

Option 4 is the one this page exists to make possible; options 1 and 2 are what is deployed today.

Verification history

  • 2026-09-21: page created. Every claim re-derived from .work/decomp/0.2.6428.27798/Assembly-CSharp.decompiled.cs, from the shipped binaries, and from live machine state; nothing carried over on trust. Six corrections to the source material are recorded here rather than dropped.
  • There are four Screen.SetResolution call sites, not two. The source material named LoadSettings (272283) and ApplyVideoSettings (272328) and missed ApplyDisplaySettings (272252) and OnValueChanged(SettingType) case RefreshRate (272942).
  • ApplyVideoSettings is not reached from Settings.OnInitComplete. OnInitComplete (271546) is hooked to WorldManager.OnGameDataLoaded in Settings.Awake (271534) as claimed, but its entire body is the ValidateSavePath check. The line the source material pointed at, 271531, is the last statement of Settings.Initialize() (271499). The conclusion the claim drew (that it fires at the main menu rather than only in a world) survives for a different reason: both Settings.Initialize() and Settings.ApplyVideoSettings() are called from GameManager.Start() (210572 and 210582), at process start.
  • "Neither is guarded by IsBatchMode" is half right. LoadSettings is unguarded, and so is the GameManager.Start() route into ApplyVideoSettings. The other route, Settings.Initialize(), returns early under if (GameManager.IsBatchMode) and never reaches its tail call. The practical claim (launch flags lose in batch mode too) holds.
  • The registry holds more than the six values claimed. UnityPlayer.dll defines 17 Screenmanager * key names and 15 of them are live on this machine, alongside UnitySelectMonitor, UnityGraphicsQuality and two unity.player_* telemetry values.
  • The "documented Unity behaviour, not verified" caveat on the registry write is narrower than stated. What is now measured: the 17 key names exist in UnityPlayer.dll, Screenmanager has zero occurrences in Assembly-CSharp, and the values are live in HKCU\Software\Rocketwerkz\rocketstation holding the rig's 800x600 while the developer's setting.xml holds 3840x2160. What remains INFERRED is only the precise moment of the native write (no game launch was observed for this page).
  • Application.companyName has zero consumers in Assembly-CSharp. The source material listed it beside productName and persistentDataPath as though it were consumed. The four line numbers it gave for the others (103119, 194894, 194896, 210491) are all exact.
  • 2026-09-21: corroborated from a second, independent direction. A concurrent rig pass recorded in TestRig/RESEARCH.md measured the same registry transition on three runs in one night and corrected an earlier rig-side claim that "nothing writes to the PlayerPrefs registry key". Two measurements taken different ways (a live key read here, a before-and-after across runs there) agree, which is why the per-run write is stated as measured rather than inferred.
  • 2026-09-21: conflict raised, NOT resolved on this pass. StationeersLaunchPadDedicatedServer states, in a section stamped 0.2.6228.27061 @ 2026-04-29, that the key is HKCU\Software\Rocketwerkz Limited\rocketstation. Three independent measurements on 2026-09-21 say Rocketwerkz with no Limited: the companyName bytes at offset 1288 of globalgamemanagers (11 characters, one occurrence in the file), the live key enumeration under HKCU\Software (no Rocketwerkz Limited key exists), and persistentDataPath resolving to ...\AppData\LocalLow\Rocketwerkz\rocketstation on disk. DrivingTheGameClientProgrammatically already says Rocketwerkz, so the two existing pages disagree with each other. Per WORKFLOW.md Rule 3 the stamped claim was left untouched pending a fresh validator; see Open questions.
  • 2026-09-21: conflict on "the Unity PlayerPrefs registry key path". Previous claim: HKCU\Software\Rocketwerkz Limited\rocketstation, on StationeersLaunchPadDedicatedServer option D, in a section stamped 0.2.6228.27061 @ 2026-04-29. New finding: HKCU\Software\Rocketwerkz\rocketstation, from the companyName bytes in globalgamemanagers and a live registry enumeration. Fresh validator verdict (spawned per WORKFLOW.md Rule 3 with the question framed neutrally and neither claim identified as current): B is correct, quoting the hex at offset 1288 verbatim, adding rocketstation_Data/app.info (Rocketwerkz / rocketstation) as an independent corroboration of the same pair, and reporting HKCU:\Software as 157 subkeys of which exactly one matches Rocket*. Result: the claim was ALWAYS WRONG, not stale. Three things settle which of "always wrong" and "changed since" it is:
  • %USERPROFILE%\AppData\LocalLow\Rocketwerkz and its rocketstation child both carry CreationTime 2023-07-01 19:04:50, so persistentDataPath, built from the same two strings, has resolved to this pair continuously for over three years.
  • A dated snapshot the developer left at ...\LocalLow\Rocketwerkz\rocketstation - Copy was created 2026-04-28 22:29:39, the day BEFORE the disputed section's stamp, and the Player.log inside it (last written 2026-04-28 22:29:15, real launch header Mono path[0] = '$(StationeersPath)/rocketstation_Data/Managed') records the running game resolving .../LocalLow/Rocketwerkz/rocketstation\Blueprints. Sessions contemporaneous with the claim were already writing under Rocketwerkz.
  • Unity does not clean up the old directory or key when companyName changes, it just starts writing a new one. No Rocketwerkz Limited leftover exists: not among the 44 company directories in LocalLow, not among the 157 HKCU:\Software subkeys, not under HKCU:\Software\AppDataLow\Software, and not as a byte string in either install's globalgamemanagers. The mechanism behind the original error is now documented in "The decoy: Rocketwerkz Limited is a real string, in a different field" above: it is the Assembly-CSharp.dll Win32 version resource CompanyName, a different field from Unity's PlayerSettings.companyName. The commit that introduced the claim, c4434503 (2026-05-02, "Verified at game version 0.2.6228.27061"), cites only the Platform.CheckIsServer() decompile at line 4433; the registry path was never inside a cited provenance. Result on this page: two subsections added (the decoy table, and the dedicated server sharing the identity), the resolved item removed from Open questions. Result on the other page: option D corrected to point here rather than restate the fact.
  • 2026-09-21: hypothesis ©, "the dedicated server genuinely uses a different key from the client", tested and rejected. The Dedicated Server build ships its own globalgamemanagers (6,205,532 bytes, different sha256 from the client's 6,212,900) but bytes 1270 to 1339 are byte-identical, and both app.info files read Rocketwerkz / rocketstation. Curated into "The dedicated server has its own copy of the same identity" above. The disputed sentence was in any case about two CLIENT instances, so a server-specific key would not have excused it.
  • 2026-09-21: the dedicated server's write to the shared key upgraded from INFERRED to MEASURED. Additive, nothing contradicted: "The dedicated server has its own copy of the same identity" had inferred the write from the identical identity strings, with no server launch observed, and a server-only rig session the same afternoon observed it. Evidence: a read-only window enumeration of the running server (one launch, four reads), reg query of the key at five points around the session and its last two launches, the rig's own release report for that session, and two reads taken outside it. The numbers in the new subsection "A server-only session writes the key: measured" are copied verbatim from that session's scratch (.work/2026-09-21-jit-probe/backup/registry-display-*.txt, .work/2026-09-21-jit-probe/regen2/windows.txt, .work/2026-09-21-jit-probe/09-unlock.txt, and .work/2026-09-21-dupe-repro/screenmanager-after-manual-restore.txt with screenmanager-after-probe.txt), so the page does not depend on those files surviving. Recorded as measured: the window, the eight moved values, two individually bracketed launches whose quits changed the stored position, the second to its own window's position, and that this batch-mode window was not placed from the stored position. Recorded as open rather than claimed: that the write is AT quit rather than during the world load both brackets also contain, that every quit writes (two of two bracketed stops did; the crash was not checked on its own), and whether the eight unchanged values are rewritten. The window's rename between the third and fourth reads was traced to the server build's GameManager.Awake (Assembly-CSharp.DedicatedServer.decompiled.cs 198446-198458). Also: the MEASURED / INFERRED paragraph rewritten to match, the server's persistentDataPath marked as still inferred (no server-side read of it exists, and the session changed nothing under that folder), one sentence added to "What is shared, and at what scope", the first open question narrowed for the server, and a second one added.

Open questions

  • The exact moment Unity's native player writes the Screenmanager * values (at SetResolution, at window close, or on a timer) was not observed, because pinning it down requires watching a launch and this pass did not. That the write happens once per run IS measured, on the rig side; only its position inside the run is open. For the dedicated server it is now bracketed but not pinned: on two launches the key still held the previous run's values 15 s and 34 s in and held new ones after the quit, and each bracket also spans the world load (see "A server-only session writes the key: measured").
  • Whether the dedicated server writes on every quit, including a crash exit, and whether it rewrites the eight display values that read the same before and after a server-only session (Screenmanager Stereo 3D, the four Default values, UnitySelectMonitor, and the native refresh rate pair). The 2026-09-21 session bracketed two clean stops individually, and both wrote.
  • Whether a rewritten companyName / productName survives a Steam file-validation pass, and whether Steam restores globalgamemanagers on the next update, is untested. Option 4 above assumes a copied install precisely to avoid finding out.