Decoded the console<->pod message layout from the MUNGA framework headers (the send/recv impl did not survive -- only headers + a test harness): NetworkPacket = 16B NetworkPacketHeader + a Receiver::Message; the egg is delivered as ReceiveEggFileMessage chunks (seq/totalLen/thisLen + notationData[1000]), a full egg packet = 1040B (matches the VPX nb<=1040 cap). Console splits the mission notation file into <=1000B chunks; pod reassembles + ACKs. Two bytes-on-the-wire details (stream framing + PPC/x86 endianness) flagged for the first live capture. Full byte tables in NET-NOTES.md. Also moves the extracted console software (Console 4.10 PPC app + per-venue Console.ini + fonts/logs, resource forks preserved) into 410console/4_10-console-extracted/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
280 lines
14 KiB
Markdown
280 lines
14 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).
|
||
|
||
**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.
|
||
|
||
## 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.
|