Skip to content

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:

  1. DynamicThing.MoveToSlot(Slot destinationSlot, Thing originThing, bool forced = false) at decompile line 2443. The primary gate; returns false without inserting when !CanEnter(destinationSlot).
  2. Thing.CanEnter(Slot destinationSlot) at decompile line 3931 (virtual). Returns a CanEnterResult struct with pass / fail plus an optional message. Vanilla rejects on entity-class mismatch, circular hierarchy, unpickable item, self-entry, and prefab-hash mismatch.
  3. Slot.TakesSpecificItem(int prefabHash) at decompile line 981. Returns false when the slot's SpecificTypePrefabHash is set and does not match the incoming item's hash. Called from CanEnter.
  4. 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 CanEnter postfix (the "Authoritative block" above) also applies to saved items. The load-time MoveToSlot calls it, so an item it now refuses is not put back into the slot: MoveToSlot returns false, and MoveToParent returns without a retry. The item stays out of its container. Where it then ends up in play was not traced: it keeps the world position Thing.Create gave it, and DynamicThing.DeserializeSave does not enqueue it with LodManager.EnqueueRequesterToUpdate, because that call is made only when the saved ParentReferenceId is 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]) consults Slot.Type only for an entity (if (IsEntity && destinationSlot.Type != Slot.Class.Entity), 326568 [312952]); otherwise it checks the item's own hierarchy, CanPickup, self-entry and Slot.TakesSpecificItem, which compares a prefab-hash list only (317566-317580 [303950-303964]). Nothing on the load path reads Slot.IsInteractable. An item whose class does not override CanEnter or MoveToSlot therefore goes back into a Slot.Class.Blocked, non-interactable slot. In this build CanEnter is overridden by DraggableThing, WearableItem, Backpack, CardboardBox, GasMask and Appliance, and MoveToSlot by Entity, Ice, BodyArmor, Clothing, Suit and SuitBase.
  • A hide applied in Awake never sees the saved item. The slot is empty at Awake on 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 a SlotDisplayButton only if (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 reports HasSlots false (return Slots.FindIndex((Slot s) => s.IsInteractable) >= 0;, 323737 [310121]), and SlotDisplay.OnPlayerInteract (295348-295355 [281741-281748]) opens a Thing's own window only when Slot.Occupant.HasSlots || Slot.Occupant.HasKeyInteractions (295350 [281743]). It is IsInteractable == false that hides the slot; the Blocked type is what refuses insertion, because Slot.IsAllowedType returns Type == dynamicThing.SlotType for any Type other than None (317331-317346 [303715-303730]; a GasCanister going into a DynamicGasCanister has its own rule there).
  • Interacting with the container in the world is refused as well. Thing.InteractWith hands a slot interaction to HandleSwitch(interaction, interactable.Slot.SlotIndex, delayedActionInstance, doAction) (327443) without force, and Thing.HandleSwitch (326821-326901 [313205-313285]) returns result.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.HidesOccupant and OccupantAlwaysVisible (DynamicThing.IsHiddenInSlot, 304654-304677), and neither is touched by a Type or IsInteractable change.

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.CanEnter is 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 the CanEnter block above in place, they cannot be re-inserted after the player removes them. A UI-only Slot.Class.Blocked hide (as SprayPaintPlus actually 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.OnFinishedLoad or equivalent) on the container type and move the child out via OnServer.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 / AllowMove techniques against 0.2.6228.27061 (still accurate). Corrected provenance: SprayPaintPlus did not adopt this page's server-authoritative technique; it ships a UI-only Slot.Class.Blocked slot hide (SprayGunSlotHiderPatch in GlowPaintPatches.cs), so its can-in-gun block is UI-level only and orphaned cans stay re-insertable until removed and reloaded. Resolved the Thing.OnFinishedLoad open question: the signature is pinned and used in production by the SprayPaintPlus v1.6.0 glow side-car (ThingOnFinishedLoadGlowPatch); SprayPaintPlus accepts 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 in Awake does to a saved occupant". Previous claim (section "Legacy-state handling", 0.2.6228.27061 @ 2026-05-21): Thing.CanEnter is not re-checked during save load and saved contents load as-is; with a CanEnter block the contents keep working but cannot be re-inserted once removed; a UI-only Slot.Class.Blocked hide 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.md section 4, then re-read here in the client and dedicated server decompiles): XmlSaveLoad.Load runs Thing.Create (whose Instantiate runs Awake) before DeserializeSave; a saved item restores itself child-side through DynamicThing.MoveToParent and MoveToSlot, which checks CanEnter; Thing.CanEnter reads Slot.Type only for an entity; so an Awake-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, CanEnter on the load path and its indifference to Slot.Type for 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.SetSlots builds no button for a non-interactable slot, Thing.HasSlots and SlotDisplay.OnPlayerInteract keep the container's window shut when that is its only slot, and Thing.HandleSwitch refuses a non-interactable slot without force), and added that a CanEnter postfix 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: Awake running inside Instantiate (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's OnChildEnterInventory unlock (which already treats a saved item landing in a Blocked slot 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.md and 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 static Succeed, three Fail overloads, a private constructor and an implicit bool conversion, 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.MoveToSlot 306632 with the CanEnter gate at 306638, Thing.CanEnter 326562, Slot.TakesSpecificItem 317566) and still hold apart from line numbers; Slot.AllowMove and the message path in "Authoritative block (server-side)" were not re-read, so those sections keep their 0.2.6228.27061 stamps. The top-level verified_in moved 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 72b2db56 on the local development branch phase3-b72, not yet on master; ships in v1.13.0): SprayGunCanArrivalPatch, a postfix on Thing.OnChildEnterInventory resolved through SprayGun, runs ApplySlotState, 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 any InventoryWindow showing 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 master 559f8d86's (the file is unchanged there since ed97d13a). In "Handling legacy contents", Spray Paint Plus named as the second in-repo user of the arrival hook, Equipment Plus's OnChildExitInventory postfixes noted, and a dragged item (DragInSlot) noted as the one placement that skips the hook. Dropped the sentence saying the mod's own RESEARCH.md (section 3.7) and the SprayGunSlotHiderPatch class comment still described the occupied slot as left visible: both are corrected in 72b2db56. 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, Item and DynamicThing declare neither OnChildEnterInventory nor OnChildExitInventory, Thing declares both virtual, and the only call of the enter hook outside overrides' base calls is in MoveToSlot (306688 [293081]). Raised by .work/2026-09-27-phase3-rig/M4-RESULT.md section 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 sha256 56763957, 2026-09-27) it failed in single player, on a listen host and on the dedicated server, with the can in a Blocked, 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; on fdcffdc5'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's loaded-spray-gun.txt and 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 CanEnter postfix refuses at load ends up once play starts. It is left out of its container at the world position Thing.Create gave it, with no LodManager.EnqueueRequesterToUpdate from DynamicThing.DeserializeSave because its saved ParentReferenceId is 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's ParentSlot, ParentReferenceId and position a few seconds into play (an InspectorPlus request on the item's type, or a live /thing read 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.MoveToSlot itself (39540-39557) does not look at the slot the item leaves.