Skip to content

RoboticArmRail family (rail pieces built by ItemKitLinearRail, plus docks / junctions / bypass)

Overview

The robotic-arm rail family is the Stationeers "Larre" robotic arm track: a chain of small-grid structures on which a RoboticArm traverses between dock positions. Rail pieces are placed by the ItemKitLinearRail multi-constructor (see ItemKitLinearRail.md). The four dock variants (atmos / cargo / collector / hydroponics) and the junction / bypass / base dock have their own separate kits (ItemKitLarreDockAtmos, ItemKitLarreDockBypass, ItemKitLarreDockCargo, ItemKitLarreDockCollector, ItemKitLarreDockHydroponics), but all 15-ish prefabs share the same rail-graph infrastructure and all implement IRoboticArmRail.

The family sits on the half-size "small grid" (SmallGrid.SmallGridSize = 0.5f), not the default 2.0-unit world grid that walls and large structures use. This matters for any code that wants to flood-fill rails via grid neighbours: the standard GridController.World.GetCell(worldGrid) + 6-neighbour walk covers world-grid structures only; rails live in a separate small-cell lattice addressed by smallCell.Rail.

Class hierarchy

Structure (Assets.Scripts.Objects.Structure)
  SmallGrid : Structure, ISmallGrid, ITooltip
    RoboticArmRailBase : SmallGrid, INetworkedRoboticArm, INetworkedStructure, ISmartRotatable
      RoboticArmRail : RoboticArmRailBase, IRoboticArmRail
        RoboticArmRailStraight : RoboticArmRail
        RoboticArmRailInnerCorner : RoboticArmRail
      RoboticArmJunction : RoboticArmRailBase, IRoboticArmJunction, IRoboticArmRail
    Device : SmallGrid, ILogicable, IPowered, ...
      SmallDevice : Device
        RoboticArmRailDeviceBase : SmallDevice, INetworkedRoboticArm, INetworkedStructure, ISmartRotatable
          RoboticArmBypass : RoboticArmRailDeviceBase, IRoboticArmBypass, IRoboticArmJunction, IRoboticArmRail
          RoboticArmDock : RoboticArmRailDeviceBase, IRoboticArmBypass, IRoboticArmJunction, IRoboticArmRail
            RoboticArmDockAtmos : RoboticArmDock, IThermal, IVolume
            RoboticArmDockCargo : RoboticArmDock, IProxySlot
            RoboticArmDockCollector : RoboticArmDock, IPhysical, IProfile, IDensePoolable, ICollector
            RoboticArmDockHydroponics : RoboticArmDock, IProxySlot

Verified by reading each class declaration line: SmallGrid : Structure, ISmallGrid, ITooltip, IReferencable, IEvaluable at Assets/Scripts/Objects/SmallGrid.cs:19; Device : SmallGrid, ILogicable, ... at Assets/Scripts/Objects/Pipes/Device.cs:21; SmallDevice : Device (abstract) at Assets/Scripts/Objects/SmallDevice.cs:7. Every rail-family class derives from Structure, NOT from LargeStructure. This is the single most important fact for SprayPaintPlus integration: the existing PaintLargeStructureGrid branch in NetworkPainterPatch does not catch rails, docks, junctions, or bypass.

Paintability: since every member descends from Structure, every member inherits Structure.SetCustomColor(int index, bool emissive) and the CustomColor / PaintableMaterial fields. Individual paintability hinges on structureRenderMode == StructureRenderMode.Standard; for the rail-family prefabs observed in vanilla this is the case (the player can paint any of them with a spray paint can).

Complete family roster (enumerated)

