diff --git a/.gitignore b/.gitignore index 7ada507..33d4ace 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ Thumbs.db desktop.ini .DS_Store +__pycache__/ + +# 410console hand-off: the docs/spec/reference/tools are tracked; the raw Mac +# archive and its extraction are not (may be committed later for completeness) +/410console/410consoleArchive.sit +/410console/4_10-console-extracted/ +/410console/.finf/ diff --git a/410console/CONSOLE-4.10-DECOMP.md b/410console/CONSOLE-4.10-DECOMP.md new file mode 100644 index 0000000..f381624 --- /dev/null +++ b/410console/CONSOLE-4.10-DECOMP.md @@ -0,0 +1,554 @@ +# Console 4.10 — Decompilation & Documentation + +Reverse-engineering notes for the classic Mac OS **operator console** that +commanded the networked Virtual World Entertainment (VWE) BattleTech / Red +Planet pods. Part of the VWE Tesla restoration project. + +- **Target dir:** `c:\VWE\TeslaRel410\410console\4_10-console-extracted\` +- **Binary:** `Console 4.10` (728,064 bytes) — classic Mac OS **fat application** + (68K `CODE` resources **+** PowerPC **PEF/CFM** fragment in the data fork). +- **Internal identity:** type `APPL`, creator **`vwe4`**, version resource + **`2.4.0b3` / "Console 2.4.0b3", © 1995–1997 FASA Interactive, Inc.** + (The "4.10" in the folder name is the site/config release, not the app's own + `vers`.) +- **Analysis method:** custom Python PEF/PPC + resource-fork tooling (no Ghidra + on this machine); capstone in PPC big-endian mode. See `410console/tools/`. +- **Status:** read-only analysis. Nothing in the target was modified. + +--- + +## 0. TL;DR — what the console does + +The console is a **document-based Mac app** (Metrowerks **PowerPlant** framework, +CodeWarrior C++). Each open document is a **Mission**. The operator picks an +*Adventure* (Solo/Team × BattleTech/Red Planet), a *Location* (arena) and a +*Scenario*, assigns *pilots/vehicles*, then drives a fleet of **cockpit pods** +through a mission lifecycle. It talks to two networks: + +1. **Pod command/control** — one **MacTCP TCP** stream per pod on **port 1501** + (console `200.0.0.1`, pods `200.0.0.11`–`200.0.0.120`). This is the protocol + that matters for the emulator. Commands (Load / Translocate / Stop / End / + Reset) go out as small binary packets; pods stream back a one-byte + **Cockpit State** (0–12) that the console records in `Console Log`. +2. **4th Dimension (4D) database** — a second client link to a 4D server for the + **pilot roster and mission scheduling** ("Get Next Departure", Add/Delete/ + Lookup Pilot, scoring). Evidenced by the `_4D_Select4DServer` / + `_4D_Find4DServer` error strings; the wire protocol for 4D was **not** + reverse-engineered (it is ACI's proprietary 4D client library). + +The rest of this document backs every claim above with offsets, string refs, +resource dumps and log excerpts. + +--- + +## 1. File inventory & formats + +| File | Size | Format / role | +|---|---|---| +| `Console 4.10` | 728 KB | The application. Fat binary: PEF/PPC in data fork, 68K `CODE` + all UI resources in the resource fork (see §5). | +| `Console Log` | 5.3 MB | Runtime log, CR-terminated records. Ground truth for the pod protocol (§7). | +| `Console.ini` | 92 KB | Live config: 586 `::`-namespaced sections — pods, adventures, locations, scenarios, vehicles (§8). | +| `Console.ini copy` | 91 KB | Prior copy of the above. | +| `Stationary.ini` | 98 KB | The **stationery/template** master config (Mac "stationery" = template document) used to seed a fresh `Console.ini`. | +| `ini Folder/DB/` | — | Per-venue config overrides: Atlanta, Chicago, Cincinnati, Denver, Detroit, Houston, Miami, Ontario, Pittsburgh, San Jose, Toronto, LaZerPark. | +| `ConsoleSorter/` + `Console Log Sorter`, `Console Log.rpl` | 0 B here | A companion log-sorter tool (data forks empty in this extraction; only `Instructions` survived). | +| `FRC.rsrc`, `KR.rsrc`, `US.rsrc` | 225 B each | Tiny locale resource stubs (French / Korean / US). | +| `Fonts/` (`Killmark`, `Stencil`) | 0 B | Bitmap/suitcase fonts for kill-marker/stencil display (data forks empty here). | +| `PlainText16 Folder/` | 173 KB | `PlainText` support (text traits). | +| `.finf/` | 32 B each | Mac Finder-info sidecars from extraction (type/creator). | +| `__MACOSX/._*` | — | **AppleDouble sidecars — these carry the resource forks.** `._Console 4.10` is 3.38 MB and holds the app's entire resource fork (§5). | + +**Resource-fork location:** the app's resource fork was preserved inside +`__MACOSX/._Console 4.10` (AppleDouble magic `0x00051607`, entry id 2 = +resource fork at file offset `0x52`, length `0x3391CA`). Finder info at offset +`0x32` reads `APPLvwe4` → **type `APPL`, creator `vwe4`**. + +--- + +## 2. PEF container structure + +First 16 bytes: `4A6F7921 70656666 70777063 00000001` = `Joy! peff pwpc` v1 +(classic Mac OS CFM PowerPC container, big-endian). + +**Container header (40 bytes):** + +| Field | Value | +|---|---| +| tag1 / tag2 / arch | `Joy!` / `peff` / `pwpc` | +| formatVersion | 1 | +| dateTimeStamp | `0xAF4A5504` | +| currentVersion | 0 | +| sectionCount | 3 | +| instSectionCount | 2 | + +**Section table** (each header 28 bytes; bodies follow the name table): + +| # | Kind | default | total | unpacked | packed | file off | notes | +|---|---|---|---|---|---|---|---| +| 0 | Code (0) | 0 | 625,536 | 625,536 | 625,536 | `0x003840` | PPC text (`0x98B80` bytes). | +| 1 | PatternInitData (2) | 0 | 127,204 | 122,866 | 88,128 | `0x09C3C0` | RLE-packed data/TOC/strings; unpacks to `0x1DFF2`. | +| 2 | Loader (4) | 0 | 0 | 0 | 14,272 | `0x000080` | Imports/exports/relocs/entry. | + +**Entry points (from the Loader header):** + +- **main** = data-section transition vector at offset `0x5154` → `[code 0x00082E5C, TOC 0x00008000]`. + So `main` executes at **code `0x82E5C`** and the runtime **TOC/`r2` = data offset `0x8000`**. + (This TOC value is the key that lets us resolve all `r2`-relative constant loads — see §6.) +- **init** = none (`-1`), **term** = none (`-1`). +- **exported symbols = 0** (nothing is exported; this is a leaf application, not a shared library). +- imported libraries = 3, imported symbols = 448, reloc sections = 1. + +Tool: `py -3.13 tools/pefparse.py "Console 4.10" header|loader|strings|dump|dis`. + +--- + +## 3. CFM imports (448 symbols across 3 libraries) — interpreted + +Only three shared libraries are imported. **Notably absent: OpenTransport, +AppleTalk, and any TCP shared library** — so the console does *not* use Open +Transport. Its IP networking is done the older way, through the **MacTCP `.IPP` +driver via the Device Manager** (see the `PBControlAsync/Sync` + `PBOpenSync` +imports and the `CTCPDriver`/`.IPP` strings in §6). + +### MathLib (6) +`num2dec, dec2num, str2dec, dec2str` (SANE-style decimal ↔ string) and +`fetestexcept, feclearexcept` (FP exception flags). Used for numeric formatting +of scores/stats. + +### InterfaceLib (436) — the Mac Toolbox surface +Grouped by subsystem (representative, not exhaustive): + +- **QuickDraw / GWorld / PICT:** `InitGraf, OpenCPort, NewGWorld*(via UGWorld), + LockPixels, CopyBits(implied), OpenCPicture, DrawPicture(implied), PlotCIcon, + PlotIconID, RGBBackColor, FrameRoundRect, InvertRgn, ClipRect …` — draws the + console windows, pod photos (PICT), mech/vehicle art, kill markers. +- **Window Manager:** `NewCWindow, GetNewCWindow, ZoomWindow, SelectWindow, + HiliteWindow, FindWindow, BeginUpdate/EndUpdate, DragWindow(implied) …` +- **Menu Manager:** `NewMenu, InsertMenu(implied), DeleteMenu, DrawMenuBar, + MenuKey, HiliteMenu, EnableItem, CountMItems, GetItemMark …` +- **Control Manager:** `NewControl, TrackControl, FindControl, Draw1Control, + SetControlValue/Min/Max, HiliteControl, SetControlAction …` (scrollers, + buttons, the toolbar). +- **List / Dialog / TextEdit:** `LGetSelect, LSetSelect, LSetCell, LGetCell` + (List Manager — player/network tables), `CautionAlert, ParamText`, + `TEInsert, TEActivate, TEGetHeight, TECut, TEUpdate` (mission notes / fields). +- **File Manager:** `FSpOpenDF, FSpCreate, FSpDelete, FSRead, FSWrite, FSClose, + PBOpenSync, PBReadSync, PBWriteSync, PBHCreateSync, PBGetEOFSync, + PBSetEOFSync, FSpCreateResFile, FSpOpenResFile, HOpenResFile …` — reads + `Console.ini`, writes `Console Log`, loads/saves mission **egg** files. +- **Device Manager (→ MacTCP):** **`PBOpenSync`, `PBControlAsync`, + `PBControlSync`** — open the `.IPP` driver and issue TCP control calls + (TCPCreate/ActiveOpen/Send/Rcv/Close) — the pod network layer. +- **Printing Manager:** `PrOpen, PrOpenDoc, PrOpenPage, PrCloseDoc, PrClose` — + prints score sheets / mission highlights. +- **Apple Event Manager:** `AEInstallEventHandler, AEProcessAppleEvent, + AECreateAppleEvent, AESend, AEGetParamDesc, AEGetNthDesc, AEPutParamDesc, + AECreateList, AEGetAttributePtr, AEResolve(+ObjectSupportLib) …` (see §9). +- **Misc:** `Random, BlockMove, GetScrap, SysEnvirons, ExitToShell, + NewRoutineDescriptor/NewFatRoutineDescriptor` (68K↔PPC mixed-mode glue). + +### ObjectSupportLib (6) — Apple Event object model +`AEResolve, AESetObjectCallbacks, AEInstallObjectAccessor, CreateObjSpecifier, +AEObjectInit, CreateOffsetDescriptor` — the app is an AppleScript object-model +server over its window/document objects (standard PowerPlant `UAppleEventsMgr`). + +--- + +## 4. Code organization (from the 27 named `CODE` segments) + +Because this is a fat binary, the resource fork still carries the CodeWarrior +**68K segment names**, which are a clean module map of the whole program +(the PPC side is the same code, differently packaged): + +``` + 1 Main 10 Utilities 19 RP Panes + 2 Commanders 11 Libraries 20 Data Objects + 3 Connection 12 Debug 21 BT Data Objects + 4 Panes 13 TCP/IP 22 RP Data Objects + 5 Table 14 MUNGA 23 Printing + 6 Files 15 Console Menus 24 RP Printing + 7 Apple Events 16 Console Windows 25 BT Printing + 8 Array 17 Console Panes 26 Console Features + 9 Features 18 BT Panes +``` + +- **Connection / TCP/IP** = the pod networking (`CTCPDriver`, `CTCPStream`, + `CTCPEndpoint`, `CTCPAsyncCall`, `CTCPResolverCall`, `NetworkEndpoint`). +- **MUNGA** = data marshalling (the mission/egg (de)serializer — "munge"). +- **BT / RP** split = per-game code (BattleTech vs Red Planet) for panes, data + objects, and printing. + +`.cp` source-module names recovered from the binary (the CodeWarrior debug +string table) include, among ~140 files: +`ConsoleApp, ConsoleDoc, ConsoleWindow, PrefsDoc, NetworkEndpoint, +NetworkEndpointList, NetworkTable, NetworkWindow, CTCPDriver, CTCPStream, +CTCPEndpoint, CTCPAsyncCall, CTCPResolverCall, MissionObject, MissionEvent, +MissionEventQueue, MissionView, MissionGraphView, MissionGraphWindow, +MissionClockView, Participant, TeamList, TeamPrtcpntList, PlayerTable, +BTScenario, BTVehicle, BTLocation, RPScenario, RPVehicle, RPLocation, Vehicle, +Weather, Experience, Adventure, ScoreSheetView, DamageMatrix, BTKillMarker` and +the PowerPlant framework classes (`LApplication, LDocument, LWindow, LView, +LPane, LTableView, LCommander, LListener/LBroadcaster(implied), UAppleEventsMgr, +UGWorld, UMemoryMgr, UReanimator, UModalDialogs`). + +--- + +## 5. Resource fork (550 resources, 39 types) + +Parsed from `__MACOSX/._Console 4.10` with `tools/rsrcparse.py`. Highlights: + +- **`cfrg` (1)** — the Code Fragment resource that points the CFM loader at the + PEF in the data fork (confirms the fat-binary layout). +- **`vers` (2)** — `2.4.0b3`, "Console 2.4.0b3", © 1995–1997 **FASA Interactive, Inc.** +- **`SIZE` / `BNDL` / `FREF` / `ICN#`…** — standard app packaging; `icm#/ics#` + application icons. +- **`ºMWC` (32000)** — Metrowerks CodeWarrior project stamp (confirms toolchain). + +### Operator commands & UI +- **Toolbar buttons** (`ICN#`/`icl4`/`icl8`, ids 1000–1011): + **Get Next Departure, Translocate, Load Mission, Add Pilot, Delete Pilot, Lookup.** +- **`MENU`/`Mcmd` (10)**: Apple, File (New / Close / Save / Save a Copy… / Page + Setup… / Print / Quit), Edit, **Configuration**, **Adventure**, **BattleTech**, + **Red Planet** + scenario menus (`BTScenario`, `RPScenario`). +- **`WIND`/`PPob` (windows)**: **`RPMR ConsoleWindow`, `RPMF ConsoleWindow`, + `BTFA ConsoleWindow`, `BTSB ConsoleWindow`** (the four game-mode console + layouts), **Mission Graph**, **Toolbar**, **Network Diagnostic**, **Lookup**. +- **`ALRT`/`DITL`**: About Box, Save-before-closing/quitting, Confirm Revert, + Low Memory Warning, **TCP Warning**, Landscape/Portrait mode. + +### Data/game resources +- **`Mech`/`MWpn`/`MDgZ` (×24/×24/×22)** — BattleTech mechs + weapons + damage + zones: *BlackHawk, Loki, MadCat, Owens, Thor, Vulture, Avatar, Sunder, + TimberWolf, Satyr, Hellbringer, Summoner, …* +- **`PICT` (×98)** — pod photos (*Carpe Diem, Calamity Jane, Man O' War,…*) and + faction crests (*Davion, Kurita, Steiner, Liao, Marik, Comstar, VGL*). These + match `pictID=` in `Console.ini`. +- **`Txtr` (×57)** — named text traits (fonts/styles for each field, plasma + screen, mission clock). + +### String lists (`STR#`) — the semantic vocabulary +61 `STR#` resources. The protocol-relevant ones: + +- **`STR# 131 "Mission State"`** — the console-side (document) mission state + descriptions, index 0..6: + 1. `Mission data is incomplete.` + 2. `The mission is ready to load.` + 3. `The mission is being loaded.` + 4. `The mission is ready for translocation.` + 5. `The mission is being translocated.` + 6. `The mission is in progress.` + 7. `The mission has been completed.` +- **`STR# 130 "Network Endpoints"`** — the 50-name roster: cockpits (*Frequent + Flyer, Privateer, Man O' War, Divine Wind, Carpe Diem, Gypsy, Puck, Icarus, + Calamity Jane, Valkyrie, Pegasus,…*), cameras (*Alpha/Beta/Gamma/Delta/Epsilon + Camera*), *Mission Review* stations, and PC/test targets (*BrownCartPC, + GaugeCartPC, ZabswareTarget*). Index 0 = `Bogon Alert!` (invalid), + `` = a free slot. +- **`STR# 1002/1003 "Network Errors"`** — explicitly a **4D database** client: + `_4D_Select4DServer`, `_4D_Find4DServer`, "connection to the database", + "asynchronous mode query already in progress", "invalid connection handle". +- **`STR# 1001 Database Errors`, `1004 Database Tasks`, `1005 General Errors`, + `128 General Strings`** (`"%s Mission %s"`, `"Mission Time & Date: %s, %s"`, + Show/Hide Toolbar, Show/Hide Network), **`300 Adventure Strings`** + (`BattleTech`, `Red Planet`), plus large game tables (scenarios, drop zones, + roles, kill/death/damage strings, textures, football positions, …). +- **`eTbl` (3)** — error tables named *Database Errors*, *Network Errors*, + *4D Open Errors* (again confirming the 4D backend). + +--- + +## 6. Pod command/control protocol (MacTCP, port 1501) + +This is the highest-value result. Evidence chain: binary strings → +disassembly (TOC-resolved) → `Console Log` → `Console.ini`. + +### 6.1 Transport +- Custom C++ classes **`CTCPDriver` / `CTCPStream` / `CTCPEndpoint` / + `CTCPAsyncCall` / `CTCPResolverCall`** wrap **MacTCP**. The literal driver + name **`.IPP`** appears at data `0x1053F` right next to `CTCPDriver` — this is + the classic MacTCP IP driver opened with `PBOpenSync`, then driven with + `PBControlAsync/Sync`. +- One TCP connection per pod. From `Console.ini` each + `[NetworkEndpoint::Cockpit::*]` sets `classID=Ptcp`, `defaultPort=1501`, + `localHostPort=1501`, and the pod's `addressIP` (`200.0.0.11`…`200.0.0.117`, + etc.). Console host = `200.0.0.1`; pods `200.0.0.11`–`200.0.0.120` (from the + log). The many `Ptcp` sub-keys (`openTimeout, listenTimeout, delay, + throughput, reliability, precedence, fragment, timeToLive, security, + queryTimeout`) are **MacTCP TCP/IP ULP parameters** passed to TCPActiveOpen / + the IP header — further proof this is MacTCP, not OT. + +### 6.2 Receive path — pod → console "Cockpit State" +Message dispatch lives at **code `0x63B3C`** (inside the participant's +message handler). The received message's low byte is the type: + +``` +0x63B3C clrlwi r0, r31, 0x18 ; r0 = msg & 0xFF (message type) +0x63B40 cmplwi r0, 0xC ; 0..12 valid +0x63B44 bgt 0x63BEC ; >12 → default → "Unknown message type!!!" +0x63B48 lwz r3, -0x6CA4(r2) ; TOC[data 0x135C] = jump table @ data 0x17B1C +0x63B4C slwi r0, r0, 2 +0x63B50 lwzx r3, r3, r0 ; handler = jumptable[type] +0x63B54 mtctr r3 ; bctr +``` + +Every case calls **`SetCockpitState(participant+0x30C, type)` at `0x62720`**: + +``` +0x62720 SetCockpitState: + stb r4, 0x289(r6) ; participant->cockpitState = type (field at +0x289) + li r4, 0xBBC ; bl BroadcastMessage ; notify the UI panes/graph +``` +(`0x62778` is the matching getter: `lbz r3, 0x289(r3)`.) The console then logs +`" Cockpit State: "`. So **the pod's message-type byte is literally +the Cockpit State number, 0–12**; type > 12 logs `"Unknown message type!!!"` +(seen twice in the 5.3 MB log). The jump table at data `0x17B1C` (13 code +pointers, default `0x63BEC`) is the physical proof of the 0–12 range. + +### 6.3 Send path — console → pod commands +Commands are transmitted by **`NetworkEndpoint::Send` at `0x629EC`**, called as +`Send(endpoint, &msgHeaderWord, payloadObj, payloadLen)`. It frames a packet = +**16-byte header + payload**: + +``` +buf = alloc(len + 0x10) +memcpy(buf+0x10, payloadObj, len) ; body +buf[0..3] = *msgHeaderWord ; message-class word (observed 0x04000000) +buf[4..7] = 0 +buf[8..11] = value from 0x3CB38 ; sequence / timestamp +CTCPStream::Write(endpoint, buf, len+0x10) ; via 0x39748 → 0x3BD28 +``` + +The **payload object** is a small serialized struct whose **byte[0] = its size** +and **byte[4] = the command code**. Two confirmed by disassembling the object +constructors: + +| Command | Ctor | payload size | **command code (byte[4])** | Verb logged | Sender | +|---|---|---|---|---|---| +| Translocate mission | `0x3EC74` | `0x0C` | **5** | `Translocating Mission…` | `0x67438` | +| Stop mission | `0x3ECC8` | `0x10` | **6** | `Stopping Mission…` | `0x6755C` | + +Other senders (same shape, codes not yet pinned): **Loading Mission** +(`0x64xxx`/`Received egg file` `0x648C0`), **Ending Mission** `0x66464` (uses a +variant send `0x5E474` that packs the destination IP from a 4-byte struct field +`+0x10..0x13`), **Reset Mission** `0x6769C`. Command codes for these are +recoverable with the same method (find the `li r0,N; stb r0,-0x10(r1)` that +seeds object byte[4]) — see Open Questions. + +### 6.4 Internal PowerPlant command/broadcast IDs +The app's own command constants cluster in **`0xBBA`–`0xBDF`** (decimal +3002–3039). Confirmed meanings: +`0xBBC` = cockpit-state-changed broadcast, `0xBC5` = "received egg file", +`0xBCB` = ending mission, `0xBBA/0xBBB` = handled in the participant +`ListenToMessage` at `0x628D8`. (`0xBC1` is the most frequent; others 0xBCE– +0xBDF map to menu/toolbar commands.) + +### 6.5 Observed mission lifecycle (from the log) +A full run for one pod, with the two state machines interleaved +(**Cockpit State N** = pod→console; **verbs** = console actions): + +``` +Cockpit State 2 (present/connected) +Cockpit State 1 (ready) +Loading Mission… → console loads the egg +Cockpit State 6 +Cockpit State 7 +Received egg file → egg in hand; "Ready to translocate" +Cockpit State 8 +Translocating Mission… → console sends egg/mission to pod (cmd code 5) +Cockpit State 9 +Cockpit State 10 (in progress / running) +Stopping Mission… (cmd code 6) +Cockpit State 11 +Ending Mission… +Cockpit State 12 +Cockpit at initiated close!!! → pod closed its TCP stream +Cockpit State 3 (closed/disconnected) +Reset Mission… → console rearms for the next departure +Cockpit State 2 … +``` + +Cockpit-state frequencies in the sample log: `1:3997 2:13258 3:12809 +6:3098 7:3093 8:3262 9:2852 10:2852 11:2990 12:2890` (states 0, 4, 5 +never appear in this log though the dispatch table has slots for them — likely +transient/internal). Verb counts: `initiated close 24702, Loading 3129, +Received egg 3124, Stopping 2996, Ending 2905, Translocating 2882, Reset 953`. + +--- + +## 7. Console Log format + +- **Encoding:** Mac Roman. **Records are CR-terminated** (`0x0D`; classic Mac, + **no LF** — 98,910 CRs for ~91,607 records in the sample). `0xC9` = "…" + ellipsis inside the mission verbs (13,415 occurrences). +- **Record grammar:** + `M/D/YYYY` `HH:MM:SS`␠`AM|PM`␠`` `CR` + where `` is usually ` ` (state/verb lines) or the + IP-embedded form `Cockpit at initiated close!!!`. +- The timestamps in this capture read year **1956** — the logging Mac's clock + was unset (default RTC), not a real date. Times of day are valid. +- **72 distinct message templates**; the load-bearing ones: + `" Cockpit State: "`, `"Cockpit at initiated close!!!"`, + `" Loading|Translocating|Stopping|Ending|Reset Mission…"`, + `" Received egg file"`, `" Unknown message type!!!"`. +- **Log writer:** each line is assembled by a string-append helper (`0x85324`, + repeatedly) and flushed by `0x851C4`; the leading date/time comes from the + global at TOC `-0x77CC(r2)` (data `0x834`). Field separator string is at data + `0x17B8`. + +--- + +## 8. Console.ini schema + +INI-style but with **no newlines** — sections are `[Name]` headers and +`key=value` pairs run together; parse by splitting on `[`. Section names use a +`::` **hierarchical namespace**. 586 sections total in the live file. + +**Section families (by count):** `RP` 342, `BT` 196, `rpl4` 34, +`NetworkEndpoint` 10, `Adventure` 3, `NetworkEndpointList` 1. + +**Every object carries a 4-char `classID`** (an OSType tag naming its C++ +class). Confirmed: `Ptcp` (TCP endpoint), `Advn` (adventure), `RPLn` (Red +Planet location); BT/RP scenario & vehicle classes follow the same pattern. + +### 8.1 Network endpoints (the pods) +``` +[NetworkEndpointList] + networkEndpoint = NetworkEndpoint::Cockpit::Frequent Flyer + networkEndpoint = NetworkEndpoint::Cockpit::Privateer + … (10 active endpoints) + +[NetworkEndpoint::Cockpit::Frequent Flyer] + classID=Ptcp hostType=0 state=0 addressUse=0 + addressIP=200.0.0.11 + defaultPort=1501 localHostPort=1501 openInterval=40 openTimeout=10 + listenTimeout=0 + resourceID=130 resourceIndex=2 pictID=308 ; STR# 130 name + PICT photo + ; MacTCP TCP/IP ULP parameters: + delay=1 throughput=1 reliability=1 precedence=0 fragment=0 + timeToLive=60 security=0 queryTimeout=60 + loadQueryInterval=1 inProgressQueryInterval=10 +``` +`resourceID=130` + `resourceIndex` selects the pod's display name from +`STR# 130`; `pictID` selects its photo PICT. + +### 8.2 Adventure → Location → Scenario tree (populates the menus) +``` +[Adventure::Solo::Red Planet] classID=Advn adventureType=2 resourceID=300 resourceIndex=2 list=RP::Solo::LocationList +[RP::Solo::LocationList] location=rpl4::Solo::Location::Yip's Yahoorama … +[rpl4::Solo::Location::Wiseguy's Wake] classID=RPLn resourceID=4000 resourceIndex=3 tag=wise pictID=601 list=RP::Wiseguy's Wake::Solo::ScenarioList +[BT::Cavern::ScenarioList] scenario=BT::Cavern::NoReturn scenario=BT::Cavern::FreeForAll +[BT::Solo::LocationList] location=BT::Solo::Ravines / Arena1 / Polar4 / Cavern … +``` +Four top-level adventures exist in the binary: +`Adventure::{Solo,Team}::{BattleTech,Red Planet}` — matching the four console +windows `BTFA/BTSB/RPMF/RPMR ConsoleWindow`. + +### 8.3 Key inventory +`classID, resourceID, resourceIndex, pictID, tag, list, scenario, location, +default, value, max, time` (structural) and the game-object keys +`vehicle/vehicel(sic)/vehicleClass, mechID, camo, color, texture, emblem, +killMarker, experience, role, position, dropZone, weather, temperature, +adventureType, language, scriptCode` plus the full `Ptcp` network key set. + +`Stationary.ini` is the same schema used as a **template** to regenerate a +site's `Console.ini`; the per-city `ini Folder/DB` files are venue +overrides. + +--- + +## 9. Apple Events + +The app installs standard AE handlers (`AEInstallEventHandler`) and is a full +AppleScript **object-model server** (`ObjectSupportLib` + `UAppleEventsMgr.cp`). +Its `aete` (id 0, "English") declares only the **standard suites** — Required +(`reqd`: run/open/print/quit), Standard/Core (`CoRe`: close/get/set/make/save/ +data size), and Miscellaneous Standards (`misc`: revert/select) — over the +generic `application`/`window`/`document` classes. `aedt` (3) = *Required Suite, +Core Suite, Misc Standards*. + +**Conclusion:** Apple Events are used for **OS/Finder integration** +(open-documents = open mission/ini files, print score sheets, quit), **not** for +commanding pods. Pod command is exclusively the MacTCP protocol in §6. (No +custom application scripting suite is defined.) + +--- + +## 10. Tooling written (`410console/tools/`) + +Reusable, dependency-light (Python 3.13 + capstone): + +- **`pefparse.py`** — PEF container parser + PIDATA (RLE) unpacker + loader + (imports/exports/entry) + string extractor + capstone PPC disassembler. + `header | loader | strings [minlen] | dump | dis `. +- **`ppcxref.py`** — TOC-aware annotator (`r2` = data `0x8000`): resolves + `r2`-relative constant loads to data addresses/strings and builds a + string→code cross-reference. `annotate | xref | toc `. +- **`rsrcparse.py`** — classic Mac resource-fork parser (reads the AppleDouble + `._` sidecar). `map | names | strn [id] | dump [id]`. + +Example: `py -3.13 tools/rsrcparse.py "4_10-console-extracted/__MACOSX/._Console 4.10" strn 131`. + +--- + +## 11. Confidence & open questions + +**High confidence (multiple corroborating sources):** +- Fat PEF/PPC app, PowerPlant/CodeWarrior, FASA Interactive, creator `vwe4`, + vers 2.4.0b3. *(PEF header + resource fork + strings.)* +- MacTCP `.IPP`, port 1501, per-pod TCP, addressing scheme. *(imports + + `CTCPDriver`/`.IPP` string + ini + log.)* +- Cockpit State machine 0–12, pod→console, dispatch at `0x63B3C`, stored at + Participant `+0x289`, logged verbatim. *(disasm + jump table + log stats.)* +- Send framing = 16-byte header + payload; command payload byte[0]=size, + byte[4]=code; Translocate=5, Stop=6. *(disasm of `0x629EC` + two ctors.)* +- Mission lifecycle ordering. *(log, thousands of runs.)* +- ini schema and 4D-database backend. *(ini + `STR#`/`eTbl` + error strings.)* + +**Open questions / next steps:** +1. **Finish the command-code table.** Pin byte[4] for Load, End, Reset, + Get-Next-Departure (method: disassemble each sender's object ctor near + `0x3EBxx–0x3EFxx` for `li r0,N; stb r0,-0x10(r1)`). Also decode the + header word-2 (`0x3CB38`) — sequence counter vs. timestamp. +2. **Header semantics.** Confirm whether the pod reads packet byte[0] (class=4) + or a payload byte as its "message type", and map the console's **command** + codes to the resulting **Cockpit State** transitions (5→9, 6→11 observed). +3. **Egg transfer mechanism.** Determine whether the `.egg` mission bundle + (note the `Egg.ini` string at data `0x98E9`, and the **MUNGA** segment) + travels to the pod over the same 1501 stream during "Translocate", or via a + separate transfer. `Received egg file` is logged **console-side** — clarify + direction (console reads egg from disk vs. receives from DB/pod). +4. **States 0/4/5.** Never seen in this log; identify their handlers + (`jumptable[0]=0x63BEC` default, `[4]/[5]=0x63B78`) and meaning. +5. **4D protocol.** Out of scope here (proprietary ACI 4D client); the console↔ + 4D link governs scheduling ("Get Next Departure") and the pilot roster. +6. **Relocations.** `pefparse` does not yet apply the Loader relocation opcode + stream (`reloc_instr_offset`); constant addresses were resolved via the TOC + value instead, which was sufficient. A full reloc pass would make every data + pointer absolute for deeper xref. + +--- + +### Appendix A — key addresses (code section, base 0) +| Addr | Role | +|---|---| +| `0x82E5C` | program `main` (via TVector @ data `0x5154`) | +| `0x63B3C` | received-message dispatch (msg&0xFF → jumptable @ data `0x17B1C`) | +| `0x62720` | `SetCockpitState(participant, state)` → Participant `+0x289`, broadcast `0xBBC` | +| `0x62778` | `GetCockpitState` (`lbz +0x289`) | +| `0x629EC` | `NetworkEndpoint::Send` (16-byte header + payload) | +| `0x39748`→`0x3BD28` | `CTCPStream::Write` | +| `0x3EC74` / `0x3ECC8` | Translocate (code 5) / Stop (code 6) command-object ctors | +| `0x66464` / `0x67438` / `0x6755C` / `0x6769C` / `0x648C0` | senders: Ending / Translocating / Stopping / Reset / Received-egg | +| `0x85324` / `0x851C4` | log-line append / flush | +| data `0x8000` | TOC base (`r2`) | +| data `0x17B1C` | 13-entry cockpit-message jump table | + +### Appendix B — reproduce +``` +cd c:\VWE\TeslaRel410\410console +py -3.13 tools\pefparse.py "4_10-console-extracted\Console 4.10" loader +py -3.13 tools\ppcxref.py xref "Mission" +py -3.13 tools\ppcxref.py annotate 0x629ec 0x60 +py -3.13 tools\rsrcparse.py "4_10-console-extracted\__MACOSX\._Console 4.10" map +py -3.13 tools\rsrcparse.py "4_10-console-extracted\__MACOSX\._Console 4.10" strn 131 +``` diff --git a/410console/battletech-port/BATTLETECH-PORT-SPEC.md b/410console/battletech-port/BATTLETECH-PORT-SPEC.md new file mode 100644 index 0000000..f86ff1f --- /dev/null +++ b/410console/battletech-port/BATTLETECH-PORT-SPEC.md @@ -0,0 +1,271 @@ +# BattleTech → TeslaConsole port spec + +**Hand-off package for the `TeslaSuite` repo.** Everything learned in the +`TeslaRel410` repo about the Mac **4.10 operator console** and the path to add +**BattleTech** support to the modernized **TeslaConsole**. Implement this over in +`TeslaSuite` (that repo's Claude has the console's own memories and can build it). +This folder is self-contained — the `reference/` files travel with it. + +> Sibling doc: `410console/CONSOLE-4.10-DECOMP.md` (full decompilation of the Mac +> `Console 4.10` PowerPC binary). This spec assumes it. + +--- + +## 0. The situation in one paragraph + +The Mac **`Console 4.10`** (decompiled — see the sibling doc) and the Windows +**`TeslaConsole.exe`** (4.11.x, which `TeslaSuite/Console/TeslaConsole` is a .NET +reconstruction of) are two generations of the *same* operator console. The Mac +one drove **both** games — BattleTech and Red Planet. **The modernized +TeslaConsole implements only Red Planet** (`TeslaConsole.RedPlanet`), and its +shell is even hardcoded to it (`TeslaConsoleForm`: `AppID != 0 → "Pod Not Running +RP"`, status strings `"RP Initalizing"`…). The task: **add a +`TeslaConsole.BattleTech` game that mirrors `TeslaConsole.RedPlanet`**, and +de-hardcode the shell to pick the game by `ApplicationID`. The transport layer is +game-agnostic and reused unchanged. + +--- + +## 1. The shared spine (reused verbatim by BattleTech — do NOT re-implement) + +Both games and both console generations use one command/control path: + +- **Munga over TCP 1501**, one `MungaSocket` per pod (`Munga Net.dll`, vendored in + `Console/lib/`). Console `200.0.0.1`, pods `200.0.0.11…`. +- `MungaGame.cs` (in `TeslaConsole`, **game-agnostic**): connect pod:1501, poll + `StateQueryMessage`→`StateResponseMessage(ApplicationID, ApplicationState)` + every ~1 s, and on `ApplicationState.WaitingForEgg` stream the egg as 1000-byte + `EggFileMessage[]` chunks, await `AcknowledgeEggFileMessage`, then run. +- **`ApplicationState`** (the named "cockpit states"): `CreatingMission, + LaunchingMission, LoadingMission, ResumingMission, RunningMission, + SuspendingMission, WaitingForLaunch, WaitingForEgg, …`. In the Mac 4.10 console + these were the **numeric Cockpit States 0–12** (decomp §6.2). +- **`HostType`** (`TeslaConsole/HostType.cs`): `GameMachineHostType=0, + MissionReviewHostType=2` (camera/spectator), `ConsoleHostType=3`. +- Reused unchanged by BT: `MungaGame`, `Pod`, `PodManager`, `Site`, `SitePanel`, + `PlasmaBitmaps`, `PlasmaFontTool`, the ordinals blocks, and the whole + provisioning/launch path (that's the separate RPC channel on TCP 53290; game + control is Munga 1501). + +**Only the egg *content* and the per-game UI/data differ between RP and BT.** + +--- + +## 2. The BattleTech egg format (authoritative) + +Reconstructed from the console-generated **`reference/cavern.egg`** (freeforall, +map cavern, night; pilot `cyd` in a `madcat`, veteran) and +**`reference/TESTARN.EGG`**. This is what `BTMission.ToEggString()` must produce. + +### Structure (section by section) +``` +[mission] adventure=BattleTech map=cavern scenario=freeforall time=night + weather=clear temperature=27 length=120 +[ordinals] ← 1st–4th place plasma bitmaps, 128×32, IDENTICAL to RP + bitmap=Ordinal::BitMap::1 … [Ordinal::BitMap::1] …32 rows… x=128 y=32 width=8 + (…::2 ::3 ::4) +[pilots] + pilot=200.0.0.113 ← one per player + one per camera +[200.0.0.113] ← one participant block per pilot line + hostType=0 advancedDamage=1 loadzones=1 name=cyd bitmapindex=1 + experience=veteran vehicle=madcat vehicleValue=0 badge=VGL + dropzone=one color=Grey patch=Red role=Role::Default +[largebitmap] + bitmap=BitMap::Large::cyd +[BitMap::Large::cyd] …32 rows… x=128 y=32 width=8 ← pilot-name plasma bitmap +[smallbitmap] + bitmap=BitMap::Small::cyd +[BitMap::Small::cyd] …16 rows… x=64 y=16 width=4 +[Role::Default] model=dfltrole ← one block per referenced role +``` + +### BattleTech vs Red Planet — the field diff +`RPMission.ToEggString()` / `RPPlayer.AppendParticipantSpecificData()` is the +template. BT differs as follows: + +| Field | Red Planet | BattleTech | +|---|---|---| +| `[mission] adventure=` | `Red Planet` | **`BattleTech`** | +| `[mission] temperature=` | `0` | **nonzero** (e.g. `27`) | +| `[mission] compression=` | present (`0`/`1`) | **absent** | +| participant `advancedDamage=` | — | **present** (`1`) advanced damage model | +| participant `experience=` | — | **present** — pilot skill (`novice`/`veteran`/`expert`) | +| participant `vehicleValue=` | — | **present** (`0`) | +| participant `patch=` | — | **present** (e.g. `Red`) | +| participant `role=` | — | **present** — `Role::`, needs a `[Role::]` block | +| participant `vehicle=` | RP racer (quark/bug…) | **a mech** (madcat/…) | +| `[Role::*]` blocks | — | **present** — `model=` (`Default`→`dfltrole`; TESTARN.EGG also has `NoReturn`) | +| `ordinals`, `largebitmap`, `smallbitmap` | present | **identical** — reuse as-is | + +Shared participant fields (same as RP): `hostType, loadzones, name, bitmapindex, +dropzone, color, badge`. + +### Wire encoding (mirror RP exactly) +`RPMission` builds the string with `\n` between every `key=value`, then +`ToEggFileMessages()` does `Replace("\r\n","\0").Replace('\n','\0')`, ASCII- +encodes, and chunks into 1000-byte `EggFileMessage(index, totalLen, thisLen, +buf)`. So on the wire the egg is **NUL-delimited**. `cavern.egg` on disk is the +same NUL-delimited blob (renders as run-together fields). **BT reuses this +verbatim** — only `ToEggString()` content changes. + +### Section-order caveat (don't chase a false byte-match) +`cavern.egg` was written by the *original* console; its section order (ordinals +early, bitmap defs inlined) differs from the modernized `RPMission.ToEggString` +order (bitmaps listed then defined, ordinals last). The pod parses the egg +**INI-style by section name**, so order is almost certainly non-critical. **Match +RP's builder structure** (it's known-good against real pods) rather than +byte-matching `cavern.egg`; use `cavern.egg` as the **field/value** reference and +verify a generated egg on a real BT pod. (If a byte-exact golden test is wanted, +generate from BT classes and pin *that*, like the existing DiffTests do.) + +--- + +## 3. The RedPlanet template → BattleTech counterparts + +Mirror `Console/TeslaConsole.RedPlanet/` into `Console/TeslaConsole.BattleTech/`. +The `.csproj` is SDK-style (globs `**/*.cs`), so new files under a new folder are +picked up automatically; add `Content Include` lines for the BT XML (see §5). + +| RedPlanet file | BattleTech counterpart | Notes | +|---|---|---| +| `RPParticipant.cs` (abstract) | `BTParticipant` | `[podIP]` + `hostType`; abstract `AppendParticipantSpecificData` | +| `RPPlayer.cs` (abstract) | `BTPlayer` | adds the BT participant fields (§2 diff); `HostType.GameMachine` | +| `RPRacePlayer` / `RPFootballPlayer` | `BTPlayer` concretes per scenario | e.g. `BTFreeForAllPlayer`, `BTTeamPlayer` | +| `RPCamera.cs` | `BTCamera` | `HostType.MissionReview`, `vehicle=camera` | +| `RPMission.cs` (abstract) | `BTMission` | `ToEggString()` per §2; `ToEggFileMessages()` reusable as-is; emit `[Role::*]` blocks for referenced roles | +| `RPRaceMission` / `RPFootballMission` | `BTFreeForAllMission` / `BTTeamMission` | `AppendMissionSpecificData` for scenario extras | +| `Scenario.cs` + `FootballScenario`/`DeathRaceScenario` | `Scenario` + BT scenarios | loaded from `BTConfig.xml`; `` filters valid maps/vehicles | +| `RPConfig.cs` + `RedPlanet/RPConfig.xml` | `BTConfig.cs` + `BattleTech/BTConfig.xml` | the data catalog (§5) | +| `RPDefaults.cs` / `RPDefaultsDialog.cs` | `BTDefaults` / `BTDefaultsDialog` | operator defaults UI | +| `RPMap.cs` / `RPVehicle.cs` | `BTMap` (arena) / `BTVehicle` (mech) | key/name/image; images under `images/battletech maps|vehicles/` | +| `RPTeam.cs` | `BTTeam` (if BT team scenario) | — | +| `MissionEvent` + `Scored/Killed/Damaged/Boost/ScoreUpdate` | BT `MissionEvent` + `Killed/Damaged/…` | BT has **DamageMatrix** + **KillMarker** (decomp §5 string lists); RP-only `Boost`/football events drop; confirm BT's set against the pod's Munga event messages | +| `RPMissionResults.cs` / `RPMissionRecorder.cs` / `RPPrintDocument.cs` | `BT*` | `.rpm`-style saved results + printout | +| `RPGame.cs` (the DockContent pane, ~66 KB) | `BTGame` | the big one: mission-config UI + the run/stop state loop driving each pod's `MungaGame` | +| `TeslaConsole/RPStrings.cs` + `RedPlanet/RPStrings.xml` | `BTStrings` | localized strings | + +`RPGame`'s engine (worth copying closely): a per-pod `GameStateData +{EggMessageSent, RunMessageSent, AbortMessageSent, StopMessageSent, +PodAppearsReset}`, an `RPGameState` machine, `sStopToIdleStates` (the +`ApplicationState`s that mean "busy, must stop first"), `mEggFileMessages`, +`mMissionRecorder`, `mMissionPlayers` (index→player, to decode incoming events), +`mLastMissionResults`. BT reuses this shape unchanged. + +--- + +## 4. Shell de-hardcoding (`TeslaConsole`) + +The shell currently assumes RP. To support both games: + +- **`TeslaConsoleForm.cs` ~line 139**: `if (pod.MungaGame.AppID.Value != 0) → + "Pod Not Running RP"`. `ApplicationID 0 = Red Planet`. Generalize: map the pod's + reported `ApplicationID` to a game (RP vs BT) and pick the right game module + + status strings ("RP Initalizing" → per-game). **OPEN: the `ApplicationID` value + for BattleTech** — see §6. +- Game selection where a game pane is created (Site/SitePanel/SiteManagement — how + `RPGame` is instantiated today): allow creating a `BTGame` for BT pods. +- Per-game status string table instead of the inline "RP …" literals. + +--- + +## 5. The BattleTech catalog (`BTConfig.xml`) — seed + sources + +Mirror `reference/RPConfig.xml.template`: a root element with per-scenario blocks +carrying scenario-specific option lists, plus top-level ``/``/ +``/`