BT410 5.3.117: the pod talks -- the entire NetNub networked layer reconstructed, and the last runtime stub falls
Twenty-two Fail sites in L4NET.CPP -- the whole console/mesh transport -- replaced with decomp-verified bodies (part_007.c @004501d4..@00452378), every error string extracted byte-for-byte from the shipped exe, every magic number resolved to a macro from the AUTHENTIC 1995 NETNUB.HPP that survives in the archive (NETCOM_VERSION 11; 0x63c == MAX_SEND_DATA_SIZE). The real-mode bridge came free: the 1995 netshare.OBJ has been linking into mungal4.lib all along via the prebuilt fallback -- this file only had to define the six globals it imports and call it. NetNub::SendCommand is the gateway (setRMBuff, DPMI int 31h/0300h on the TSR's vector, getRMBuff); BC4.52's <dos.h> really has int386x -- the binary's own "int386x():ERROR=" text named the RTL call. The topology, decomp-proven: console listens on 1501, the pod mesh on 1502; hosts before ours in the egg get an OPEN, ours becomes the local host by address match, hosts after ours get a LISTEN -- and CheckBuffers heals the ordering race by SWAPPING sockets when a peer lands on the wrong listen. The console's listen is a wildcard that learns its address; the console connection SURVIVES mission teardown (Shutdown zeroes both counters anyway -- reproduced faithfully). Send discipline: direct while the retry buffer is empty, everything queues behind a backlog in per-host FIFO, one round-robin drain per idle slot; only a dumped-to-avoid-blocking send is retried, and RemovePacket is empty in the binary because delivery already compacted the pad buffer. Internal connect/disconnect events ride the GetNetworkClientPointer(0)->ReceiveNetworkPacket self-dispatch (the decomp's FUN_0041d7f0 resolves to the ClientID-0-is-me idiom). Ghidra had dropped register and trailing arguments at half a dozen call sites (GetRemoteHost's HostID among them) -- the surviving HPP signatures arbitrated each. Stubs: 8 across 8 files -- ALL RUNTIME STUBS ARE NOW CLEAR; what remains is the authoring-side CreateStreamedSubsystem family, which missions never call. Single-user regression clean on the rig; the networked path awaits its console + NetNub-TSR live session (two-agreeing-runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
# L4NET.CPP — the NetNub networked layer (reconstructed 2026-08-04, 5.3.117)
|
||||
|
||||
The pod's entire console/mesh transport: the protected-mode client of the
|
||||
real-mode NetNub TSR. Every body verified against the shipped binary
|
||||
(part_007.c cluster @004501d4..@00452378); every error string extracted
|
||||
byte-for-byte from BTL4OPT.EXE; the authentic interface header
|
||||
(CODE/RP/MUNGA_L4/NetNub/NETNUB.HPP, NETCOM_VERSION 11) supplied every
|
||||
struct and function code, and each binary literal resolved to one of its
|
||||
macros (0x63c == MAX_SEND_DATA_SIZE, 0x2c == MULTIPLE_SEND_BUFFER_SIZE(0),
|
||||
0x640 == MAX_RECEIVE_DATA_SIZE, 10 == the frame's destination limit).
|
||||
|
||||
## The real-mode bridge
|
||||
|
||||
`NetNub::SendCommand` (@00452100) is the gateway: `setRMBuff()` pushes the
|
||||
PM Netcom's Function/Status/Buffer_Length + buffer down to the real-mode
|
||||
shared block, DPMI int 31h/0300h fires the TSR's interrupt (vector from
|
||||
`getRMNumbers()`), `getRMBuff()` pulls the reply back. The bridge routines
|
||||
are NOT recompiled — the SURVIVING 1995 `netshare.OBJ`
|
||||
(CODE/RP/MUNGA_L4) has been a mungal4.lib member all along via the
|
||||
prebuilt-fallback (`netshare` in ORDER_mungal4); this file only had to
|
||||
supply the C-side globals it imports (`Function_Ptr`, `Buffer_Length_Ptr`,
|
||||
`baseadr:offs`, `RMFunction_Off`, `RMBuffer_Length_Off` — binary homes
|
||||
0x522b84..0x522b98) and `extern "C"` prototypes (TASM uppercased the OBJ's
|
||||
symbols; tlink32 matches caselessly — no `/c`). BC4.52's 32-bit <dos.h>
|
||||
really does have `int386x`/`segread` — the binary's own error text
|
||||
("int386x():ERROR=") named the RTL call. The register image zeroes exactly
|
||||
the fields the binary zeroes (reserved/flags/fs/gs/ss:sp).
|
||||
|
||||
## The topology (all decomp-proven)
|
||||
|
||||
- Console listens on TCP **1501**, pod mesh on TCP **1502**.
|
||||
- `StartConnecting`: hosts BEFORE ours in the egg list get an active OPEN
|
||||
(they already listen); our entry is recognized by address and adopted as
|
||||
the local host; hosts AFTER ours get a LISTEN. That ordering rule forms
|
||||
the peer-to-peer mesh without collisions. A slave then Sends
|
||||
AcknowledgeEggFile to the console; MasterMode Fails ("host is in an
|
||||
illegal startup mode" — 4.10 pods are always slaves).
|
||||
- `CheckBuffers` (@004518b8) is the pump: deliver a whole buffered packet;
|
||||
else advance ONE connection state machine per call (OPEN must match its
|
||||
address — Verify; the console LISTEN is a wildcard that learns its
|
||||
address; a mesh peer landing on the WRONG listen gets its socket SWAPPED
|
||||
to the record that expects it — the race healer), then one
|
||||
MULTIPLE_RECEIVE drains the wire into per-host pad buffers
|
||||
(pad_size/pad_head/pad_tail — the surviving L4HOST.HPP names them).
|
||||
- Wire frame = `NetworkPacketHeader{clientID, gameID, fromHost, timeStamp}`
|
||||
+ message; the netnub strips the leading socket handle.
|
||||
`GetNextMungaPacket` round-robins hosts from `lastHostIteratorPosition`,
|
||||
copies out one whole packet, memmove-compacts the pad. `RemovePacket`
|
||||
is EMPTY in the binary (@004520f8) — delivery already dequeued.
|
||||
- Send discipline: buffer empty → direct send, and only a
|
||||
NETNUB_DATA_DUMPED (would-block) result queues the message; buffer
|
||||
non-empty → everything queues BEHIND (per-host FIFO) + one drain attempt.
|
||||
`ExecuteBackground` drains one request per idle slot. The MessageBuffer
|
||||
copies each message (SendRequest owns the copy), tables queues by host,
|
||||
and round-robins them. A disconnect during send posts the internal
|
||||
HostDisconnected event through `GetNetworkClientPointer(0)->
|
||||
ReceiveNetworkPacket(NULL, &msg)` — the self-dispatch idiom the decomp's
|
||||
`FUN_0041d7f0(this,0)` resolves to (ClientID 0 == the manager).
|
||||
- `Shutdown`: game hosts close + destroy; THE CONSOLE CONNECTION SURVIVES
|
||||
the mission (pod returns to the console-only lobby). The binary zeroes
|
||||
BOTH connection counters even though the console stays online —
|
||||
reproduced faithfully (the console's own disconnect path recreates its
|
||||
listener rather than decrementing through zero).
|
||||
- `Mode`: reliable == the netnub BLOCKS in send (SET_SWITCHES
|
||||
blocking_switch=1); unreliable == non-blocking, dumps join the retry
|
||||
buffer. The ctor forces ReliableMode; the engine flips to Unreliable
|
||||
during map load.
|
||||
|
||||
## Sources & drift notes
|
||||
|
||||
- RP411's L4NET.CPP is the 2007 WinSock port — its 1995 NetNub code
|
||||
survives only in comment blocks (used line-for-line for the ctor RM
|
||||
plumbing). CODE/RP's L4NET.HPP + NETNUB.HPP + NETSHARE.ASM are
|
||||
authentic-form.
|
||||
- Ghidra dropped register/trailing args at several sites
|
||||
(GetRemoteHost's HostID, SendMessageToNetnub's host argument,
|
||||
CreateWeaponExplosions-style stack args) — the HPP signatures and
|
||||
HostManager's Find-based GetRemoteHost settle each.
|
||||
|
||||
## Runtime status
|
||||
|
||||
Single-user path regression-clean on the rig (mission soak, no faults).
|
||||
The NETWORKED path compiles + links (0 unresolved) but awaits its live
|
||||
test: a console + NetNub-TSR session (net_loop-style conf) driving OUR
|
||||
BTL4REC — the console handshake (egg → Ack → connections → LoadMission),
|
||||
a two-pod mesh, and mission teardown back to the lobby. Two-agreeing-runs
|
||||
rule applies.
|
||||
Reference in New Issue
Block a user