RIO tap capture of a full TeslaConsole mission cycle: LampFloor 0x3E + entry cluster (0x16/0x17/0x1e) bright at game-ready (customer entry), off at mission drop, bright again at mission end (the state=2 hold), all swept off the moment StopMission arrives, then game exit. The egress window is purely the gap between mission end and the console's StopMission send (measured 3.4s with TeslaConsole vs Console 4.10's proper customer-exit delay) -- not a game timer, not the exit code. Confirms the operator's remembered sequence byte for byte and pins the TeslaSuite fix: a configurable delay before StopMission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
619 lines
35 KiB
Markdown
619 lines
35 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 — 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.
|
||
|
||
## MODERN CONSOLE MILESTONE (2026-07-09 late): TeslaSuite .NET console runs the pod, fully hands-free
|
||
|
||
With the host bridging recipe above, the modern .NET console connected
|
||
"near instantly", queued and ran a real 2-minute BT mission on the
|
||
DOSBox pod, and the mission exited ON TIME. Recycle test (wire-logged):
|
||
mission-end teardown (netnub FINs then RSTs the session -- a clean
|
||
close) -> console starts SYN-retrying 11s later -> rides through the
|
||
dead-air phase AND a connection-refused phase (netnub's stack answers
|
||
RST before BTL4OPT issues its TCP_LISTEN; the SBK upload sits in this
|
||
window) -> SYN-ACK the moment the listener opens. **54 seconds from
|
||
teardown to re-established session, zero operator interaction.**
|
||
Modern console + net_loop.conf pod = a fully autonomous mission loop;
|
||
the only operator action is queueing the next mission. (Console 4.10
|
||
by contrast needs an app relaunch per mission.)
|
||
|
||
## Post-mission egress hold — decoded live + from source (2026-07-09 late)
|
||
|
||
The period flow after a mission timer expires (user memory: "lights come
|
||
on so the customer can leave the cockpit, then the pod closes out"),
|
||
confirmed on the wire and in CODE/:
|
||
|
||
1. Game fades the view to the black hold (fog sweep to 0.01/0.05 rgb 0)
|
||
and sends the wrap-up to the console (ConsolePlayerMechScoreUpdate /
|
||
team scores / RankAndScore -- BTCNSL.CPP message set).
|
||
2. **Game commands the RIO egress lamps**: `T 84` = LampRequest on the
|
||
serial tap right at mission end. RIO SetLamp(lamp, state), states =
|
||
solid/flashSlow/flashMed/flashFast + two brightness channels
|
||
(L4RIO.HPP RIOCommand/LampState enums).
|
||
3. Game HOLDS (RIO polling continues, view stays black). Its 1/s
|
||
StateQuery->StateResponse exchange with the console flips a state
|
||
field to 2 (= mission-over/hold; it reads 0 in the ready phase).
|
||
Console polls per Stationary.ini loadQueryInterval=1.
|
||
4. **The console ends the hold: Application__StopMissionMessage carrying
|
||
an ExitCodeID** (APPMSG.HPP: console msg set = StateQuery/CheckLoad/
|
||
RunMission/StopMission/KeyCommand/Suspend/Resume/LoadMission/Abort).
|
||
ExitCodeID is a remote-ops menu: Abort, RunRedPlanet/BattleTech (+
|
||
SinglePlayer variants), test patterns, TestPlasmaDisplay, ResetRIO,
|
||
RunAudioTest, RunNortonDiskDoctor, CheckDiskUsage, Refresh*,
|
||
ChangeScreenMode, SoftwareReset, ClearCrashlog, KillSpoolFile,
|
||
RunCamera/MissionReview -- the game exits through netnub with that
|
||
code and the pod's GO.BAT-era loop dispatches it. Handler impl is
|
||
compiled-only (StopMissionMessageHandler declared in BTL4APP.HPP;
|
||
body not in the archive).
|
||
Observed: Console 4.10 closed the hold within seconds; the .NET
|
||
console left the pod holding for minutes until it sent its close.
|
||
The hold length is entirely the console's call = the customer-egress
|
||
window.
|
||
|
||
Also caught live: sliding the bridge seat trim far back (forward -10)
|
||
puts the eye inside the mech's hull meshes -- z-fighting reads as a
|
||
"smeared" render and the canopy cage disappears (you are behind it).
|
||
Not a render bug; trim back to the per-mech eye position (Mad Cat ~
|
||
-1.8) restores the view.
|
||
|
||
**MEASURED lamp choreography (2026-07-09 late, RIO tap, TeslaConsole
|
||
close):** `LampFloor = 0x3E` ("floor lamp (entry/exit)", L4CTRL.HPP;
|
||
lamps 0x16/0x17/0x1e form the entry cluster with it; 0x3D = Panic).
|
||
Timeline: game boot 98.9s -> floor+entry BRIGHT, all panel lamps dim
|
||
(ENTRY lighting -- pod lit while the customer climbs in); mission drop
|
||
120.7s -> entry lamps OFF; mission end 151.5s -> floor+entry BRIGHT
|
||
(the egress hold, StateResponse state=2); StopMission arrives 154.9s ->
|
||
full lamp sweep off, floor last, game exits. **Egress window = the gap
|
||
between mission end and the console's StopMission send** (TeslaConsole
|
||
= 3.4s; Console 4.10 waited a proper customer-exit delay) -- it is NOT
|
||
a game-side timer and does not appear to depend on the exit code. Lamp
|
||
state byte: flash bits0-1 (solid/slow/med/fast), ch1 bits2-3
|
||
(off/dim/bright), ch2 bits4-5. Extraction tool: scratchpad
|
||
lamp_read.py pattern (tap lines are one byte each).
|
||
|
||
## Open questions / notes
|
||
- Exact TCP listen port(s) — not in the source grep; get from NETNUB.EXE
|
||
or a capture at milestone 3.
|
||
- Does WATTCP need a real ARP peer for the gateway at boot, or does it
|
||
proceed with a static IP and only ARP on connect? Affects whether the
|
||
stand-in console must answer ARP for 200.0.0.1.
|
||
- `NETCLIENT=PNW` (PARAMETR.bat) selects Personal NetWare — file-server
|
||
side, not the game's TCP path; likely irrelevant to egg delivery and
|
||
can stay unloaded under emulation.
|
||
- RP uses the identical MUNGA net brick + its own WATTCP.CFG — everything
|
||
here carries over to Red Planet.
|