Skip to content

ASRD → GNS gameplay transport - #1100

Closed
ywgATustcbbs wants to merge 2 commits into
ReactiveDrop:reactivedrop_betafrom
ywgATustcbbs:gns
Closed

ASRD → GNS gameplay transport#1100
ywgATustcbbs wants to merge 2 commits into
ReactiveDrop:reactivedrop_betafrom
ywgATustcbbs:gns

Conversation

@ywgATustcbbs

@ywgATustcbbs ywgATustcbbs commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

TL;DR

This PR replaces Source's legacy gameplay transport with GameNetworkingSockets (GNS) while preserving the existing Source message serialization, sign-on, handlers, game state, and gameplay logic.

The interception boundary is above CNetChan packetization: Source still creates and processes its normal INetMessage objects, while GNS takes ownership of the underlying connection, reliability, ordering, retransmission, fragmentation, and transport lifecycle. The integration is implemented through validated runtime hooks and interface substitution; no engine.dll binary file is modified.

Current milestone

PASS: one client + one standalone dedicated server.

Validated on both local and real-network connections, including:

  • connection and Source sign-on;
  • map loading;
  • normal gameplay;
  • mission completion;
  • 5 minutes of continuous playable runtime. (Only tested for 5 mins, doesn't necessarily mean it crashed after 5 mins.)

This is the current M1 milestone. It does not yet claim multi-client, listen-server, long-duration soak, or full production networking coverage.

Why GNS

This transport replacement provides a much cleaner foundation for future networking work:

  • Steam Datagram Relay (SDR): GNS can integrate with SDR without changing the Source-facing message bridge. SDR connectivity has already been experimentally verified, although SDR support is not implemented in this PR.
  • IPv4 + IPv6: the transport architecture can be extended to support both without changing the Source gameplay protocol.
  • Removes the legacy Source gameplay transport bottleneck: gameplay traffic no longer depends on CNetChan's legacy packet/reliability transport. The current implementation does not impose a custom bandwidth cap and uses GNS defaults, which also eliminates legacy Source choke behavior on the replaced path.
  • Fine-grained network tuning: reliability lanes, unreliable delivery, reordering, timeout behavior, MTU behavior, and other transport parameters can be tuned independently.
  • Encryption: supported by the GNS transport.
  • Better NAT traversal: GNS provides a path toward substantially better NAT traversal than legacy direct-IP Source networking; the relevant P2P/NAT traversal integration is not implemented yet.
  • Runtime hooking only: the implementation validates and patches process memory at runtime. It does not modify the distributed engine.dll file.

AI disclosure

For clarity about how this PR was produced:

  1. Architecture, design, and implementation plans: written by a human with AI assistance.
  2. Code: entirely written by AI. The human author did not manually write any lines of code.
  3. Code review: performed by AI. The human author did not manually review the code.
  4. Testing: a combination of automated testing and human hands-on gameplay testing.
  5. PR description/documentation: primarily written by AI and reviewed by the human author.

The human role has primarily been defining requirements, architecture and constraints, making design decisions, directing implementation/review agents, and performing real gameplay validation.

Test setup

Copy the following files into the corresponding client/server game directories, replacing the existing files where applicable:

server.dll
client.dll
missionchooser.dll
asrd_gns_wrapper.dll

Example standalone dedicated-server launch command:

srcds.exe -console -appid 582400 -ip 0.0.0.0 -port 27016 -gns-port 27015 -game reactivedrop +sv_allow_lobby_connect_only 0 +map lobby

-gns-port 27015 specifies the UDP port that GNS binds for gameplay transport.

The normal Source -port and the GNS -gns-port are intentionally separate in this example.


ASRD → GNS gameplay transport

Status: M1 is complete. The client and standalone dedicated-server path has passed technical validation, including a five-minute playable mission run. This establishes that the replacement boundary is feasible and stable within the tested M1 scope; it is not yet a claim of production-complete networking coverage.

Why

The project needs GameNetworkingSockets (GNS) to own the gameplay transport while keeping Alien Swarm: Reactive Drop's existing Source message and game-state semantics intact. Replacing the transport at that boundary avoids rewriting the Source serialization, dispatch, sign-on, and gameplay code, and gives the project one place to define connection ownership, delivery lanes, retransmission, MTU handling, and lifecycle cleanup.

This is also intended to make unreliable delivery behavior deterministic under loss and reordering. The reorder queue now has a wall-clock bound, and automatic window selection compares independently measured candidates instead of stopping at the first window that happens to cross a delivery threshold. The change removes success-path log spam while retaining diagnostics that are useful for failures and lifecycle transitions.

What changed

  • Added an opaque asrd_gns_wrapper ABI. Client/server game DLLs depend only on the wrapper header and import library; GNS, protobuf, Abseil, and utf8 support are statically linked into the wrapper. The target remains Win32/x86 and uses Windows BCrypt/CNG, Winsock, WinMM, and IP Helper rather than a separate OpenSSL or GameNetworkingSockets.dll runtime dependency.
  • Added client, server, and role-neutral runtime lifecycle adapters. They bind GNS connection handles to Source client/session/player context, preserve Source sign-on and message registration, and perform all Source-facing work on the engine/game thread.
  • Added the Source-to-GNS message bridge. Source INetMessage objects are serialized into a versioned envelope, transported over GNS, then decoded and passed back through Source ReadFromBuffer and Process.
  • Added a narrow set of validated engine hooks and channel bindings: direct-IP connect intent, engine message registration, the Source INetChannel send surface, and a dedicated-server hibernation wake point. These are the minimum interception points needed to move transport ownership without replacing Source message handlers or game logic.
  • Added explicit reliable/unreliable lane selection, CLC_Move compatibility parsing, packet ownership checks, and the bounded unreliable reorder ring.
  • Added fixed-cost residence histograms and rolling counters for automatic window selection. Per-packet/per-update success logging was removed; failure, rejection, connection-state, lifecycle, and final-statistics logging remains.
  • Hook locations, calling conventions, vtable slots, and expected instruction bytes are bound to the validated fixed engine.dll build. Every mismatch fails closed before an unknown target is patched or called.

The Source upper layer is deliberately not replaced: message serialization, Source handlers, player/session context, sign-on calls, and game-thread dispatch remain the compatibility surface. GNS owns transport and connection identity, not the meaning of Source messages.

Architecture

Hook and binding boundary: the minimum engine cut

The chosen boundary is immediately after Source has created a semantic network message, but before CNetChan turns it into legacy packets. On receive, the boundary is immediately after GNS delivery/reordering, but before Source's existing ReadFromBuffer and Process handlers. This is the smallest cut that transfers connection and transport ownership while retaining Source sign-on, message objects, handler state, user-command processing, entity updates, and game rules.

Boundary Mechanism Purpose
Direct-IP connect intent A validated detour intercepts the engine path reached by the connect console command. Starts the GNS client lifecycle and deliberately does not call the legacy connect function as a fallback.
Engine message registration A validated detour wraps the engine's RegisterMessage implementation, calls the original, then records the live INetMessage, channel, and handler context. Builds the type-to-handler registry required to dispatch GNS payloads back into the exact Source message implementation.
Source channel send surface A small INetChannel adapter is supplied through ConnectionStart; it is temporarily bound for dispatch and promoted to the lifecycle channel while connected. Server-side Source context receives the corresponding adapter during ConnectionStart/ClientConnect. Captures SendNetMsg, raw SendData, SendDatagram, Transmit, and shutdown/lifecycle calls before legacy packet construction.
Inbound Source dispatch The game-thread bridge looks up the captured message, sets its channel, calls ReadFromBuffer, then calls Process; client updates are bracketed by the validated PacketStart/PacketEnd call-ins. Reuses Source's upper-layer parsing and behavior without feeding the payload through the legacy packet receiver.
Dedicated-server hibernation A validated SV_Think call-site wrapper pumps only the GNS control frame while the dedicated server is hibernating, then invokes the original target exactly once. Keeps accept/close/lifecycle progress alive without introducing another timer or thread.

Only the connect, registration, and dedicated wake entries are binary detours. Message send interception is primarily an interface substitution: existing Source code continues calling INetChannel, but the session's channel object is the GNS adapter. Lifecycle methods such as ConnectionStart, ClientConnect, SetSignonState, PacketStart, PacketEnd, and ClientDisconnect are validated engine call-ins rather than wholesale replacements.

Why CNetChan is bypassed instead of hooking UDP

CNetChan is not just a UDP serializer. It owns the legacy connection state, sequence and acknowledgement space, reliable buffering/retransmission, choking, fragmentation, timeouts, and packet receive state machine. Keeping it below GNS would leave two transports trying to own ordering, reliability, fragmentation, timeout, and lifecycle. Reliable Source packets tunneled through reliable GNS messages would also create nested reliability and head-of-line behavior, while nested fragmentation would make MTU and recovery behavior harder to reason about.

A lower socket or UDP hook is therefore too late in the pipeline. At that point the semantic message type, effective reliability, voice/snapshot provenance, and Source update boundaries have already been flattened into legacy datagrams. A packet hook could only tunnel those opaque datagrams and would still require the legacy handshake and CNetChan receive state to remain authoritative. That would preserve the old transport inside GNS rather than replace it, and it would prevent the bridge from mapping Source messages to GNS lanes or applying message-aware unreliable reordering.

The adapter instead implements only the channel surface actually required by Source's upper layer. SendNetMsg, SendData, and SendDatagram are converted into GNS envelopes before legacy packetization; GNS owns the connection, lanes, acknowledgements, retransmission, and MTU behavior. On receive, the captured Source handlers are invoked directly on the game thread. This keeps the hook surface small, auditable, and fail-closed while avoiding parallel CNetChan and GNS transport state.

Connection and lifecycle

On the client, the GNS connect-intent path parses and normalizes a real IPv4 endpoint, retires any previous generation, binds the current Source context, and starts a wrapper connection. Once GNS reports a connection, a registration adapter captures the engine's message registry and promotes the temporary channel adapter to the persistent lifecycle owner. A stale connection generation cannot deliver events into a newer session. Deferred disconnect closes the GNS handle before the normal Source disconnect path is queued.

On the server, the wrapper listens, queues incoming/connected/failed/closed events, and lets the game-thread control frame accept and map them to Source context. Source sign-on is finalized before the real-player transition. Dedicated mode has a dedicated-only wake/control pump so hibernation does not prevent lifecycle processing; listen-server role activation is kept separate from the dedicated path. Terminal events tear down the Source context and the GNS mapping together. The current mapping policy rejects duplicate/unsupported extra gameplay mappings rather than silently sharing a session.

Wrapper callbacks do not call Source code. They enqueue opaque events under the wrapper's synchronization and are polled from the engine/game thread, which keeps transport-thread work independent of Source object lifetime and dispatch rules.

Message transport

Each envelope carries direction, message type, reliability/provenance, flags, and sequence metadata. There are three send lanes:

  • reliable (R);
  • unreliable realtime (U_REALTIME); and
  • unreliable normal (U_NORMAL).

The two unreliable lanes share a session-local global sequence gate; reliable traffic is not placed in that gate. Callback provenance and effective reliability choose the lane without changing the Source message's reliability meaning. GNS supplies lane ordering, acknowledgements, retransmission, and MTU fragmentation. Received payloads are drained and dispatched only on the game thread.

The CLC_Move compatibility layer reads the move body at the bit level, classifies first/contiguous/gap/stale/partial-overlap ranges, and computes the existing drop metadata. It does not replace Source's move processing or invent a second command namespace.

Reorder and timeout behavior

The physical reorder ring is capped at 128 slots, with logical candidates 16, 32, 48, 64, 80, 96, 112, 128. Every packet records its first-arrival wall-clock timestamp. The single reorder_deadline is 30 ms for the real ring, the current-window model, and every candidate simulator.

The receive epoch is only a safe boundary for checking timeout. Each receive pass first drains the GNS queue completely (until receive returns zero), dispatches the reliable traffic, and only then checks timeout. If progress is needed, the code scans occupied slots from the logical tail toward the head, selects the farthest expired slot, pops from the head through and including that slot, and immediately performs another continuous head drain. No timer or timeout thread is added, and timeout is no longer based on an extra epoch count.

Automatic window selection

Every candidate and a separate D_current measurement use the same generated traffic, loss/reordering model, 30 ms deadline, warm-up rule, eligible sequence range, and measurement period. The formal metrics are:

  1. P95 packet residence/wait time;
  2. average residence/wait time; and
  3. delivered packets / eligible packets delivery rate.

The implementation uses a fixed 1 ms/256-bin residence histogram and bounded rolling counters rather than sorting samples or allocating per packet. The project baseline supplies D_min = 98%; the selector does not derive that threshold from the candidates.

Selection is deterministic:

  1. If the best candidate delivery rate is below D_min, choose window 32 immediately and do not optimize latency.
  2. Otherwise, only candidates at or above D_min - 0.5% enter latency comparison.
  3. Prefer lower P95 when the difference exceeds 5 ms; if P95 is within 5 ms, compare average residence time using the same 5 ms threshold.
  4. If both latency measures are near-ties, prefer higher delivery only when the difference exceeds 0.5%.
  5. If all three comparisons are near-ties, keep the current window. D_current is used only for final stability/no-op decisions and is never substituted for a candidate or used to compute D_best.

The former “first window reaching 98%”, “choose the next window”, and epoch-count timeout rules are not retained alongside this selector.

Compatibility boundaries

  • Client: The takeover path is validated against the fixed 32-bit engine.dll build for which the expected PE metadata, RVAs, signatures, and instruction bytes are recorded. Starting the client, issuing connect, sign-on, map transition, gameplay, and mission completion passed the M1 run. Endpoints must be real/non-loopback IPv4 addresses; the client must not connect to 127.0.0.1. Generic or mismatched engine builds fail closed.
  • Listen server: Role-specific initialization and hook guards are present and compile, but listen-server gameplay is outside the completed M1 runtime matrix.
  • Dedicated server: Standalone dedicated-server startup, listening, client connection, sign-on, map transition, gameplay, and mission completion passed the M1 run. The hibernation-safe control pump is active only in this role. Empty-server shutdown remains governed by the existing rd_server_shutdown_when_empty ConVar, whose default is 0; this PR does not claim an automatic 60-second exit when that setting is disabled.
  • Legacy UDP and connectionless traffic: The gameplay takeover path blocks the legacy client connect path after GNS intent is accepted, but this PR does not claim that every legacy UDP use has disappeared. A2S/server-browser, connectionless/OOB, relay/P2P, and other paths not routed through the bridge remain outside the migration boundary. Complete transport exclusivity has not been independently proven with a retained capture.
  • Engine diagnostics: net_graph currently crashes the client and must remain disabled. The precise failing access is not yet isolated; the leading compatibility concern is that this diagnostic path expects concrete CNetChan state/layout beyond the minimal INetChannel adapter contract.
  • Third-party hooks: No engine.dll file is modified. Hook installation is process-wide, role-aware, idempotent, and fail-closed on build/signature/expected-byte mismatch, with byte/protection restoration on failure. Coexistence with arbitrary third-party binary hooks has not been runtime-verified; a collision is expected to disable the takeover rather than patch an unknown image.
  • Scope: The current lifecycle and test evidence cover the intended single mapped gameplay session. Multi-client scaling and every Source networking feature are not claimed by this change.

Verification

Static gates

The root contract suite passes:

python -m unittest discover -s tools/tests -p 'test_*.py' -v
47 tests passed, exit code 0

The existing Python harness performs a Release Win32 rebuild of reactivedrop_vs13.sln:

MSBuild.exe reactivedrop_vs13.sln /t:Rebuild /p:Configuration=Release /p:Platform=Win32 /m /nologo

The recorded build completed with exit code 0 and 0 errors. Warnings remain in the legacy/third-party build; they do not change the 0-error gate result.

Static coverage includes sequence-generation and retirement behavior, rolling-stat eviction/reset, selector thresholds, timeout ordering, packet ownership, and reliable/unreliable ring exclusion. The source branch is represented by consolidated commit f772d83559b151fa433652f488c7571e6b5e6c17, whose parent is base 564fb7568ae9d7da7f175a778b9378acb0017613.

Runtime evidence

The M1 runtime test used the real client and a standalone dedicated server. It completed the following path:

Scenario Result
Start the dedicated server PASS
Start the client PASS
Connect through the connect console command PASS
Establish GNS transport and complete Source sign-on PASS
Change map PASS
Play one mission through mission completion/results PASS
Remain in the playable test flow for five minutes PASS

Within this client/dedicated-server scenario, connection ownership, the message hook boundary, Source dispatch, map transition, and sustained gameplay were stable enough to complete M1. This is the technical validation milestone for the replacement approach. It does not extend the claim to listen servers, multi-client load, arbitrary engine builds, every diagnostic feature, or extended soak/stress testing.

Known limitations and follow-ups

  1. Fix the client crash caused by enabling net_graph. Until the exact access is isolated and supported, net_graph must remain disabled; the minimal adapter is not claimed to reproduce the complete concrete CNetChan diagnostic surface.
  2. Extend runtime coverage beyond M1: listen server, disconnect/reconnect, multi-client behavior, longer soak runs, loss/reordering stress, and packet-capture classification of remaining legacy/OOB traffic.
  3. Decide explicitly whether dedicated empty-server shutdown should enable the existing rd_server_shutdown_when_empty setting or receive a separate GNS-specific policy; the current default does not start the 60-second grace period.
  4. Validate only the supported fixed engine.dll build unless new independent RVA/signature evidence is added; do not assume compatibility with other engine revisions or arbitrary hook combinations.
  5. Remove the development-only asrd_gns_smoke_probe and its project entries before final delivery.
  6. The residence histogram intentionally has 1 ms resolution and saturates at 256 ms; this is sufficient for the current 30 ms selector but is not a substitute for a high-range latency distribution in future diagnostics.

@mithrand0

Copy link
Copy Markdown
Contributor

No idea what you generated, but it's not Steam Datagram Relay (SDR).

Instruction how to port existing code are here:
https://partner.steamgames.com/doc/features/multiplayer/steamdatagramrelay

@mithrand0 mithrand0 closed this Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants