Skip to content

ChatBroadcast

Stationeers chat is not a separate protocol; player chat messages flow through the game's native network message system as ChatMessage instances. The four-step flow from client input to every client's console and chat bubble is documented below, plus the server-side broadcast recipe for mods, the console rendering constraints (the F3 console is ImGui, not TextMeshPro), and the louder AnnounceMessage channel.

Flow

Stationeers chat uses the game's native network message system, not a separate chat protocol. The flow:

  1. Player types in chat UI, client constructs a ChatMessage and sends it to the server via NetworkClient.SendToServer(chatMessage)
  2. Server receives the message, calls ChatMessage.Process(hostId)
  3. Process() prints to the server console, then broadcasts to all clients via NetworkServer.SendToClients(this, NetworkChannel.GeneralTraffic, -1L)
  4. Each client receives the broadcast, prints to their console, and shows a chat bubble above the sender's character model (only when HumanId resolves to a Human that is not the local player; see the verbatim below)

ChatMessage (0.2.6403.27689 decompile lines 278421-278461), verbatim, including Process as it runs on every receiving side:

public class ChatMessage : ProcessedMessage<ChatMessage>
{
    public long HumanId { get; set; }

    public string DisplayName { get; set; }

    public string ChatText { get; set; }

    public override void Process(long hostId)
    {
        PrintToConsole();
        if (NetworkManager.IsServer)
        {
            NetworkServer.SendToClients(this, NetworkChannel.GeneralTraffic, -1L);
        }
        Human human = Thing.Find<Human>(HumanId);
        if ((bool)human && (bool)InventoryManager.Parent && InventoryManager.Parent.ReferenceId != HumanId)
        {
            human.SetChatText(ChatText);
        }
    }

    public override void Deserialize(RocketBinaryReader reader)
    {
        HumanId = reader.ReadInt64();
        DisplayName = reader.ReadString();
        ChatText = reader.ReadString();
    }

    public override void Serialize(RocketBinaryWriter writer)
    {
        writer.WriteInt64(HumanId);
        writer.WriteString(DisplayName);
        writer.WriteString(ChatText);
    }

    public void PrintToConsole()
    {
        ConsoleWindow.Print(DisplayName + ": " + ChatText, ConsoleColor.Cyan, clearLine: false, aged: false, unformatted: true);
    }
}

Server-side broadcast recipe for mods

Works on a dedicated server with no local player; this is exactly the SayCommand batch branch:

ChatMessage chatMessage = new ChatMessage
{
    ChatText = ...,
    DisplayName = "Server",   // or a mod tag
    HumanId = -1
};
chatMessage.PrintToConsole();
NetworkServer.SendToClients(chatMessage, NetworkChannel.GeneralTraffic, -1L);
  • Each receiving client's Process prints the line into its own console. SendToClients(msg, NetworkChannel.GeneralTraffic, -1L) = reliable channel, -1L = all clients.
  • HumanId = -1 suppresses the chat bubble: Process only routes to human.SetChatText(ChatText) when HumanId resolves to a Human, which is what a system line wants.
  • Single-player: with no network session both IsServer and IsClient are false; call PrintToConsole() alone (or just ConsoleWindow.Print). The say command itself is CommandScope.MultiplayerOnly but the underlying APIs are not.

Rendering constraints: the console is ImGui, not TextMeshPro

ConsoleWindow.Print (222963-223019), verbatim head:

