Skip to content

GameMessageFactory (vanilla network messages)

Reference page for vanilla Stationeers network messages that mods ride directly rather than defining custom LaunchPadBooster messages. Covers the SetLogicFromClient routing rules, the Interactable.State -> Mode sync channel, and the reliability caveat around hostId on server-side message dispatch.

SetLogicFromClient (vanilla)

SetLogicFromClient vanilla network message. Fields: LogicId (device ReferenceId), LogicType, Value. Server-side handler calls Device.SetLogicValue. No equivalent exists for (LogicSlotType, slotIndex) writes.

WriteLogicValue multiplayer-safe routing

Multiplayer-safe logic value write. On the server: call SetLogicValue directly (we are authoritative). On a client: send SetLogicFromClient to the server, which applies the change authoritatively and replicates to all clients.

WriteLogicSlotValue limitation

/// <summary>
/// Multiplayer-safe logic slot value write.
///
/// Vanilla has SetLogicFromClient for LogicType writes, but NO equivalent network
/// message exists for (LogicSlotType, slotIndex) writes.  Therefore:
///   - Server: call Device.SetLogicValue(LogicSlotType, slotId, value) directly.
///   - Client: log a warning and skip.  This is a known limitation; a proper
///             fix would require a custom NetworkMessage or a mod-specific packet.
/// </summary>

Active cartridge sync via vanilla Mode (Interactable.State)

Mode propagates through Interactable.State (vanilla networking). No custom message needed. GetCartridge() must be called manually after setting Mode because vanilla's setter does not call it. WriteLogicValue on client sends SetLogicFromClient to server. WriteLogicSlotValue on server calls directly; on client logs a warning.

AttackWithMessage hostId unreliability

Captures the painting player's Human ReferenceId when the server processes a remote client's AttackWithMessage. The hostId parameter from vanilla message dispatch is no help on the server: NetworkManager._hostId is only maintained client-side, and at 0.2.6428.27798 every vanilla message processed on a listen host or a dedicated server gets hostId -1 (the full chain is in LaunchPadBoosterNetworking, "Where Process's id comes from, and why a vanilla message has none"). AttackParentId in the message body is the only attacker identifier the message carries, but it is not authoritative: it is whatever id the client put there, and nothing on the server checks it against the sender.

The client fills it in OnServer.AttackWith (client decompile 39634-39658, dedicated server 39630-39654), the only place either build constructs an AttackWithMessage; InventoryManager calls it with Parent (294842):

if (thing.AttackWith(attack, runSimulation) != null && Assets.Scripts.Networking.NetworkManager.IsClient)
{
    NetworkClient.SendToServer(new AttackWithMessage
    {
        AttackParentId = attackParent.ReferenceId,
        TargetId = targetId,
        ActiveHandSlotId = activeHandSlotId,
        OffHandSlotId = offHandSlotId,
        AttackPosition = attackPosition,
        IsDestroy = isDestroy,
        IsCopy = isCopy,
        CompletionRatio = completedRatio
    });
}

The server acts for whatever that id resolves to. AttackWithMessage.Process (283200-283211, dedicated server 269593-269604), verbatim:

public override void Process(long hostId)
{
    Thing thing = Thing.Find(AttackParentId);
    Slot activeHand = thing.Slots[ActiveHandSlotId];
    Thing thing2 = Thing.Find(TargetId);
    if ((bool)thing2)
    {
        Slot otherHand = thing.Slots[OffHandSlotId];
        Attack attack = new Attack(activeHand, otherHand, AttackPosition, thing2, CompletionRatio, null, IsDestroy, IsCopy);
        thing2.AttackWith(attack);
    }
}
  • Nothing checks the id against the sender. Process never reads hostId, and the dispatch path (NetworkManager.ReceiveEvents -> HandleGeneralTraffic -> NetworkBase.DeserializeReceivedData -> Process) has no sender check. The Attack it builds (310760-310797) carries no sender or connection either, so no AttackWith override further down can check. A client can therefore act as any Thing that has the slots it names, with whatever that Thing holds in Slots[ActiveHandSlotId] (Attack takes SourceItem = activeHand.Occupant, 310783).
  • No null check. An AttackParentId that resolves to nothing throws NullReferenceException at thing.Slots[ActiveHandSlotId]. DeserializeReceivedData catches only EndOfStreamException, and only around Deserialize (39295-39303). HandleGeneralTraffic, ReceiveEvents, NetworkManager.ManagerUpdate (while (ReceiveEvents()) { }, 278873) and the manager loop in GameManager.Update (210693-210696) have no try/catch, so the exception propagates out of GameManager.Update and ends that frame's manager loop early.
  • The other ways a vanilla Process runs pass the same value on. DeferredMessageQueue stores the hostId a message's own Process received (DeferUntilExists, 280624-280680, from 14 call sites that pass hostId) and replays it with p.Message.Process(p.HostId) (280723), and FabricatorJobDeleteMessage is also run locally with Process(-1L) (403080). On a server, then, no vanilla Process is handed a sender id.

Every member cited is textually identical in the dedicated-server decompile. Spray Paint Plus's PaintAttackerTracker_Remote records AttackParentId as the painter; what that does and does not protect is in SprayPaintPlusNetworking, "The server checks who sent a preference mask".

Verification history

  • 2026-04-20: page created from the Research migration. Primary sources: F0126b (SetLogicFromClient vanilla message), F0126c (active cartridge sync via vanilla Mode + SetLogicFromClient routing). Additional sources: F0331 (WriteLogicSlotValue multiplayer limitation comment), F0377 (WriteLogicValue multiplayer-safe routing), F0386 (PaintAttackerTracker_Remote hostId unreliability).
  • 2026-09-27: conflict on "whether AttackParentId identifies the attacker authoritatively". Previous claim (section "AttackWithMessage hostId unreliability", 0.2.6228.27061): "The hostId parameter from vanilla message dispatch is unreliable on the server (NetworkManager._hostId is only maintained client-side); AttackParentId in the message body is the authoritative source identifier." New finding (0.2.6428.27798, client and dedicated-server decompiles): AttackParentId is the id the client writes (AttackParentId = attackParent.ReferenceId in OnServer.AttackWith), and AttackWithMessage.Process acts for Thing.Find(AttackParentId) without checking who sent the message; vanilla dispatch hands Process no sender id. Fresh validator verdict: B is correct. A's first half holds at 0.2.6428.27798 (hostId is useless on a server and _hostId is only written on client paths); "authoritative" is wrong, because the id is client-supplied and trusted without verification, so a client can name any Thing that has the needed slots. The two builds are identical; the 0.2.6228.27061 lines and the mod comment behind F0386 could not be checked. Result: the section was rewritten with the send and receive excerpts, the -1 value and the missing check, plus two further reads from the same pass, made by the validator and re-read here: Process does not null-check the attacker, and the other two ways a vanilla Process runs (the DeferredMessageQueue replay and a local Process(-1L)) pass the same value on. Section restamped to 0.2.6428.27798.

Open questions

None at creation.