Skip to content

Resources receive game events as one stream, in the order the server committed them ​

Status: Accepted (2026-11-06)

Context ​

The server core raises many kinds of events for the Lua resources: connects and disconnects, commands and chat, damage and deaths, vehicle entries, colshape crossings, streaming changes, dialog answers and more. They come from packet handling, from the facade calls of resources and from periodic work (timeouts, streaming, reconciliation).

If each kind waited in its own queue and was delivered by its own function, the order between kinds would be fixed by the order of those calls ("damage before deaths", "entry before state change"), kept correct by hand, and every new event would touch many places. Resources would see an order that is not the order in which things happened.

Connect and disconnect have a second concern: they must be observable by a resource after the fact, with no callback machinery inside the core.

Decision ​

  • One stream. GameEvent (src/event.rs) is one enum with a variant per kind. The stream is a first-in, first-out queue of events, each with the step time it was raised in (ADR 0046). It lives in the roster; packet handling, the facade (ServerApi) and the roster all write to it, at the moment the server commits the fact.
  • Order of commitment. Resources see the events of a step in the order the server committed them. An event raised by a handler goes to the end of the stream and is delivered in the same step.
  • Delivery. routing takes events one at a time (Session::next_event) and handles them in one match. An observe-only event goes to its handlers; a refusable one goes to its handlers and is resolved right after them (ADR 0038). Each handler sees the state left by the events before it.
  • Events whose source is gone. An event about a vehicle, object, checkpoint or colshape removed after the event was raised is dropped when taken. Events that need a live player (action input, offer answers, streaming changes, crossings, view events) leave with a removed player; damage, death and the rest still reach the resources.
  • Reconciliation writes at the time it runs. The pass over players' life states and interiors (at most every 50 ms) writes the changes it finds into the stream when it finds them.
  • A bounded pass. One delivery pass delivers at most 8192 events; the rest wait for the next pass in the same order, and the server logs a warning when a backlog starts. An endless chain of handlers raising each other's events (a vehicle respawned from its own vehicle_respawn handler) does not hang the step. Before the next packet is accepted, a pass runs over what earlier packets left; a chain longer than the limit interleaves with later packets, since waiting for its end would stop the server from accepting packets.
  • Vehicle syncer changes merge into one event per vehicle within a step; an event of an earlier step still waiting in the stream is not rewritten.
  • A departed player's id is freed when its player_disconnect is taken from the stream, so a newcomer never gets the id before the resources have seen the previous owner leave, whatever the order or the pass limit (ADR 0021).
  • Connect and disconnect are observe-only facts.
    • A connect is raised once per new player, when it joins (as a guest or accepted by the auth resource). A repeated ClientHello raises nothing; a refused connection raises nothing, since there is no player. The same join raises player_connect, the start of the gamemode's pre-spawn flow (ADR 0023).
    • A disconnect is raised by every removal path with its reason: quit, timeout, kicked or overflow (Disconnect); resources get player_disconnect(player, reason). The operator's kick reason is logged at the call, not carried in the event.
  • Events are SDK types. Event payloads address players by PlayerId like the rest of the server SDK; the roster writes them directly.
  • Tests take events through test_support (drain!) by the same rules as routing; a connect in the test harness first takes the departures of earlier steps, as the server loop does before the next packet.

Consequences ​

  • A new event is a variant of GameEvent, the place that raises it, an arm in routing and its dispatch.
  • The order of different kinds within one step is the order in which they happened; the resource documentation describes it.
  • The roster depends on the SDK event types, as it already did on PlayerId.

Alternatives considered ​

  • One enum, but delivered in a fixed order by kind (a rank per kind). Behavior stays the same, but the order remains a hand-written rank table. Rejected.
  • A stream only for packet handling, roster queues delivered as a separate phase. Two mechanisms, and the wrong order for events the roster raises. Rejected.
  • Events raised by a handler wait for the next step. A simpler loop, but events that now arrive in the same step would move a step later, and player_disconnect would still need an exception. Rejected.
  • A subscriber interface or callback registry in the core. Rejected: the resource host is the one consumer, and a stream it takes from keeps the core free of callbacks into scripts.
  • Return events from each packet handler. Rejected: events raised by facade calls and by periodic work would need another path, and every call site would carry them.

Documentation licensed under CC BY 4.0. DarqlineMP is not affiliated with Rockstar Games or Take-Two Interactive. Grand Theft Auto is a trademark of Take-Two Interactive Software, Inc.