public static void Print(string output, ConsoleColor color = ConsoleColor.White, bool clearLine = false, bool aged = true, bool unformatted = false)
{
    if (GameManager.IsBatchMode)
    {
        output = $"{DateTime.Now:HH:mm:ss}: {output}";
        if (CustomLogFile)
        {
            switch (color)
            {
            case ConsoleColor.DarkRed:
            case ConsoleColor.DarkMagenta:
            case ConsoleColor.Red:
            case ConsoleColor.Magenta:
                UnityEngine.Debug.LogError(output);
                break;
            case ConsoleColor.DarkYellow:
            case ConsoleColor.Yellow:
                UnityEngine.Debug.LogWarning(output);
                break;
            default:
                UnityEngine.Debug.Log(output);
                break;
            }
        }
        else if (!output.Contains("<color="))
        {
            _systemConsoleInput?.PrintToConsole(output, color);
        }
        return;
    }
    uint consoleColor = GetConsoleColor(color);
    ConsoleLine[] consoleBuffer = _consoleBuffer;
    if (consoleBuffer == null || consoleBuffer.Length <= 0)
    {
        _prematureLogQueue.Enqueue(new PrematureLog { output = output, color = color, clearLine = clearLine, aged = aged, unformatted = unformatted });
        return;
    }
    for (int num = _consoleBuffer.Length - 1 - 1; num >= 0; num--)
    {
        _consoleBuffer[num + 1].Apply(_consoleBuffer[num]);
    }
    if (aged)
    {
        _consoleBuffer[0].Set(output, consoleColor, 0f);
    }
    else
    {
        _consoleBuffer[0].Set(output, consoleColor);
    }
}