Decompile scan of /Objects/RoboticArm/*.cs for class * : IRoboticArm* or class * : RoboticArm* yielded exactly these concrete classes. No others exist in Assembly-CSharp at game version 0.2.6228.27061:

Class File Base chain (to Structure) Rail interfaces
RoboticArmRailStraight Objects/RoboticArm/RoboticArmRailStraight.cs RoboticArmRail -> RoboticArmRailBase -> SmallGrid -> Structure IRoboticArmRail
RoboticArmRailInnerCorner Objects/RoboticArm/RoboticArmRailInnerCorner.cs RoboticArmRail -> RoboticArmRailBase -> SmallGrid -> Structure IRoboticArmRail
RoboticArmJunction Objects/RoboticArm/RoboticArmJunction.cs RoboticArmRailBase -> SmallGrid -> Structure IRoboticArmJunction, IRoboticArmRail
RoboticArmBypass Objects/RoboticArm/RoboticArmBypass.cs RoboticArmRailDeviceBase -> SmallDevice -> Device -> SmallGrid -> Structure IRoboticArmBypass, IRoboticArmJunction, IRoboticArmRail
RoboticArmDock Objects/RoboticArm/RoboticArmDock.cs RoboticArmRailDeviceBase -> SmallDevice -> Device -> SmallGrid -> Structure IRoboticArmBypass, IRoboticArmJunction, IRoboticArmRail
RoboticArmDockAtmos Objects/RoboticArm/RoboticArmDockAtmos.cs RoboticArmDock -> ... inherited
RoboticArmDockCargo Objects/RoboticArm/RoboticArmDockCargo.cs RoboticArmDock -> ... inherited
RoboticArmDockCollector Objects/RoboticArm/RoboticArmDockCollector.cs RoboticArmDock -> ... inherited
RoboticArmDockHydroponics Objects/RoboticArm/RoboticArmDockHydroponics.cs RoboticArmDock -> ... inherited

The RoboticArm class itself (Objects/RoboticArm/RoboticArm.cs) is the traversing arm unit that slides along the rail; it is NOT a chain member and does NOT implement IRoboticArmRail. Painting "the assembly" does not include RoboticArm unless SprayPaintPlus explicitly opts in.

No intermediate "RoboticArmBase" / "RoboticArmMember" mixin interface exists. The lowest common denominator across every chain member is IRoboticArmRail (and therefore also INetworkedRoboticArm since RoboticArmRailBase and RoboticArmRailDeviceBase both implement it, but INetworkedRoboticArm is a member-of-network marker rather than a "thing on the chain" marker).

Concrete rail structures (Assembly-CSharp vs prefab naming)

Assembly-CSharp declares only two concrete RoboticArmRail subclasses, but resources.assets carries eight rail-only prefab variants (plus nine more across junctions/docks). The 8 "pure rail" prefabs all bind to one of the two rail classes; the prefab differences are mesh/transform only.

Confirmed via UnityPy.load('resources.assets') and GameObject-name filter on Structure + RoboticArm:

Rail-only prefabs (built by ItemKitLinearRail, confirmed 8 entries):

  • StructureRoboticArmRailStraight
  • StructureRoboticArmRailStraightStop
  • StructureRoboticArmRailCorner
  • StructureRoboticArmRailCornerStop
  • StructureRoboticArmRailInnerCorner
  • StructureRoboticArmRailOuterCorner
  • StructureRoboticArmRailScrewLeft
  • StructureRoboticArmRailScrewRight

Additional rail-graph prefabs with their own kits:

  • StructureRoboticArmDock (base dock)
  • StructureLarreDockAtmos
  • StructureLarreDockBypass
  • StructureLarreDockCargo
  • StructureLarreDockCollector
  • StructureLarreDockHydroponics

Plus junction variants if present (junction prefab not observed under the StructureRoboticArm prefix in the name scan, suggesting the junction is integrated into dock prefabs or uses a different name).

C# class binding by prefab-name suffix:

Prefab suffix Class
RailStraight, RailStraightStop, RailCorner, RailCornerStop, RailOuterCorner, RailScrewLeft, RailScrewRight RoboticArmRailStraight
RailInnerCorner RoboticArmRailInnerCorner (has dedicated behaviour in RoboticArmNetwork.HandleInnerCorners)
Dock variants RoboticArmDock and its four subclasses
Bypass RoboticArmBypass

The mapping of "7 straight-style prefabs to the same RoboticArmRailStraight class" is inferred from the decompiled class declarations: RoboticArmRailStraight has zero body overrides (its file contains only public class RoboticArmRailStraight : RoboticArmRail {}), and RoboticArmRailInnerCorner is the only other rail subclass that exists. Prefab probing to confirm the exact MonoBehaviour class bound to each of the 8 prefabs was attempted but UnityPy's typetree failed on these MonoBehaviours (script resolution silently errored); the class roster is reliable from decompilation, and the per-prefab mapping assumption is consistent with the absence of any other RoboticArmRail* class.

Connection model: SmallGrid, not world grid

Key fact: rails occupy SmallCell positions, NOT Cell positions. The two lattices are separate; 6-neighbour walking on Cell.NeighborCells will never traverse a rail chain.

SmallGrid constants:

public static float SmallGridSize = 0.5f;
public static float SmallGridOffset = 0.25f;

SmallCell carries a dedicated Rail reference (not just a generic structure list):

// Assets.Scripts.GridSystem.SmallCell
public class SmallCell
{
    ...
    public IRoboticArmRail Rail;
    ...
}

RoboticArmNetwork.ConnectedRoboticArm walks the chain by sampling the small-cell lattice at a rail's OpenEnd world position:

// Objects.RoboticArm.RoboticArmNetwork
private bool ConnectedRoboticArm(IRoboticArmRail current, Connection end,
    out IRoboticArmRail connected, out Connection connectedEnd)
{
    Grid3 localGrid = GridController.World.WorldToLocalGrid(
        end.Transform.position, SmallGrid.SmallGridSize, SmallGrid.SmallGridOffset);
    SmallCell smallCell = GridController.World.GetSmallCell(localGrid);
    if (smallCell == null) { connected = null; connectedEnd = null; return false; }
    if (smallCell.Rail != null && !smallCell.Rail.BeingDestroyed && smallCell.Rail != current
        && ((SmallGrid)smallCell.Rail).IsConnected(end, out var connectedEnd2))
    {
        connected = smallCell.Rail;
        connectedEnd = connectedEnd2;
        return true;
    }
    if (smallCell.Device != null && !smallCell.Device.BeingDestroyed && smallCell.Device != current
        && smallCell.Device.IsConnected(end, out var connectedEnd3)
        && smallCell.Device is IRoboticArmRail roboticArmRail)
    {
        connected = roboticArmRail;
        connectedEnd = connectedEnd3;
        return true;
    }
    connected = null; connectedEnd = null;
    return false;
}

For a connected flood-fill over a rail chain, the canonical walk is the network's own RebuildNetwork style BFS via INetworkedStructure.ConnectedStructures().

Network-level chain traversal

Each rail piece reports connected neighbours via RoboticArmRailBase.ConnectedStructures():

public List<INetworkedStructure> ConnectedStructures()
{
    return ConnectedRoboticArms();
}

ConnectedRoboticArms() is inherited from SmallGrid. It samples the small-cell lattice at every entry of OpenEnds and keeps whichever occupant answers the one-argument IsConnected:

// Assets.Scripts.Objects.SmallGrid.ConnectedRoboticArms() :318859
public List<INetworkedStructure> ConnectedRoboticArms()
{
    FoundRoboticArms.Clear();
    foreach (Connection openEnd in OpenEnds)
    {
        Grid3 localGrid = base.GridController.WorldToLocalGrid(openEnd.Transform.position, SmallGridSize, SmallGridOffset);
        SmallCell smallCell = base.GridController.GetSmallCell(localGrid);
        if (smallCell != null)
        {
            if (smallCell.Rail != null && smallCell.Rail != this as IRoboticArmRail && ((SmallGrid)smallCell.Rail).IsConnected(openEnd) && smallCell.Rail is INetworkedRoboticArm item)
            {
                FoundRoboticArms.Add(item);
            }
            else if (smallCell.Device != null && smallCell.Device != this && smallCell.Device.IsConnected(openEnd) && smallCell.Device is INetworkedRoboticArm item2)
            {
                FoundRoboticArms.Add(item2);
            }
        }
    }
    return FoundRoboticArms;
}

This is the neighbour source for the membership BFS (StructureNetwork.GetNeighbours), and it is not the predicate the RailList walk uses. The two differ in both arity and branching; see "The two walks use different connection predicates" below.

The network itself is a RoboticArmNetwork : StructureNetwork instance referenced by each rail via RoboticArmRailBase.RoboticArmNetwork:

public StructureNetwork StructureNetwork { get; set; }
public RoboticArmNetwork RoboticArmNetwork => StructureNetwork as RoboticArmNetwork;

RoboticArmRailDeviceBase (the parent of RoboticArmBypass and RoboticArmDock) declares the exact same two members verbatim; both base classes share the StructureNetwork/RoboticArmNetwork pair, so the accessor works uniformly from every family member.

RoboticArmNetwork maintains four parallel lists once OnNetworkChanged has walked the chain:

// Objects.RoboticArm.RoboticArmNetwork
public readonly List<IRoboticArmJunction> JunctionList = new List<IRoboticArmJunction>();
public readonly List<RailNode>             RailNodeList = new List<RailNode>();
public readonly List<RoboticArmDock>       DockList     = new List<RoboticArmDock>();
public readonly List<IRoboticArmRail>      RailList     = new List<IRoboticArmRail>();

Critical fact re-confirmed against the OnNetworkChanged body: RailList holds every single member of the chain, regardless of subclass, because the walk adds roboticArmRail unconditionally. The typed lists (DockList, JunctionList) are filtered subsets populated inside the same loop:

// Objects.RoboticArm.RoboticArmNetwork.OnNetworkChanged excerpt
RailList.Add(roboticArmRail);
if (roboticArmRail is RoboticArmDock roboticArmDock)
{
    roboticArmDock.Flipped = flag;
    DockList.Add(roboticArmDock);
}
if (roboticArmRail is IRoboticArmJunction roboticArmJunction)
{
    roboticArmJunction.JunctionIndex = JunctionList.Count;
    JunctionList.Add(roboticArmJunction);
}

Important: RoboticArmBypass and RoboticArmDock both implement IRoboticArmJunction (see interface section below), so the "junction" members of JunctionList include bypass and dock pieces in addition to the plain RoboticArmJunction class. JunctionList is not a pure "junction-only" filter; it is "every member that implements IRoboticArmJunction."

RailList is the track-ordered walk, starting from the "leftmost" rail discovered from the starting dock. It is not the network's membership, and on a real world the two routinely disagree: see "RailList is not the persisted membership" below for the measured divergence and for which list answers which question. For "every piece that belongs to this network", read StructureList.

RailList is rebuilt whenever the chain topology changes; consumers should re-fetch after any OnNetworkChanged cycle.

IsConnected override: angular alignment matters

Both RoboticArmRailBase and RoboticArmRailDeviceBase override IsConnected to add an angular-alignment check for rail-to-rail connections (world grid coincidence is not sufficient):

public override bool IsConnected(Connection otherEnd)
{
    Grid3 facingGrid = otherEnd.GetFacingGrid();
    foreach (Connection openEnd in OpenEnds)
    {
        if ((openEnd.ConnectionType & otherEnd.ConnectionType) != NetworkType.None
            && !(facingGrid != openEnd.GetLocalGrid()))
        {
            if (!(otherEnd.Parent is IRoboticArmRail))
                return true;
            return ThreadedManager.IsThread
                ? RocketMath.CompareVectors(openEnd.TransformUp, otherEnd.TransformUp)
                : RocketMath.CompareVectors(openEnd.Transform.up, otherEnd.Transform.up);
        }
    }
    return false;
}

Two rail pieces only connect if their Transform.up vectors align. This is what prevents a rail chain from branching arbitrarily: the chain follows a single continuous "belt" orientation through each piece.

Re-read verbatim at 0.2.6428.27798 on both bases; the two bodies are identical to each other and to what is printed above. What matters as much as the body is which overload carries it: this is the one-argument SmallGrid.IsConnected(Connection), the only one of the two declared virtual. The two-argument IsConnected(Connection, out Connection) is non-virtual and unoverridden, so the alignment test does not apply to callers that use it. RoboticArmNetwork.ConnectedRoboticArm is exactly such a caller; see "The two walks use different connection predicates" below for what that costs.

Painting behaviour

Rails inherit PaintableMaterial and CustomColor from Structure. Whether they actually accept paint depends on Structure.structureRenderMode: the base SetCustomColor throws NotImplementedException for any mode other than Standard (batched-mesh structures share a combined renderer and cannot be coloured per-instance):

// Assets.Scripts.Objects.Structure.cs
public StructureRenderMode structureRenderMode;
...
public override void SetCustomColor(int index, bool emissive = false)
{
    if (structureRenderMode == StructureRenderMode.Standard)
    {
        base.SetCustomColor(index, emissive);
        ...
    }
    ...
}

Rail prefabs use Standard render mode in the vanilla game (the player can paint a rail individually with the current spray paint can), so SetCustomColor works. SprayPaintPlus already guards against the Batched exception in PaintSafe; rails will route through the catch only if a specific rail prefab's render mode is configured otherwise, which has not been observed.

Interface hierarchy among chain members

The three rail-family interfaces form a strict subset chain:

// Objects.RoboticArm.IRoboticArmRail
public interface IRoboticArmRail : IReferencable, IEvaluable
{
    List<RailNode> RailNodes { get; }
    SmallCell SmallCell { get; set; }
    Vector3 Pivot { get; }
    SmallGrid AsSmallGrid { get; }
    Connection OtherEnd(Connection end);
    void RailNetworkUpdated();
}

// Objects.RoboticArm.IRoboticArmJunction
public interface IRoboticArmJunction : IRoboticArmRail, IReferencable, IEvaluable
{
    int JunctionIndex { get; set; }
}

// Objects.RoboticArm.IRoboticArmBypass
public interface IRoboticArmBypass : IRoboticArmJunction, IRoboticArmRail, IReferencable, IEvaluable
{
    Vector3 BypassPosition { get; }
    bool CanOpen { get; }
    bool CanClose { get; }
    void SetOpen(int state);
}

Subset relationships (via interface inheritance, verified by reading the three interface files):

  • Every IRoboticArmBypass is an IRoboticArmJunction is an IRoboticArmRail.
  • RoboticArmJunction implements IRoboticArmJunction (therefore IRoboticArmRail).
  • RoboticArmBypass and RoboticArmDock implement IRoboticArmBypass (therefore IRoboticArmJunction and IRoboticArmRail).
  • Plain rails (RoboticArmRail and subclasses) implement only IRoboticArmRail.

Consequence: a single is IRoboticArmRail test matches every chain member. Casting to SmallGrid via the AsSmallGrid getter or casting to Structure directly works for all members because every one of them is Structure-derived.

Network ownership and enumeration entry points

Two parallel ownership structures exist:

1. Per-member back-reference. Every chain member exposes RoboticArmNetwork as a property returning StructureNetwork as RoboticArmNetwork. This works from any starting point (rail, junction, bypass, dock):

// RoboticArmRailBase AND RoboticArmRailDeviceBase both declare:
public StructureNetwork StructureNetwork { get; set; }
public RoboticArmNetwork RoboticArmNetwork => StructureNetwork as RoboticArmNetwork;

"Works from any starting point" was read out of the declarations, and it has now been measured as well, on a listen host that had loaded the Spray Paint Plus fixture world at 0.2.6428.27798 on 2026-09-23. GET /thing on StructureLarreDockCargo 210219 reports runtime type RoboticArmDockCargo, RoboticArmNetwork.ReferenceId 210186 and RoboticArmNetwork.StructureList count 35, which is the same network object and the same count that rail StructureRoboticArmRailInnerCorner 182955 on that network reports; the same pairing holds on network 179533 between dock 176391 and its rails, at 19. So a dock is not a special case for enumeration: a consumer that dispatches on INetworkedRoboticArm reaches the identical membership whichever piece of the assembly it was handed. Worth stating explicitly because the two bases arrive at that interface from different sides of the hierarchy, RoboticArmRailBase from SmallGrid and RoboticArmRailDeviceBase from Device, and a consumer that also has a branch for pipes or cables has to know that Device branches off SmallGrid separately from the pipe family, where Pipe, Cable and Chute all derive from SmallSingleGrid, so no is Pipe test ever reaches a dock.

2. Global static registry. RoboticArmNetwork maintains a static list of all live networks, updated via OnAssignedReference / OnDeregister:

// Objects.RoboticArm.RoboticArmNetwork
public static readonly List<RoboticArmNetwork> AllRoboticArmNetworks = new List<RoboticArmNetwork>();

public override void OnAssignedReference()
{
    base.OnAssignedReference();
    AllRoboticArmNetworks.Add(this);
}

protected override void OnDeregister()
{
    base.OnDeregister();
    AllRoboticArmNetworks.Remove(this);
}

public static long[] AllNetworkIds => ReferencableNetworkHelper.GetValidNetworkIds(AllRoboticArmNetworks);

GetValidNetworkIds is not a plain projection: it drops nulls and networks that fail IsNetworkValid(), so the array is strictly shorter than AllRoboticArmNetworks whenever a tracked network is null or memberless. This array is what the save writes (worldData.RoboticArmNetworks = RoboticArmNetwork.AllNetworkIds;):

// Networks.ReferencableNetworkHelper
public static long[] GetValidNetworkIds<T>(List<T> networks) where T : ReferencableNetwork
{
    List<long> list = new List<long>(networks.Count);
    foreach (T network in networks)
    {
        if (network != null && network.IsNetworkValid())
        {
            list.Add(network.ReferenceId);
        }
    }
    return list.ToArray();
}

IsNetworkValid() resolves to ReferencableNetwork<TMember>.IsNetworkValid(), which is Members != null ? Members.Count > 0 : false; neither StructureNetwork nor RoboticArmNetwork overrides it. A second overload taking a DensePool<T> filters on IsNetworkValid() only, with no null check; it is not the one RoboticArmNetwork uses.

Adding to and removing from the static registry chains to the base, so a RoboticArmNetwork lands in three static lists on assignment (ReferencableNetworkHelper.AllNetworks, StructureNetwork.AllStructureNetworks, AllRoboticArmNetworks) and leaves all three on deregister.

Canonical enumeration API for "every member of this chain, given any starting member":

// 'start' is any IRoboticArmRail / RoboticArmJunction / RoboticArmBypass / RoboticArmDock / RoboticArmDockAtmos etc.
RoboticArmNetwork network = ((INetworkedRoboticArm)start).RoboticArmNetwork;
if (network == null) return;  // piece was just placed and not yet networked
foreach (INetworkedStructure member in network.StructureList)
{
    if (member is Structure s && !s.BeingDestroyed)
    {
        s.SetCustomColor(colorIndex);
    }
}

StructureList is the membership: it is the Members list of ReferencableNetwork<INetworkedStructure>, it is written only by Add and Remove, and it is what the save restores from each piece's RoboticArmNetworkId. Cast to Structure, not to IRoboticArmRail: RoboticArmRailBase and RoboticArmRailDeviceBase declare INetworkedRoboticArm but not IRoboticArmRail, so nothing in the type system guarantees a network member is a rail. Filter on BeingDestroyed, because a piece mid-teardown is still in Members until OnDestroy reaches Remove.

Do not use RailList for this. It is a derived cache rebuilt from track geometry, it is empty on any network with no dock, and on a dock-bearing network it can contain pieces belonging to other networks. See "RailList is not the persisted membership" below for the code and the measurements.

RailList is the right list for track order, RailNodeList indices, JunctionIndex, loop detection, and anything else that follows the arm's path rather than ownership. On those questions StructureList is the wrong list, because it is in insertion order and carries no geometry.

StructureNetwork.StructureList (a List<INetworkedStructure>) is inherited from Networks.StructureNetwork. It is a get-only property, not a field, and it returns the member list owned by the generic base class ReferencableNetwork<TMember>:

// Networks.StructureNetwork
public abstract class StructureNetwork : ReferencableNetwork<INetworkedStructure>, ISyncListable
{
    public List<INetworkedStructure> StructureList => Members;
}

// Networks.ReferencableNetwork<TMember>
public abstract class ReferencableNetwork<TMember>(long referenceId = 0L) : ReferencableNetwork(referenceId) where TMember : class, INetworkMember
{
    public readonly List<TMember> Members = new List<TMember>();
}

The metadata carries a getter and no setter, and the getter is a straight load of the base-class field:

.property instance class [mscorlib]System.Collections.Generic.List`1<class Networks.INetworkedStructure> StructureList()
{
    .get instance class [mscorlib]System.Collections.Generic.List`1<class Networks.INetworkedStructure> Networks.StructureNetwork::get_StructureList()
}

.method public hidebysig specialname
    instance class [mscorlib]System.Collections.Generic.List`1<class Networks.INetworkedStructure> get_StructureList () cil managed
{
    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<!0> class Networks.ReferencableNetwork`1<class Networks.INetworkedStructure>::Members
    IL_0006: ret
}

This was a field until 0.2.6428.27798, and the change broke shipped mods

StructureList was a plain public field (public List<INetworkedStructure> StructureList = new List<INetworkedStructure>();) when this page was first written against 0.2.6228.27061. It is a property at 0.2.6428.27798. The change arrived with the game update of 12-13 August 2026, which replaced 0.2.6403.27689: Research/ section stamps run at 0.2.6403.27689 through 2026-08-11 and at 0.2.6428.27798 from 2026-08-13, and published Spray Paint Plus builds compiled against the older shape threw for players from that update until v1.11.1 rebuilt against the new one (Mods/SprayPaintPlus/CHANGELOG.md v1.11.1; Mods/SprayPaintPlus/playtests/NetworkWalkSurvivesGameUpdate.cs). Measured on the artifacts: the published v1.10.1 DLL carries ldfld ... Networks.StructureNetwork::StructureList at every access site and no get_StructureList; the current build of the same mod carries callvirt ... get_StructureList() and no ldfld.

The player-visible failure, because Mono fails the JIT of the whole method containing the dead field load rather than just that line:

[EXCEPTION] missingfieldexception: field not found:
system.collections.generic.list-1<networks.inetworkedstructure>
networks.structurenetwork.structurelist

Consequences for mod code:

  • A rebuild against the current assembly is the whole fix. The C# source does not change, because network.StructureList compiles identically against a field and against a property; only the emitted IL differs.
  • Reflection must target the property. AccessTools.Field(typeof(StructureNetwork), "StructureList") returns null at the current version. Use AccessTools.PropertyGetter(typeof(StructureNetwork), "StructureList"), or read the inherited Members field directly. For a read that survives the next flip of this kind without a rebuild, see ../Patterns/HarmonyFieldOrProperty.md.
  • The list instance cannot be replaced. Members is readonly (initonly in IL) and StructureList has no setter. The contents are still mutable.

How the list is populated

The breadth-first walk lives on the generic base as RebuildNetworkCore, over an abstract GetNeighbours that StructureNetwork implements with ConnectedStructures(). There is no RebuildNetwork method on StructureNetwork at this version; the only void RebuildNetwork(...) left in the assembly is a private static on CableNetwork, a different family that does not derive from ReferencableNetwork.

// Networks.ReferencableNetwork<TMember>
protected void RebuildNetworkCore(TMember origin, ReferencableNetwork oldNetwork = null)
{
    Queue<TMember> queue = new Queue<TMember>(GetNeighbours(origin));
    HashSet<TMember> hashSet = new HashSet<TMember>();
    while (queue.Count > 0)
    {
        TMember val = queue.Dequeue();
        if (hashSet.Contains(val) || queue.Contains(val) || val.IsBeingDestroyed)
        {
            continue;
        }
        hashSet.Add(val);
        foreach (TMember neighbour in GetNeighbours(val))
        {
            if (!hashSet.Contains(neighbour))
            {
                queue.Enqueue(neighbour);
            }
        }
        OnRebuildVisit(val, oldNetwork);
        Add(val);
    }
}

// Networks.StructureNetwork
protected override IEnumerable<INetworkedStructure> GetNeighbours(INetworkedStructure segment)
{
    return segment.ConnectedStructures();
}

The server entry point does not run the walk on the existing instance. It creates a fresh network, seeds it with the origin member, and runs the core on the new instance with the old one passed in:

// Networks.ReferencableNetwork<TMember>
public virtual void RebuildNetworkServer(TMember member)
{
    if (GameManager.GameState != GameState.None && !base.BeingDestroyed)
    {
        ReferencableNetwork<TMember> referencableNetwork = CreateNewNetwork();
        OnRebuildNetworkCreated(referencableNetwork);
        referencableNetwork.Add(member);
        RegisterRebuildServer(member.ReferenceId, referencableNetwork.ReferenceId, base.ReferenceId);
        referencableNetwork.RebuildNetworkCore(member, this);
    }
}

OnRebuildVisit and OnRebuildNetworkCreated are both empty virtuals on the base. Neither StructureNetwork nor RoboticArmNetwork overrides them; the only OnRebuildNetworkCreated override in the assembly is on the rocket-part network, which carries display and flight settings across a rebuild. The client half is RebuildNetworkClient(INetworkMember, ReferencableNetwork), a sealed override that routes into the same RebuildNetworkCore.

For painting the entire assembly, read StructureList, not RailList. StructureList is Members, so it is exactly the pieces that belong to this network in every case the code permits; RailList is a cache re-derived from track geometry that is empty on a dockless network and over-collects on a dock-bearing one. The element type is INetworkedStructure rather than IRoboticArmRail, so the loop needs one is Structure test per element, which is the whole cost of reading the correct list. The next section carries the code and the measurements.

RailList is not the persisted membership: it is cleared on every Add and Remove, and rebuilt from a dock

RailList and StructureList answer different questions, and on a real world they routinely disagree. StructureList is Members, the set restored from each rail's RoboticArmNetworkId (see "Membership on load and on join comes from the saved id, not from geometry" in ../GameSystems/StructureGridSnapping.md). RailList is a cache that OnNetworkChanged clears and refills, and both mutators of Members end in an OnNetworkChanged call, load included:

// Networks.ReferencableNetwork<TMember>.Add(TMember) :193260
        member.Network = this;
        lock (Members)
        {
            Members.Add(member);
        }
        OnMemberAdded(member);
        OnNetworkChanged();
        return true;

Remove reaches it one step further out, through RefreshNetwork, which is also what deregisters a network that has just lost its last member:

// Networks.ReferencableNetwork<TMember>.Remove(TMember) :193281
            member.Network = null;
            lock (Members)
            {
                Members.Remove(member);
            }
            OnMemberRemoved(member);
            RefreshNetwork();
            return true;

// Networks.ReferencableNetwork<TMember>.RefreshNetwork() :193305
        public void RefreshNetwork()
        {
            lock (Members)
            {
                for (int num = Members.Count - 1; num >= 0; num--)
                {
                    if (Members[num] == null)
                    {
                        Members.RemoveAt(num);
                    }
                }
            }
            OnNetworkChanged();
            ...

The null prune in RefreshNetwork is Members[num] == null on an unconstrained type parameter, so it is plain reference equality and not Unity's overloaded fake-null operator. A destroyed-but-not-yet-collected MonoBehaviour survives that prune, which is the second reason a consumer filters on BeingDestroyed rather than on a null check.

Three properties of the refill matter to anything that enumerates an assembly.

1. No dock means an EMPTY RailList. The rebuild returns before adding anything when the network's StructureList holds no RoboticArmDock:

// Objects.RoboticArm.RoboticArmNetwork.OnNetworkChanged() :176827
protected override void OnNetworkChanged()
{
    base.OnNetworkChanged();
    JunctionList.Clear();
    RailNodeList.Clear();
    DockList.Clear();
    RailList.Clear();
    Looping = false;
    if (!GetStartingDock(out var startingDock))
    {
        return;
    }
    GetLeftMostRail(startingDock, out var rail, out var rightHandEnd);
    ...
// Objects.RoboticArm.RoboticArmNetwork.GetStartingDock(out RoboticArmDock) :176949
private bool GetStartingDock(out RoboticArmDock startingDock)
{
    List<RoboticArmDock> list = new List<RoboticArmDock>();
    foreach (INetworkedStructure structure in base.StructureList)
    {
        if (structure is RoboticArmDock item)
        {
            list.Add(item);
        }
    }
    if (list.Count == 0)
    {
        startingDock = null;
        return false;
    }
    list.Sort(CompareDocks);
    startingDock = list[0];
    return true;
}

CompareDocks is (a, b) => a.ReferenceId.CompareTo(b.ReferenceId), so the walk always starts at the lowest-ReferenceId dock on the network.

2. The walk is geometric and has no membership filter, so it crosses persisted networks. ConnectedRoboticArm resolves the next piece out of the small-cell lattice at the current piece's open end and never looks at Network, StructureNetwork or RoboticArmNetworkId:

// Objects.RoboticArm.RoboticArmNetwork.ConnectedRoboticArm(...) :176988
private bool ConnectedRoboticArm(IRoboticArmRail current, Assets.Scripts.Objects.Connection end, out IRoboticArmRail connected, out Assets.Scripts.Objects.Connection connectedEnd)
{
    Grid3 localGrid = GridController.World.WorldToLocalGrid(end.Transform.position, SmallGrid.SmallGridSize, SmallGrid.SmallGridOffset);
    SmallCell smallCell = GridController.World.GetSmallCell(localGrid);
    if (smallCell == null)
    {
        connected = null;
        connectedEnd = null;
        return false;
    }
    if (smallCell.Rail != null && !smallCell.Rail.BeingDestroyed && smallCell.Rail != current && ((SmallGrid)smallCell.Rail).IsConnected(end, out var connectedEnd2))
    {
        connected = smallCell.Rail;
        connectedEnd = connectedEnd2;
        return true;
    }
    if (smallCell.Device != null && !smallCell.Device.BeingDestroyed && smallCell.Device != current && smallCell.Device.IsConnected(end, out var connectedEnd3) && smallCell.Device is IRoboticArmRail roboticArmRail)
    {
        connected = roboticArmRail;
        connectedEnd = connectedEnd3;
        return true;
    }
    connected = null;
    connectedEnd = null;
    return false;
}

3. The walk is strictly two-ended, so it cannot branch. Each step advances with connection = connected.OtherEnd(connectedEnd), and every implementation of OtherEnd in the family is the same two-element pick. All four are byte-identical, including the ones on the pieces named "junction":

// Objects.RoboticArm.RoboticArmRail :177102, RoboticArmJunction :176699,
// RoboticArmBypass :172903, RoboticArmDock :174368 -- identical in all four
public Assets.Scripts.Objects.Connection OtherEnd(Assets.Scripts.Objects.Connection end)
{
    if (end != OpenEnds[0])
    {
        return OpenEnds[0];
    }
    return OpenEnds[1];
}

ConnectedRoboticArms(), the membership neighbour source, iterates the whole OpenEnds collection instead (OpenEndsPermutation is declared new int[6], so a piece can carry more than two). A member reachable only through a third end is therefore in StructureList and absent from RailList. Not exercised by the measurements below, but it follows from the code, and it is the opposite failure to the over-collection in point 2: RailList can be short as well as long.

The two walks use different connection predicates

SmallGrid declares two IsConnected overloads, and only one of them is virtual:

// Assets.Scripts.Objects.SmallGrid :318645
public virtual bool IsConnected(Connection otherEnd)

// Assets.Scripts.Objects.SmallGrid :318662
public bool IsConnected(Connection otherEnd, out Connection connectedEnd)

Both bodies test the same two things, a ConnectionType mask overlap and facingGrid == openEnd.GetLocalGrid(). The difference is what overrides them. RoboticArmRailBase and RoboticArmRailDeviceBase override the one-argument virtual one to add the angular-alignment test documented under "IsConnected override: angular alignment matters" above; the two-argument overload is non-virtual and has no override anywhere in the family, so the up-vector test never runs on it.

The membership walk calls the virtual one: ((SmallGrid)smallCell.Rail).IsConnected(openEnd) inside ConnectedRoboticArms(). The cast to SmallGrid does not suppress virtual dispatch, so the rail override fires and the up-vector test applies.

The RailList walk calls the non-virtual one: ((SmallGrid)smallCell.Rail).IsConnected(end, out var connectedEnd2) inside ConnectedRoboticArm, above.

So RailList is built with a strictly looser predicate than membership. Two rail pieces that meet at one small cell with mismatched Transform.up are in different networks by the membership rule and are still traversed into each other's RailList. This is an independent route to the divergence in point 2, on top of the saved-id route: membership on load comes from RoboticArmNetworkId and is not re-derived from geometry at all.

Measured, on a loaded world

Read with GET /thing?refIds=<a member>&fields=RoboticArmNetwork.RailList,RoboticArmNetwork.StructureList&expand=true against a listen host that had loaded the committed fixture world Mods/SprayPaintPlus/playtests/fixtures/SprayPaintPlusWorld.save, on 2026-09-23, game 0.2.6428.27798. That world's <RoboticArmNetworks> list holds six ids; five were read directly.

Network id StructureList (persisted members) Holds a RoboticArmDock RailList
179533 19 yes, StructureLarreDockCargo 176391 19, exactly its own members
210186 35 yes, StructureLarreDockCargo 210219 54
183078 12 no 0
183727 6 no 0
414638 9 no 0
183729 1 no not read directly

A second load on the same day re-read the StructureList column of every row through a different code path, the membership guards of the Spray Paint Plus playtest a rail stroke follows persisted membership, dock or no dock, and got the same six numbers; that run also read network 183729's StructureList directly off its single member 183728 and got 1, which closes the one cell above that had been inferred from world.xml rather than read. It re-read three RailLists at the same time (183727 at 0, 210186 at 54, 179533 at 19) and they too were unchanged. RailList on 183729 is still not read directly.

The live StructureList count matches the saved membership exactly on all five networks read: grouping the 82 rail-bearing things in that world's world.xml by RoboticArmNetworkId gives {414638: 9, 179533: 19, 183078: 12, 210186: 35, 183727: 6, 183729: 1}, and <RoboticArmNetworks> lists those same six ids with none unused and none empty. That is the direct confirmation that StructureList is the persisted set.

The 54 on network 210186 is its own 35 plus exactly the 19 members of networks 183078 (12), 183727 (6) and 183729 (1), which sit at the far end of the same physical track. Re-derived as reference-id sets rather than counts: RailList(210186) - StructureList(210186) is 19 ids; 12 of them are the whole of StructureList(183078), 6 are the whole of StructureList(183727), and the remaining one is 183728, a StructureRoboticArmRailInnerCorner which the save records as the single member of network 183729. Nothing of StructureList(210186) is missing from its RailList, and neither of the two networks further away (179533, 414638) contributes anything. On network 179533 the two sets are equal. So on this world three whole assemblies appear in a RailList they do not belong to, while their own RailList is empty.

Consequences for anything that walks an assembly:

  • A pass driven off RailList does nothing at all on a dockless assembly, with no exception and no log line, because the list is empty rather than null. Half the rail networks in one real world are in that state, and a dockless network is an ordinary thing rather than a corrupt one: RoboticArmRailBase.OnRegistered creates one with new RoboticArmNetwork(0L).Add(this) for the first rail placed away from anything to merge with.
  • A pass driven off RailList on a dock-bearing assembly can reach pieces on other networks, which is correct for the arm (the arm really can traverse the whole physical track) and wrong for anything keyed to network identity.
  • StructureList is the only list that answers "what belongs to this network", in every case the code permits. That is the list to read for painting, for counting, and for any per-network decision.
  • RailList cannot be authored offline by editing RoboticArmNetworkId in world.xml, which is the one thing that works for CableNetworkId, PipeNetworkId and ChuteNetworkId. The id decides which network object a rail joins; the contents of RailList are then re-derived from the lattice.

Rail nodes (arm traversal, not painting)

IRoboticArmRail exposes RailNodes, a list of RailNode control points that the arm slides between:

public interface IRoboticArmRail : IReferencable, IEvaluable
{
    List<RailNode> RailNodes { get; }
    SmallCell SmallCell { get; set; }
    Vector3 Pivot { get; }
    SmallGrid AsSmallGrid { get; }
    Connection OtherEnd(Connection end);
    void RailNetworkUpdated();
}

Rail nodes are a motion-path abstraction used by RoboticArmDock.MoveOnRail(). They are NOT relevant to painting; painting cares about StructureList on the RoboticArmNetwork only. RailNodeList and RailList are the arm's view of the track and are cleared and refilled together by the same OnNetworkChanged walk, so both are empty on a network with no dock.

Verification history

  • 2026-04-20: page creation. Class hierarchy and rail-family enumeration verified against decompiled Assembly-CSharp.dll in game version 0.2.6228.27061. Prefab name list verified via UnityPy scan of resources.assets.
  • 2026-09-21: conflict on "Networks.StructureNetwork.StructureList member kind", in section "Network ownership and enumeration entry points". Previous claim: a public field, public List<INetworkedStructure> StructureList = new List<INetworkedStructure>();. New finding: a get-only property, public List<INetworkedStructure> StructureList => Members;, over public readonly List<TMember> Members on a generic base class ReferencableNetwork<TMember> that StructureNetwork now derives from. Fresh validator verdict: the new finding is correct. The validator confirmed it three ways against the live assembly at 0.2.6428.27798: decompiled C#, metadata (a .property with a .get and no .set, zero .field ... StructureList anywhere in the assembly), and IL (every call site is callvirt ... get_StructureList(), with no ldfld/stfld of StructureList anywhere). Result: the declaration is corrected and the IL is recorded; a subsection documents the field-to-property change, its window, and the MissingFieldException it produced in shipped mods; the page now links ../Patterns/HarmonyFieldOrProperty.md.
  • 2026-09-21: conflict on "what populates the structure list", same section. Previous claim: protected override void RebuildNetwork(INetworkedStructure, StructureNetwork) on StructureNetwork, with an inline BFS over ConnectedStructures(). New finding: no such method exists on StructureNetwork at this version; the BFS is protected void RebuildNetworkCore(TMember origin, ReferencableNetwork oldNetwork = null) on ReferencableNetwork<TMember>, iterating an abstract GetNeighbours that StructureNetwork overrides to segment.ConnectedStructures(), with a new OnRebuildVisit hook before Add. Fresh validator verdict: the new finding is correct. An assembly-wide search returned five RebuildNetwork* declarations, none on StructureNetwork; the only void RebuildNetwork(...) is a private static on CableNetwork, a family that does not derive from ReferencableNetwork. Result: the old body is replaced by the current one, plus the RebuildNetworkServer entry point that runs the walk on a freshly created network rather than on the existing instance. The previous body reads as an accurate transcription of the pre-refactor assembly, so this is version drift rather than an original transcription error.
  • 2026-09-21: conflict on "RoboticArmNetwork.AllNetworkIds", same section. Previous claim: a get { ... } block building a List<long> and adding every network's ReferenceId unconditionally. New finding: public static long[] AllNetworkIds => ReferencableNetworkHelper.GetValidNetworkIds(AllRoboticArmNetworks);. Fresh validator verdict: the new finding is correct. Result: the declaration is corrected and GetValidNetworkIds recorded, including the behavioural difference the old snippet hid: it drops nulls and networks failing IsNetworkValid() (Members.Count > 0), so the array is strictly shorter than the registry whenever a tracked network is null or memberless, and that filtered array is what the save writes. OnAssignedReference and OnDeregister were re-read in the same pass and still match what the page shows.
  • 2026-09-23: additive. New section "RailList is not the persisted membership: it is cleared on every Add and rebuilt from a dock", recording the OnNetworkChanged early return when GetStartingDock finds none, the absence of any membership filter in ConnectedRoboticArm, and a live reading of all six rail networks in the Spray Paint Plus fixture world. Nothing already on the page was changed or removed: the 2026-04-20 claim that RailList.Add is unconditional inside the loop is still exactly right, and the 2026-04-20 sentence naming the "starting dock" already implied one. What was missing is what happens when there is no dock (the list stays empty) and the fact that the walk crosses persisted-network boundaries. One sentence elsewhere on the page is now in tension with this and was deliberately left alone rather than rewritten: "For painting the entire assembly, RailList is preferred over StructureList", in "How the list is populated". It holds for an assembly with a dock and fails outright for one without, so it needs a reconciliation pass rather than an in-place edit; see Open questions.
  • 2026-09-23: conflict on "which list enumerates a rail assembly", in sections "Network-level chain traversal", "Network ownership and enumeration entry points" (subsections "Canonical enumeration API" and "How the list is populated") and "Rail nodes". Previous claim: "For painting the entire assembly, RailList is preferred over StructureList because it is the list the network itself considers canonical and is already filtered to IRoboticArmRail members", with a canonical code sample iterating network.RailList. New finding: RailList is not the membership at all; it is a cache that OnNetworkChanged clears and refills from track geometry, it is empty whenever the network holds no RoboticArmDock, and on a dock-bearing network it can contain pieces belonging to other networks, so StructureList is the list to read for membership. Fresh validator verdict: the new finding is correct. The validator read OnNetworkChanged, GetStartingDock, GetLeftMostRail, ConnectedRoboticArm, ConnectedRoboticArms, both SmallGrid.IsConnected overloads, ReferencableNetwork<TMember>.Add / Remove / RefreshNetwork and RoboticArmRailBase.DeserializeSave, and independently re-parsed the five live /thing readings; it reported 3 of 3 empty RailLists corresponding exactly to the 3 networks with no dock, and the 19-piece excess on network 210186 corresponding exactly to three whole neighbouring networks. It added two mechanisms the finding had not named, both re-confirmed here against the decompile before persisting: the RailList walk calls the non-virtual two-argument IsConnected(Connection, out Connection) while the membership walk calls the virtual one-argument IsConnected(Connection) that RoboticArmRailBase and RoboticArmRailDeviceBase override with the up-vector test, so RailList is built with a strictly looser predicate; and every OtherEnd in the family is a two-element pick over OpenEnds[0] / OpenEnds[1] while ConnectedRoboticArms() iterates all of OpenEnds, so RailList can also come up short where membership branches. Result: the preference sentence is reversed, the canonical code sample now iterates StructureList with an is Structure test and a BeingDestroyed filter, the "Rail nodes" closing sentence is corrected, two subsections were added for the predicate asymmetry and the two-ended walk, the Add-only claim is widened to Add and Remove (via RefreshNetwork), the measurement is restated as reference-id set identities and cross-checked against the fixture's world.xml grouping, and the corresponding Open question is retired. RailList keeps its documented uses: track order, RailNodeList indices, JunctionIndex, loop detection.
  • 2026-09-23: additive, confirmatory. The measurement table in "RailList is not the persisted membership" was re-read on a second live load of the same fixture world, from the membership guards of the Spray Paint Plus playtest a rail stroke follows persisted membership, dock or no dock, and every number matched: StructureList at 19, 35, 12, 6, 9 and 1, and RailList at 19, 54 and 0. Network 183729's StructureList was read directly for the first time (1, off member 183728), so that cell is no longer inferred from world.xml. Nothing on the page was changed; one sentence was added under the table to record the second reading.
  • 2026-09-23: additive, confirmatory. Two paragraphs added under the RoboticArmNetwork accessor in "Network ownership and enumeration entry points", recording that the "works from any starting point" claim, which had only ever been read out of the two base-class declarations, was measured live: a RoboticArmDockCargo and a RoboticArmRailInnerCorner on network 210186 report the same RoboticArmNetwork.ReferenceId and the same StructureList count (35), and dock 176391 and the rails of network 179533 likewise agree at 19. The note also spells out the hierarchy consequence a consumer needs, that Device branches off SmallGrid separately from the SmallSingleGrid pipe family, so a type test for pipes cannot claim a dock. Nothing already on the page was changed and the section stamp was left at 2026-09-21, because only that one claim was re-read.
  • 2026-04-20: expansion pass. Added the complete family roster table, interface hierarchy section (IRoboticArmRail / IRoboticArmJunction / IRoboticArmBypass), network ownership and enumeration entry points (RailList, AllRoboticArmNetworks static list, StructureNetwork.StructureList fallback). Re-confirmed via direct read of RoboticArmNetwork.OnNetworkChanged that RailList is the complete membership set (every call RailList.Add(roboticArmRail) is unconditional; DockList / JunctionList are subsets filtered by is-checks inside the same loop). Re-confirmed against RoboticArmRailBase.cs and RoboticArmRailDeviceBase.cs that both bases declare the identical RoboticArmNetwork accessor, so traversal from any chain member works the same way. No prior claims contradicted; all additions are additive.

Open questions

  • Whether a dockless assembly's RailList ever fills later in a session. The code says it must: building a dock onto the track runs RoboticArmRailBase.OnRegistered, which either merges into the existing network or creates one, and either path ends in Add, so OnNetworkChanged runs with GetStartingDock now succeeding. What is not measured is the in-game behaviour, only the loaded-world state, so this stays open until a rig run watches an empty RailList fill on dock placement.
  • Whether the over-collection measured on network 210186 is driven by the saved-id route, by the looser IsConnected predicate, or by both. Both mechanisms are verified in the code and either one alone is sufficient to produce the observed result; no per-piece Transform.up comparison was made on that world, so which one actually fired there is unknown. Answering it needs the up-vectors of the pieces on the boundary cells between 210186 and 183078 / 183727.
  • The three interface declarations printed in "Interface hierarchy among chain members" have drifted. At 0.2.6428.27798 the assembly declares public interface IRoboticArmRail : ISmallGrid, ITooltip, IReferencable, IEvaluable (and IRoboticArmJunction / IRoboticArmBypass likewise carry ISmallGrid, ITooltip), where the page shows only IReferencable, IEvaluable. The member lists are unchanged and every subset relationship the section draws still holds, so nothing built on that section is wrong. Correcting a verified declaration needs the fresh-validator protocol in ../WORKFLOW.md, which was not run for this, so the printed base lists are left as they stand. The same drift is present in the copy of IRoboticArmRail reprinted under "Rail nodes".
  • Every section other than "Network ownership and enumeration entry points", "Network-level chain traversal", "IsConnected override: angular alignment matters", "RailList is not the persisted membership" and "Rail nodes" still carries a 0.2.6228.27061 stamp. The network refactor that moved StructureNetwork onto ReferencableNetwork<TMember> landed between those two versions, so the network-facing claims elsewhere on the page are the ones to distrust first. Spot-checked and still correct at 0.2.6428.27798, though not restamped because the rest of their sections was not re-read: RoboticArmRailBase and RoboticArmRailDeviceBase both still declare public StructureNetwork StructureNetwork { get; set; } and public RoboticArmNetwork RoboticArmNetwork => StructureNetwork as RoboticArmNetwork, and RoboticArmNetwork still declares RailList, DockList, JunctionList and RailNodeList as readonly lists. Still not re-read at all: the prefab roster, the class-hierarchy section, and the painting-behaviour section.
  • Exact C# class binding for each of the 8 rail-only prefabs (e.g. does StructureRoboticArmRailCorner really use RoboticArmRailStraight or a third class that the decompile missed). UnityPy typetree extraction silently failed on these MonoBehaviours; resolving the m_Script PPtr per rail prefab would confirm the mapping. Functionally the distinction does not matter for a flood-fill painter, because every rail-family piece is reachable through RoboticArmNetwork.StructureList regardless of its concrete class.
  • Whether StructureRoboticArmJunction prefab exists under a name the scan did not cover, or the junction functionality is only ever built via dock prefabs. The RoboticArmJunction C# class exists and is instantiable, so a prefab should exist.