The plasma serial2 addition put COM2 (IRQ 3) in conflict with nicirq=3: netnub phase worked, then BTL4OPT opened the plasma port and NIC RX died, so the pod never checked in to the console. NIC now on IRQ 10 in all net confs + NET.CFG INT 10 (both sides must agree). Verified live: two full console missions with plasma running. Also: launch_pod.ps1 prepends the Npcap dir to PATH itself (a bare shell gave "NO PACKET DRIVER FOUND"); net_diag.conf = packet-stack-only boot with the NCSA Telnet suite on E: for DOS-side TCP tests; net_loop.conf promoted to full production parity (sound + plasma + IRQ 10) -- the GO.BAT-style instant netnub relaunch is also what lets the console recover its zombie TCP session between missions (see NET-NOTES). NET-NOTES: full 7/08-09 forensics writeup (TAP RX counter lie, console IP .10, SBK re-upload measured, zombie-session mechanics). LAST.EGG = the operator's missions from tonight's runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
479 lines
27 KiB
Markdown
479 lines
27 KiB
Markdown
# 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 <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.
|
||
|
||
## 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.
|