ConsoleLine (221505 onward) stores uint Color and a plain string Text and renders through ImGui (ImGui.ColorConvertFloat4ToU32 at 221521). Constraints that follow:

  • TextMeshPro rich-text tags (<color=...> etc.) do NOT render in the in-game console; the console colors a whole line via the ConsoleColor argument only. Chat lines are always Cyan (PrintToConsole, 278459), with the sender name baked into the text as DisplayName + ": " + ChatText. (Contrast: the build-cursor tooltip IS a TextMeshPro surface, which is why the placement loop's <color=red> markup renders there; see StructurePlacementValidation.)
  • On a dedicated server (IsBatchMode) WITHOUT CustomLogFile, any line CONTAINING <color= is silently dropped from the system console (the !output.Contains("<color=") guard at 222987). With CustomLogFile, output is routed to UnityEngine.Debug.Log/LogWarning/LogError by color band instead. Batch-mode lines are timestamped HH:mm:ss. So embedding rich-text tags in ChatText would render as literal tags on clients and can vanish entirely from the server console log. Use plain text.
  • Multi-line: ConsoleLine.Set splits on \n into continuations (221532-221544), so newlines are legal in a printed line.
  • No length cap was found in ChatMessage.Serialize (plain WriteString) or ConsoleWindow.Print at this layer; the chat INPUT field may cap what a player can type, but a mod-constructed message is not length-gated by the send path. Keep messages one line and short anyway (console lines wrap poorly and the overlay ages out).
  • The console is the surface players toggle with F3: KeyMap.ToggleConsole = KeyCode.F3 (43997, default assignment repeated at 44065). New lines also appear transiently on the HUD overlay (the aged fade path; chat uses aged: false so chat lines persist until scrolled).

AnnounceMessage: the louder channel

Alternative channel: AnnounceMessage (278462-278492) plus the announce command (AnnounceCommand, 96082-96110). The server sends NetworkServer.SendToClients(new AnnounceMessage { AnnounceText = ... }, NetworkChannel.GeneralTraffic, -1L) and calls AnnounceMessage.Display(text) locally. Display prints "Announcement: " + text in Yellow AND pops a modal ConfirmationPanel on non-batch clients. Louder than chat; suitable for rare, important notices, not per-placement spam.

public static void Display(string text)
{
    ConsoleWindow.Print("Announcement: " + text, ConsoleColor.Yellow, clearLine: false, aged: false, unformatted: true);
    if (!GameManager.IsBatchMode && (bool)Singleton<ConfirmationPanel>.Instance)
    {
        Singleton<ConfirmationPanel>.Instance.ShowWithRawMessage("ServerAnnouncement", text, "ButtonOk");
    }
}

Namespace trap for the recipe above: ChatMessage lives in Assets.Scripts.Networking, but NetworkServer (213543) and its SendToClients overloads live in the bare Assets.Scripts namespace (the last namespace declaration before 213543 is Assets.Scripts at 195722), so the send call needs using Assets.Scripts; in addition to using Assets.Scripts.Networking;. SendToClients<T> (213921) defaults both trailing parameters (channel = NetworkChannel.GeneralTraffic, excludeConnectionId = -1L).

Reaching one client: SendToClient (singular)

The recipe above broadcasts. To put a line on exactly one client, NetworkServer has a unicast form. SendToClient<T> (213907), verbatim:

public static void SendToClient<T>(MessageBase<T> message, NetworkChannel channel, long clientConnectionId) where T : MessageBase<T>, new()
{
    Client client = Client.Find(clientConnectionId);
    if (client != null)
    {
        Assets.Scripts.Networking.NetworkManager.SendNetworkMessageToClient(client, channel, message);
    }
}

Note the differences from the plural form, all of them easy to get wrong:

  • Neither trailing parameter is optional. SendToClients<T> defaults channel and excludeConnectionId; SendToClient<T> defaults nothing, so the channel must be passed explicitly.
  • It silently no-ops when clientConnectionId does not resolve to a Client. There is no return value and no log line, so a wrong or stale connection id looks exactly like a delivered message.
  • The last parameter means the opposite of the plural form's. SendToClients(..., excludeConnectionId) takes an id to EXCLUDE (-1L = send to everyone); SendToClient(..., clientConnectionId) takes the id to send TO. Passing -1L here sends to nobody.

Related overloads on NetworkServer:

Line Signature
213902 SendToClientReliable<T>(MessageBase<T>, NetworkChannel, Client), returns UniTask
213907 SendToClient<T>(MessageBase<T>, NetworkChannel, long clientConnectionId)
213916 SendToClient<T>(MessageBase<T>, NetworkChannel, Client) (overload taking an already-resolved Client)
213921 SendToClients<T>(MessageBase<T>, NetworkChannel = GeneralTraffic, long excludeConnectionId = -1L)
213926 / 213931 / 213945 SendToClientsDirect(...) raw-byte broadcast overloads

This is the mechanism a mod needs when a message concerns one player rather than the server population, because the in-game console is process-local: a ConsoleWindow.Print on the server is invisible to every client (see ConsoleWindow). The pattern is to send a custom message and print locally in the receiving side's Process(), exactly as ChatMessage.Process does above.

Verification history

  • 2026-07-27: additive, during a repo-wide console-output audit. Added the "Reaching one client: SendToClient (singular)" section: the page documented only the broadcast form, and the unicast form is what a mod needs when a message concerns one player. Verified verbatim against 0.2.6403.27689: SendToClient<T> at 213907 with where T : MessageBase<T>, new(), three non-optional parameters, silent no-op when the connection id does not resolve, plus the sibling overload table at 213902 / 213916 / 213921 / 213926 / 213931 / 213945. Called out the parameter-semantics reversal between SendToClients(..., excludeConnectionId) and SendToClient(..., clientConnectionId), where -1L means "everyone" in the first and "nobody" in the second. No existing claim was contradicted, so no fresh validator was required.
  • 2026-07-14 (second pass, same day): added the namespace note (NetworkServer and SendToClients in bare Assets.Scripts at 213543/213921 vs ChatMessage in Assets.Scripts.Networking) after a mod build failed to resolve NetworkServer with only the Networking using. Additive.
  • 2026-04-20: page created from the Research migration; verbatim content lifted from F0070 (Plans/LLM/RESEARCH.md:9-14).
  • 2026-07-14: re-verified and extended against the 0.2.6403.27689 decompile during the mixed-tier cable network guard research pass. The four-step flow stands, with step 4 refined (the bubble requires HumanId to resolve to a Human other than the local player). Added: the full ChatMessage class verbatim including Process (278421-278461), the server-side broadcast recipe (the SayCommand batch branch: DisplayName = "Server", HumanId = -1 to suppress the bubble, PrintToConsole() + SendToClients(..., NetworkChannel.GeneralTraffic, -1L)), the "Rendering constraints" section (ConsoleWindow.Print verbatim head 222963-223019; ConsoleLine renders via ImGui ColorConvertFloat4ToU32 at 221521 so TextMeshPro tags do not render; one ConsoleColor per line, chat = Cyan at 278459; dedicated-server CustomLogFile routing with HH:mm:ss timestamps vs the silent <color= drop at 222987; \n continuations 221532-221544; no length cap at this layer; F3 = KeyMap.ToggleConsole 43997/44065), and the AnnounceMessage / announce command channel (278462-278492 / 96082-96110; yellow console line plus a modal ConfirmationPanel on every non-batch client).

Open questions

None at creation.