# 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: 1. **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. 2. **WATTCP** — the TCP/IP stack. Confirmed by `WATTCP.CFG` in `REL410/BT`, `REL410/RP`, and per-pod `VGL_LABS/THISPOD`: ``` my_ip = 200.0.0.113 netmask = 255.255.255.0 gateway = 200.0.0.1 nameserver = 200.0.0.1 ``` 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**. 3. **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 shared `Netcom` struct (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_Socket` is ~4300 bytes = classic WATTCP. 4. **L4NetworkManager** (`L4NET.HPP/.TCP`) — the game's net brick. The **console is master and connects to the pods**; the pod receives `ReceiveEggFileMessage` (the mission egg), replies `AcknowledgeEggFileMessage` ("connected, ready, send the next host"), and tracks `HostConnected/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) and `misc/ethernet_slirp.cpp` (user-mode virtual net / NAT). Plus `ethernet_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** 1. **NIC up**: `[ne2000]` on, NE2000.COM packet driver loaded, WATTCP/ NetNub start clean; pod boots via `netnub -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.) 2. **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). 3. **Decode the console→pod egg protocol**: `NetworkPacketHeader` + message framing from `network.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.) 4. **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 `-egg` bypass. **This is the headline goal.** 5. *(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.CFG` from the directory the `.COM` loads *from*, and the stock `NWCLIENT\NET.CFG` says `Link 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 emulator `NET.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 as `btl4opt -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.ini`** is 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):** 1. *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`. (NetNub `RECEIVE_PACKET` returns up to 1600 B; MAX aligns with one packet.) 2. *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): 1. **DOSBox-X pcap backend had to be rebuilt + npcap DLL path.** config.h had `C_PCAP 1` but the stale `ethernet.o` predated it; force-recompile (`rm src/misc/ethernet.o ethernet_pcap.o; make`). Runtime: npcap installs its DLLs in `C:\Windows\System32\Npcap\` (npcap-only mode), NOT System32, so DOSBox couldn't load wpcap.dll -> **launch dosbox-x.exe with `C:\Windows\System32\Npcap` prepended to PATH.** 2. **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. 3. **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. 4. **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 `realnic` to the bridge miniport = "Microsoft Network Adapter Multiplexor Driver". GOTCHA: DOSBox `realnic` matches a substring of the pcap device NAME (`\Device\NPF_{GUID}`) and parses a *leading-digit* value as an interface index -- the bridge GUID `5DB5521D...` starts with 5 -> picked iface #5 (Bluetooth!). Use a letter-leading fragment: `realnic=DB5521D`. 5. Diagnostics: added `PCAP TX/RX` counters to `ethernet_pcap.cpp` (SendPacket/GetPackets) -- confirmed the pod WAS transmitting all along; the bridge forwarding was the only gap. Capture the wire with `dumpcap -i -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`; saved `net-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 `-egg` bypass. **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: 1. **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. 2. **NE2000 `BX_PANIC` fixed 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.cpp` `BX_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`. 3. **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.bat` replicate that so the pod stays connected and ready. 4. **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_BASE` in vpxlog.cpp) and we'd put the NE2000 at `nicbase=300` — the NIC swallowed the game's VDB palette writes, so `vdb_pal` stayed zero and `pal_draw` mapped 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 DOS `NET.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 \DUMP` mid-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 FOUND` at netnub = wpcap.dll not on PATH** of the shell that launched DOSBox. launch_pod.ps1 now prepends `C:\Windows\System32\Npcap` itself 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) then `netnub -p -f btl4opt > nn.log`; TSRs and SET env persist from the autoexec. ## 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.