rrt/docs/subsystem-views/multiplayer.md

21 KiB

Multiplayer

Primary atlas source:

Current grounded owners:

  • multiplayer_window_init_globals
  • multiplayer_window_service_loop
  • multiplayer_register_session_event_callbacks
  • multiplayer_dispatch_requested_action
  • multiplayer_preview_dataset_service_frame
  • multiplayer_transport_service_frame
  • multiplayer_transport_service_worker_once
  • multiplayer_transport_service_route_callback_tables
  • multiplayer_transport_service_status_and_live_routes

Current bounded state blocks:

  • session globals at 0x006d40d0
  • active session-event transport at 0x006cd970
  • preview dataset at 0x006cd8d8
  • Multiplayer window backing block at 0x006d1270
  • selector-view store rooted at [transport+0xab4]

What this note is for:

  • Multiplayer window and preview-dataset ownership
  • Session-event transport routing
  • GameSpy-facing callback and live-route semantics
  • Selector-view refresh, retry, and probe state

Latest local closure:

  • The remaining multiplayer branch is narrower now: the status-route callback vector is bounded through selector-text or averaged-sample publication, control-id-list seeding, scalar-query forwarding, and the validated cookie or extended-payload callbacks. The selector-view sidecar is tighter too: [entry+0x80] now reads as the averaged millisecond probe sample and [entry+0x54] as the displayed version/build companion integer from the local X...X|%d marker family. Separately, the route-callback side now has its own compact GameSpy-style server or route descriptor family with primary and optional secondary endpoint tuples plus keyed fields like hostname, gamever, numplayers, and numservers, feeding the route-binding compatibility gate. The compact decode owners are tighter too: 0x5907d0 is now the allocate-and-append lane for self-consistent compact payloads, while 0x590d00 is the keyed upsert-by-primary-endpoint lane that reuses an existing descriptor when one already matches and then notifies the transport owner callback. The route-callback-table runtime above that decode path is tighter too: 0x5905e0 now constructs one transport-owned callback-table block, 0x5906f0 tears down the decoded schema dictionary rooted at [this+0x08], 0x590540/0x5905a0 acquire and release refcounted shared schema strings through the global pool, and 0x5908c0 now reads as the live receive/decode state machine serviced by 0x591290 in table states 2/3. The owner-callback mode split above that runtime is tighter now too: mode 0 comes from the generic append-notify lane 0x590370, mode 1 from compact upsert 0x590d00, mode 2 from generic remove-notify 0x590430, and modes 6/5/3 from the receive/decode state machine 0x5908c0. The higher transport bring-up split is tighter too: 0x596090 now clearly constructs [transport+0xba4] through 0x5905e0 with owner callback 0x595a40, then seeds the local field-cache family [transport+0x1724] through 0x5a08f0 with helper 0x595b60, and then constructs [transport+0x1164] through the same 0x5905e0 path but with owner callback 0x595bc0. The live-route lifecycle above it is tighter too: 0x590740 now cleanly resets one table's live route plus decode-side runtime without destroying the outer object, 0x5907a0 is the broader destroy path that also releases the active descriptor collection, 0x590ed0 opens the route handle into [this+0x4a0], stages the initial outbound request, and seeds state 3 plus the receive buffer, 0x5911e0 is the state-2/3 socket-service wrapper above 0x5908c0, 0x5912c0 is the one-shot send-with-reopen-retry helper, and 0x590ea0 is the shared disconnect publication and reset tail. The recurring service helper 0x591290 is tighter too: it now first clears the staged intrusive descriptor list through 0x590490 before entering the state-driven seed-or-receive branch. The upstream openers are tighter now too: 0x5962e0 is the field-subscription route-table owner above [transport+0xba4], while 0x596530 is the gsi_am_rating reopen path above [transport+0x18bc]. On that latter branch, 0x590dc0 is now bounded as the state-0 raw-endpoint seed pass over the live route handle, repeatedly pulling endpoint tuples through 0x58bc7e record type 0x1f3 before stamping descriptor flag byte 0x15 with 0x11. That makes the remaining flag-bit question much narrower too: current evidence now supports reading byte-0x15 bit 0x1 as a primary-endpoint-seed or endpoint-only source marker. In the gsi_am_rating dispatcher, clear-bit descriptors can take the richer direct transition lane, while set-bit descriptors are pushed through the queued enrichment path and later still suppress that direct transition even when the ready bit is present. The adjacent capacity-descriptor side is tighter too: 0x595bc0 no longer reads as a vague progress helper. Mode 0 is now clearly the live publish lane, and it lines up with the generic descriptor append-notify owner callback at 0x590370: it samples hostname, numwaiting, maxwaiting, numservers, and numplayers from the current descriptor before publishing an opcode-2 descriptor block, while still carrying three cached side scalars from the local capacity sidecar at [transport+0x1778]. That sidecar is tighter now too: current evidence says it behaves as one cached pointer into the transient work-record family at [transport+0x1780], because every meaningful branch in 0x595bc0 reads the same +0x0c/+0x10/+0x18 metadata triplet and the replay modes later consume the pointer through 0x5933a0. The negative result is stronger too: local text-side xrefs still show no direct store to [transport+0x1778], and a wider sweep also failed to show any obvious lea-based replay-band writer. The owned request lifecycle now tightens that further too: 0x593330/0x593370/0x593380/0x5934e0/0x5933a0 fully own [transport+0x1780], 0x1784, and 0x1788, while still leaving [transport+0x1778] outside that family; the neighboring active opcode reset path 0x5929a0 also only targets [transport+0x17fc]. Constructor and teardown passes around 0x596090/0x5961b0/0x5962e0 tighten that negative result further: those owners seed or clear the neighboring replay-band fields while still leaving [transport+0x1778] untouched. A full-binary literal-offset sweep tightens it further still: the only direct 0x1778 hit in RT3.exe is the read in 0x595bc0. One nearby ambiguity is now closed too: the mode-5 mirror path in 0x595a40 and 0x595e10 does not seed [transport+0x1778]; it writes [transport+0x54] and mirrors the same staged route companion dword only into queue-side slot [transport+0x1724+0x24] through 0x005a0940. So the sidecar writer remains upstream of this leaf capacity publisher. The payload split is tighter too: 0x592ae0 now grounds opcode 2 as a seven-dword descriptor payload with an owned string slot at +0x08, so live mode supplies a populated payload while modes 3 and 5 deliberately enqueue an all-zero payload and reuse only the sidecar metadata in the wrapper. Those two modes are tighter now too: they are not generic replay guesses, they are the live receive-state owner callbacks emitted by 0x5911e0 -> 0x5908c0. So they are best read as delayed metadata replays over one cached work record, not over a standalone custom cache object. The capacity owner split itself is tighter now too: 0x595bc0 only does real work for modes 0, 3, and 5; the upstream table still delivers modes 1, 2, and 6, but those are explicit no-ops in the capacity leaf. So the callback-owner edge is now effectively closed: the route-callback table feeds the capacity publisher only through the live append lane and the two replay lanes, while the remaining missing piece is still the upstream sidecar producer, not the owner-mode wiring. The producer side is tighter now too: callback-table attach, bound-route requests, selector-text route requests, and the type-9 text fastpath all stage that same +0x0c/+0x10/+0x18 triplet through 0x5934e0, so the capacity replay sidecar is clearly reusing one broader transport work-record family. The generic owner-callback split above that family is tighter now too: 0x590370 is the shared append-notify lane in mode 0, while 0x590430 is the distinct remove-notify-and-stage lane in mode 2. So 0x595bc0 only owns the live append-style publish path plus delayed replay modes 3/5, not the remove path. The neighboring transient work queue is tighter too: 0x593330/0x593370/0x593380 now bound [transport+0x1780] as a real construct/clear/destroy collection owner, while 0x5933a0, 0x5934e0, and 0x593570 now ground the remove, allocate, and completion side over that same family. The small sibling 0x593400 is tighter too: it is a pure uniqueness predicate over work-record field +0x0c. Its caller is tighter now too: 0x58d720 is an immediate-drain quiescence gate over one transport context id, using 0x593400 for the queued work family at [transport+0x1780] and 0x592970 for the active opcode-record collection at [transport+0x17fc]. The strongest current read is that 0x5934c0 seeds that shared drain context id first, then the transport copies it into queued work field +0x0c and active opcode-record field +0x14 before the immediate-drain roots wait on one shared disappearance test rather than one vague “drain until something settles” loop. The currently grounded roots are 0x58df20, the neighboring formatted selector-text publish path at 0x58dfb0, and callback-table registration at 0x58e200. The callback-table attach side now has a tighter active-opcode owner stack too: 0x5927b0 services and retires one active opcode record, 0x592800 performs the broader context-or-idle retirement sweep, 0x5929a0 removes active records by opcode type, and 0x5929f0 removes only opcode-3 field-snapshot records keyed by the subscribed callback-pair payload. That also corrects 0x595b80: its final cleanup is not a queued field-snapshot drain, but an active opcode-type-3 purge beneath the field cache reset. The adjacent route-callback descriptor-table lifecycle is tighter too: 0x590410 now grounds [table+0x5bc] as a real staged intrusive descriptor-list head, 0x590430 is the generic remove-notify-and-stage path, 0x590490 releases that staged list, and 0x5904d0 releases the active descriptor collection before tearing the staged list down. That also tightens 0x5962e0: its earlier “release active descriptors” step is now explicitly this same 0x5904d0 family, not a vague collection clear. The callback-table attach side now constrains the same work-record metadata family a little further too: 0x593650 deliberately duplicates its first caller metadata dword into both work fields +0x0c and +0x10, while carrying the second caller metadata dword in +0x18. The lower opcode wrappers are tighter now too: 0x592a40 turned out not to be the real 0x08-byte binding leaf at all, but the explicit opcode-1 trigger wrapper whose constructor is a no-op and whose active-side service is 0x5913c0. The real lower binding leaf is 0x592c40, which builds the local 0x08-byte payload later deep-copied by the explicit opcode-5 family 0x591540/0x591570/0x591580. The earlier opcode-4 reading was just the table-indexing mistake: 0x5928a0 multiplies the pushed selector by 0x10, so selector 4 lands on the row at 0x5e2044, not the row at 0x5e2034. So the replay-side triplet is still a broader transport callback-wrapper family, not one fixed route-only tuple. The type-9 text fastpath confirms the same split from the other side too: 0x593d00 only emits the follow-on callback lane when work field +0x10 is nonnull, and then forwards (+0x10, +0x18, +0x0c) into 0x593170 as callback function, callback companion, and trailing drain context. The nearby field-subscription side is tighter too: 0x592b50 now clearly uses [transport+0x1774] as a cached progress percentage under the [transport+0xba4] callback table, and 0x5962e0 seeds that percentage to 1 just before the first immediate mode-3 snapshot. The nearby route-callback-table lifecycle is tighter now too: 0x596090 seeds [transport+0xba0] as the callback-plumbing enable latch, clears staged payload slot [transport+0xb50], and constructs the three owner branches rooted at [transport+0xba4], [transport+0x1164], and [transport+0x18bc]; 0x596210 is the recurring service sweep over those same three tables plus the local field-cache and queued-descriptor families; 0x596060 is the explicit gsi_am_rating reset; and 0x596530 is the reopen-from-stored-label sibling above that same am-rating table. The matching local cleanup is tighter too: 0x5962c0 is the explicit staged route-callback payload clear on [transport+0xb50], while 0x595ce0 now clearly resets only the capacity-descriptor route callback table at [transport+0x1164], not the field-subscription table at [transport+0xba4]. The only meaningful gap left on the capacity side is no longer a local writer search. The carried sidecar fields are no longer anonymous: current evidence now says they are just the same cached callback-wrapper triplet reused by other work-record families, namely drain context id +0x0c, callback function +0x10, and callback companion +0x18. The negative result is stronger now too: the neighboring replay-band fields [transport+0x176c], [transport+0x1770], [transport+0x1774], [transport+0x177c], [transport+0x1780], and [transport+0x1784] all have direct local lifecycle owners, but [transport+0x1778] still only appears as the single read in 0x595bc0; even the broader callback-owner lifecycle now skips it while touching those neighbors, including bring-up 0x596090, recurring service 0x596210, am-rating reset/reopen 0x596060/0x596530, teardown 0x5961b0, and field-subscription open 0x5962e0. The constructor now closes that negative result further: 0x58dc50 bulk-zeroes the full transport body and still never seeds [transport+0x1778] before later explicit neighbor initialization. So this edge is now locally closed: no writer is grounded anywhere in RT3.exe, and the remaining staging path is best read as an upstream callback or worker handoff rather than one missing ordinary field store in the local text cluster. The callback-binding family at 0x5934e0 -> 0x593650 -> 0x58f2f0 now gives the cleanest local boundary for that claim: RT3 stages and later consumes the shared work-record metadata triplet, but the sidecar itself still appears only as a borrowed cached pointer at 0x595bc0, never as a locally seeded replay-band field. One adjacent staged-route callback is tighter now too: 0x595860 is the submit-result handler below 0x5958e0, using the already-grounded third selector-generation counter at [transport+0xac0] plus the target at [transport+0xb48] to choose whether staged route-callback traffic can advance the route-mode state machine from mode 2 into 3 and later 4. The counter beneath that decision is tighter too: 0x594e30 now reads as a selector-view entry counter for slot 2 flag bit 0x20, optionally filtered by the current transport name buffer. The selector-view mutation side beneath that callback family is tighter too: 0x594a30 is now the direct keyed store remover, 0x594fb0 clears one selector-slot ownership pointer plus its slot-local flag dword and drops the whole entry when no slots remain, 0x595010 rekeys one selector-view entry under a new name while preserving the 0x40.. runtime band, and callback root 0x59f9c0 is now bounded as the lane that clears one named selector-view slot, publishes callback slot 18, and can still re-enter the route-mode setter from the same slot-2 state and generation gates. The next adjacent callback roots are tighter now too: 0x5950a0 clears one selector slot from every selector-view entry in the keyed store, 0x59fab0 is the rename or relabel sibling above 0x595010, 0x59faf0 updates one selector slot's fixed sample-text buffer and refreshes the active selector object when present, and 0x59fb60 replaces one selector slot's name set, requests the default profile-key bundle for that slot, and publishes callback slot 20. Slot 16 is tighter now too: current grounded caller 0x59f440 forwards the staged route-callback payload handle from [transport+0xb50] through 0x592ea0 just before route mode 5. The last adjacent callback root in that block is tighter now too: 0x59fbd0 is the built-in per-slot profile-key query sibling. It resolves the caller selector name into one slot index, forwards the caller trio into 0x596b90, and then publishes callback slot 28; that lower helper indexes one slot-specific built-in string pair from [transport+0x189c] and [transport+0x18ac], reuses the generic per-key handler 0x596970, and only republishes slot 28 when that lower query path succeeds. The compact-header side is tighter too: 0x58fe20 and 0x58ff20 now show that compact payloads always carry the primary IPv4 dword and that header bit 0x10 only gates whether the primary port word is inline or inherited from the owner default port. The variable tails are tighter too: 0x58fe90 now validates the 0x40 inline keyed-property vector against the owner schema, and 0x58fe50 validates the signed-0x80 trailing string-pair tail before decode. 0x58ff60 then grounds bit 0x02 as the inline secondary IPv4 dword branch, bit 0x20 as the paired secondary-port word branch with owner-port fallback, bit 0x08 as one still-unresolved auxiliary inline dword stored at [descriptor+0x10], bit 0x40 as one inline keyed-property vector decoded through the property-store writers, and signed bit 0x80 as one trailing string-pair tail. The descriptor-state side is tighter too: queue insertion now cleanly splits pending tags 0x4/0x8 from serviced-ready bits 0x1/0x2, and the gsi_am_rating lane now separates direct primary-endpoint handling from queued-descriptor handling. Current evidence now also says byte [descriptor+0x14] is not queue-only: 0x58ff60 can also OR in bit 0x1 after the inline keyed-property vector and bit 0x2 after the signed string-pair tail. Byte [descriptor+0x15] bit 0x1 is source-side too, not queue-generated: it is explicitly seeded by the primary-endpoint refresh path and preserved by the compact descriptor decode path before the transport later uses it in the gsi_am_rating lane both to choose direct-vs-queued handling and to suppress the direct 0x595dc0 transition even after ready bit 0x2 is present. The descriptor body is tighter too: [descriptor+0x20] is now the intrusive next-link used by the transport-owned primary-endpoint list, and [descriptor+0x1c] is now the special numeric scalar behind the current queryid/ping fallback pair. The compact-only auxiliary dword at [descriptor+0x10] is tighter in a different way: a local xref scan now shows it being preserved by later generic preservation helpers such as generic_record_0x1c_deep_copy_with_owned_string_at_0x08 0x591410 and the callback-marshaling wrappers 0x591480 and 0x591510, but still no dedicated semantic reader has been recovered. So the main remaining uncertainty is the exact higher-level meaning of header bit [descriptor+0x15] & 0x1, plus whether [descriptor+0x10] ever matters outside that preservation path. The route-mode setter is clearer too: 0x595650 now reads as a compact state machine over the transport-owned route tables, where mode 0 splits the gsi_am_rating branch into direct primary-endpoint handling versus queued insertion, mode 1 adds the ready-bit gate and the same queued fallback, mode 2 removes pending descriptors from the queued family, mode 3 forces mode 2 when the primary-endpoint table is empty, mode 4 updates the deferred route-status state around [this+0x1ed4] and [this+0x1ed8], and mode 5 copies the staged route companion dword at [this+0x490] into [this+0x54] while mirroring that value into queue-side slot [this+0x1724+0x24] through 0x005a0940. The route-event dispatcher side is cleaner too: the mode-5 tails in both callback families do not copy a descriptor-local field. They mirror the transport-staged companion dword at [this+0x490] into [this+0x54] and that same queue-side slot instead, not into [this+0x1778]. The gsi_am_rating maintenance lane is tighter now too: it sorts the primary-endpoint descriptor table through 0x590310 in mode 1 with key gsi_am_rating, then selects the new head through 0x590480 rather than treating the table as pure insertion order.