Skip to content

DynamicThing

Assets.Scripts.Objects.DynamicThing is the Thing subclass for objects that can be carried and can live in a slot. Two things are recorded here so far: a breaking change (SetSlotTypes was removed), and how an item goes into a slot (MoveToSlot and the container hooks it calls).

SetSlotTypes(Slot.Class) was REMOVED in 0.2.6420.27780

Verified 2026-08-12 in 0.2.6420.27780, re-confirmed 2026-08-13 in 0.2.6428.27798 (the same metadata string scan, with DynamicThing still passing as the control). The removal has held across two game builds, so treat it as permanent rather than as an accident of one release. The method void Assets.Scripts.Objects.DynamicThing.SetSlotTypes(Assets.Scripts.Objects.Slot.Class) existed in 0.2.6403.27689 and does not exist in 0.2.6420.27780.

Evidence, two independent kinds:

1. Metadata. The string SetSlotTypes does not occur anywhere in the new Assembly-CSharp.dll, while DynamicThing does (control). .NET stores method names in the #Strings heap, so a method whose name is absent from the assembly's bytes does not exist in it. The same scan finds SetSlotTypes present in ScriptedScreens.dll, which was compiled against the older build.

2. Runtime. A client with mods that call it throws MissingMethodException: Method not found: void Assets.Scripts.Objects.DynamicThing.SetSlotTypes(Assets.Scripts.Objects.Slot/Class) from two different call sites, in this order:

# a mod's own entrypoint
StationeersLaunchPad.Entrypoints.StationeersModsEntrypoint.Initialize (LoadedMod mod)
StationeersLaunchPad.Loading.LoadedMod.LoadEntrypoints ()
StationeersLaunchPad.Loading.LoadStrategyLinearSerial.LoadEntryPoints ()

# and the GAME'S OWN prefab load, once the first caller is disabled
Assets.Scripts.Objects.Prefab.LoadAll ()
WorldManager.LoadGameDataAsync ()
WorldManager.Initialize ()
Assets.Scripts.GameManager.Start ()

Why this matters beyond the mod that calls it

The second stack is the important one. When the throwing call happens inside Prefab.LoadAll(), it propagates through WorldManager.Initialize() and out of GameManager.Start(), which never completes. The client then sits at phase == "menu" with gameInitialized == false for ever, rendering normally at full frame rate, with every mod apparently loaded. One third-party mod calling a removed method is enough to stop the client reaching a usable main menu, and the symptom does not name the mod: the BepInEx LogOutput.log stops early and records nothing about it. Only the Unity player log carries the exception.

That failure is easy to misread as StationeersLaunchPad's documented Workshop park, which is a different condition with a different signature:

plugin count gameInitialized cause
Workshop park <= 2 false a failed Steam Workshop query
this full count (36-37 observed) false a removed method thrown out of Prefab.LoadAll

Known callers: two, for one day

Scanning every Workshop assembly under steamapps\workshop\content\544550 for the string on 2026-08-12 found exactly two, both by the same author, and both failed on that build:

  • ScriptedScreens.dll (Workshop 3666779631), version 0.9.5.0
  • StationeersLua.dll (Workshop 3659911735), version 0.9.5.0

Disabling only the first was not enough: the second then threw the same exception from Prefab.LoadAll instead of from its own entrypoint, so both had to be disabled for the client to boot.

Both shipped 1.0.0.0 on 2026-08-13 and neither calls the method any more. The same metadata string scan against the updated assemblies returns SetSlotTypes absent from both, with DynamicThing still present in each as the control. The window in which this stopped a client from booting was about one day wide, and the "disable these two" instruction that briefly lived in this repository's rig notes is retired. As of 2026-08-13 the scan finds no caller in any installed Workshop assembly.

0.9.5.0 (2026-08-12) 1.0.0.0 (2026-08-13)
ScriptedScreens.dll calls SetSlotTypes yes no
StationeersLua.dll calls SetSlotTypes yes no
Client reaches a usable menu with both enabled no yes

No replacement method was identified, and none was needed by either caller. SetSlotType (singular) is also absent from 0.2.6420.27780 and from 0.2.6428.27798, so this was never a rename to the obvious neighbouring name.

MoveToSlot and the container hooks

Read in the 0.2.6428.27798 client and dedicated server decompiles. Every body cited in this section is textually identical in the two (each was extracted from both files and compared), so client line numbers come first and the dedicated server's follow in brackets where they were pinned.

DynamicThing.MoveToSlot (306632-306697 [293025-293090]) is how an item goes into a slot. Verbatim:

public virtual bool MoveToSlot(Slot destinationSlot, Thing originThing, bool forced = false)
{
    if (base.IsBeingDestroyed || destinationSlot == null)
    {
        return false;
    }
    if (!CanEnter(destinationSlot))
    {
        return false;
    }
    if ((bool)destinationSlot.Occupant)
    {
        return false;
    }
    foreach (MachineInputTrigger currentMachineInputTrigger in CurrentMachineInputTriggers)
    {
        currentMachineInputTrigger.ParentMachine.OnInputTriggerExit(this, currentMachineInputTrigger);
    }
    if (IsChild)
    {
        Thing parent = ParentSlot.Parent;
        Slot parentSlot = ParentSlot;
        ParentSlot = null;
        parentSlot.Empty();
        parent.OnChildExitInventory(this);
        parentSlot.RefreshSlotDisplay();
        OnExitInventory(parent);
        parentSlot.OnExitEvent();
        HandleCollisionWith(parentSlot.Parent, ignore: false);
        if ((bool)Joint)
        {
            UnityEngine.Object.Destroy(Joint);
            RigidBody.mass = _startMass;
            if (parent.HasAuthority)
            {
                RigidBody.interpolation = RigidbodyInterpolation.Interpolate;
            }
        }
    }
    else
    {
        base.WorldGrid = new WorldGrid(CenterPosition);
        base.GridController.AlertGridWatchers(base.WorldGrid, this, GridEvent.GridEventType.Leave);
    }
    if (IsForceKinematic)
    {
        ForceKinematicClient(kinematic: false, localCheck: true);
    }
    destinationSlot.Take(this);
    ParentSlot = destinationSlot;
    Transform parent2 = (destinationSlot.Location ? destinationSlot.Location : destinationSlot.Parent.ThingTransform);
    SetPhysics(on: false);
    ThingTransform.SetParent(parent2);
    base.ThingTransformLocalPosition = Vector3.zero;
    base.ThingTransformLocalRotation = Quaternion.identity;
    ParentSlot.RefreshSlotDisplay();
    ParentSlot.Parent.OnChildEnterInventory(this);
    OnEnterInventory(ParentSlot.Parent);
    ParentSlot.OnEnterEvent();
    HandleCollisionWith(ParentSlot.Parent);
    if (!(this is Human))
    {
        LodManager.EnqueueRequesterToRemove(this);
    }
    return true;
}

Refusals, in order: a Thing being destroyed or a null slot (306634), a failed CanEnter (306638 [293031]), an occupied slot (306642 [293035]). originThing and forced are not read in the body. Six classes override MoveToSlot (Entity, Ice, BodyArmor, Clothing, Suit, SuitBase; see SlotInsertionBlock), and each override calls base.MoveToSlot once.

The exit half runs only for an item that is already in a slot (if (IsChild), 306650). The item forgets its slot (ParentSlot = null;, 306654), the old slot is emptied (parentSlot.Empty();, 306655 [293048]), and only then does the old container hear of it (parent.OnChildExitInventory(this);, 306656 [293049]). The old slot's display, the item's own OnExitInventory and the slot's OnExitEvent follow (306657-306659).

The enter half: destinationSlot.Take(this); (306680 [293073]) and ParentSlot = destinationSlot; (306681), the transform parenting (306682-306686), ParentSlot.RefreshSlotDisplay(); (306687 [293080]), then the container hook ParentSlot.Parent.OnChildEnterInventory(this); (306688 [293081]), then the item's own OnEnterInventory(ParentSlot.Parent); (306689 [293082]) and the slot's ParentSlot.OnEnterEvent(); (306690 [293083]). So when a container's OnChildEnterInventory runs, the item is already in the slot and the slot's display has been refreshed, and the item's own enter hook has not run yet. Slot.Take (317067-317085 [303451-303469]) raises the slot's own OnOccupantChange event when the occupant changes, earlier still:

public void Take([JetBrains.Annotations.NotNull] DynamicThing child)
{
    if (Assets.Scripts.Networking.NetworkManager.IsServer)
    {
        if ((bool)child)
        {
            child.NetworkUpdateFlags |= 1;
        }
        if (child != _dynamicThing && (bool)_dynamicThing)
        {
            _dynamicThing.NetworkUpdateFlags |= 1;
        }
    }
    if (!(_dynamicThing == child))
    {
        _dynamicThing = child;
        OnOccupantChange?.Invoke();
    }
}

OnChildEnterInventory has one caller. Thing declares it, public virtual void OnChildEnterInventory(DynamicThing newChild) (327135 [313519]), and apart from the overrides' own base.OnChildEnterInventory calls, the line in MoveToSlot above is the only call of it in either build. 81 classes override it, and every one of those overrides calls base.OnChildEnterInventory exactly once, unconditionally, so a Harmony postfix on Thing.OnChildEnterInventory runs once for every item MoveToSlot puts into any container. SprayGun, Tool, ItemRenamable, Item and DynamicThing declare neither OnChildEnterInventory nor OnChildExitInventory, so a TargetMethod that looks the name up through any of them gets Thing's own method (see HarmonyInheritedMethods). Thing's body (327135-327152) sets the child's scale, raises the container's OnSlot event through CheckOnSlot() and calls SetSlotOccupantTransformData(newChild).

OnChildExitInventory has three callers, all in DynamicThing, and each runs after the slot is emptied. Thing declares it, public virtual void OnChildExitInventory(DynamicThing previousChild) (327163 [313547]); 73 classes override it, and all of them except RoboticArmDockCollector (176092) call base. The callers:

  • MoveToSlot, above (306656 [293049], after parentSlot.Empty(); at 306655).
  • MoveToWorld(Vector3 worldPosition, Quaternion worldRotation, Vector3 velocity, Vector3 angularVelocity, float force = 0f) (306769-306860 [293162-293253]): ParentSlot?.Empty(); (306830), ParentSlot = null; (306831), parent.OnChildExitInventory(this); (306832 [293225]). The parameterless MoveToWorld(float force = 0f) (306877-306900) ends in a call to it (306899).
  • OnDestroy (306492-306531 [292885-292924]): ParentSlot.Empty(); (306520), ParentSlot = null; (306521), parent.OnChildExitInventory(this); (306526 [292919]). OnDestroy calls MoveToWorld() first (306505), and its own call sits behind if (ParentSlot != null) (306516), so it runs only for an item that is still in a slot after that. When the game is quitting (306494) or GameManager.GameState is None (306500), OnDestroy returns before either.

The paths that restore or mirror a placement all go through MoveToSlot. Apart from the overrides' base calls, the client decompile has seven direct calls of it: OnServer.MoveToSlot (39546), MoveToSlotMessage.Process (282161, deferred until the InventoryManager exists; otherwise it uses OnServer.MoveToSlot, 282166), CryoTube.OnChildExitInventory returning its mask (376072), and the four load, join and update call sites in the list below (305538, 305583, 307211, 307271). Almost every placement a player, an interaction or a machine makes goes through OnServer.MoveToSlot, which is called from 151 places. The ones that put a saved or remote item into its slot:

  • a save load: DynamicThing.MoveToParent (MoveToSlot(thing.Slots[parentSlotId], thing);, 305538 [291931]), or MoveToParentWhenReady when the container is not registered yet (305583 [291976]); see SaveLoadOrdering;
  • a multiplayer join: DynamicThing.DeserializeOnJoin (MoveToSlot(thing.Slots[index], thing);, 307211 [293604]);
  • a Thing a server sends mid-session: Thing.DeserializeNew (328208-328217 [314592-314601]), which is Create<Thing> followed by thing.DeserializeOnJoin(reader); (328215);
  • a client applying the server's parent change: DynamicThing.ProcessUpdateTransform (307245-307277 [293638-293670]). It sends any other occupant of the target slot to the world first (slot.Occupant?.MoveToWorld();, 307261), then calls MoveToSlot(slot, thing); (307271 [293664]); a parent id of 0 sends the item to the world instead (307275). Only a client reaches it: NetworkManager.ReceiveEvents hands a NetworkChannel.StateTick packet to FragmentHandler.ReceiveState only if (NetworkRole == NetworkRole.Client) (279287-279291), and from there ApplyState (218234-218240) calls ReadStateImmediate (218119), which calls Thing.DeserializeDeltaState (328248-328267), whose thing.ProcessUpdate(reader, networkUpdateType); (328265) calls ProcessUpdateTransform(reader); when update bit 1 is set (328370-328373);
  • a normal insertion on the authority: OnServer.MoveToSlot (39540-39557 [39536-39553]) calls childThing.MoveToSlot(slot, slot.Parent); (39546 [39542]) when GameManager.RunSimulation, and otherwise sends a MoveToSlotMessage to the server.

