Starship Horizons
Log In
☰
Knowledge Base›Packets And Topics

Packets And Topics

State lives on the server. The client is told about it through the WebSocket, and what arrives is distributed inside the page over a broadcast bus. These are two separate mechanisms and confusing them is the source of a lot of trouble.

Packets: what the server sends

A packet is a named message from the server — MAP, VESSEL-WAYPOINTS, ALERT, DAMAGE-TEAMS, ACTIVE-CONSOLES, and dozens more. Packets are upper‑case and hyphenated by convention.

The server does not send everything to everyone. A console asks for the packets it needs:

this.AcceptPacket("DAMAGE-TEAMS");

Several things follow:

  • Accepting is refcounted. Several widgets can want the same packet. The socket tracks owners and only sends a rejection to the server when the last one releases it. This is why widgets must claim through this.AcceptPacket rather than the global — see Widgets.
  • Accepting delivers immediately. The server answers a new claim by sending the current value straight away, rather than waiting for the next change. This is convenient and it is also a race: a widget that mounts while the page is loading something over REST can have the socket's answer arrive first and win.
  • A second claim is answered by the page. When something on the page already holds the packet, the server is not asked again. Instead the topics that packet raised last time are broadcast again, at once, inside the claim itself, to everyone subscribed. A widget that claims a packet before it has subscribed to the topic misses that replay, so subscribe first.
  • Claims outlive the connection. A claim made before the socket has opened is kept and sent when it connects, and every claim is sent again after a reconnect. You do not have to wait for the socket to claim a packet.

A screen can also declare packets it always wants, in its screen registration, which is how admin stations get message and debug traffic from the outset.

Topics: what the page distributes

When a packet arrives, the client updates its state and then broadcasts one or more topics — lower‑case, hyphenated. Widgets subscribe to topics, not to packets:

this.Subscribe("vessel-shields", (m) => this.Render());

The mapping is not one to one. One packet can raise several topics — the MAP packet updates the current map and raises map-info, and related packets raise location-current and planetary-system-detail. And one topic can be raised by several packets, or by page code with no packet involved at all. The Game Master's radar, for one, reports a right-click as gm-radar-context and leaves the menu to the page.

This is exactly why releasing a topic subscription says nothing about whether a packet is still needed, and why the two are tracked separately.

State and events are treated differently. Most topics carry state, which is why a second claim can replay them. A topic that reports an event instead (a removal, a hit, a sound, a log line) is broadcast with transient: true and is never replayed, because replaying it would make it happen twice. If your own code raises an event topic, mark it the same way:

game.Broadcast({ topic: "my-widget-fired", value: id, transient: true });

Without the mark, an event raised in the course of handling a packet can be kept with that packet's topics and replayed to the next widget that claims it.

The topics you will actually use

There are over a hundred. These are the ones nearly every widget wants:

TopicRaised when
readyThe page has loaded and the client runtime has started. The socket is still opening at this point. Raised once per page, but a widget built later that subscribes to it is still called, just after the code that subscribed has finished.
socket-connected, offlineThe socket opened, or closed.
resetThe session reset. Re‑render, and remember this is not startup.
resizeThe console changed size.
vesselThe bound vessel's general state changed.
vessel-idThis console has been given a vessel.
vessel-initializedThe vessel is fully populated. Prefer this to vessel-id — it is a superset, and a widget that renders on vessel-id often finds half the data missing.
vessel-componentsThe component set changed.
vessel-shields, vessel-damage, vessel-alertCombat state.
contacts, contact-updated, contact-removedThe sensor picture.
target-flight, target-tactical, target-scienceThe three separate target selections.
map-info, location-currentWhere the ship is.
mission, game-start, sessionSession lifecycle.

Traps worth knowing in advance

Contact topics arrive in storms. In a busy mission contact-updated can fire many times per second. A widget that re‑renders its whole list on every one will dominate the frame. Coalesce, or render on a timer and let the topic only mark you dirty.

The session state, not the mission state, is authoritative for whether a mission is actually running. A mission can be loaded without being started, and code that checks the wrong one acts too early.

Editor pages must guard against live packets. An editor holds one specific record it loaded over REST. A packet describing whatever the server currently has loaded is a different thing, and letting it land replaces the record being edited. The map editor sets a flag for exactly this reason. Packets are also cut down to what the consoles draw: the MAP packet carries each spectator camera as only the fields the Spectator screen reads. An editor reads the full record over REST.

The catalogs are refilled, not kept. game.VesselClasses, game.Components, game.Models and the other content tables are emptied every time the console identifies itself, which is on every connect and reconnect, and again when the server's set of modules changes (the page raises module-data-reset). They are then refilled by the packets the page has claimed. Two consequences: a position in one of these tables is not a stable identity, so key anything you keep by record id; and a table you filled yourself over REST, with no matching packet claimed, is emptied by the next identify (which can come moments after the page loads) and stays empty. Keep a list you fetched in your own variable and render from that.

Read the payload, not the arrival. planetary-system-detail is also raised, with null, when the ship leaves a planetary system. A handler that treats the topic itself as "we are at a body now" gets that wrong.

The fast lane

Most state reaches a console with the game frame. A station that is flown by hand can ask for its own vessel's position more often than that:

FastVessel(33);         // this console's vessel, about every 33 ms
FastVessel(33, 500);    // ...and the motion of contacts within 500 m
FastVessel(0);          // off

With a radius, what is close enough to race, ram or shoot at is drawn at the same moment as your own hull rather than a game frame or more behind it. The server clamps the interval to between 16 and 200 ms, carries at most twelve nearby contacts, and sends nothing for a hull that is not moving. The request is remembered and repeated after every reconnect. The nearby contacts' records move them and do nothing else: they never add or remove a contact and they raise no topic, so a widget that lists contacts is not woken by them. The stock Pilot station asks for FastVessel(33, 500).

Sending state the other way

See Server Commands.

Last updated 19 October 2026