DistrictServerCSharp: .NET 8 port of the C++ district server (draft, refs #12) - #16
DistrictServerCSharp: .NET 8 port of the C++ district server (draft, refs #12)#16eaxiumnet wants to merge 9 commits into
Conversation
Adds the .NET 8 project file, entry point (Program.cs with --selftest / --drive / --resolver-check modes), the configuration layer (DistrictConfig, env + INI settings), the rotating logger, and the account model + registry. This is the first commit of the wire-identical C# port of DistrictServer.cpp. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
… field decoders Ports the ApbUdp.cpp wire layer: LSB-first BitReader/BitWriter, the XTEA handshake cipher (ECB/CBC x endianness), the BTEA/XXTEA district cipher (DistrictCrypto), the UE3 packet parser + all packet/field builders, the controller/actor field decoders, and the TCP control-link Network class. Includes the byte-identical self-test vectors (SelfTest.cs). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…t state, diagnostics Ports the DistrictServer.cpp core: the AUTH/USES/WELCOME/JOIN/ANS handshake (HandshakeService), per-endpoint channel-sequence allocation, the reliable TX queue with retry + ACK gating, binary NMT control, transport-state tracking with keep-alive ACK throttling, the possession/lifecycle reset hooks, and the packet diagnostics (SaveCapture + XTEA-mode forensics). Also the world registration + character-handoff control link (WorldControl). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…ycle, remote-pawn, controller, package resolver Ports the district subsystems: GRI match-start replication, the level-streaming plan, spawn-zone markers + selection, the ACK-gated pawn bootstrap + possession, remote-pawn multiplayer with the dead-peer lifecycle, controller field decoding (movement / 371 / visibility / customisation replicator dispatch), and the cooked-package net-index resolver that parses cooked .u packages directly. Also re-includes DistrictServerCSharp/Packages in .gitignore (the root .gitignore's NuGet `packages/` pattern matched it case-insensitively). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Ports the --drive harness: a fake WorldServer (scratch TCP) + two fake clients (scratch UDP) that run the full join path AUTH -> WELCOME -> JOIN -> 371 spawn-zone -> district-enter answer -> pawn-open -> all 8 ACK-gated possession stages, verify each player sees the other's remote pawn, and drive the customisation replicator to ClientNotifyTransferComplete. Scratch ports only; never touches the live instances. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…kers Adds PORTING.md (the authoritative C++ -> C# function map, §3/§7, verification, deploy + rollback §6) and the two gates that keep the port honest: check_port_sync.py inventories every C++ function and asserts it has a C# home (exits 1 on any port gap), and check_deploy_sync.py compares the deployed DistrictServer.dll sha in the three instance folders against the build output. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Net/UdpListener.cs is the top-level receive loop, ported from
UdpListenerThread in DistrictServer.cpp. It was the one file still
missing from the port branch, and it is load-bearing: Program.cs
(Startup/Main) and Drive/DriveHarness.cs both construct it, so the
branch did not compile without it.
Responsibilities:
- Binds the configured UDP port and owns the recvfrom loop.
- Three-pass decrypt/parse mirroring the C++ order. The first AUTH
bunch is plaintext; everything after is BTEA-encrypted with the
account session key. Decrypt-then-parse runs FIRST for a keyed
endpoint, because a ciphertext datagram can decode as a plausible
plaintext packet by chance and would silently discard a real
message.
- Constructs and wires the service graph (handshake, reliable queue,
GRI startup, spawn zone, streaming, pawn lifecycle, controller
feedback, remote pawn, binary control, transport state), including
the two cycles that need post-construction injection:
PawnLifecycleService.RemotePawnReplication and the Lifecycle reset
delegates.
- Fixed dispatch order per received packet: AUTH, reliable-ACK
retirement, post-possession unlock, hard-landing recovery,
bMatchHasBegun, reliable retry, transport ACK, customisation
replicator, controller actor feedback, possession gate, text
control, binary control.
- Possession gate keyed by client endpoint (address, port) rather
than account id. The C++ used a global account-id set that was
never cleared, so a reconnect of the same account from a new
socket had its district-enter ASK received but never answered
(insert returned false) and the client parked at "Entering
district" forever. A fresh connection now gets its own entry and
re-runs the answer.
- WSAECONNRESET (dead-peer ICMP port-unreachable) is attributed to
the last-pushed remote-pawn viewers for dead-peer cleanup and
logged at most once per 10 s instead of once per send cycle.
Also ignore _drive_scratch/ for parity with _scratch_drive/; both
directory spellings have been used by local drive runs.
Verified in a clean worktree: dotnet build -c Release reports 0
warnings / 0 errors, and all three offline modes pass without a
client or a live stack --
--selftest SELF-TEST PASS (AUTH bunch round-trip, ACK,
challenge)
--resolver-check RESOLVER-CHECK PASS (controller ordinal 12426,
holdable 15985, inventory 25478)
--drive DRIVE PASS (registered at World Server, WELCOME,
two-account sequences complete, remote pawn
opened both directions, ClientNotifyTransferComplete)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four are silent behaviour differences against the C++ oracle, not compile errors. Verified against DistrictServer.cpp line by line. 1. HandshakeService.ProtectOutgoingPacket aliased the caller's buffer when the packet was already word-aligned and >= 8 bytes, so DistrictCrypto.Encrypt (in place) turned the caller's plaintext into ciphertext. The oracle takes its packet BY VALUE (DistrictServer.cpp:1412), so the caller's buffer is never touched there. ReliableQueue stores that same array as PendingServerReliable.ClearPacket (ReliableQueue.cs:50) and re-sends it on retry (:158), which then encrypted already-encrypted bytes. Deterministic for spawn-zone / HUD-marker labels, which this build never ACKs. Fix: copy unconditionally. 2. JOIN never cleared per-account controller feedback state, so a reconnect reusing the same account id inside one process kept the previous startup/marker barrier: field 372 sent no fresh marker and the player was never possessed. The oracle erases it on JOIN (DistrictServer.cpp:13137-13142). Lifecycle.ResetControllerFeedback was already wired (Lifecycle.cs:24, UdpListener.cs:83) but had no call site; this adds it. 3. ControllerFeedbackService's OnMarkersSent handler indexed _feedbackStates directly. Unlike std::map::operator[], the .NET indexer getter throws KeyNotFoundException -- straight out of the UDP receive loop -- when no controller field has been decoded yet. Fix 2 makes that window reachable, so both ship together. Now goes through the existing GetOrCreateState. 4. The spawn-location latch was gated on IsActionDistrict and handled only the by-channel reference form. The oracle gates it on firstReceipt alone and accepts two forms: the bridge channel (action) and the static spawn-zone template NetIndex 72913/72914 (Social) -- DistrictServer.cpp:12474-12506. On Social the latch therefore never ran, so PawnLifecycleService/RemotePawnReplication found no selected location and the pawn ignored the chosen zone. Ports TryGetSpawnZoneLocation (the oracle's Social branch, cpp:397) to DistrictConfig and restores the oracle's structure. Also drops the dead DistrictCrypto.ProtectOutgoingPacket duplicate, which had zero callers and diverged twice from the oracle (rejected < 8 bytes the oracle zero-pads; its slice threw on unaligned input), and repoints Tools/check_port_sync.py at the real port in HandshakeService so the drift gate stops crediting the dead copy. Divergence 4 was found by that gate once it pointed at the right function: it now reports 160/160 mapped, zero doc gaps, zero port gaps. Build is clean (0 warnings). Not yet exercised against a live client. Refs burber#12 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed
Divergence 4 is worth calling out separately: it was not in the audit. It Gate now: 160/160 mapped, 0 doc gaps, 0 port gaps. Build clean, 0 warnings. Not yet exercised against a live client — these are source-fidelity fixes |
Audit of the C++ oracle turned up three documentation claims that assert a C++ counterpart which does not exist. None of them changes shipped behaviour, but each one would have mislead the next person diffing the two implementations. 1. BitWriter.WriteCompressedRotator said "Verified round-trip against the C++ (yaw 16384 reconstructs to 16384)". There is no C++ rotator writer to round-trip against: grep for "Rotator" across DistrictServer/ returns nothing. The oracle has WriteCompressedVector (ApbUdp.cpp:262) and a single AimRotation int READ (ApbUdp.cpp:2410), and neither side of the port has a rotator reader, so no round-trip harness exists anywhere. Relabelled PORT-ORIGINAL: the encoding is client-RE-derived and hand-checked only, not yet exercised against a live client. The encoding description itself is unchanged. 2. PORTING.md §3.8 listed BuildUnreliableActorVectorFieldPacket and BuildUnreliableActorRotatorFieldPacket in the C++ column. Both are phantoms. The oracle has exactly one unreliable field builder, BuildUnreliableActorFloatFieldPacket (ApbUdp.cpp:1221, called from DistrictServer.cpp:10753) — which had no row at all. Now mapped, with the vector/rotator builders marked as port additions for the remote-pawn path. 3. PORTING.md §7 item 2 listed BuildActorRotatorFieldPacket "(zero C++ callers)" — also a phantom — and claimed the raw-float paths are "unused by the port". FloatToWireBits (:1828) and WriteFloatBits (:2185) both have live C++ callers (PRI stats :1867/:1873, non-compressed HUD-marker location :2264-:2266) and both ARE mirrored, inline via BitConverter.SingleToUInt32Bits at four C# call sites. Reworded to say so, and to explain that they stay in §7 only because there is no named C# home to point a §3 row at. Removing the phantom from §7 makes the gate's own arithmetic add up: it listed 8 not-ported entries but only ever matched 7. Gate after: default MAPPED 160, NOT-PORTED 0, UNMAPPED 0 (0 doc, 0 port gaps) --all MAPPED 161, NOT-PORTED 7, UNMAPPED 61 (61 doc, 0 port gaps) MAPPED 160 -> 161 and doc gaps 62 -> 61 are the newly-documented float builder; the stale "~68" figure in §7 is now the measured 61. Build clean (0 warnings, 0 errors). Docs and comments only — no code paths touched. Refs burber#12 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Provenance audit: three doc claims asserting a C++ counterpart that does not existFollow-up to the fidelity pass above (
Fixes: #1 relabelled Side effect worth noting: deleting the §7 phantom makes the gate's arithmetic add up — it had been listing 8 not-ported entries while only ever matching 7.
Net effect of this and the previous commit: the port's own docs no longer claim verification that was never performed. The one place a live-client check would now buy the most is |
Implements the C# port proposed in #12. Opened as a draft: the history is reviewable and every offline gate is green, but the one verification gap named in #12 — a live-client smoke test — is still open, and no maintainer feedback on the architecture has landed yet. Nothing here removes, rewrites, or competes with the C++ tree.
What this adds
DistrictServerCSharp/— a .NET 8 reimplementation ofDistrictServer.cpp(~14k lines) split by responsibility, plus two drift gates underTools/. 42 files, +14,220 lines, zero lines deleted.DistrictServer/(C++) is untouched and remains the protocol oracle and the rollback path.Bits/,Crypto/,Protocol/,Net/Handshake/Core/Gri/,Streaming/,SpawnZone/Pawn/,Controller/Packages/.udirectly)WorldControl/Drive/Commits
Seven incremental commits, each building on the last, bottom-up so review can follow the wire layer before the services that use it:
06dcebc4c9bd0b52130de67e69d9c48183ba28cd8dPORTING.mdfunction map + drift-gate and deploy-sync checkers3c9fd1fVerification
Run against a pristine
git archiveexport of the branch tip into an empty directory, so nothing untracked can contribute:--drivemilestones: registered at World Server → WELCOME → both account sequences complete → remote pawn opened in both directions →ClientNotifyTransferComplete.--resolver-checkPASS, run in a tree with a real cookedAPBGame.uconfigured (it needs a local client path, so it is not part of the pristine-export run): parses the package and reproduces the live C++ net indices exactly — controller ordinal 12426, holdable 15985 → 47088, storage 25478 → 56581.Port completeness is machine-checked rather than asserted.
Tools/check_port_sync.pyinventories every C++ function and fails if one has no C# home: DistrictServer.cpp 175/175 mapped, 0 not-ported, 0 unmapped.PORTING.md§7 lists the only intentional exclusions — deadUnreal.cpp(not in the C++ build, zero callers) and three zero-caller raw-float helpers whose behaviour is covered inline byBitConverter.SingleToUInt32Bits. Note that--allalso reports ~68 doc gaps for theApbUdp.cppbit/parse primitives; those are ported wholesale as theBits//Protocol/layers and mapped at layer granularity, so the gate exits 0 for them by design. Only a non-zero port gap count means something is genuinely unported.What is NOT proven
No live-client session has been run against this build. The C# port is what is currently deployed in my local stack (Social 6969 / Financial 6970 / Waterfront 6971, managed apphost, all three up since 13:31:41 today), but every district log it has produced so far is from
--drive— the only AUTH identities present are the harness accounts0000000001and0000000002, never a real client account. So the offline gates and the simulated two-player path are green; end-to-end behaviour against the real 1.1 client is untested on this branch. That test is what should move this out of draft, and I would rather state the gap than let green CI-style output imply coverage it does not have.Relationship to the open C++ PRs
Three behavioural fixes were found while porting and apply to both trees. #13 lands them in the C++ oracle; they are native to the C# code here, so the two implementations stay wire-identical rather than diverging:
insertreturned false — and the client parked at "Entering district" forever.#14 is the WorldServer half of the registration-lifecycle work and is independent of this branch.
Review notes
Happy to split this into smaller PRs, restructure the layering, or change anything about the approach — including keeping it on the fork indefinitely if a parallel implementation is not wanted in-tree. The commit boundaries are designed so any prefix of them is a coherent, buildable subset if you would prefer to take it in stages.
Refs #12.
🤖 Generated with Claude Code