Skip to content

MultiConstructor

Vanilla MultiConstructor : Stackable, IConstructionKit, IConstructionStarter, IReferencable, IEvaluable at Assets.Scripts.Objects.MultiConstructor. The held-item kit form for placing one of N prefab variants (rail-kit with 8 pieces, etc.). Sibling to Constructor (single-prefab kit).

Class layout

public class MultiConstructor : Stackable, IConstructionKit, IConstructionStarter, IReferencable, IEvaluable
{
    [Header("Multi-Constructable")]
    public List<Structure> Constructables = new List<Structure>();

    [NonSerialized]
    public int LastSelectedIndex;

    public override void OnPrefabLoad()                          // strips null entries from Constructables, clamps LastSelectedIndex
    public virtual void Construct(Grid3, Quaternion, int optionIndex, Item offhandItem, bool authoringMode, ulong, [int quantity])
    public override string GetStationpediaCategory() => Localization.GetInterface(StationpediaCategoryStrings.KitCategory);
    public bool CanBuild(int index)                              // checks Quantity >= Constructables[index].BuildStates[0].Tool.EntryQuantity
    public List<Thing> GetConstructedPrefabs()
}

Constructables is a serialized list of every prefab the kit can place (e.g. all 8 rail variants for the rail-kit). LastSelectedIndex persists which variant the player last picked, so swapping back to the kit reopens on the same option.

Construct

Two overloads. The arity-7 form looks up the EntryQuantity for the chosen variant and forwards to the arity-8 form.

public virtual void Construct(Grid3 localPosition, Quaternion targetRotation, int optionIndex, Item offhandItem, bool authoringMode, ulong steamId)
{
    int entryQuantity = Constructables[optionIndex].BuildStates[0].Tool.EntryQuantity;
    Construct(localPosition, targetRotation, optionIndex, offhandItem, authoringMode, steamId, entryQuantity);
}

public virtual void Construct(Grid3 localPosition, Quaternion targetRotation, int optionIndex, Item offhandItem, bool authoringMode, ulong steamId, int quantity)
{
    if (authoringMode || OnUseItem(quantity, null))
    {
        CreateStructureInstance createStructureInstance = new CreateStructureInstance(Constructables[optionIndex], localPosition, targetRotation, steamId);
        if (PaintableMaterial != null && CustomColor.Normal != null)
        {
            createStructureInstance.CustomColor = CustomColor.Index;
        }
        if (GameManager.RunSimulation)
        {
            Constructor.SpawnConstruct(createStructureInstance);
        }
    }
}

Identical Quaternion handling to Constructor.Construct: the incoming targetRotation is stored in CreateStructureInstance.LocalRotation and forwarded to Constructor.SpawnConstruct. Note the asymmetry: MultiConstructor.Construct only calls SpawnConstruct when GameManager.RunSimulation == true, omitting the client-side new ConstructionCreationMessage(...).SendToServer() branch. Multi-kit placement on a remote client therefore relies on the calling code (e.g. InventoryManager.PlacementMode) to dispatch the network message via OnServer.UseMultiConstructor(...) rather than going through this method.

Differences from Constructor

Aspect Constructor MultiConstructor
Built prefab One: BuildStructure One of N: Constructables[optionIndex]
Per-prefab quantity QuantityUsed (single int field) Constructables[i].BuildStates[0].Tool.EntryQuantity (per-variant)
OnUsePrimary override Yes; calls Construct(targetLocation.ToGridPosition(), targetRotation, ...) None; placement dispatched at the inventory layer
Client network branch in own code Yes: new ConstructionCreationMessage(...).SendToServer() No; relies on caller
Constructables mutation hook n/a OnPrefabLoad strips nulls, clamps LastSelectedIndex

For mod patterns that need to insert a new variant into an existing rail-kit / wall-frame-kit, see Patterns/PrefabCloning.md "AddToConstructor" (the recipe that calls mirrorDef.constructor.Constructables.Insert(insertIndex + 1, mirroredDevice as Structure)).

They are siblings, not parent and child

MultiConstructor does not derive from Constructor. Both derive from Stackable and meet only at the IConstructionKit interface:

public class Constructor : Stackable, IConstructionKit                                                    // 301144
public class MultiConstructor : Stackable, IConstructionKit, IConstructionStarter, IReferencable, IEvaluable   // 312705

Line numbers are the 0.2.6428.27798 decompile. The consequence is for any code that walks ElectronicReader.GetAllConstructors(thing) (which returns List<IConstructionKit>) and tries to prefer the single-prefab kit:

  • kit is Constructor is a correct and exact discriminator: it is true for a Constructor and false for a MultiConstructor, with no inheritance overlap to worry about either way.
  • But for a target whose only kit is a MultiConstructor, the preference branch never fires and the code falls through to whatever its fallback is, typically kits[0]. Cables and pipes are exactly that case, so a "prefer a Constructor" branch is dead code on the two most commonly painted prefab families in the game and cannot be exercised by testing against them. Pick a kit-built Structure with a single-prefab kit (a ladder, a wall frame) to cover that branch.

./Thing.md, "Lookup for a placed Structure (kit was involved)", carries the reverse-lookup recipe this matters to, and now records what a cable resolves to.

Verification history

  • 2026-04-25: page created from a six-agent decompile pass on the placement pipeline. Source content lifted verbatim from /tmp/decompile_check/Assets/Scripts/Objects/MultiConstructor.cs. No conflicts.
  • 2026-09-24 (game version 0.2.6428.27798): added "They are siblings, not parent and child" under "Differences from Constructor". Both class declarations re-read against the current decompile (Constructor 301144, MultiConstructor 312705): both derive from Stackable and share only IConstructionKit, so MultiConstructor is not a subclass of Constructor. Additive; the page's opening line already called them siblings, and nothing on the page claimed otherwise, so no fresh validator. Recorded because the consequence is not obvious from either page: code that prefers a Constructor when walking ElectronicReader.GetAllConstructors has a branch that can never fire for a cable or a pipe, whose only kit is a MultiConstructor. Found while covering the Spray Paint Plus eyedropper's as-built pick, which is written that way and was being tested against cables.

Open questions

None.