SlotInsertionBlock¶
How to block a specific (Thing, Slot) combination from being inserted, via Harmony patches. Researched for the SprayPaintPlus v1.4.0 glow-paint gun (blocking a SprayCan from the SprayGun once the gun goes ammo-less). Note that mod ultimately shipped a UI-only Slot.Class.Blocked slot hide in GlowPaintPatches.cs, not the server-authoritative CanEnter block documented here; the can-in-gun block in SprayPaintPlus is therefore UI-level only.
Insertion call chain¶
Vanilla insertion flows through, in order:
DynamicThing.MoveToSlot(Slot destinationSlot, Thing originThing, bool forced = false)at decompile line 2443. The primary gate; returnsfalsewithout inserting when!CanEnter(destinationSlot).Thing.CanEnter(Slot destinationSlot)at decompile line 3931 (virtual). Returns aCanEnterResultstruct with pass / fail plus an optional message. Vanilla rejects on entity-class mismatch, circular hierarchy, unpickable item, self-entry, and prefab-hash mismatch.Slot.TakesSpecificItem(int prefabHash)at decompile line 981. Returnsfalsewhen the slot'sSpecificTypePrefabHashis set and does not match the incoming item's hash. Called fromCanEnter.Slot.AllowMove(DynamicThing thing, Slot destinationSlot)at decompile line 659 (static). UI-level affordance gate; does NOT block insertion, only controls whether the "can insert here" feedback is rendered.
Authoritative block (server-side)¶
Postfix Thing.CanEnter and overwrite the returned CanEnterResult via ref:
[HarmonyPatch(typeof(Thing), nameof(Thing.CanEnter))]
public class BlockFooIntoBar
{
public static void Postfix(Thing __instance, Slot destinationSlot, ref CanEnterResult __result)
{
if (__instance is Foo && destinationSlot?.Parent is Bar)
__result = CanEnterResult.Fail("explanation");
}
}
This fires on every insertion attempt on the authoritative side. Server-side MoveToSlotMessage.Process calls MoveToSlot which calls CanEnter; the server rejects and the move is not broadcast. A cheating client cannot force the insertion because the server does not accept the resulting MoveToSlotMessage.
CanEnterResult.Fail(string reason) is the factory for the reject path; the returned struct carries a human-readable reason that vanilla UI surfaces surface when hovering.
CanEnterResult shape¶
CanEnterResult is a plain struct: two public fields, a Succeed value, three Fail factories and an implicit conversion to bool. There are no severity levels. Verbatim from the 0.2.6428.27798 client decompile (328542-328591; the dedicated server's copy, 314926-314975, is identical):
public struct CanEnterResult
{
private static readonly StringBuilder StateMessageBuilder = new StringBuilder();
public bool Result;
public string Reason;
public static CanEnterResult Succeed = new CanEnterResult(result: true, string.Empty);
public static CanEnterResult Fail(string reason)
{
return new CanEnterResult(result: false, reason);
}
private static void AppendStateMessage(Assets.Scripts.Localization2.GameString gameString, string arg0)
{
StateMessageBuilder.Append(gameString.AsString(arg0));
}
private static void AppendStateMessage(Assets.Scripts.Localization2.GameString gameString, string arg0, string arg1)
{
StateMessageBuilder.Append(gameString.AsString(arg0, arg1));
}
public static CanEnterResult Fail(Assets.Scripts.Localization2.GameString reason, string arg0)
{
StateMessageBuilder.Clear();
AppendStateMessage(reason, arg0);
return new CanEnterResult(result: false, StateMessageBuilder.ToString());
}
public static CanEnterResult Fail(Assets.Scripts.Localization2.GameString reason, string arg0, string arg1)
{
StateMessageBuilder.Clear();
AppendStateMessage(reason, arg0, arg1);
return new CanEnterResult(result: false, StateMessageBuilder.ToString());
}
private CanEnterResult(bool result, string reason)
{
Result = result;
Reason = reason;
}
public static implicit operator bool(CanEnterResult canEnterResult)
{
return canEnterResult.Result;
}
}
A caller reads the verdict from Result, or through the implicit bool as DynamicThing.MoveToSlot does (if (!CanEnter(destinationSlot)), 306638), and the message from Reason. The constructor is private, so a patch can only produce a result through Succeed or a Fail overload; overriding an existing refusal means assigning CanEnterResult.Succeed to __result. Succeed is a plain static field, not readonly.
UI affordance (client-side)¶
Patch Slot.AllowMove as a prefix that returns false and short-circuits:
[HarmonyPatch(typeof(Slot), nameof(Slot.AllowMove))]
public class BlockFooIntoBarUI
{
public static bool Prefix(DynamicThing thing, Slot destinationSlot, ref bool __result)
{
if (thing is Foo && destinationSlot?.Parent is Bar)
{
__result = false;
return false;
}
return true;
}
}
Without this, the client UI can briefly show the insert as succeeding before the server rejects and the item snaps back. With both the CanEnter postfix and the AllowMove prefix in place, the UI stays consistent with the rule.
Legacy-state handling¶
What a block does to an item that was already in the slot when the world was saved depends on which technique it uses. A save load, and a multiplayer join, put every saved item back through the same MoveToSlot gate a player's insertion goes through, and they do it after the container's Awake has run. Read in the 0.2.6428.27798 client and dedicated server decompiles; every body cited here is textually identical in the two (client lines first, the dedicated server's in brackets).
Loading re-runs CanEnter. A saved item puts itself back into its container when the ITEM deserializes. DynamicThing.DeserializeSave (305502-305523 [291895-291916]) calls MoveToParent (305525-305558 [291918-291951]), which calls MoveToSlot(thing.Slots[parentSlotId], thing); (305538 [291931]), or, when the container is not registered yet, MoveToParentWhenReady (305560-305586 [291953-291979]), which ends in MoveToSlot(parent.Slots[parentSlotId], parent); (305583 [291976]). A joining client does the same from DynamicThing.DeserializeOnJoin (MoveToSlot(thing.Slots[index], thing);, 307211 [293604]). DynamicThing.MoveToSlot (306632-306697 [293025-293090]) opens with:
if (base.IsBeingDestroyed || destinationSlot == null)
{
return false;
}
if (!CanEnter(destinationSlot)) // 306638 [293031]
{
return false;
}
if ((bool)destinationSlot.Occupant) // 306642 [293035]
{
return false;
}
The container's Awake runs before its saved contents arrive. XmlSaveLoad.Load creates each Thing with Thing.Create, whose UnityEngine.Object.Instantiate runs Awake, before it deserializes it, and the item above finds its container by reference id, which Thing.Create registers only after Instantiate. The join path also creates each record's Thing before applying the record, and writes a container before its contents. Full chain: SaveLoadOrdering, "Awake runs before a Thing's saved contents are placed, on load and on join". That Awake runs inside Instantiate is Unity engine behaviour: inferred and corroborated there, not read.
What this means for each technique:
- A
CanEnterpostfix (the "Authoritative block" above) also applies to saved items. The load-timeMoveToSlotcalls it, so an item it now refuses is not put back into the slot:MoveToSlotreturnsfalse, andMoveToParentreturns without a retry. The item stays out of its container. Where it then ends up in play was not traced: it keeps the world positionThing.Creategave it, andDynamicThing.DeserializeSavedoes not enqueue it withLodManager.EnqueueRequesterToUpdate, because that call is made only when the savedParentReferenceIdis 0 (305517-305520). - A block that only re-types or hides the slot does not stop a saved item.
Thing.CanEnter(326562-326589 [312946-312973]) consultsSlot.Typeonly for an entity (if (IsEntity && destinationSlot.Type != Slot.Class.Entity), 326568 [312952]); otherwise it checks the item's own hierarchy,CanPickup, self-entry andSlot.TakesSpecificItem, which compares a prefab-hash list only (317566-317580 [303950-303964]). Nothing on the load path readsSlot.IsInteractable. An item whose class does not overrideCanEnterorMoveToSlottherefore goes back into aSlot.Class.Blocked, non-interactable slot. In this buildCanEnteris overridden byDraggableThing,WearableItem,Backpack,CardboardBox,GasMaskandAppliance, andMoveToSlotbyEntity,Ice,BodyArmor,Clothing,SuitandSuitBase. - A hide applied in
Awakenever sees the saved item. The slot is empty atAwakeon a load and on a join, so a guard such as "leave the slot alone if it is occupied" never fires on those paths. The hide blocks the slot, and the saved item then lands in it.
What an occupied Blocked, non-interactable slot does to its item. The item really is in the slot (destinationSlot.Take(this); then ParentSlot = destinationSlot;, 306680-306681), and the container still uses it: for a spray gun, public SprayCan SprayCan => Slots[0].Occupant as SprayCan; (361166 [347534]). A player cannot get it out:
- The inventory screen draws nothing for it.
InventoryWindow.SetSlots(247387-247430 [234499-234542]) creates aSlotDisplayButtononlyif (slot.IsInteractable)(247392 [234504]), so the slot and its item have no button to click, drag or read. A container whose only slot is non-interactable also reportsHasSlotsfalse (return Slots.FindIndex((Slot s) => s.IsInteractable) >= 0;, 323737 [310121]), andSlotDisplay.OnPlayerInteract(295348-295355 [281741-281748]) opens a Thing's own window only whenSlot.Occupant.HasSlots || Slot.Occupant.HasKeyInteractions(295350 [281743]). It isIsInteractable == falsethat hides the slot; theBlockedtype is what refuses insertion, becauseSlot.IsAllowedTypereturnsType == dynamicThing.SlotTypefor anyTypeother thanNone(317331-317346 [303715-303730]; aGasCanistergoing into aDynamicGasCanisterhas its own rule there). - Interacting with the container in the world is refused as well.
Thing.InteractWithhands a slot interaction toHandleSwitch(interaction, interactable.Slot.SlotIndex, delayedActionInstance, doAction)(327443) withoutforce, andThing.HandleSwitch(326821-326901 [313205-313285]) returnsresult.Fail(GameStrings.ThingCurrentlyNotInteractable, slot2.ToTooltip())when!force && (!slot2.IsInteractable || slot2.IsLocked)(326859-326861 [313243-313245]). - How the item is drawn in the world does not change. An occupant's own visibility follows
Slot.HidesOccupantandOccupantAlwaysVisible(DynamicThing.IsHiddenInSlot, 304654-304677), and neither is touched by aTypeorIsInteractablechange.
So the item stays hidden and out of a player's reach until something makes the slot interactable again. Not examined: other ways out (the container being destroyed, machines that empty slots, other mods, and the server's message handlers; OnServer.MoveToSlot itself just calls childThing.MoveToSlot(slot, slot.Parent), 39546, without looking at the slot the item leaves), and the container prefab's own slot data (HidesOccupant, a world Collider), which a decompile cannot show.
Handling legacy contents. A mod that must leave a saved item reachable has to decide once the contents are in place. Thing.OnFinishedLoad is such a point: on a load, XmlSaveLoad.LoadWorld waits for DynamicThing.AwaitingMoveWhenReady to drain before World.OnLoadingFinished starts the sweep, and on a join the sweep runs after ProcessThings (see SaveLoadOrdering). From there a mod can give an occupied slot back, or move the item out with OnServer.MoveToWorld on the authority. The other point is the arrival itself: MoveToSlot calls the container's OnChildEnterInventory once the item is in (ParentSlot.Parent.OnChildEnterInventory(this);, 306688 [293081]), on a load, a join and a normal insertion alike (a dragged item, which DragInSlot places, is the one exception; see DynamicThing, "MoveToSlot and the container hooks"). Equipment Plus uses that one: its DynamicSlots.RefreshSlots, run from OnChildEnterInventory postfixes, unlocks any of its Blocked slots that holds an occupant, and its doc comment names the case ("the save-load case where vanilla's MoveToSlot placed a chip into one of our Blocked slots", Mods/EquipmentPlus/EquipmentPlus/DynamicSlots.cs). It runs RefreshSlots from OnChildExitInventory postfixes as well (TabletSlotOnRemove, SensorLensesSlotOnRemove), so its slots are reconciled when an item leaves too. Not in a release yet: Spray Paint Plus is the second in-repo user of the arrival hook, with SprayGunCanArrivalPatch (next paragraph), and it hooks only the arrival. That is in commit fdcffdc5 on master. The released v1.12.1 has no arrival hook, and after a load with glow paint on, its can stays in a hidden, blocked slot. For a CanEnter block there is nothing left in the slot to eject, because the item was refused at load.
Spray Paint Plus is the worked case. In the released v1.12.1, and on master 559f8d86 (Mods/SprayPaintPlus/SprayPaintPlus/GlowPaintPatches.cs, unchanged there since ed97d13a; v1.12.1 carries the same two guards at lines 221-222), SprayGunSlotHiderPatch makes the gun's can-slot decision in Awake through GlowPaintPatches.ApplySlotState, which with glow paint on returns early for a slot that is already Blocked (line 166), then for an occupied slot (line 167), and otherwise blocks it (lines 168-171). On this build, therefore, a save with a can in the gun, loaded with glow paint on, blocks the empty slot and then puts the can into it, hidden and out of reach. The occupied-slot branch is reachable only from GlowPaintPatches.ReapplySlotState, on a joining client whose own guess of the glow setting was off while the host's is on. A later load with glow paint off gives the slot back, because a freshly instantiated slot is the vanilla one and the off branch leaves it alone. This is read from the mod source and the decompile, and was seen on the test rig on 2026-09-27 on a build without the fix below (Release DLL sha256 56763957), by the check a spray gun saved with a can in it keeps the can visible and removable: after a glow-on load of a save whose spray gun holds a can, that gun's slot read Blocked and not interactable with the can in it, in single player, on a listen host, on the dedicated server's own copy and on a joiner of that server that joined guessing glow on. A joiner that joined guessing glow off read the loaded gun's slot Bottle and interactable and the empty gun's Blocked: the occupied-slot branch of ReapplySlotState, and its blocking of an empty slot. The later load with glow paint off was not part of the run.
Not in a release yet. The fix below is in commit fdcffdc5 on master, and ships in Spray Paint Plus v1.13.0. The released v1.12.1 behaves as the previous paragraph says: after a load with glow paint on, its can stays in a hidden, blocked slot. The fix decides at the arrival as well. SprayGunCanArrivalPatch is a postfix on Thing.OnChildEnterInventory, resolved through SprayGun by TargetMethod (typeof(SprayGun).GetMethod("OnChildEnterInventory", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(DynamicThing) }, null)), because nothing between the two overrides it: in both 0.2.6428.27798 decompiles SprayGun, Tool, ItemRenamable, Item and DynamicThing declare no OnChildEnterInventory, and Thing declares it virtual (327135 [313519]). Harmony therefore patches Thing's own method; the postfix, whose __instance is typed Thing (see HarmonyInheritedMethods), runs for every container and returns at once for anything that is not a spray gun. It runs GlowPaintPatches.ApplySlotState, which now follows one rule, SprayGunSlotRule: the slot is hidden exactly when glow paint is on and the slot is empty. The hider in Awake, the join fix-up ReapplySlotState and the arrival all go through it, so a slot with a can in it is always shown. A can that lands in a blocked slot therefore gets the slot back as it arrives, with the vanilla type captured from the first gun the mod saw (Bottle), IsInteractable true and the vanilla icon, on every machine that places the can, each on its own copy of the slot. When that changes the slot, GlowPaintPatches.RefreshWindowsShowing rebuilds every InventoryWindow in InventoryWindowManager.Instance.Windows whose Parent is the gun, open or closed, with HandleOccupantChange(), and puts a visible one's buttons back in the scroll list with RegisterVisibleSlots; on a load or a join there is no such window yet (see InventoryWindow, "Window lifecycle"). Taking the can out does not hide the slot again; the next load does, when the gun awakes empty and is blocked like any other. Read from the mod source at fdcffdc5 and the decompiles, and measured on 2026-09-28 by the same check (Mods/SprayPaintPlus/playtests/LoadedGunKeepsItsCanReachable.cs) on fdcffdc5's build (Release DLL sha256 c0c00c18), which passed in all three sessions where the earlier build had failed at its first slot assertion: after the same load the loaded gun's slot read Bottle and interactable with the can in it, on the authority in single player and on a listen host, on the dedicated server's own copy, and on a joiner with either guess, while the empty gun's read Blocked and not interactable throughout. The check reads the slot, not an inventory window, and does not take the can out, so the window rebuild and what happens when the can is taken out are still read from the source only.
Superseded reading (0.2.6228.27061), kept for the record¶
Superseded on 2026-09-27, and wrong at 0.2.6428.27798: the load path does call CanEnter, and a hide applied in Awake never sees the occupied slot (see above and the Verification history). The 0.2.6228.27061 decompile is no longer on disk, so whether this held for that build cannot be re-checked. The section as it stood, verbatim:
Thing.CanEnteris NOT re-checked during save load. Items already parented in slots at save time load as-is regardless of the current predicate. When a mod introduces a new block against a previously-valid(Thing, Slot)combination, saves from before the block load normally, and the previously-loaded contents become orphaned: they still function (the gun still fires). With theCanEnterblock above in place, they cannot be re-inserted after the player removes them. A UI-onlySlot.Class.Blockedhide (asSprayPaintPlusactually ships) does not have this property: the occupied slot is left untouched at load, so its contents stay removable and re-insertable until the slot is emptied and the world reloaded.To eject orphaned contents cleanly on load, postfix a post-load hook (
Thing.OnFinishedLoador equivalent) on the container type and move the child out viaOnServer.MoveToWorld. This is optional polish; many mods ship without it and document the edge case instead.
Verification history¶
- 2026-04-21: page created. Decompile findings sourced from Assembly-CSharp.dll (Thing.CanEnter at line 3931, DynamicThing.MoveToSlot at line 2443, Slot.AllowMove at line 659, Slot.TakesSpecificItem at line 981) surfaced by a sub-agent pass in support of SprayPaintPlus v1.4.0 (block SprayCan from SprayGun).
- 2026-05-21: re-confirmed the insertion call chain and the
CanEnter/AllowMovetechniques against 0.2.6228.27061 (still accurate). Corrected provenance:SprayPaintPlusdid not adopt this page's server-authoritative technique; it ships a UI-onlySlot.Class.Blockedslot hide (SprayGunSlotHiderPatchinGlowPaintPatches.cs), so its can-in-gun block is UI-level only and orphaned cans stay re-insertable until removed and reloaded. Resolved theThing.OnFinishedLoadopen question: the signature is pinned and used in production by theSprayPaintPlusv1.6.0 glow side-car (ThingOnFinishedLoadGlowPatch);SprayPaintPlusaccepts the orphaned-can edge case by design rather than ejecting. - 2026-09-27: conflict on "whether a save load re-checks
CanEnter, and what a slot hide applied inAwakedoes to a saved occupant". Previous claim (section "Legacy-state handling", 0.2.6228.27061 @ 2026-05-21):Thing.CanEnteris not re-checked during save load and saved contents load as-is; with aCanEnterblock the contents keep working but cannot be re-inserted once removed; a UI-onlySlot.Class.Blockedhide leaves an occupied slot untouched at load, so its contents stay removable and re-insertable. New finding (0.2.6428.27798, from a Spray Paint Plus fixture assessment's decompile read,.work/2026-09-27-fx-assessment/RESULT.mdsection 4, then re-read here in the client and dedicated server decompiles):XmlSaveLoad.LoadrunsThing.Create(whoseInstantiaterunsAwake) beforeDeserializeSave; a saved item restores itself child-side throughDynamicThing.MoveToParentandMoveToSlot, which checksCanEnter;Thing.CanEnterreadsSlot.Typeonly for an entity; so anAwake-time hide sees an empty slot and blocks it, the saved item lands in the blocked slot, and the join path has the same shape. Fresh validator verdict: B is correct on all three parts asked (the load and join order,CanEnteron the load path and its indifference toSlot.Typefor a spray can, and the resulting branch, location and reachability), with every relevant body identical in the two decompiles. It read directly what the assessment had inferred, that the item cannot be taken out (InventoryWindow.SetSlotsbuilds no button for a non-interactable slot,Thing.HasSlotsandSlotDisplay.OnPlayerInteractkeep the container's window shut when that is its only slot, andThing.HandleSwitchrefuses a non-interactable slot withoutforce), and added that aCanEnterpostfix refusing at load leaves the item out of the slot rather than loading it as-is. Its remaining inference is the same as this page's:Awakerunning insideInstantiate(Unity engine behaviour). Result: "Legacy-state handling" rewritten from the verdict and restamped 0.2.6428.27798, with the Spray Paint Plus case restated and Equipment Plus'sOnChildEnterInventoryunlock (which already treats a saved item landing in aBlockedslot as the save-load case) cited as the in-repo precedent; the old section kept verbatim under its 0.2.6228.27061 stamp as "Superseded reading (0.2.6228.27061), kept for the record"; the load-order chain written up on./SaveLoadOrdering.mdand cross-linked from../Protocols/WorldXml.md. - 2026-09-27: added "CanEnterResult shape" from the 0.2.6428.27798 decompiles (client 328542-328591, dedicated server 314926-314975, identical), which closes the open question on the struct: two public fields (
Result,Reason), a staticSucceed, threeFailoverloads, a private constructor and an implicitboolconversion, no severity levels. Additive. In the same pass items 1 to 3 of "Insertion call chain" were re-read at 0.2.6428.27798 (DynamicThing.MoveToSlot306632 with theCanEntergate at 306638,Thing.CanEnter326562,Slot.TakesSpecificItem317566) and still hold apart from line numbers;Slot.AllowMoveand the message path in "Authoritative block (server-side)" were not re-read, so those sections keep their 0.2.6228.27061 stamps. The top-levelverified_inmoved to 0.2.6428.27798 for the two sections stamped at that version. - 2026-09-27: added Spray Paint Plus's arrival fix to "Legacy-state handling", under a not-in-a-release-yet marker (commit
72b2db56on the local development branchphase3-b72, not yet on master; ships in v1.13.0):SprayGunCanArrivalPatch, a postfix onThing.OnChildEnterInventoryresolved throughSprayGun, runsApplySlotState, which now follows one rule (SprayGunSlotRule: hidden exactly when glow paint is on and the slot is empty), so a can that lands gives the slot back on every machine that places it, and anyInventoryWindowshowing the gun is rebuilt; taking the can out does not hide the slot again. The v1.12.1 account is kept as the released build's and master559f8d86's (the file is unchanged there sinceed97d13a). In "Handling legacy contents", Spray Paint Plus named as the second in-repo user of the arrival hook, Equipment Plus'sOnChildExitInventorypostfixes noted, and a dragged item (DragInSlot) noted as the one placement that skips the hook. Dropped the sentence saying the mod's ownRESEARCH.md(section 3.7) and theSprayGunSlotHiderPatchclass comment still described the occupied slot as left visible: both are corrected in72b2db56. Additive: the section already named the arrival as a decision point and predicted this use of it. Re-read in both 0.2.6428.27798 decompiles for this entry:SprayGun,Tool,ItemRenamable,ItemandDynamicThingdeclare neitherOnChildEnterInventorynorOnChildExitInventory,Thingdeclares both virtual, and the only call of the enter hook outside overrides'basecalls is inMoveToSlot(306688 [293081]). Raised by.work/2026-09-27-phase3-rig/M4-RESULT.mdsection 5 item 1. The fix is read from source, not measured in game. Section restamped. - 2026-09-28: the Spray Paint Plus fix this page marks landed on master as
fdcffdc5; the cited files are unchanged from the branch commits named in the entries above, so only the markers and sources were updated to cite it. Still not in a release (v1.12.1 is the published build). - 2026-09-28 (game version 0.2.6428.27798): the two Spray Paint Plus paragraphs of "Legacy-state handling" said "not measured in game" and "not yet measured in game"; they now carry the rig readings of the check
a spray gun saved with a can in it keeps the can visible and removable. On a build without the fix (Release DLL sha25656763957, 2026-09-27) it failed in single player, on a listen host and on the dedicated server, with the can in aBlocked, non-interactable slot after a glow-on load and after a join guessing glow on, and the join fix-up's occupied-slot branch seen on a joiner that guessed glow off; onfdcffdc5's build (c0c00c18, 2026-09-28) it passed in all three, with the slot given back. Named what the runs did not cover: a later load with glow paint off, the window rebuild, and taking the can out. Read from each cell'sloaded-spray-gun.txtand verdict. Additive; the source reading is unchanged. Section restamped; the marker stays, since the fix is not in a release.
Open questions¶
- Where a saved item that a
CanEnterpostfix refuses at load ends up once play starts. It is left out of its container at the world positionThing.Creategave it, with noLodManager.EnqueueRequesterToUpdatefromDynamicThing.DeserializeSavebecause its savedParentReferenceIdis not 0; whether it then falls, stays put or is cleaned up was not traced. Resolve with a save that holds such an item, loaded with the block in place, and a read of the item'sParentSlot,ParentReferenceIdand position a few seconds into play (an InspectorPlus request on the item's type, or a live/thingread on the test rig). - Whether anything other than a player takes an item out of a
Blocked, non-interactable slot: the container being destroyed or deconstructed, machines that empty slots, and the server's handlers for client inventory messages were not read.OnServer.MoveToSlotitself (39540-39557) does not look at the slot the item leaves.