Key Binding¶
Stationeers' rebindable input system stores per-action key assignments by name in a player-local XML settings file, with full preservation of left/right modifier-key distinction at every layer (Unity input, in-memory storage, on-disk persistence, and the rebind UI). Mods that want to programmatically rebind a key (or detect a specific left/right modifier) work entirely within Unity's KeyCode vocabulary, with no abstraction layer in the way.
Layer summary¶
| Layer | API surface | Distinguishes Left/Right? |
|---|---|---|
| Unity input | Input.GetKey(KeyCode) / Input.GetKeyDown(KeyCode) |
Yes — KeyCode.LeftShift (304) and KeyCode.RightShift (303) are separate enum members; same for Control and Alt |
| KeyManager wrapper | KeyManager.GetButton(KeyCode key) (Assembly-CSharp.dll line 43799-43806) |
Yes — passes through to Input.GetKey after one console-window guard |
| In-memory rebindable map | KeyMap.ThirdPersonControl, KeyMap.MoveAllOfType, etc. — public static KeyCode fields |
Yes — each holds one specific KeyCode |
KeyMap mirror (_-prefixed KeyWrap objects) |
KeyMap._ThirdPersonControl.AssignKey(KeyCode) |
Yes |
| Per-setting metadata | KeyItem.Key ([XmlElement] public KeyCode Key, line 42944) |
Yes — KeyCode field, serialized as enum name |
| On-disk persistence | <savePath>/setting.xml written by Settings.SettingData.Save() via XmlSerialization |
Yes — KeyCode serializes as enum name ("LeftShift" / "RightShift"); round-trips losslessly |
| Rebind UI capture | InputKeyWindow.Update() iterates Enum.GetValues(typeof(KeyCode)) and picks the first one for which Input.GetKeyDown(keyCode) returns true |
Yes — captures exactly what the player presses |
KeyManager.GetButton verbatim¶
From KeyManager (line 43799):
public static bool GetButton(KeyCode key)
{
if (key != KeyMap.ToggleConsole && ConsoleWindow.IsOpen)
{
return false;
}
return Input.GetKey(key);
}
GetButtonDown (line 43781) and GetButtonUp (line 43790) follow the identical pattern, calling Input.GetKeyDown / Input.GetKeyUp respectively. The console-open guard suppresses all input checks except the console-toggle key.
KeyMap structure¶
KeyMap (top-level class, no namespace) holds two parallel sets of fields per binding:
public static KeyCode <Name>— the raw KeyCode for direct comparison.public static readonly KeyWrap _<Name>— aKeyWrapobject carrying the same key plus event-binding hooks (Bind(InputPhase, Action, KeyInputState)).
Both are kept in sync. Defaults are assigned in a method that runs at startup (line 43358 onwards) like:
KeyMap.ToggleLight = KeyCode.L;
KeyMap.MouseControl = KeyCode.LeftAlt;
KeyMap.ThirdPersonControl = KeyCode.LeftShift;
KeyMap.HideAllWindows = KeyCode.BackQuote;
// ...
KeyMap._ToggleLight.AssignKey(KeyCode.L);
KeyMap._MouseControl.AssignKey(KeyCode.LeftAlt);
KeyMap._ThirdPersonControl.AssignKey(KeyCode.LeftShift);
// ...
When loading from settings (line 43624 onwards):
KeyMap.ThirdPersonControl = GetKey("ThirdPersonControl");
KeyMap._ThirdPersonControl.AssignKey(GetKey("ThirdPersonControl"));
GetKey(string name) (line 43528) reads from KeyItemLookup:
public static KeyCode GetKey(string _name)
{
KeyItemLookup.TryGetValue(_name, out var value);
return value?.Key ?? KeyCode.None;
}
KeyItem (the per-binding metadata record)¶
From KeyItem (line 42938):
public class KeyItem
{
[XmlElement]
public string Name;
[XmlElement]
public KeyCode Key;
[XmlIgnore]
public KeyCode DefaultKey;
[XmlIgnore]
public int KeyHash;
[XmlIgnore]
public ControlsAssignment Display;
[XmlIgnore]
public int Index;
[XmlIgnore]
public bool Hidden;
public event Action OnChanged;
public KeyItem(string name, KeyCode key, bool isHidden = false)
{
Name = name;
Key = key;
DefaultKey = key;
Hidden = isHidden;
}
public void Changed() => this.OnChanged?.Invoke();
}
KeyItem.Key is the live binding (mutable). DefaultKey is the original assignment (used by "reset to default" UI). The [XmlElement] KeyCode Key field is the only thing that gets serialized; XmlSerializer writes enum values as their name strings, preserving Left/Right distinction.
Persistence: setting.xml¶
KeyManager.AllKeys is the master List<KeyItem>. Settings.SaveSettings() (line 249472) bundles it into the live settings record:
public static void SaveSettings()
{
CurrentData.SettingsVersion = GameManager.GetGameVersion();
// ... controller axes ...
CurrentData.KeyList = KeyManager.AllKeys;
// ... voice notifications, audio mode ...
CurrentData.Save();
}
The save path resolves to <save folder>/setting.xml (Settings.SettingData.Path getter, line 248582):
[XmlIgnore]
public static string Path
{
get
{
if (string.IsNullOrEmpty(_path))
{
_path = System.IO.Path.Combine(StationSaveUtils.GetSavePath(), "setting.xml");
}
return _path;
}
}
public void Save()
{
this.SaveXml(Path);
}
KeyList is serialized as a sequence of KeyItem elements, each with <Name>...</Name> and <Key>...</Key>. Example:
(or <Key>RightShift</Key> if the player rebound it).
Rebind UI capture path¶
InputKeyWindow.Update() (line 222669) is the per-frame poll that captures the next-pressed key once a rebind has been initiated:
public static Array KeyCodes = Enum.GetValues(typeof(KeyCode));
private void Update()
{
if (!InputControlBase.IsRecording)
{
return;
}
foreach (KeyCode keyCode in KeyCodes)
{
if (Input.GetKeyDown(keyCode))
{
_currentKey = keyCode;
Instance.AssignmentText.text = Localization.GetKeyName(_currentKey).ToUpper();
InputControlBase.IsRecording = false;
AssignmentAnimator.SetBool("Input", value: false);
break;
}
}
}
Enum.GetValues(typeof(KeyCode)) enumerates ALL Unity KeyCode members including LeftShift (304), RightShift (303), LeftAlt, RightAlt, LeftControl, RightControl as separate entries. The first one for which Input.GetKeyDown(keyCode) returns true wins.
Subtlety: enum iteration order matches numeric order. RightShift (303) appears before LeftShift (304); same for Control (Right=305, Left=306) and Alt (Right=307, Left=308). If the player holds BOTH shifts down before pressing for rebind, RightShift gets captured first because the loop breaks on the first match. In normal use (player presses one specific key), the captured value matches the physical key pressed.
On submit, InputKeyWindow.ButtonInputSubmit() (line 222688) calls _currentAssignment.Assign(_currentKey):
public void Assign(KeyCode key, bool refresh = true)
{
Name.text = KeyItem.Name.ToProper();
Assignment.text = Localization.GetKeyName(key);
KeyItem.Key = key;
Register(KeyItem);
if (refresh)
{
KeyManager.LoadKeyboardSetting();
}
KeyItem.Changed();
}
KeyManager.LoadKeyboardSetting() (line 43571) re-reads every binding from KeyItemLookup into the corresponding KeyMap.<Name> field plus the _<Name>.AssignKey(...) mirror. Then the next KeyManager.GetButton(KeyMap.<Name>) call sees the new value.
Programmatic rebind from a mod¶
To change a key binding from mod code (e.g. to resolve a default-binding conflict):
if (KeyManager.KeyItemLookup.TryGetValue("ThirdPersonControl", out var keyItem) && keyItem != null)
{
if (keyItem.Key == KeyCode.LeftShift)
{
keyItem.Key = KeyCode.RightShift;
KeyMap.ThirdPersonControl = KeyCode.RightShift;
KeyMap._ThirdPersonControl.AssignKey(KeyCode.RightShift);
keyItem.Changed(); // fires OnChanged listeners
Settings.SaveSettings();// persists the updated KeyList to setting.xml
}
}
All four updates are required for a clean rebind:
keyItem.Key— for nextLoadKeyboardSettingre-sync and for any code that reads viaKeyItemLookup.KeyMap.<Name>— for any code that reads the raw KeyCode field directly (e.g. vanilla camera handler readsKeyManager.GetButton(KeyMap.ThirdPersonControl)).KeyMap._<Name>.AssignKey(KeyCode)— for theKeyWrap-driven event bindings (Bind(InputPhase, Action, ...)).Settings.SaveSettings()— to persist; without it the change is in-memory-only and reverts on next launch.
keyItem.Changed() is optional but recommended — it fires the OnChanged event so any UI listening to the binding (e.g. the controls panel showing the assigned key) refreshes.
Settings.SaveSettings() writes the entire setting.xml, not just the changed key. It is inexpensive and safe to call after any in-memory settings change.
MouseInspect: a vanilla LeftShift binding that does NOT conflict with bare-Shift mods¶
KeyMap.MouseInspect defaults to KeyCode.LeftShift (Assembly-CSharp.dll line 43370):
Despite sharing the LeftShift default with KeyMap.ThirdPersonControl, MouseInspect is in KeyManager.NeverConflict (line 43046):
This explicitly exempts MouseInspect from the in-game key-conflict warning UI even when its key collides with another binding's key. The exemption exists because MouseInspect is structured as a SECONDARY modifier that requires a primary mode to be active first — its three usage sites are all gated by being in a different game state than Mode.Normal:
| Site | Class.Method | Gate | Effect |
|---|---|---|---|
| Click suppression | SlotDisplay.PrimaryAction (line 229295), SlotDisplay.SecondaryAction (line 229314) |
InputMouse.IsMouseControl (default Alt held — Cursor.visible == true) |
Suppresses click and smart-swap actions while in mouse-control mode (so the player can hover over slots without accidentally clicking) |
| Placement-rotation reverse | InventoryManager.PlacementMode (line 270450) |
CurrentMode == Mode.Placement (player holding a constructor) |
Reverses scroll-rotation: scroll-up rotates "previous" instead of "next" |
| Precision-placement | InventoryManager.PrecisionPlacementMode (line 270658) |
CurrentMode == Mode.PrecisionPlacement |
Similar reverse-rotation modifier in precision mode |
Implication for scroll-modifier mods that bind LeftShift in Mode.Normal (e.g. EquipmentPlus's lens cycle): no conflict. When MouseInspect's first usage condition is true (InputMouse.IsMouseControl, i.e. Alt held), the cursor is visible — and InventoryManager.ManagerUpdate early-returns before calling either CheckDisplaySlotInput or NormalMode (line 269276 — see ./ScrollInputHandling.md "Cursor-visible gate"). When MouseInspect's other usages fire, CurrentMode is Mode.Placement or Mode.PrecisionPlacement, so the dispatch switch routes to those mode handlers and skips NormalMode. A mod that prefixes InventoryManager.NormalMode is therefore never running while MouseInspect could fire.
The shared LeftShift default is annoying-looking in the controls UI but operationally harmless. Mods with a bare-LeftShift scroll binding can ignore it.
Implications for scroll-modifier mods¶
For mods that bind scroll combos involving the Shift modifier:
CameraController.CacheCameraPosition(see../GameClasses/CameraController.md) gates its zoom block onKeyManager.GetButton(KeyMap.ThirdPersonControl), defaultKeyCode.LeftShift.- A mod using
KeyManager.GetButton(KeyCode.LeftShift)(orInput.GetKey(KeyCode.LeftShift)) for a scroll modifier would conflict with the vanilla camera zoom on default keybinds — both fire on bare LeftShift+scroll. - A mod can sidestep this WITHOUT touching the vanilla camera handler by:
- Programmatically rebinding
KeyMap.ThirdPersonControlfromLeftShifttoRightShift(above pattern), only when it currently equals the default LeftShift (respect player customization). - Detecting
KeyCode.LeftShiftspecifically in the mod's own scroll handler. - After the rebind, vanilla camera fires only on RightShift+scroll; LeftShift+scroll is free for the mod.
This avoids a Harmony patch on the camera handler while preserving vanilla camera-zoom functionality on a different physical key. Trade-off: the player's first-launch experience shows the camera key moved unexpectedly; the mod must surface this (in-game chat message or README) to avoid surprise.
Verification history¶
- 2026-04-27: page created. All sections verified by
ilspycmddecompile ofE:/Steam/steamapps/common/Stationeers/rocketstation_Data/Managed/Assembly-CSharp.dllat game version 0.2.6228.27061. Triggered by EquipmentPlus's TODO B modifier-redesign discussion (resolving the "ALT is reserved by the game, can we use Left/Right shift distinction instead?" question). Documents the full input pipeline from Unity'sInput.GetKeythroughKeyManager.GetButton,KeyMap.<Name>raw fields,KeyItem.KeyXML-serialized records, on-disksetting.xmlpersistence, and theInputKeyWindow.Updaterebind capture loop. Confirms Left/Right modifier distinction is preserved at every layer with concrete verbatim code excerpts. Also documents the four-step programmatic-rebind pattern for mods that want to resolve binding conflicts without patching vanilla. - 2026-04-27b: added "MouseInspect: a vanilla LeftShift binding that does NOT conflict with bare-Shift mods" section. Triggered by an EquipmentPlus user question about the apparent shared-LeftShift conflict between
KeyMap.MouseInspect(default LeftShift) and EquipmentPlus's lens-cycle binding (also LeftShift+scroll). Verified via decompile that MouseInspect is inKeyManager.NeverConflict(line 43046) and that all three of its usage sites are gated by game state different fromMode.Normal(mouse-control mode active =Cursor.visible; orCurrentMode == Mode.Placement / PrecisionPlacement). Mods that prefixInventoryManager.NormalModeare never running concurrently with MouseInspect. Additive section; no prior content changed. Top-levelverified_atadvanced to 2026-04-27b to indicate a same-day update.
Open questions¶
None at creation.