A dragged item does not go through MoveToSlot. DynamicThing.DragInSlot (305907-305933 [292300-292326]) is taken for a saved item whose record says it was dragged (305552, 305579), for a join record with the drag flag (307207), and in ProcessUpdateTransform for a DraggableThing or Entity moving into an Entity (307263-307268). It takes the slot (destinationSlot.Take(this);, 305928) and calls CheckOnSlot() (305931), and calls neither container hook.

A swap empties one slot before it fills the other. OnServer.SwapSlotsAsync (40123-40168 [40119-40164]) runs on the authority; on a client, OnServer.SwapSlots sends a SwapSlotsMessage instead (40005-40013). For two slots it calls the OnServer helpers in this order:

MoveToWorld(dynamicThing);         // 40152
MoveToSlot(dynamicThing2, slot);   // 40153
MoveToSlot(dynamicThing, slot2);   // 40154

On the authority OnServer.MoveToWorld(DynamicThing, float) (39744-39758) calls dynamicThing.MoveToWorld(force) and OnServer.MoveToSlot calls childThing.MoveToSlot(slot, slot.Parent). Each container therefore sees an exit and then an enter, and the first slot is briefly empty. When the first item has a Joint (40142-40149), only the first two moves are made.

Verification History

  • 0.2.6420.27780, 2026-08-12: SetSlotTypes confirmed absent by metadata string scan (with DynamicThing as a passing control) and by two distinct runtime MissingMethodException stacks. Callers enumerated across all installed Workshop assemblies. The gameInitialized-never-true consequence observed directly on two separate boots of a rig instance.
  • 0.2.6428.27798, 2026-08-13: re-verified. The method is still absent from Assembly-CSharp.dll on the newer build, so the removal held across the update. Additive finding, contradicting nothing above: both known callers shipped 1.0.0.0 on 2026-08-13 (file timestamps 15:50, <Version>1.0.0.0</Version> in each About.xml) and the string scan now returns zero hits in either assembly. The caller list for this build is therefore empty, and the "both must be disabled" consequence no longer applies to any installed mod. The removal itself, its two runtime stacks and the Prefab.LoadAll blast radius are unchanged and still correct for any assembly compiled against the older API.
  • 0.2.6428.27798, 2026-09-27: added "MoveToSlot and the container hooks", read in the client and dedicated server decompiles with every cited member extracted from both and compared (textually identical): the full MoveToSlot body, its refusal order and its exit and enter halves; Slot.Take raising OnOccupantChange; OnChildEnterInventory called only from MoveToSlot (81 overrides, each calling base once, unconditionally); the three OnChildExitInventory callers, each after the slot is emptied (73 overrides, all but RoboticArmDockCollector calling base); the load, join, DeserializeNew, client-update and OnServer.MoveToSlot paths into MoveToSlot, with the client-only StateTick chain to ProcessUpdateTransform; DragInSlot taking a slot without either hook; and OnServer.SwapSlotsAsync moving one item to the world before the other goes in. Raised by the Spray Paint Plus can-slot fix, whose decompile read (.work/2026-09-27-phase3-rig/M4-RESULT.md section 5) listed these points; each was re-read here, and the caller counts and the DragInSlot exception were added. Additive; the SetSlotTypes section was not re-read. Intro reworded to name both topics; an Open questions section added, which the page lacked.

Open questions

None.