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>
14 KiB
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:
- 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. - WATTCP — the TCP/IP stack. Confirmed by
WATTCP.CFGinREL410/BT,REL410/RP, and per-podVGL_LABS/THISPOD: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.my_ip = 200.0.0.113 netmask = 255.255.255.0 gateway = 200.0.0.1 nameserver = 200.0.0.1 - 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 sharedNetcomstruct (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_Socketis ~4300 bytes = classic WATTCP. - L4NetworkManager (
L4NET.HPP/.TCP) — the game's net brick. The console is master and connects to the pods; the pod receivesReceiveEggFileMessage(the mission egg), repliesAcknowledgeEggFileMessage("connected, ready, send the next host"), and tracksHostConnected/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) andmisc/ethernet_slirp.cpp(user-mode virtual net / NAT). Plusethernet_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
- NIC up:
[ne2000]on, NE2000.COM packet driver loaded, WATTCP/ NetNub start clean; pod boots vianetnub -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.) - 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).
- Decode the console→pod egg protocol:
NetworkPacketHeader+ message framing fromnetwork.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.) - 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
-eggbypass. This is the headline goal. - (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.CFGfrom the directory the.COMloads from, and the stockNWCLIENT\NET.CFGsaysLink 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 emulatorNET.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 asbtl4opt -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.iniis 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):
- 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. (NetNubRECEIVE_PACKETreturns up to 1600 B; MAX aligns with one packet.) - 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.