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.xmlstill reads<ScreenWidth>3840</ScreenWidth> <ScreenHeight>2160</ScreenHeight> <RefreshRate>120</RefreshRate> <FullScreen>true</FullScreen>, untouched, and HKCU\Software\Rocketwerkz\rocketstationreadsScreenmanager 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.xmlon every boot (four call sites below), so a poisonedScreenmanager 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 ofAssembly-CSharpexecutes. - The game never re-asserts window POSITION.
Screenmanager Window Position X/Y(observed at 1519 / 760) have no counterpart insetting.xmland no game-side writer:Assembly-CSharpcontains zero OS-level window-position calls. EverySetWindowPosoccurrence 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) isImGui.SetWindowPoson an in-game debug panel. The onlyuser32.dllimports areSetWindowTextandFindWindow(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:
companyNameneedslength + 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.productNameneedslength + 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:
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
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.logat 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.BatchModeWindowthat 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 (
inWorldwas 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:
- Per-instance
setting.xmlvia-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: theScreen.SetResolution(800, 600, false, ...)that results is exactly what writesScreenmanager Resolution Width = 800. Cheap, already in use, insufficient on its own. - Rewrite
Settings.CurrentDatain memory before the game applies it. This is what the rig'sWindowModedoes, and it is strictly better than callingScreen.SetResolutionafterwards (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. - 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. - Give the rig its own
companyName/productName. The only option that actually separates the scopes: a different key and a differentpersistentDataPath, soPlayerCookie-v2.xmlseparates 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.SetResolutioncall sites, not two. The source material namedLoadSettings(272283) andApplyVideoSettings(272328) and missedApplyDisplaySettings(272252) andOnValueChanged(SettingType)caseRefreshRate(272942). ApplyVideoSettingsis not reached fromSettings.OnInitComplete.OnInitComplete(271546) is hooked toWorldManager.OnGameDataLoadedinSettings.Awake(271534) as claimed, but its entire body is theValidateSavePathcheck. The line the source material pointed at, 271531, is the last statement ofSettings.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: bothSettings.Initialize()andSettings.ApplyVideoSettings()are called fromGameManager.Start()(210572 and 210582), at process start.- "Neither is guarded by
IsBatchMode" is half right.LoadSettingsis unguarded, and so is theGameManager.Start()route intoApplyVideoSettings. The other route,Settings.Initialize(), returns early underif (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.dlldefines 17Screenmanager *key names and 15 of them are live on this machine, alongsideUnitySelectMonitor,UnityGraphicsQualityand twounity.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,Screenmanagerhas zero occurrences inAssembly-CSharp, and the values are live inHKCU\Software\Rocketwerkz\rocketstationholding the rig's 800x600 while the developer'ssetting.xmlholds 3840x2160. What remains INFERRED is only the precise moment of the native write (no game launch was observed for this page). Application.companyNamehas zero consumers inAssembly-CSharp. The source material listed it besideproductNameandpersistentDataPathas 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.mdmeasured 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 isHKCU\Software\Rocketwerkz Limited\rocketstation. Three independent measurements on 2026-09-21 sayRocketwerkzwith noLimited: thecompanyNamebytes at offset 1288 ofglobalgamemanagers(11 characters, one occurrence in the file), the live key enumeration underHKCU\Software(noRocketwerkz Limitedkey exists), andpersistentDataPathresolving to...\AppData\LocalLow\Rocketwerkz\rocketstationon disk. DrivingTheGameClientProgrammatically already saysRocketwerkz, so the two existing pages disagree with each other. PerWORKFLOW.mdRule 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 stamped0.2.6228.27061 @ 2026-04-29. New finding:HKCU\Software\Rocketwerkz\rocketstation, from thecompanyNamebytes inglobalgamemanagersand a live registry enumeration. Fresh validator verdict (spawned perWORKFLOW.mdRule 3 with the question framed neutrally and neither claim identified as current): B is correct, quoting the hex at offset 1288 verbatim, addingrocketstation_Data/app.info(Rocketwerkz/rocketstation) as an independent corroboration of the same pair, and reportingHKCU:\Softwareas 157 subkeys of which exactly one matchesRocket*. Result: the claim was ALWAYS WRONG, not stale. Three things settle which of "always wrong" and "changed since" it is: %USERPROFILE%\AppData\LocalLow\Rocketwerkzand itsrocketstationchild both carry CreationTime2023-07-01 19:04:50, sopersistentDataPath, 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 - Copywas created2026-04-28 22:29:39, the day BEFORE the disputed section's stamp, and thePlayer.loginside it (last written2026-04-28 22:29:15, real launch headerMono path[0] = '$(StationeersPath)/rocketstation_Data/Managed') records the running game resolving.../LocalLow/Rocketwerkz/rocketstation\Blueprints. Sessions contemporaneous with the claim were already writing underRocketwerkz. - Unity does not clean up the old directory or key when
companyNamechanges, it just starts writing a new one. NoRocketwerkz Limitedleftover exists: not among the 44 company directories inLocalLow, not among the 157HKCU:\Softwaresubkeys, not underHKCU:\Software\AppDataLow\Software, and not as a byte string in either install'sglobalgamemanagers. The mechanism behind the original error is now documented in "The decoy:Rocketwerkz Limitedis a real string, in a different field" above: it is theAssembly-CSharp.dllWin32 version resourceCompanyName, a different field from Unity'sPlayerSettings.companyName. The commit that introduced the claim,c4434503(2026-05-02, "Verified at game version 0.2.6228.27061"), cites only thePlatform.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 bothapp.infofiles readRocketwerkz/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 queryof 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.txtwithscreenmanager-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'sGameManager.Awake(Assembly-CSharp.DedicatedServer.decompiled.cs198446-198458). Also: the MEASURED / INFERRED paragraph rewritten to match, the server'spersistentDataPathmarked 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 (atSetResolution, 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 fourDefaultvalues,UnitySelectMonitor, and the native refresh rate pair). The 2026-09-21 session bracketed two clean stops individually, and both wrote. - Whether a rewritten
companyName/productNamesurvives a Steam file-validation pass, and whether Steam restoresglobalgamemanagerson the next update, is untested. Option 4 above assumes a copied install precisely to avoid finding out.