38 KiB
Networking subsystem — recon & bring-up plan
Goal: bring the pod network up under the emulator so a pod boots the
production way (netnub -f BTL4OPT, mission egg delivered over the
wire) instead of the -egg test.egg dev bypass. This is also the
substrate the ops-console port will plug into.
What the pod network actually is (recon 2026-07-04)
The stack, bottom to top:
- Ethernet NIC + Novell ODI (AUTOEXEC.BAT):
lsl(Link Support Layer) →lnepci(Lance/PCnet PCI ODI driver) →odipkt(Dan Lanciani's ODI→Packet-Driver shim). The ODI/Netware login side (IPXODI/VLM/NET LOGIN in STARTNET.BAT) is for file-server access; the game itself only needs the packet-driver interface odipkt exposes.NET.CFG: LNEPCI, FRAME Ethernet_II + 802.2. - WATTCP — the TCP/IP stack. Confirmed by
WATTCP.CFGinREL410/BT,REL410/RP, and per-podVGL_LABS/THISPOD:So each pod is a static host on an isolated 200.0.0.0/24 LAN; the ops console is almost certainly 200.0.0.1.my_ip = 200.0.0.113 netmask = 255.255.255.0 gateway = 200.0.0.1 nameserver = 200.0.0.1 - NetNub (
netnub.exe, real-mode) — launches the game as a child (netnub -f BTL4OPT) and is the network server for the protected-mode game. Interface (NetNub/NETNUB.HPP): a sharedNetcomstruct (version 11, 64KB buffer) + a software interrupt. The game sets a Function code (TCP_OPEN=3, TCP_LISTEN=4, TCP_CLOSE=5, RESOLVE_ADDRESS=6, CHECK_SOCKET=7, UDP_*, plus remote file OPEN/READ/WRITE/SEEK/CLOSE 12-19), copies the marked fields to real mode, INTs, copies back.tcp_Socketis ~4300 bytes = classic WATTCP. - L4NetworkManager (
L4NET.HPP/.TCP) — the game's net brick. The console is master and connects to the pods; the pod receivesReceiveEggFileMessage(the mission egg), repliesAcknowledgeEggFileMessage("connected, ready, send the next host"), and tracksHostConnected/HostDisconnected. If the console drops, the pod is built to auto-start anyway.
Topology to replicate:
[ops console 200.0.0.1] --TCP--> [pod 200.0.0.113] (+ more pods .114..)
(master, egg source) (listens, ACKs, runs mission)
Emulator enablers (already in the fork)
- NE2000 ISA NIC emulated (
hardware/ne2000.cpp, Bochs-derived); config[ne2000] ne2000=true, nicbase=, nicirq=, macaddr=, backend=. - Two Ethernet backends built:
misc/ethernet_pcap.cpp(bridge to a host NIC via npcap) andmisc/ethernet_slirp.cpp(user-mode virtual net / NAT). Plusethernet_nothing.
Key simplification: the emulated card is an NE2000, not a PCI Lance, so
lnepci won't bind. We don't need the Novell ODI chain at all — WATTCP
finds a packet driver by scanning INT 0x60-0x80 for the PKT DRVR
signature, so we load a generic NE2000 packet driver (Crynwr
NE2000.COM) directly against the emulated card's base/IRQ. That drops
lsl/lnepci/odipkt/VLM entirely and hands NetNub/WATTCP the packet
interface they expect.
Bring-up plan
Backend choice. Two viable paths:
- pcap + host-only adapter (recommended, matches real topology): bridge the NE2000 to a host virtual switch; run the pod at 200.0.0.113 and the stand-in console at 200.0.0.1 on that segment. WATTCP's static IP + LAN assumptions hold exactly; the console connects inbound to the pod with no NAT. Cost: npcap + a host-only/loopback adapter + admin.
- slirp (fallback, self-contained): no host NIC/admin, but it's NAT and defaults to 10.0.2.0/24 — the pod LISTENS, so inbound needs slirp host-forwarding and a guest-network/IP reconciliation with WATTCP's hard-coded 200.0.0.113. Investigate whether DOSBox-X slirp allows the custom net + static guest IP + inbound forward cleanly.
Milestones
- NIC up:
[ne2000]on, NE2000.COM packet driver loaded, WATTCP/ NetNub start clean; pod boots vianetnub -f BTL4OPT(no-egg) and sits waiting for the console. Verify NetNub reports net address 200.0.0.113 and a TCP_LISTEN is queued. (New scratch conf, mirror the RIO/sound conf pattern.) - L3/L4 reachability: from the host segment, confirm the pod answers ARP/ping at 200.0.0.113 and a raw TCP connect to its listen port completes (proves NE2000↔backend↔host path end-to-end).
- Decode the console→pod egg protocol:
NetworkPacketHeader+ message framing fromnetwork.hpp/hostmgr.hpp+ the ReceiveEggFileMessage layout, cross-checked with a live capture of the pod's listen/ACK. (Pin the listen port here — not yet found in source; grep NETNUB.EXE strings / capture.) - Eggs over the wire: a minimal host-side stand-in console
(Python) connects to the pod, pushes a mission egg, handles the ACK →
pod runs the mission with no
-eggbypass. This is the headline goal. - (later, joins the console-port workstream): replace the stand-in with the ported/emulated Mac ops console; multi-pod coordination (HostConnected/Disconnected, mission review, camera ship).
Milestone 1 — DONE (2026-07-04): pod boots on the network path
Verified end to end under DOSBox-X (slirp backend), no -egg bypass:
[ne2000] ne2000=true, nicbase=300, nicirq=3, backend=slirp→ NE2000 emulated at Base=0x300 irq=3; slirp 4.9.1 initialized.- The Novell ODI chain works against the emulated NE2000, no external
packet driver needed:
lsl→ne2000(Novell/Eagle NE2000 MLID v1.53, from NWCLIENT) →odipkt(FTP Software ODI packet driver). ODIPKT installed at SINT 0x60, MLID NE2000, MAC CE:3D:72:67:38:69, frames Ethernet_II (board 1) + 802.2 (board 2). - GOTCHA: the ODI tools read
NET.CFGfrom the directory the.COMloads from, and the stockNWCLIENT\NET.CFGsaysLink Driver LNEPCI— with no NE2000 section the MLID defaults to 802.2-only and odipkt fails ("An MLID could not be found"). Fix without touching ALPHA_1: keep an emulatorNET.CFG(Link Driver NE2000+FRAME Ethernet_II) beside copies of lsl/ne2000/odipkt on a scratch drive and load from there. netnub -f btl4opt(no egg) launches the game asbtl4opt -net 250224, sets up the game↔netnub channel at INT 0x61 (separate from odipkt's 0x60), initializes the network manager ("Changing blocking from 0 to 1"), and the game boots through the VPX handshake to an open (blank) render window — waiting for a console to deliver a mission egg.
Working scratch files: scratchpad/net_stageB.conf,
scratchpad/net/{NET.CFG,LSL.COM,NE2000.COM,ODIPKT.COM}. Launch env:
VPXLOG + VPX_RESPOND=1 + VPX_RENDER=1 (VPX board must answer or the game
exits before networking).
Console side: a Mac emulator stands in for the ops console (user, 2026-07-04)
The user is building a Mac emulator running the real 410console as the
console peer (instead of a from-scratch Python stand-in). This merges the
networking and console-port workstreams: the real console software will
connect to the pod and push eggs. Implication for topology — two separate
emulators (DOSBox pod + Mac console) must share an L2 segment, which slirp
(NAT, per-process isolation) cannot bridge. Plan: move the pod's NE2000
to backend=pcap on a host-only/loopback adapter; bridge the Mac emulator
to the same adapter; pod=200.0.0.113, console=200.0.0.1 on 200.0.0.0/24.
Then milestone 3 (protocol) can be captured live from the real console
traffic rather than reverse-engineered blind.
Console emulator = SheepShaver (2026-07-04)
The real ops console is a Power Macintosh 6100/66 (PowerPC 601) → emulate with SheepShaver (PPC Mac, Mac OS 7.5.2–9.0.4). Basilisk II (68k) is out. GOTCHA: the 6100's OWN ROM does NOT work in SheepShaver ("Unsupported ROM type" — SheepShaver emulates a PCI 9500; the 6100 is NuBus). Use a compatible old-world PPC ROM instead (7100/66, 7500, 7600, or 8500) — PPC apps are Toolbox/OS-based, not ROM-specific, so the 410console app runs regardless. Target Mac OS 7.5.5–7.6.1 (console era). Networking: SheepShaver TAP ↔ DOSBox-X NE2000 pcap, both bridged to a host adapter on 200.0.0.0/24 (console .1, pod .113).
Console 4.10 OS compatibility (from the binary, 2026-07-04): classic
PowerPC PowerPlant/CodeWarrior app; PEF imports are only InterfaceLib,
MathLib, ObjectSupportLib. NO AppearanceLib (⇒ does NOT need Mac OS
8) and NO Open Transport libs (⇒ classic MacTCP API). Resource fork
has the startup check "TCP/IP not installed. Install either MacTCP or
OpenTransport…" via Gestalt/SysEnvirons — so it accepts either stack.
→ Runs System 7.1.2–Mac OS 9.0.4; practical floor 7.5 (ObjectSupportLib
ships with 7.5). Recommended SheepShaver target: System 7.5.5 / 7.6.1
(period-correct for a 6100, includes Open Transport 1.1.x so the TCP/IP
Gestalt check passes, most stable under SheepShaver w/ a 7100/7500/8500
old-world ROM). Set TCP/IP control panel to 200.0.0.1.
Leverage the real 6100 (user has it): (1) image its hard drive → get the exact console software + OS + MacTCP/OpenTransport config → drop into SheepShaver for a faithful reproducible console; (2) fastest path to a first egg + LIVE protocol capture = put the real 6100 on a physical Ethernet with the pcap-bridged pod (needs an AAUI→RJ45 transceiver) and capture the console→pod egg exchange off the wire (hands us milestone 3). Sequence: real 6100 first (seeds image + capture) → SheepShaver as the archival console built from that image.
Console software EXTRACTED + protocol port FOUND (2026-07-04)
The user unstuffed 410consoleArchive.sit (via infinitemac.org) to
4_10extractedConsole/. Contents: Console 4.10 (the app), per-site
config, fonts, logs. This means the dead 6100 is NOT a blocker — we have
the console software directly; run it in SheepShaver (no disk image
needed; fresh Mac OS 7.6.1/8.1 + MacTCP set to 200.0.0.1).
- App is PowerPC — data fork magic
Joy!peffpwpc(PEF/PowerPC), Metrowerks CodeWarrior 1993-95; 3.4MB resource fork. Confirms SheepShaver (needs standard shared libs: InterfaceLib/MathLib, present in any 7.5+ install). Console.iniis the master config:[NetworkEndpoint::Cockpit::*]sections define every pod. THE TCP PORT IS 1501 (defaultPort/localHostPort) — this answers the milestone-3 "listen port unknown" question. Console connects to each pod IP:1501; pod LISTENS on 1501.
Cockpit endpoint roster (base Console.ini) — our emulated pod = "Puck" 200.0.0.113:
| cockpit | IP | hostType |
|---|---|---|
| Frequent Flyer | 200.0.0.11 (sic) | 0 |
| Privateer | 200.0.0.112 | 0 |
| Puck | 200.0.0.113 | 0 |
| Carpe Diem | 200.0.0.114 | 0 |
| Man O' War | 200.0.0.115 | 0 |
| Divine Wind | 200.0.0.116 | 0 |
| Icarus | 200.0.0.117 | 0 |
| Gypsy | 200.0.0.118 | 0 |
| Alpha Mission Review | 200.0.0.119 | 2 |
| Alpha Camera | 200.0.0.120 | 2 |
hostType 0 = playable cockpit, 2 = special (mission-review / camera ship).
ini Folder/ holds real per-venue configs (DBAtlanta/Chicago/Houston/
Toronto/LaZerPark/... — the actual VWE centers), same .11x/1501 scheme.
Revised topology: SheepShaver console @200.0.0.1 → TCP 200.0.0.113:1501 → pod "Puck". Remaining protocol unknown is just the on-stream message framing (NetworkPacketHeader + ReceiveEggFileMessage) — capture it once the console connects, or read it from network.hpp.
Egg-delivery protocol — decoded from source (2026-07-04)
Decoded from CODE/RP/MUNGA/{NETWORK,RECEIVER,HOSTID}.HPP +
MUNGA_L4/L4NET.HPP. The real send/receive implementation (framing on
the byte stream) did NOT survive in the archive — only headers + a test
harness (L4NET.TCP's TestClass, #if 0). So the logical message
layout below is solid; two low-level details (stream framing + endianness)
need a live capture or a binary disasm to pin — see caveats.
Transport: console → TCP connect to pod IP : 1501 → the pod
(NetNub TCP_LISTEN) accepts. All base types are 32-bit
(Enumeration=int, size_t, LWord, Time::ticks=long → 4 bytes each).
On-wire unit = NetworkPacket = NetworkPacketHeader + a Receiver::Message.
NetworkPacketHeader (16 bytes):
| off | field | type |
|---|---|---|
| 0 | clientID | ClientID enum (0=NetworkMgr,2=HostMgr,5=Console...) |
| 4 | gameID | Enumeration |
| 8 | fromHost | HostID (Enumeration) |
| 12 | timeStamp | Time (long ticks) |
Receiver::Message header (12 bytes) that every message starts with:
| off | field | type |
|---|---|---|
| 0 | messageLength | size_t (= sizeof the whole message) |
| 4 | messageID | Enumeration (ReceiveEggFileMessageID etc.) |
| 8 | messageFlags | LWord (bit0 ReliableFlag=1) |
ReceiveEggFileMessage (the egg carrier; messageLength = 1024):
| off | field | type |
|---|---|---|
| 0 | (Receiver::Message header) | 12 B |
| 12 | sequenceNumber | int (chunk index) |
| 16 | notationFileLength | int (total egg size) |
| 20 | thisMessageLength | int (bytes valid in this chunk, ≤1000) |
| 24 | notationData[1000] | char (the egg chunk) |
So a full egg packet on the wire = 16 (header) + 1024 (message) = 1040
bytes — matches the nb≤1040 payload cap seen on the VPX/iserver link.
Egg-delivery algorithm: the console splits the mission egg (a text
"notation file", same INI/notation format as Console.ini) into
ceil(len/1000) chunks; sends each as a ReceiveEggFileMessage with
sequenceNumber 0..N, notationFileLength=total, thisMessageLength≤1000. The
pod's ReceiveEggFileMessageHandler reassembles into eggTempBuffer by
sequence, and when notationFileLength bytes have arrived, parses it as
the mission notation file. Pod replies AcknowledgeEggFileMessage
("connected, ready, next host"). messageID values start at
NetworkClient::NextMessageID; ReceiveEggFileMessageID is the first
NetworkManager message ID.
Caveats to confirm with the first live capture (or a binary disasm of Console 4.10 send / BTL4OPT receive):
- Stream framing: whether each 1040-B NetworkPacket is one discrete
NetNub/WATTCP record, or the receiver frames within the TCP stream via
the leading
messageLength. (NetNubRECEIVE_PACKETreturns up to 1600 B; MAX aligns with one packet.) - Endianness: console is big-endian PPC, pod is little-endian
x86 — the multi-byte header/length ints must be byte-swapped by one
side (or sent in network order).
notationData(egg text) is endian-agnostic. The capture will show which order the length fields use; a stand-in sender must match it.
Everything needed to PARSE a capture and BUILD a stand-in egg-sender is here except those two bytes-on-the-wire details, which the console-connect milestone resolves immediately.
Console VM standup (user, 2026-07-04): SheepShaver + OS 9.0.4 + old-world ROM
Valid combo (old-world ROMs support the full 7.5.2–9.0.4 range); OS 9's
Open Transport provides the MacTCP-API compat the app's Gestalt check
wants, so Console 4.10 runs. Networking initially set to "Basilisk II
Router" — NOTE that's NAT (assigns a 10.x IP, routes to host), fine
for building/updating the VM but it CANNOT reach the pod at 200.0.0.113.
For the console↔pod link both must be bridged (TAP) on one L2 segment:
console SheepShaver TCP/IP = manual 200.0.0.1 / 255.255.255.0 on a TAP;
pod DOSBox-X moves off backend=slirp to backend=pcap bound to the
same TAP/bridge (200.0.0.113 already via WATTCP). Then the console's
outbound TCP to 200.0.0.113:1501 reaches the pod and we capture the egg
exchange (resolves the framing + endianness caveats above).
MILESTONE: real console <-> real pod talking over the wire (2026-07-05)
The emulated SheepShaver console and the emulated DOSBox pod now exchange the live console protocol on TCP 1501. Hard-won setup (all required):
- DOSBox-X pcap backend had to be rebuilt + npcap DLL path. config.h
had
C_PCAP 1but the staleethernet.opredated it; force-recompile (rm src/misc/ethernet.o ethernet_pcap.o; make). Runtime: npcap installs its DLLs inC:\Windows\System32\Npcap\(npcap-only mode), NOT System32, so DOSBox couldn't load wpcap.dll -> launch dosbox-x.exe withC:\Windows\System32\Npcapprepended to PATH. - Two-TAP Windows bridge (single shared TAP fails: SheepShaver holds the
user-mode handle, DOSBox rides NDIS, frames don't cross). Console
SheepShaver -> TAP1 (
ether tap+ etherguid); pod -> TAP2; both bridged. - TAP2 media status = Always-Connected (registry MediaStatus=1 on the TAP2 class key, needs elevation; the GUI toggle didn't persist). Without it TAP2 reports "unplugged" and the bridge won't forward to it.
- Pod pcap binds to the BRIDGE MINIPORT, not a member TAP. Injecting on
a bridge member (TAP2) isn't re-forwarded by the bridge (the pod TX'd
but the console never saw it). Bind DOSBox
realnicto the bridge miniport = "Microsoft Network Adapter Multiplexor Driver". GOTCHA: DOSBoxrealnicmatches a substring of the pcap device NAME (\Device\NPF_{GUID}) and parses a leading-digit value as an interface index -- the bridge GUID5DB5521D...starts with 5 -> picked iface #5 (Bluetooth!). Use a letter-leading fragment:realnic=DB5521D. - Diagnostics: added
PCAP TX/RXcounters toethernet_pcap.cpp(SendPacket/GetPackets) -- confirmed the pod WAS transmitting all along; the bridge forwarding was the only gap. Capture the wire withdumpcap -i <bridge#> -f "arp or tcp port 1501" -w cap.pcapng.
Verified protocol on the wire (little-endian, matches the source decode):
- Console->pod StateQuery (32B): hdr clientID=4(App) gameID=0 fromHost=1 ts=.. ; msg len=16 id=3 flags=0 ; body requestingHost=1.
- Pod->console StateResponse (40B): hdr clientID=5 (ConsoleClientID) gameID=0 fromHost=0 ts=.. ; msg len=24 id=1 flags=1 (Reliable) ; body respondingHostID=0, applicationState=1, application=1 (BTL4/BattleTech). (My earlier synthetic guess had clientID=4/flags=0/host=1/state=0 -- all wrong, which is why the real console rejected the stand-in. These are the correct values for pod_responder.py.)
- Console polls StateQuery ~every 2s; pod ACKs + StateResponse each time. Clean 3-way handshake; zero-length "dup ACKs" are benign keepalives.
OPEN: with this working, the console still hadn't enabled mission-send in
the stripped-down (keyboard-only, no-RIO, no-sound) pod boot -- retrying
with a full RIO+sound boot (net_full.conf) so the pod presents its normal
state (applicationState may change when fully booted). Need the console UI
readout to know what state it wants before it will send the mission egg.
EGG DELIVERED OVER THE WIRE (2026-07-05) — headline goal achieved
The real SheepShaver console sent a real mission egg to the real DOSBox pod over the emulated network, captured + reassembled byte-perfect:
- Console -> pod, TCP 1501, 8x ReceiveEggFileMessage chunks (1040-B
NetworkPackets each = 16 hdr + 1024 msg), 7514 bytes total, declared
notationFileLength=7514 (exact match). Reassembled with
net-tools/decode_egg.py; savednet-tools/captured_cavern_mission.egg. - The egg is a text NOTATION file (same format as Console.ini). Contents =
the operator's actual mission:
[mission] adventure=BattleTech map=cavern scenario=freeforall time=night weather=clear temperature=27 length=120;[pilots] pilot=200.0.0.113;[200.0.0.113] name=cyd vehicle=madcat experience=veteran badge=VGL dropzone=one color=Grey advancedDamage=1 role=Role::Default+ embedded pilot badge bitmaps + ordinal fonts. - Confirms the whole decoded protocol end to end: StateQuery/StateResponse
poll, then egg chunks, pod ACKs, no
-eggbypass. Networking DONE.
BUT the pod CRASHES loading the mission (game-side, NOT network):
nn.log shows Reference to a page you don't own / PF cr2=FF008B5B / Unhandled exception 000E at 00FF:219D ErrCode 0004 / NETNUB Munga exited code 14 -- a page fault (wild pointer 0xFF008B5B) in the mission-load path
right after the egg arrives. Suspects: (a) RIO was DISABLED for this run
(net_pcap.conf) and the mission sets up the player's vehicle (madcat) /
controls, which may deref a null RIO/control struct; (b) missing cavern
content or a pilot-config/badge path bug. Note: cavern/night via -egg test.egg with RIO ENABLED rendered fine earlier this session, so RIO-off
is the leading suspect. Real pods run RIO-on headless (no focus stealing),
so this may not reproduce there. NEXT: retry with RIO enabled + DOSBox
priority=highest,highest (so clicking the console can't starve the RIO's
ACK deadline during load); if it still faults, disassemble around game
code 00FF:219D. This joins the Division-renderer/crash workstream.
FULL NETWORKED MISSION WORKS END TO END (2026-07-05, later) — all fixed
A complete networked mission now runs start to finish: console queues a mission -> egg over the wire -> pod loads it -> RIO live -> all cockpit heads render -> mission ends on the console timer. Four fixes got here:
-
Mission-load crash was RIO-OFF (confirmed). Booting with the RIO enabled (
net_full.conf: serial1 COM1 rxpollus:100 rxburst:16, full RIO+sound production boot) — the page fault at 00FF:219D is gone and the mission loads + runs. Real pods run RIO-on, so this was the whole thing. -
NE2000
BX_PANICfixed in the emulator. A full production boot runs the packet driver's internal loopback self-test, which sets the NIC's TCR inhibit-CRC bit; Bochs'ne2000.cppBX_PANIC'd and aborted the whole emulator. Patched the TCR write handler to record crc_disable/ext_stoptx instead of panicking (harmless for an emulated NIC; pcap/host frames+CRCs). Committed copy:vpx-device/ne2000.cpp. -
Pod persistence via GO.BAT loop. When the console has no mission queued, the pod connects, sees nothing, and cleanly exits (not a crash) — the real pod's GO.BAT immediately relaunches netnub.
net_loop.conf+net-boot/loop.batreplicate that so the pod stays connected and ready. -
HEADLINE FIX — blank cockpit heads = NE2000/VDB I/O port collision. All 5 MFD + radar heads decoded to pure black during a live mission even though the DOSBox SVGA gauge framebuffer was FULL (captured mid-mission: SENSOR CLUSTER/MYOMERS/SRM 4/mech wireframes/pilot name). Root cause: the VDB video splitter board is hardwired at 0x300-0x31A (palettes 0x300/0x308/0x310;
VDB_BASEin vpxlog.cpp) and we'd put the NE2000 atnicbase=300— the NIC swallowed the game's VDB palette writes, sovdb_palstayed zero andpal_drawmapped every index to black. The VDB spam into 0x300-0x31A also corrupted NIC registers, dropping the console's EndMission -> the mission overran its timer (second symptom, same cause). FIX (config-only): move the NIC to 0x340 in the DOSBox conf (nicbase=340) AND the DOSNET.CFG(PORT 340) — both must agree; the game uses odipkt so it's base-agnostic. VDB keeps 0x300 (game hardwires it; real pod's NIC lived elsewhere for this reason). Non-networked gauge runs never hit this (no NIC). Verified: heads render perfectly (radar with contact blips + SPEED/HEADING/ARMOR + mission clock; MFDs full) AND the mission ends cleanly on the timer (Sending EndMission, score 1000). Diagnostic recipe:New-Item <VPX_DUMPDIR>\DUMPmid-mission dumps win0/3/4.bmp; PrintWindow (flag 2, GL) the DOSBox SVGA window to prove the framebuffer has content; framebuffer-full + heads-black => palette/port.
Committed: net_full.conf, net_loop.conf, net-boot/ (drivers, NET.CFG
@340, loop.bat, README), vpx-device/ne2000.cpp, vpx-device/ethernet_pcap.cpp.
Post-reboot outage forensics + IRQ 3 conflict (2026-07-08/09)
A host reboot took the console<->pod link down for hours; every layer got blamed before the real bug fell out. Findings, in the order they mattered:
- TAP-Windows V9 "Bytes received" is ALWAYS 0 -- the driver never counts frames its user-mode app (SheepShaver) injects. Do not diagnose from the adapter-status dialogs: read the bridge-miniport counters (they do count) or sniff the wire (Npcap + ctypes; see render-bridge/probes/ for the pattern). Bridge/TAPs/SheepShaver prefs were all healthy the whole time.
NO PACKET DRIVER FOUNDat netnub = wpcap.dll not on PATH of the shell that launched DOSBox. launch_pod.ps1 now prependsC:\Windows\System32\Npcapitself so no shell can reproduce this.- THE REAL BUG -- IRQ 3 double-booking: the plasma readout (serial2 =
host COM2, added 2026-07-07) sits on IRQ 3, and the NE2000 was also on
nicirq=3. The netnub phase works (COM2 not yet open: ARP answered, egg flows) -- then BTL4OPT opens the plasma port (arg4=p) and NIC RX dies for good. Looks exactly like a mid-boot wedge. Fix: NIC on IRQ 10 in net_full.conf / net_diag.conf / net_loop.conf AND net-boot/NET.CFG (INT 10) -- DOSBox device and ODI driver must agree. Verified: pod stayed ARP-responsive through two full missions with plasma live. - Console's live IP is 200.0.0.10 (not .1 as assumed from the gateway entry); it ARP-scans the cockpit roster slots (.11, .112-.120) continuously while looking for pods.
- Zombie console session between missions: netnub exits after each mission without closing TCP (no FIN -- the DOS stack just vanishes). Console 4.10 keeps waiting on the dead socket; with the pod sitting at a DOS pause for minutes, classic Mac TCP retransmit backoff grows to multi-minute intervals and the console looks permanently wedged (silent on the wire). Operator fix: relaunch the Console app (app only). Real fix: behave like retail -- GO.BAT relaunched netnub within seconds, so the console's early retransmissions hit the fresh stack and recover. net_loop.conf (now FULL production parity: sound + plasma + IRQ 10) is the conf to use for multi-mission sessions; verify console self-recovery with it next time.
- SoundFont upload measured (smldW device counters): the game blindly re-uploads the full SBK on EVERY netnub->game launch -- counter goes 3,513,581 -> exactly 7,027,162 per card, no validation read-back, so pre-loading the cards cannot short-circuit it. BUT the re-upload itself is only ~20-30s; the slow first boot is dominated by something else (pacer question still open). Loading a new egg into the already-running game does NOT re-upload.
- Manual mission-N start in the same DOSBox:
32rtm.exe -x(saying "resident" is fine) thennetnub -p -f btl4opt > nn.log; TSRs and SET env persist from the autoexec.
Console 4.10 between-missions behavior — characterized (2026-07-09 session 2)
Live A/B runs with the wire watcher (net_loop.conf pod, three missions):
- net_loop.conf VERIFIED: the pod is fully retail-faithful. netnub auto-relaunches after mission end AND after a console disconnect (both observed); full cycle back to ready ~60s including the ~30s SBK re-upload. Four unattended relaunches in one evening, zero touch.
- Console 4.10 endpoint dead-end — precisely characterized: after a mission ends, the console's TCP retries its old session, the fresh netnub answers FIN then RST (so the app's stack KNOWS the session died and a live pod is at ready) -- and the app never issues another query. Clicking around the UI (new mission, network toggles) does NOT re-arm it; only relaunching the app does (endpoints rebuild from the INI / stationery at launch). Stationary.ini = the master stationery: full 26-cockpit roster with per-endpoint params (openTimeout=10, queryTimeout=60, loadQueryInterval=1, inProgressQueryInterval=10).
- PRINTER: select one. With no Chooser selection the app stalls on Printing Manager calls (much slower launch; the old 'nilP' throw at BTFAConsoleDoc.cp:117 is doc-init printing). A serial printer spooled to file makes the app launch much faster. It does NOT fix the mission-end dead-end (no print job is even attempted at mission end -- spool stays empty).
- Operational rhythm that works: fly mission -> pod recycles itself -> relaunch the Console app (fast with a printer selected) -> select pod -> ready. Under two minutes of operator time per turnaround. Deep fix would be PPC reverse-engineering of the endpoint state machine; deferred -- the modernized .NET TeslaConsole is the long-term driver.
- Cosmetic: VDB head windows (pentapus MFDs/radar) go choppy while DOSBox is unfocused and smooth when focused; total CPU was 14% so it is not starvation -- DWM background-window throttling suspected. Backlog.
Modern console on the HOST — WORKING (2026-07-09, verified TCP connect)
Host (TeslaSuite .NET console) <-> pod on the same machine. Verified:
Test-NetConnection 200.0.0.113 -Port 1501 connects with source
200.0.0.1. Recipe (both steps elevated):
- Host IP on the bridge (no gateway -- only 200.0.0.x routing changes):
New-NetIPAddress -InterfaceAlias 'Network Bridge' -IPAddress 200.0.0.1 -PrefixLength 24 - Npcap SendToRx so pod (pcap-injected) frames reach the host stack --
without it the host's ARP gets no answer (neighbor stays Incomplete;
hairpin-tested: injected frames provably never reach the local
stack). The per-handle MODE_SENDTORX API needs Npcap >= 1.83
user-mode DLLs (our wpcap.dll/Packet.dll predate it; npcap.sys is
1.88 and has the registry side), so use the driver-level value:
Value = semicolon-separated
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\npcap\Parameters' ` -Name SendToRxAdapters -Value '\Device\{5DB5521D-2D56-40E8-9E3D-3B36C9EE7C8F}' -Type String Stop-Service npcap -Force; Start-Service npcap\Device\{bridge GUID}entries (parser: NPF_GetRegistryOption_AdapterName, npcap Packet.c); read ONCE at driver start (hence the service bounce); relaunch the pod after (its pcap handle dies with the driver).
THE TRADE: while SendToRxAdapters covers the bridge, ALL Npcap
injections on it are indicated as receives INSTEAD of transmits --
pod frames reach the host stack but no longer reach the TAP members, so
the SheepShaver Mac console is DEAF while this is set (and wire-
injection diagnostics can't reach the pod either; pod RX is unaffected).
Revert: Remove-ItemProperty ... -Name SendToRxAdapters + service
bounce + pod relaunch. Host connect/disconnect churns netnub (exit +
~60s relaunch) -- expected.
BOTH CONSOLES AT ONCE — WORKING (2026-07-10, net-tools/tap2_mirror.py):
keep SendToRxAdapters set (host console works) and run the mirror
daemon, which turns TAP2's free application side into a userspace
bridge port for the Mac: (a) pod-sourced frames destined for the Mac
MAC or broadcast are written into TAP2 -> Windows bridge forwards
member->member to TAP1 -> SheepShaver hears; (b) REQUIRED second
direction: writing pod-src frames into TAP2 teaches the bridge's MAC
table that the pod lives on TAP2, so the Mac's unicasts to the pod get
steered INTO TAP2 -- a reader thread drains them and re-injects on the
bridge (RX-indicated up under SendToRx = exactly where the pod's pcap
listens). Loop guards: pod-src frames never re-injected; 0.2s
frame-hash dedup on the mirror side. Verified live: Console 4.10
readied the pod through it (counters: mirrored=24 pod->Mac,
reinjected=105 Mac->pod) with the .NET console path intact. Run
detached: pythonw net-tools/tap2_mirror.py (log/pid in the vwe-pod
work dir; pod MAC + TAP2 GUID are constants at the top). Operate
ALTERNATING (one console readied at a time -- two masters vs one netnub
is untested). Cleaner future fix stands: Npcap >=1.83 user-mode DLLs +
dual-handle MODE_SENDTORX in the fork's pcap backend, which retires
both the registry setting and the mirror.
MODERN CONSOLE MILESTONE (2026-07-09 late): TeslaSuite .NET console runs the pod, fully hands-free
With the host bridging recipe above, the modern .NET console connected "near instantly", queued and ran a real 2-minute BT mission on the DOSBox pod, and the mission exited ON TIME. Recycle test (wire-logged): mission-end teardown (netnub FINs then RSTs the session -- a clean close) -> console starts SYN-retrying 11s later -> rides through the dead-air phase AND a connection-refused phase (netnub's stack answers RST before BTL4OPT issues its TCP_LISTEN; the SBK upload sits in this window) -> SYN-ACK the moment the listener opens. 54 seconds from teardown to re-established session, zero operator interaction. Modern console + net_loop.conf pod = a fully autonomous mission loop; the only operator action is queueing the next mission. (Console 4.10 by contrast needs an app relaunch per mission.)
Post-mission egress hold — decoded live + from source (2026-07-09 late)
The period flow after a mission timer expires (user memory: "lights come on so the customer can leave the cockpit, then the pod closes out"), confirmed on the wire and in CODE/:
- Game fades the view to the black hold (fog sweep to 0.01/0.05 rgb 0) and sends the wrap-up to the console (ConsolePlayerMechScoreUpdate / team scores / RankAndScore -- BTCNSL.CPP message set).
- Game commands the RIO egress lamps:
T 84= LampRequest on the serial tap right at mission end. RIO SetLamp(lamp, state), states = solid/flashSlow/flashMed/flashFast + two brightness channels (L4RIO.HPP RIOCommand/LampState enums). - Game HOLDS (RIO polling continues, view stays black). Its 1/s StateQuery->StateResponse exchange with the console flips a state field to 2 (= mission-over/hold; it reads 0 in the ready phase). Console polls per Stationary.ini loadQueryInterval=1.
- The console ends the hold: Application__StopMissionMessage carrying an ExitCodeID (APPMSG.HPP: console msg set = StateQuery/CheckLoad/ RunMission/StopMission/KeyCommand/Suspend/Resume/LoadMission/Abort). ExitCodeID is a remote-ops menu: Abort, RunRedPlanet/BattleTech (+ SinglePlayer variants), test patterns, TestPlasmaDisplay, ResetRIO, RunAudioTest, RunNortonDiskDoctor, CheckDiskUsage, Refresh*, ChangeScreenMode, SoftwareReset, ClearCrashlog, KillSpoolFile, RunCamera/MissionReview -- the game exits through netnub with that code and the pod's GO.BAT-era loop dispatches it. Handler impl is compiled-only (StopMissionMessageHandler declared in BTL4APP.HPP; body not in the archive). Observed: Console 4.10 closed the hold within seconds; the .NET console left the pod holding for minutes until it sent its close. The hold length is entirely the console's call = the customer-egress window.
Also caught live: sliding the bridge seat trim far back (forward -10) puts the eye inside the mech's hull meshes -- z-fighting reads as a "smeared" render and the canopy cage disappears (you are behind it). Not a render bug; trim back to the per-mech eye position (Mad Cat ~ -1.8) restores the view.
MEASURED lamp choreography (2026-07-09 late, RIO tap, TeslaConsole
close): LampFloor = 0x3E ("floor lamp (entry/exit)", L4CTRL.HPP;
lamps 0x16/0x17/0x1e form the entry cluster with it; 0x3D = Panic).
Timeline: game boot 98.9s -> floor+entry BRIGHT, all panel lamps dim
(ENTRY lighting -- pod lit while the customer climbs in); mission drop
120.7s -> entry lamps OFF; mission end 151.5s -> floor+entry BRIGHT
(the egress hold, StateResponse state=2); StopMission arrives 154.9s ->
full lamp sweep off, floor last, game exits. Egress window = the gap
between mission end and the console's StopMission send -- it is NOT
a game-side timer and does not appear to depend on the exit code.
MEASURED BOTH CONSOLES: TeslaConsole = 3.4s AND Console 4.10 = 3.4s
(lamp-ON to full-sweep-off). No egress-delay key exists in
Console.ini / venue INIs (checked).
EGRESS DELAY = COMPILED IN, TWO-PHASE (2026-07-10 disasm; cross-build verified) -- NOT an INI/env value, and NOT a Rel410 regression. The StopMission close path in BTL4OPT.EXE uses nested class handlers:
- OUTER handler (runs for the pod, Rel410 @0x47c2c4) is re-entrant on a
flag @[this+0xd4]: FIRST call schedules ITSELF to re-fire in
3.0*timebase + 0.5≈ 3 seconds (3.0f @0x47c350) and returns; the RE-ENTRY (3s later) runs cleanup (0x44e13c/0x44fa6c) then calls the inner handler. - INNER handler (L4Application, Rel410 @0x47b864) calls base shutdown (0x44eeb4), lights egress lamps 0x16/0x17/0x1e ON (0x47bba8 flag=1, the "LightsOut"-named routine @0x4fd5ac), and schedules a 30.0s timer (30.0f @0x47b8e4) for lamps-off/exit.
- timebase global @0x52140c (runtime-set, read sim-wide); ftol @0x4dcd94.
CROSS-BUILD: byte-identical in ALL four binaries -- BTLIVE (May-96, handlers @0x489a08/0x48a3c0), BTRAVINE (Sep-96, @0x47b850/0x47c2b0), BTDAVE (@0x489a08/0x48a3c0), Rel410 (@0x47b864/0x47c2c4). So Rel410 did NOT cut a 30s hold; every build has both the 3.0 and 30.0 constants.
EFFECTIVE behavior = the 3s phase (matches the measured 3.4s
lamp-on->off exactly). Step 0 diagnostic (net_full single-shot, no
loop, 2026-07-10) PROVED this is the game's own behavior, not a
GO.BAT-loop artifact: with only a DOS pause waiting, the floor lamp
held 3.9s (mission-end 108.8s -> off 112.7s) and btl4opt EXITED itself
to COMMAND. The process exits AT the stage-1 (3.0f) timer; the deeper
30s timer is never reached. The operator's remembered ~20-30s "lights
on" is the ENTRY lighting (floor lamp on game-ready->drop, measured
15.9-21s across runs) plus the between-games boot/idle gap, NOT the
post-mission egress phase. Tools: scratchpad btdis2.py +
stopmission_cmp3.py + lamp_read.py (pefile+capstone, py -3.13).
ISSUE CLOSED 2026-07-10 (WON'T-FIX, operator decision): the floor egress lights are a curiosity only -- no surviving Tesla cockpit still has the floor lamps installed, so reproducing the ~30s hold has no restoration value. The ~3s behavior is authentic and left as-is. Full findings + a ready (unplanned) Step 1 patch recipe in emulator/EGRESS-HOLD-FIX-PLAN.md. Supersedes the earlier "3.4s = print time" AND "authentic 30s hold" readings; not revisiting. Lamp state byte: flash bits0-1 (solid/slow/med/fast), ch1 bits2-3 (off/dim/bright), ch2 bits4-5. Extraction tool: scratchpad lamp_read.py pattern (tap lines are one byte each).
Open questions / notes
- Exact TCP listen port(s) — not in the source grep; get from NETNUB.EXE or a capture at milestone 3.
- Does WATTCP need a real ARP peer for the gateway at boot, or does it proceed with a static IP and only ARP on connect? Affects whether the stand-in console must answer ARP for 200.0.0.1.
NETCLIENT=PNW(PARAMETR.bat) selects Personal NetWare — file-server side, not the game's TCP path; likely irrelevant to egg delivery and can stay unloaded under emulation.- RP uses the identical MUNGA net brick + its own WATTCP.CFG — everything here carries over to Red Planet.