Inventory Auto-Equip APIs¶
The vanilla APIs a mod needs to programmatically move items between a player's slots: InventoryManager.SmartStow (find-a-spot-and-stow), HumanHandsBehaviour.SwapHands (flip active vs inactive hand without moving items), and OnServer.MoveToSlot (universal multiplayer-safe slot move). All three are usable from mod code without role checks; OnServer wraps the network round-trip for clients automatically.
InventoryManager.SmartStow¶
Signature (Assembly-CSharp.dll line 269801):
Behavior: looks for a free slot to move selectedSlot.Occupant into, with a search hierarchy:
ParentHuman.GetFreeSlot(selectedSlot.Occupant.SlotType, _excludeHandSlots)— first available player-slot of matching type, excluding hands.- If that returned the BackpackSlot AND the SuitSlot already contains a
SuitBase, reject (suit takes the back slot visually). - Fall back to the item's "original slot" if recorded in
_originalSlots(per-stow memory) and that origin is still on the same RootParent. - Fall back to
FindFreeSlotOpenWindowsSlotPriority(...)— search currently-open inventory windows. - Fall back to scanning every slot on
ParentHuman.Slots, looking inside any non-hand-slot occupant for a free slot of matching type (e.g. inside the toolbelt, backpack, etc.). - If still no slot:
UIAudioManager.Play(UIAudioManager.ActionFailHash)and return (silent failure to the caller).
Pre-condition for the primary path: LeftHandSlot.Occupant == selectedSlot.Occupant || RightHandSlot.Occupant == selectedSlot.Occupant. The function is intended for stowing-from-hand. Calling with a non-hand source slot exercises a different code path further into the body (not yet documented here).
Returns: void. To detect success/failure from the caller:
- Capture var stowed = activeHandSlot.Occupant before the call.
- Call InventoryManager.SmartStow(activeHandSlot).
- If activeHandSlot.Occupant != stowed (typically null after success), the move worked. Otherwise it failed (item stayed in hand; the fail sound was played).
Multiplayer behavior: SmartStow internally calls OnServer.MoveToSlot which wraps a network request on clients. So the active-hand state on a remote client may not reflect the move until the server's broadcast arrives a tick later. For in-frame success detection on clients, consider deferring the next operation by a frame OR running auto-equip only on host/single-player for the first cut.
HumanHandsBehaviour.SwapHands¶
Signature (Assembly-CSharp.dll line 271889):
Caller pattern from vanilla (line 43170-43173):
private static void SwapHandsOnKeyUp()
{
Human.LocalHuman?.HumanHandsBehaviour._SwapHandsOnKeyUp();
}
_SwapHandsOnKeyUp() (line 271881) is a wrapper that gates on !InventoryManager.Instance.IsUsingSmartTool && !_human.IsUnresponsive && !_human.IsSleeping then calls SwapHands().
Behavior: does NOT move items between physical hand slots. Instead it flips which hand is "active" by swapping InventoryManager.Instance.ActiveHand and InventoryManager.Instance.InactiveHand. The physical contents of LeftHandSlot and RightHandSlot are unchanged.
From the player's perspective: an item that was in their off-hand before the call is now in their active hand after, because we changed which physical hand counts as active.
Side effects (line 271889-271925):
- Cancels precision-placement mode if active.
- Cancels multi-constructor placement.
- Fires the static
SwapHandsEvent. - Updates
PanelHands.Instance.HandleSwitchHands(...)to flip the UI bindings. - Re-runs
RefreshDisplaySlotBindings()andAnimateActiveHands(). - If the now-active hand holds a
Constructorand we're in placement mode, re-runsUpdatePlacement(...). - Adjusts
CursorManager.CursorHitMaskto add/removeLayerMasks.CursorVoxelbased on whether the active hand holds aPickaxe/MiningDrill. - If the now-active hand holds a
Tablet, callstablet.InActiveHand(). - If the now-active hand holds an
OreDetector, callsoreDetector.InActiveHand().
So calling Human.LocalHuman.HumanHandsBehaviour.SwapHands() is a complete "switch which hand is active" operation, fully wired into the vanilla side effects. A mod that wants to make a previously-off-hand item active should use this instead of moving items manually.
OnServer.MoveToSlot¶
Verbatim from Assets.Scripts.OnServer.MoveToSlot (Assembly-CSharp.dll line 39173):
public static void MoveToSlot(DynamicThing childThing, Slot slot)
{
if ((object)childThing != null && !childThing.IsBeingDestroyed)
{
if (GameManager.RunSimulation)
{
childThing.MoveToSlot(slot, slot.Parent);
return;
}
NetworkClient.SendToServer(new MoveToSlotMessage
{
ChildId = childThing.netId,
ParentId = slot.Parent.netId,
SlotId = slot.SlotIndex,
Drag = false
});
}
}
Used in ~20 places across vanilla (Assembly-CSharp.dll lines 41645, 107019, 109915, 113186, 140171, 140260, 146288, 146650, 147060, 158037, 158057, 158126, 160550, 160563, 161048, 161222, 161227, 161239, 161740, 161773, 161774, plus call sites inside InventoryManager.SmartStow and elsewhere).
The vanilla call sites do NOT check NetworkManager.IsServer before calling. The function routes by GameManager.RunSimulation (true on host / single-player → calls Thing.MoveToSlot directly; false on remote client → sends MoveToSlotMessage).
Critical: does NOT auto-swap when target slot is occupied. The vanilla body just calls childThing.MoveToSlot(slot, slot.Parent) — if slot already has an occupant, the move is rejected (no displacement, no swap). Drag-and-drop swap UX is implemented at the UI layer with multiple OnServer.MoveToSlot calls in sequence using a temporary slot.
For mod code that wants to swap two occupied slots, use a 3-way pattern via a temporary location (typically the off-hand if free): move A to temp, move B to A's slot, move A from temp to B's slot. EquipmentPlus's Phase 2 AutoEquipTabletCoroutine uses this pattern with the off-hand as temp; if off-hand is also occupied, the swap aborts with a player-facing console message.
Human slot fields (canonical names + spellings)¶
Assets.Scripts.Objects.Entities.Human exposes its equipment slots as public [NonSerialized] Slot fields (line 339531 onwards). The exact field names matter because the codebase has both naming conventions in different contexts:
[NonSerialized] public Slot SuitSlot;
[NonSerialized] public Slot HelmetSlot;
[NonSerialized] public Slot GlassesSlot;
[NonSerialized] public Slot BackpackSlot;
[NonSerialized] public Slot LeftHandSlot;
[NonSerialized] public Slot RightHandSlot;
[NonSerialized] public Slot UniformSlot;
[NonSerialized] public Slot ToolbeltSlot; // NOTE: 'Toolbelt' is one word, lowercase 'b'
The KeyMap.ToolBeltSlot static is spelled differently (KeyMap.ToolBeltSlot with capital B) because that's the BIND-NAME used by the rebind UI, distinct from the slot field. Don't confuse them.
InventoryManager.ActiveHandSlot returns the active hand's underlying Slot (the one in LeftHandSlot or RightHandSlot). InventoryManager.Instance.ActiveHand returns the SlotDisplay wrapper; use .Slot to drill to the underlying slot.
Auto-equip recipe (for tablet / lens / similar use cases)¶
Composite recipe combining the three APIs above for a "Ctrl+scroll auto-equip a tablet" workflow:
// 1. If active hand already has the target item, just use it.
var active = InventoryManager.ActiveHand;
if (active?.Slot?.Get() is AdvancedTablet activeTablet)
{
UseTablet(activeTablet);
return;
}
// 2. If off-hand has it, flip which hand is active. No item moves.
var inactive = InventoryManager.Instance.InactiveHand;
if (inactive?.Slot?.Get() is AdvancedTablet)
{
Human.LocalHuman?.HumanHandsBehaviour?.SwapHands();
return; // subsequent input cycles uses the now-active tablet
}
// 3. Search inventory for the target item.
// Tool belt occupant slots, then backpack occupant slots, then suit slots, no nested.
// (Implementation detail per the consumer; not vanilla.)
if (!TryFindInInventory<AdvancedTablet>(out var found, out var foundSlot))
return; // no-op; target not present
// 4. If active hand is empty, just move it directly.
if (active.Slot.Get() == null)
{
OnServer.MoveToSlot(found, active.Slot);
return;
}
// 5. Active hand holds something else — try to stow it first.
var prevOccupant = active.Slot.Get();
InventoryWindowManager.Instance?.SmartStow(active.Slot);
if (active.Slot.Get() == null)
{
// SmartStow succeeded; equip target.
OnServer.MoveToSlot(found, active.Slot);
return;
}
// 6. SmartStow failed (no free slot). Fall back to swap: previous item
// moves to where the target was, target moves to active hand.
// Caveat on remote clients: both moves are async; the visible result
// arrives a tick later and intermediate frames may show the swap
// in-progress. Acceptable for v1; revisit if it causes UX issues.
OnServer.MoveToSlot(prevOccupant, foundSlot);
OnServer.MoveToSlot(found, active.Slot);
Multiplayer note: each OnServer.MoveToSlot on a remote client is async. The recipe above is correct on host/single-player (each move applies before the next runs); on remote clients, the moves are queued network requests, so step 5 (success detection) can produce a false-negative in the same frame the SmartStow request was sent. For tighter detection, gate the auto-equip on NetworkManager.IsServer and require a vanilla manual equip on remote clients, OR run a coroutine that waits one tick before checking.
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 Phase 2 design pass (auto-equip a tablet on Ctrl+scroll when none is in hand). Documents the three vanilla APIs (SmartStow,SwapHands,OnServer.MoveToSlot) and a composite recipe for the auto-equip use case, including the in-frame multiplayer-async caveat for remote-client success detection. - 2026-04-27b: added "Human slot fields (canonical names + spellings)" section. Verbatim list of public
[NonSerialized] Slotfields onAssets.Scripts.Objects.Entities.Human(line 339531 onwards). Critical detail:Human.ToolbeltSlotis spelled with lowercase 'b' as one word, distinct fromKeyMap.ToolBeltSlot(capital B, used as the rebind binding-name). Triggered by EquipmentPlus's Phase 2 inventory-search implementation needing the correct field name. - 2026-04-27c: corrected class name for
SmartStow(Slot)static method throughout the page. The method lives onInventoryManager(Assembly-CSharp.dll line 269801, inside the InventoryManager class body), NOT onInventoryWindowManager. The page-creation entry mistakenly attributed it toInventoryWindowManager. Note thatInventoryWindowManagerDOES have a separate, instance-method, no-argSmartStow()(the one bound to the SmartStow keybind viaKeyManager.SmartStowat line 43190); for a specific slot, callers use the staticInventoryManager.SmartStow(Slot). Caught during EquipmentPlus Phase 2 build (compile error: "No overload for method 'SmartStow' takes 1 arguments" onInventoryWindowManager.SmartStow(slot)). - 2026-04-27d: added the verbatim
OnServer.MoveToSlotbody (line 39173) and the critical no-swap clarification. Confirmed by reading the function: it routes byGameManager.RunSimulation(host: directchildThing.MoveToSlot(slot, slot.Parent)call; client:MoveToSlotMessagesend), and there is NO occupied-target handling in the body — moves into occupied slots are rejected, no displacement, no swap. Added recipe note about the 3-way swap pattern via the off-hand as temp slot (used by EquipmentPlus's Phase 2AutoEquipTabletCoroutine). Triggered by an EquipmentPlus user-test failure: the original 2-step swap fallback (move prevOccupant -> sourceSlot, then tablet -> activeHandSlot) failed silently because sourceSlot still held the tablet at the time of the first move.
Open questions¶
- The non-hand-source code path of
SmartStow(whenselectedSlot.Occupantis NOT in either hand) was not traced past line 269807. Likely fall-through to theOnServer.MoveToSlotblock lower in the method, but the gating logic differs. Document on first need. OnServer.MoveToSloton remote clients: exact mechanism (custom INetworkMessage? proxy method? RPC?) not yet decomposed. Vanilla call sites use it as if synchronous, which suggests the engine handles the round-trip transparently. Worth a focused decompile pass if a future mod needs precise async semantics.