Three-mission live session on net_loop.conf: the pod side is fully retail-faithful (netnub auto-relaunches on mission end AND console disconnect, ~60s cycle). Console 4.10's between-missions dead-end is app-level: its TCP receives FIN+RST from the fresh netnub yet the app never re-queries; only an app relaunch (endpoints rebuilt from the INI) recovers it. Selecting a printer in the Chooser makes the app launch much faster (Printing Manager stalls without one) but does not change the dead-end. Documented the working operator rhythm and the plan for bridging the host-side .NET console onto the pod LAN (bridge adapter IP 200.0.0.1/24, possible Npcap SendToRx for pod->host hairpin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
525 lines
29 KiB
Markdown
525 lines
29 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.
|
||
|
||
## 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 — bridging plan (2026-07-09, in progress)
|
||
|
||
To let a Windows-host app (TeslaSuite .NET console) talk to the pod:
|
||
give the **Network Bridge adapter** a static host IP `200.0.0.1/24`
|
||
(no gateway; only 200.0.0.x routing changes). Host->pod already proven
|
||
(host-stack frames appear in pod-side pcap). Pod->host may need Npcap's
|
||
**SendToRx** per-adapter option (pcap-injected frames are normally not
|
||
indicated up to the host stack on the same adapter -- the classic
|
||
DOSBox-pcap guest<->host limitation); verify with a TCP connect to
|
||
200.0.0.113:1501 and add the option only if needed. Note: a 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.
|