Files
TeslaRel410/emulator/NET-NOTES.md
T
CydandClaude Fable 5 384418f385 NET-NOTES: host-side console bridging WORKING (bridge IP + Npcap SendToRx)
Windows host apps can now talk to the pod directly: static 200.0.0.1/24
on the Network Bridge adapter plus the Npcap SendToRxAdapters registry
value for the bridge GUID (driver-start read, so bounce the npcap
service and relaunch the pod). Verified with a host TCP connect to
200.0.0.113:1501. Documented the trade (pod frames go up to the host
stack INSTEAD of out to the TAP members -- SheepShaver console is deaf
while set), the revert, and the future both-at-once fix (Npcap >=1.83
user-mode DLLs + a second per-handle MODE_SENDTORX send handle in the
fork's pcap backend). Groundwork for the TeslaSuite .NET console
driving the DOSBox pod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 10:44:45 -05:00

550 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.29.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.57.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.2Mac 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.29.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 <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`; 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 <VPX_DUMPDIR>\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.
## 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):
1. 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`
2. 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:
```
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
```
Value = semicolon-separated `\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. PROPER BOTH-AT-ONCE FIX (future): upgrade Npcap
user-mode DLLs to >=1.83 and patch the fork's pcap backend to mirror
every send onto a second handle set to per-handle MODE_SENDTORX --
frames then go out to the TAPs AND up to the host. Host
connect/disconnect churns netnub (exit + ~60s relaunch) -- expected.
## 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.