Track the 410console hand-off docs; ignore the raw Mac archive
The BattleTech port spec, the Console 4.10 decompilation notes, the golden reference eggs, and the PEF/rsrc analysis tools come into the repo. The .sit archive and its extraction (Mac binaries, __MACOSX/.finf metadata) stay untracked for now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,3 +22,10 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
desktop.ini
|
desktop.ini
|
||||||
.DS_Store
|
.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/
|
||||||
|
|||||||
@@ -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<City>/` | — | 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),
|
||||||
|
`<Available>` = 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
|
||||||
|
`"<IP> Cockpit State: <type>"`. 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 <IP> 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`␠`<message>` `CR`
|
||||||
|
where `<message>` is usually `<pod-IP> <text>` (state/verb lines) or the
|
||||||
|
IP-embedded form `Cockpit at <pod-IP> 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:
|
||||||
|
`"<IP> Cockpit State: <N>"`, `"Cockpit at <IP> initiated close!!!"`,
|
||||||
|
`"<IP> Loading|Translocating|Stopping|Ending|Reset Mission…"`,
|
||||||
|
`"<IP> Received egg file"`, `"<IP> 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<City>` 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 <sec> <off> <len> | dis <sec> <off> <count>`.
|
||||||
|
- **`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 <start> <count> | xref <substr> | toc <off>`.
|
||||||
|
- **`rsrcparse.py`** — classic Mac resource-fork parser (reads the AppleDouble
|
||||||
|
`._` sidecar). `map | names <TYPE> | strn [id] | dump <TYPE> [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
|
||||||
|
```
|
||||||
@@ -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::<key>`, needs a `[Role::<key>]` block |
|
||||||
|
| participant `vehicle=` | RP racer (quark/bug…) | **a mech** (madcat/…) |
|
||||||
|
| `[Role::*]` blocks | — | **present** — `model=<rolemodel>` (`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`; `<invalid>` 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 `<map>`/`<vehicle>`/
|
||||||
|
`<weather>`/`<time>` catalogs and `<invalid>` exclusions. For BT:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<BattleTech>
|
||||||
|
<freeforall name="Free For All">
|
||||||
|
<experience key="novice" name="Novice"/>
|
||||||
|
<experience key="veteran" name="Veteran"/>
|
||||||
|
<experience key="expert" name="Expert"/>
|
||||||
|
<color key="Grey" name="Grey"/> …
|
||||||
|
<badge key="VGL" name="VGL"/> …
|
||||||
|
<patch key="Red" name="Red"/> …
|
||||||
|
<role key="Default" model="dfltrole"/>
|
||||||
|
<role key="NoReturn" model="…"/>
|
||||||
|
</freeforall>
|
||||||
|
<!-- team/other scenarios from the Console.ini BT tree -->
|
||||||
|
<map key="cavern" name="Cavern" image="images/battletech maps/Cavern.bmp"/>
|
||||||
|
<map key="arena1" name="…"/>
|
||||||
|
<vehicle key="madcat" name="Mad Cat" image="images/battletech vehicles/Mad Cat.bmp"/>
|
||||||
|
<vehicle key="loki" name="Loki"/> …
|
||||||
|
<time key="day" name="Day"/> <time key="night" name="Night"/>
|
||||||
|
<weather key="clear" name="Clear"/> …
|
||||||
|
</BattleTech>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Known values** (from this repo's eggs + reversing this session):
|
||||||
|
- **scenario**: `freeforall` (confirmed). More in the Console.ini BT tree.
|
||||||
|
- **map (arena)**: `cavern`, `arena1` (confirmed). Full list: Console.ini BT
|
||||||
|
LocationList + game maps.
|
||||||
|
- **vehicle (mech)**: `madcat` (Mad Cat). Others seen in this repo's mech data:
|
||||||
|
`loki`, `thor`, `vulture`, `bhk1` (Black Hawk); OWN/PGN skeletons exist too.
|
||||||
|
(Torso-twist limits found this session: MadCat ±130°, Loki/Thor 110°, Vulture
|
||||||
|
90°; some skeletons — BLH/OWN/PGN — have no torso joint.)
|
||||||
|
- **experience**: `veteran` (confirmed); `novice`/`expert` are the other BTL4
|
||||||
|
pilot levels.
|
||||||
|
- **color**: `Grey` (confirmed). **badge**: `VGL`. **patch**: `Red`.
|
||||||
|
- **role**: `Default` (`model=dfltrole`), `NoReturn` (from TESTARN.EGG).
|
||||||
|
- **time**: `day`, `night`. **weather**: `clear`. **temperature**: numeric (27).
|
||||||
|
**length**: seconds (120).
|
||||||
|
|
||||||
|
**Where to get the exhaustive catalog (in this repo):**
|
||||||
|
- `410console/4_10-console-extracted/Console.ini` — the Mac console's full
|
||||||
|
`[Adventure::…::BattleTech]` → LocationList → scenario/vehicle tree (minified;
|
||||||
|
parse by `::`-namespaced sections; decomp §8.2 shows the RP tree shape).
|
||||||
|
- `ALPHA_1/REL410/BT/` — the game itself: `GAUGE/L4GAUGE.CFG`, `*.RES` resources,
|
||||||
|
`*.SKL` skeletons (mech list), and the `.egg` files.
|
||||||
|
- `CODE/BT/` — BattleTech game source (mech/weapon/damage classes; the
|
||||||
|
DamageMatrix/KillMarker semantics for the event model).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Open questions — resolve in the TeslaSuite repo
|
||||||
|
|
||||||
|
1. **`ApplicationID` for BattleTech.** RP is `0`. A BT pod (running `BTL4OPT`)
|
||||||
|
reports its own value in `StateResponseMessage.Application`. Get it by
|
||||||
|
decompiling `Console/lib/Munga Net.dll`'s `ApplicationID` enum, or by reading a
|
||||||
|
live BT pod's state response. Needed for §4 shell game-selection.
|
||||||
|
2. **Full BT scenario list** + per-scenario `<invalid>` maps/vehicles — from the
|
||||||
|
Console.ini BT tree.
|
||||||
|
3. **BT in-match event messages.** RP receives `Scored/Killed/Damaged/Boost/
|
||||||
|
ScoreUpdate` `MungaMessage`s during a game. Confirm BT's set (BT models
|
||||||
|
DamageMatrix + KillMarker) against `Munga Net.dll` / a live BT match.
|
||||||
|
4. **Image assets** for BT mechs/arenas (RP ships `images/red planet …`; BT needs
|
||||||
|
`images/battletech …`). Sourceable from the game `.RES`/gauge art or recreated.
|
||||||
|
5. **Whether BT reuses RP's exact plasma bitmaps/ordinals** — `cavern.egg` shows
|
||||||
|
YES (identical `[ordinals]` + `[BitMap::Large|Small::*]` mechanism, same
|
||||||
|
128×32 / 64×16). Reuse `PlasmaBitmaps`/`PlasmaFontTool` unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verification
|
||||||
|
|
||||||
|
- **Golden-egg check:** construct the `cavern.egg` scenario in BT classes
|
||||||
|
(freeforall/cavern/night/clear/27/120, pilot `cyd`/`madcat`/`veteran`/`VGL`/
|
||||||
|
`Grey`/`Red`/`Role::Default`) and diff `ToEggString()` field-by-field against
|
||||||
|
`reference/cavern.egg`. Values must match; section order may not (see §2 caveat).
|
||||||
|
- **Live check:** feed a generated egg to a real BT pod over Munga 1501 and drive
|
||||||
|
the state machine `WaitingForEgg → … → RunningMission`.
|
||||||
|
- Add BT cases to `Console/tests/TeslaConsole.DiffTests` alongside the RP ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. `reference/` contents (travels with this spec)
|
||||||
|
|
||||||
|
- `cavern.egg` — console-generated BattleTech egg (the golden field reference).
|
||||||
|
- `TESTARN.EGG` — 2nd BT egg (shows multiple `[Role::*]` blocks).
|
||||||
|
- `RPConfig.xml.template` — the RP catalog XML to mirror as `BTConfig.xml`.
|
||||||
|
- `L4PLASMA.HPP` — the pod plasma-display class (128×32) the egg bitmaps target.
|
||||||
|
|
||||||
|
## 9. The plasma tie-in (context)
|
||||||
|
|
||||||
|
The console **authors** the pod's plasma-scoreboard graphics: the egg's
|
||||||
|
`[BitMap::Large::<pilot>]` (128×32) / `[BitMap::Small::<pilot>]` (64×16) and the
|
||||||
|
`[ordinals]` 1st–4th place bitmaps are rendered on the pod's **plasma display**
|
||||||
|
(`L4PLASMA.HPP`: `plasmaWidth=128, plasmaHeight=32`, serial out on DOS COM2). So
|
||||||
|
console → egg → pod plasma panel. `PlasmaBitmaps`/`PlasmaFontTool` in TeslaConsole
|
||||||
|
generate them; BT reuses both. (In `TeslaRel410` the pod side of this is emulated
|
||||||
|
— see that repo's plasma-display notes.)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# battletech-port — hand-off package
|
||||||
|
|
||||||
|
Everything needed to add **BattleTech** support to the modernized **TeslaConsole**
|
||||||
|
(in the `TeslaSuite` repo), mirroring its existing **Red Planet** implementation.
|
||||||
|
Assembled in the `TeslaRel410` repo from the Mac 4.10 console decompilation + the
|
||||||
|
game's own data. **Transplant this whole folder into the TeslaSuite repo** and
|
||||||
|
implement from the spec there (that repo can build the .NET solution).
|
||||||
|
|
||||||
|
- **`BATTLETECH-PORT-SPEC.md`** — start here. The full port spec: shared Munga/1501
|
||||||
|
spine, the BattleTech egg format + the BT-vs-RP field diff, the RedPlanet→BattleTech
|
||||||
|
class map, the shell de-hardcoding, the `BTConfig.xml` catalog + data sources,
|
||||||
|
open questions, and verification.
|
||||||
|
- **`reference/cavern.egg`** — console-generated BattleTech egg (golden field reference).
|
||||||
|
- **`reference/TESTARN.EGG`** — 2nd BT egg (shows multiple `[Role::*]` blocks).
|
||||||
|
- **`reference/RPConfig.xml.template`** — the RP catalog XML to mirror as `BTConfig.xml`.
|
||||||
|
- **`reference/L4PLASMA.HPP`** — the pod plasma-display class the egg bitmaps target.
|
||||||
|
|
||||||
|
Context: `../CONSOLE-4.10-DECOMP.md` (the Mac console decompilation this builds on).
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
//===========================================================================//
|
||||||
|
// File: L4plasma.hpp //
|
||||||
|
// Project: MUNGA Brick: Applications-specific controls module //
|
||||||
|
// Contents: Interface specification for plasma display //
|
||||||
|
//---------------------------------------------------------------------------//
|
||||||
|
// Date Who Modification //
|
||||||
|
// -------- --- -----------------------------------------------------------//
|
||||||
|
// 04/20/95 CPB Initial coding. //
|
||||||
|
//---------------------------------------------------------------------------//
|
||||||
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All rights reserved //
|
||||||
|
// PROPRIETARY and CONFIDENTIAL //
|
||||||
|
//===========================================================================//
|
||||||
|
|
||||||
|
#if !defined(L4PLASMA_HPP)
|
||||||
|
# define L4PLASMA_HPP
|
||||||
|
|
||||||
|
# if !defined(L4VB8_HPP)
|
||||||
|
# include <l4vb8.hpp>
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if !defined(L4SERIAL_HPP)
|
||||||
|
# include <l4serial.hpp>
|
||||||
|
# endif
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
//################################ Plasma ################################
|
||||||
|
//########################################################################
|
||||||
|
class PlasmaDisplay :
|
||||||
|
public Video8BitBuffered
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
plasmaWidth=128,
|
||||||
|
plasmaHeight=32
|
||||||
|
};
|
||||||
|
|
||||||
|
PlasmaDisplay(Word port, Word int_num);
|
||||||
|
|
||||||
|
~PlasmaDisplay();
|
||||||
|
|
||||||
|
Logical
|
||||||
|
TestInstance() const;
|
||||||
|
|
||||||
|
void
|
||||||
|
ShowInstance(char *indent);
|
||||||
|
|
||||||
|
Logical
|
||||||
|
Update(Logical forceall);
|
||||||
|
|
||||||
|
void
|
||||||
|
WaitForUpdate();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void
|
||||||
|
Scan(Logical forceall);
|
||||||
|
|
||||||
|
Logical
|
||||||
|
forceAll,
|
||||||
|
updateInProgress;
|
||||||
|
|
||||||
|
PCSerial
|
||||||
|
*pc_serial;
|
||||||
|
int
|
||||||
|
currentTransferLine;
|
||||||
|
char
|
||||||
|
updateFlag[32];
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<RedPlanet>
|
||||||
|
<race name="Martian Death Race">
|
||||||
|
<invalid type="map" key="headmf" />
|
||||||
|
|
||||||
|
<color key="Red" name="Red" />
|
||||||
|
<color key="Blue" name="Blue" />
|
||||||
|
<color key="Green" name="Green" />
|
||||||
|
<color key="White" name="White" />
|
||||||
|
<color key="Pink" name="Pink" />
|
||||||
|
<color key="Aqua" name="Aqua" />
|
||||||
|
<color key="Yellow" name="Yellow" />
|
||||||
|
<color key="Purple" name="Purple" />
|
||||||
|
<color key="Black" name="Black" />
|
||||||
|
|
||||||
|
<badge key="None" name="None" />
|
||||||
|
<badge key="Celtic" name="Celtic" />
|
||||||
|
<badge key="Desert" name="Desert" />
|
||||||
|
<badge key="Flames" name="Flames" />
|
||||||
|
<badge key="Giraffe" name="Giraffe" />
|
||||||
|
<badge key="Hawaii" name="Hawaii" />
|
||||||
|
<badge key="Korean" name="Korean Camo" />
|
||||||
|
<badge key="Lightning" name="Lightning" />
|
||||||
|
<badge key="Razzle Dazzle" name="Razzle Dazzle" />
|
||||||
|
<badge key="Rising Sun" name="Rising Sun" />
|
||||||
|
<badge key="Tiger Stripe" name="Tiger Stripes" />
|
||||||
|
</race>
|
||||||
|
|
||||||
|
<football name="Martian Football">
|
||||||
|
<invalid type="map" key="pain" />
|
||||||
|
<invalid type="map" key="headoff" />
|
||||||
|
|
||||||
|
<team key="Red/Pink" name="Red/Pink" runnerColor="Pink" teamColor="Red" />
|
||||||
|
<team key="Blue/Aqua" name="Blue/Aqua" runnerColor="Aqua" teamColor="Blue" />
|
||||||
|
<team key="Green/Yellow" name="Green/Yellow" runnerColor="Yellow" teamColor="Green" />
|
||||||
|
<team key="Black/Purple" name="Black/Purple" runnerColor="Purple" teamColor="Black" />
|
||||||
|
|
||||||
|
<position key="crusher" name="Crusher" />
|
||||||
|
<position key="blocker" name="Blocker" />
|
||||||
|
<position key="runner" name="Runner" />
|
||||||
|
</football>
|
||||||
|
|
||||||
|
<map key="wise" name="Wiseguy's Wake" image="images/red planet maps/Wiseguy's Wake.bmp" />
|
||||||
|
<map key="lyzlane" name="Lyz's Lane" image="images/red planet maps/Lyz's Lane.bmp" />
|
||||||
|
<map key="yip" name="Yip's Yahoorama" image="images/red planet maps/Yip's Yahoorama.bmp" />
|
||||||
|
<map key="blade" name="Blade's Edge" image="images/red planet maps/Blade's Edge.bmp" />
|
||||||
|
<map key="otto" name="Berserker's Bahnzai" image="images/red planet maps/Berserker's Bahnzai.bmp" />
|
||||||
|
<map key="frstrm" name="Firestorm's Fury" image="images/red planet maps/Firestorm's Fury.bmp" />
|
||||||
|
<map key="burnt" name="Burnt Cookies" image="images/red planet maps/Burnt Cookies.bmp" />
|
||||||
|
<map key="brewers" name="Brewer's Bane" image="images/red planet maps/Brewer's Bane.bmp" />
|
||||||
|
<map key="pain" name="Paingod's Passage" image="images/red planet maps/Pain God's Passage.bmp" />
|
||||||
|
<map key="headoff" name="Freezemoon's Freeway" />
|
||||||
|
<map key="headmf" name="Freezemoon's Freeway" />
|
||||||
|
|
||||||
|
<vehicle key="quark" name="Quark" image="images/red planet vehicles/Quark.bmp" />
|
||||||
|
<vehicle key="lepton" name="Lepton" image="images/red planet vehicles/Quark.bmp" />
|
||||||
|
<vehicle key="proton" name="Proton" image="images/red planet vehicles/Quark.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="bug" name="Bug" image="images/red planet vehicles/Bug.bmp" />
|
||||||
|
<vehicle key="speck" name="Speck" image="images/red planet vehicles/Bug.bmp" />
|
||||||
|
<vehicle key="blkspk" name="Black Speck" image="images/red planet vehicles/Bug.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="chigger" name="Chigger" image="images/red planet vehicles/Skeeter.bmp" />
|
||||||
|
<vehicle key="flea" name="Flea" image="images/red planet vehicles/Skeeter.bmp" />
|
||||||
|
<vehicle key="roach" name="Roach" image="images/red planet vehicles/Skeeter.bmp" />
|
||||||
|
<vehicle key="skeeter" name="Skeeter" image="images/red planet vehicles/Skeeter.bmp" />
|
||||||
|
<vehicle key="wasp" name="Wasp" image="images/red planet vehicles/Skeeter.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="barge" name="Barge" image="images/red planet vehicles/Mule.bmp" />
|
||||||
|
<vehicle key="broccoli" name="Screaming Broccoli" image="images/red planet vehicles/Mule.bmp" />
|
||||||
|
<vehicle key="burro" name="Burro" image="images/red planet vehicles/Mule.bmp" />
|
||||||
|
<vehicle key="mule" name="Mule" image="images/red planet vehicles/Mule.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="bttlbrg" name="Battle Barge" image="images/red planet vehicles/Vole.bmp" />
|
||||||
|
<vehicle key="gator" name="Gator" image="images/red planet vehicles/Vole.bmp" />
|
||||||
|
<vehicle key="grunt" name="Grunt" image="images/red planet vehicles/Vole.bmp" />
|
||||||
|
<vehicle key="vole" name="Vole" image="images/red planet vehicles/Vole.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="bull" name="Bull" image="images/red planet vehicles/Bull.bmp" />
|
||||||
|
<vehicle key="tarntula" name="Tarantula" image="images/red planet vehicles/Bull.bmp" />
|
||||||
|
<!--<vehicle key="blktran" name="Black Tarantula" image="images/red planet vehicles/Bull.bmp" />-->
|
||||||
|
|
||||||
|
<vehicle key="mantis" name="Mantis" image="images/red planet vehicles/Roadblock.bmp" />
|
||||||
|
<vehicle key="ripper" name="Ripper" image="images/red planet vehicles/Roadblock.bmp" />
|
||||||
|
<vehicle key="roadblk" name="Road Block" image="images/red planet vehicles/Roadblock.bmp" />
|
||||||
|
<vehicle key="spitter" name="Spitter" image="images/red planet vehicles/Roadblock.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="puck" name="Armadillo" image="images/red planet vehicles/Armadillo.bmp" />
|
||||||
|
|
||||||
|
<vehicle key="dragon" name="Dragon" image="images/red planet vehicles/Police.bmp" />
|
||||||
|
|
||||||
|
<time key="day" name="Day" />
|
||||||
|
<time key="night" name="Night" />
|
||||||
|
|
||||||
|
<weather key="clear" name="Clear" />
|
||||||
|
<weather key="fog" name="Light Fog" />
|
||||||
|
<weather key="soup" name="Heavy Fog" />
|
||||||
|
</RedPlanet>
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
[mission]
|
||||||
|
adventure=BattleTech
|
||||||
|
map=arena1
|
||||||
|
scenario=freeforall
|
||||||
|
time=day
|
||||||
|
weather=clear
|
||||||
|
temperature=27
|
||||||
|
length=600
|
||||||
|
[ordinals]
|
||||||
|
bitmap=Ordinal::BitMap::1
|
||||||
|
bitmap=Ordinal::BitMap::2
|
||||||
|
bitmap=Ordinal::BitMap::3
|
||||||
|
bitmap=Ordinal::BitMap::4
|
||||||
|
[Ordinal::BitMap::1]
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=000038000003C000001FF00000000F00
|
||||||
|
bitmap=0000F8000003C000003FF80000000F00
|
||||||
|
bitmap=0001F8000003C00000707C0000000F00
|
||||||
|
bitmap=0001F8000003C00000603C0000000F00
|
||||||
|
bitmap=00007801FC0FF00000003C3DF807FF00
|
||||||
|
bitmap=00007803FE0FF00000003C3FFC0FFF00
|
||||||
|
bitmap=00007803C703C00000003C3E3C1F0F00
|
||||||
|
bitmap=000078078303C0000000783C1E1E0F00
|
||||||
|
bitmap=000078078003C0000000783C1E1E0F00
|
||||||
|
bitmap=00007807C003C0000000F03C1E1E0F00
|
||||||
|
bitmap=00007807F003C0000001E03C1E1E0F00
|
||||||
|
bitmap=00007803FC03C0000003C03C1E1E0F00
|
||||||
|
bitmap=00007801FE03C0000007803C1E1E0F00
|
||||||
|
bitmap=000078007F03C000000F003C1E1E0F00
|
||||||
|
bitmap=000078001F03C000001E003C1E1E0F00
|
||||||
|
bitmap=000078000F03C000003C003C1E1E0F00
|
||||||
|
bitmap=000078060F03C0000078003C1E1E0F00
|
||||||
|
bitmap=000078071E03E0000078003C1E1F1F00
|
||||||
|
bitmap=00007803FE01F000007FFC3C1E0FFF00
|
||||||
|
bitmap=00007801FC00F000007FFC3C1E07EF00
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
x=128
|
||||||
|
y=32
|
||||||
|
width=8
|
||||||
|
[Ordinal::BitMap::2]
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=001FFF0000003C0000003C03C0780000
|
||||||
|
bitmap=001FFF0000003C0000007C03C0780000
|
||||||
|
bitmap=00000E0000003C000000FC03C0780000
|
||||||
|
bitmap=00003C0000003C000001BC03C0780000
|
||||||
|
bitmap=0000700F3E1FFC000003BC0FF07BF000
|
||||||
|
bitmap=0001E00F7E3FFC0000073C0FF07FF800
|
||||||
|
bitmap=0003800FFE7C3C0000063C03C07C7800
|
||||||
|
bitmap=0007F80FFE783C00000C3C03C0783C00
|
||||||
|
bitmap=0007FE0F80783C0000183C03C0783C00
|
||||||
|
bitmap=00001E0F00783C0000383C03C0783C00
|
||||||
|
bitmap=00000F0F00783C0000703C03C0783C00
|
||||||
|
bitmap=00000F0F00783C00007FFF03C0783C00
|
||||||
|
bitmap=00000F0F00783C00007FFF03C0783C00
|
||||||
|
bitmap=00000F0F00783C0000003C03C0783C00
|
||||||
|
bitmap=00000F0F00783C0000003C03C0783C00
|
||||||
|
bitmap=00000F0F00783C0000003C03C0783C00
|
||||||
|
bitmap=00180F0F00783C0000003C03C0783C00
|
||||||
|
bitmap=001C1F0F007C7C0000003C03E0783C00
|
||||||
|
bitmap=000FFE0F003FFC0000003C01F0783C00
|
||||||
|
bitmap=0007FC0F001FBC0000003C00F0783C00
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
x=128
|
||||||
|
y=32
|
||||||
|
width=8
|
||||||
|
[Ordinal::BitMap::3]
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=001FFF03C07800000000FC03C0780000
|
||||||
|
bitmap=001FFF03C07800000003FC03C0780000
|
||||||
|
bitmap=001E0003C07800000007C003C0780000
|
||||||
|
bitmap=001E0003C0780000000F0003C0780000
|
||||||
|
bitmap=001E000FF07BF000000F000FF07BF000
|
||||||
|
bitmap=001E000FF07FF800001E000FF07FF800
|
||||||
|
bitmap=001E0003C07C7800001E0003C07C7800
|
||||||
|
bitmap=001FFC03C0783C00001EFC03C0783C00
|
||||||
|
bitmap=001FFE03C0783C00001FFE03C0783C00
|
||||||
|
bitmap=00001F03C0783C00001F1F03C0783C00
|
||||||
|
bitmap=00000F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00000F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00000F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00000F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00000F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00000F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00180F03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=001C1F03E0783C00001F1F03E0783C00
|
||||||
|
bitmap=000FFE01F0783C00000FFE01F0783C00
|
||||||
|
bitmap=0007FC00F0783C000007FC00F0783C00
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
x=128
|
||||||
|
y=32
|
||||||
|
width=8
|
||||||
|
[Ordinal::BitMap::4]
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=001FFF03C07800000007FC03C0780000
|
||||||
|
bitmap=001FFF03C0780000000FFE03C0780000
|
||||||
|
bitmap=00000F03C0780000001F1F03C0780000
|
||||||
|
bitmap=00000F03C0780000001E0F03C0780000
|
||||||
|
bitmap=00000F0FF07BF000001E0F0FF07BF000
|
||||||
|
bitmap=00001F0FF07FF800001E0F0FF07FF800
|
||||||
|
bitmap=00001E03C07C7800001E0F03C07C7800
|
||||||
|
bitmap=00003E03C0783C00001E0F03C0783C00
|
||||||
|
bitmap=00003C03C0783C00000F1E03C0783C00
|
||||||
|
bitmap=00003C03C0783C000007FC03C0783C00
|
||||||
|
bitmap=00007803C0783C000007FC03C0783C00
|
||||||
|
bitmap=00007803C0783C00000F1E03C0783C00
|
||||||
|
bitmap=00007803C0783C00001E0F03C0783C00
|
||||||
|
bitmap=0000F003C0783C00001E0F03C0783C00
|
||||||
|
bitmap=0000F003C0783C00001E0F03C0783C00
|
||||||
|
bitmap=0000F003C0783C00001E0F03C0783C00
|
||||||
|
bitmap=0000F003C0783C00001E0F03C0783C00
|
||||||
|
bitmap=0000F003E0783C00000F1E03E0783C00
|
||||||
|
bitmap=0000F001F0783C00000FFE01F0783C00
|
||||||
|
bitmap=0000F000F0783C000007FC00F0783C00
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
x=128
|
||||||
|
y=32
|
||||||
|
width=8
|
||||||
|
[pilots]
|
||||||
|
pilot=200.0.0.113
|
||||||
|
[200.0.0.113]
|
||||||
|
hostType=0
|
||||||
|
advancedDamage=1
|
||||||
|
loadzones=1
|
||||||
|
name=cyd
|
||||||
|
bitmapindex=1
|
||||||
|
experience=veteran
|
||||||
|
badge=VGL
|
||||||
|
patch=Yellow
|
||||||
|
role=Role::Default
|
||||||
|
dropzone=one
|
||||||
|
vehicle=madcat
|
||||||
|
vehicleValue=0
|
||||||
|
color=White
|
||||||
|
[largebitmap]
|
||||||
|
bitmap=BitMap::Large::cyd
|
||||||
|
[BitMap::Large::cyd]
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=000000000000000000003C0000000000
|
||||||
|
bitmap=000000000000000000003C0000000000
|
||||||
|
bitmap=000000000000000000003C0000000000
|
||||||
|
bitmap=000000000000000000003C0000000000
|
||||||
|
bitmap=000000000000FCF00E0FBC0000000000
|
||||||
|
bitmap=000000000003FE781C1FFC0000000000
|
||||||
|
bitmap=000000000007FE781C3C7C0000000000
|
||||||
|
bitmap=00000000000F82783C3C3C0000000000
|
||||||
|
bitmap=00000000001F003C38783C0000000000
|
||||||
|
bitmap=00000000001E003C78783C0000000000
|
||||||
|
bitmap=00000000001E003C70783C0000000000
|
||||||
|
bitmap=00000000001E003E70783C0000000000
|
||||||
|
bitmap=00000000001E001EE0783C0000000000
|
||||||
|
bitmap=00000000001E001EE0783C0000000000
|
||||||
|
bitmap=00000000001F001FE0783C0000000000
|
||||||
|
bitmap=00000000000F020FC07C7C0000000000
|
||||||
|
bitmap=000000000007FE0FC03FFC0000000000
|
||||||
|
bitmap=000000000003FE0F803FFC0000000000
|
||||||
|
bitmap=000000000000FC07800F3C0000000000
|
||||||
|
bitmap=00000000000000070000000000000000
|
||||||
|
bitmap=00000000000000070000000000000000
|
||||||
|
bitmap=000000000000000F0000000000000000
|
||||||
|
bitmap=000000000000000E0000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
x=128
|
||||||
|
y=32
|
||||||
|
width=8
|
||||||
|
[smallbitmap]
|
||||||
|
bitmap=BitMap::Small::cyd
|
||||||
|
[BitMap::Small::cyd]
|
||||||
|
bitmap=00000000000000000000000000000000
|
||||||
|
bitmap=00000000000000000000000000600000
|
||||||
|
bitmap=00000000006000000000000000600000
|
||||||
|
bitmap=000001EC33E000000000030666600000
|
||||||
|
bitmap=000003066660000000000303C6600000
|
||||||
|
bitmap=00000303C66000000000030186600000
|
||||||
|
bitmap=000001E183E000000000000300000000
|
||||||
|
bitmap=00000003000000000000000000000000
|
||||||
|
x=64
|
||||||
|
y=16
|
||||||
|
width=4
|
||||||
|
[Role::Default]
|
||||||
|
model=dfltrole
|
||||||
|
[Role::NoReturn]
|
||||||
|
model=noretun
|
||||||
Binary file not shown.
@@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
pefparse.py -- Minimal parser/disassembler for classic Mac OS CFM
|
||||||
|
Preferred Executable Format (PEF) PowerPC containers ('Joy!peff pwpc').
|
||||||
|
|
||||||
|
Written for the VWE Tesla restoration project to decompile "Console 4.10".
|
||||||
|
Big-endian PowerPC. Pure-python; only external dep is capstone (optional,
|
||||||
|
for the `dis` command).
|
||||||
|
|
||||||
|
PEF reference: Apple "Mac OS Runtime Architectures", ch. Preferred
|
||||||
|
Executable Format. Container header (40 bytes), then N section headers
|
||||||
|
(28 bytes each), then the section name table, then section bodies.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
py -3.13 pefparse.py <file> header # container + section headers
|
||||||
|
py -3.13 pefparse.py <file> loader # loader section: imports/exports/entry
|
||||||
|
py -3.13 pefparse.py <file> strings # printable strings in code+data
|
||||||
|
py -3.13 pefparse.py <file> dis [secidx] [start] [count] # disassemble
|
||||||
|
py -3.13 pefparse.py <file> dump <secidx> <off> <len> # hex dump section body
|
||||||
|
"""
|
||||||
|
import sys, struct, string
|
||||||
|
|
||||||
|
def u32(b, o): return struct.unpack_from('>I', b, o)[0]
|
||||||
|
def u16(b, o): return struct.unpack_from('>H', b, o)[0]
|
||||||
|
def s32(b, o): return struct.unpack_from('>i', b, o)[0]
|
||||||
|
def u8(b, o): return b[o]
|
||||||
|
|
||||||
|
SECTION_KINDS = {
|
||||||
|
0: 'Code', 1: 'UnpackedData', 2: 'PatternInitData', 3: 'Constant',
|
||||||
|
4: 'Loader', 5: 'Debug', 6: 'ExecutableData', 7: 'Exception', 8: 'Traceback',
|
||||||
|
}
|
||||||
|
|
||||||
|
class Section:
|
||||||
|
def __init__(self, idx, hdr, container):
|
||||||
|
(self.name_off, self.default_addr, self.total_size, self.unpacked_size,
|
||||||
|
self.packed_size, self.container_off, self.kind, self.share,
|
||||||
|
self.align, self.rsvd) = hdr
|
||||||
|
self.idx = idx
|
||||||
|
self.name = None
|
||||||
|
self._container = container
|
||||||
|
|
||||||
|
def raw(self):
|
||||||
|
"""Raw (possibly packed) bytes of the section body as stored."""
|
||||||
|
return self._container[self.container_off:self.container_off + self.packed_size]
|
||||||
|
|
||||||
|
def data(self):
|
||||||
|
"""Unpacked section body. PatternInitData(2) is RLE-decompressed."""
|
||||||
|
raw = self.raw()
|
||||||
|
if self.kind == 2:
|
||||||
|
return unpack_pidata(raw, self.unpacked_size)
|
||||||
|
return raw
|
||||||
|
|
||||||
|
class PEF:
|
||||||
|
def __init__(self, blob):
|
||||||
|
self.blob = blob
|
||||||
|
assert blob[0:4] == b'Joy!', "not a PEF (missing Joy!)"
|
||||||
|
assert blob[4:8] == b'peff', "not a PEF (missing peff)"
|
||||||
|
self.arch = blob[8:12]
|
||||||
|
self.format_version = u32(blob, 12)
|
||||||
|
self.timestamp = u32(blob, 16)
|
||||||
|
self.old_def_ver = u32(blob, 20)
|
||||||
|
self.old_imp_ver = u32(blob, 24)
|
||||||
|
self.current_ver = u32(blob, 28)
|
||||||
|
self.section_count = u16(blob, 32)
|
||||||
|
self.inst_section_count = u16(blob, 34)
|
||||||
|
self.sections = []
|
||||||
|
o = 40
|
||||||
|
for i in range(self.section_count):
|
||||||
|
hdr = struct.unpack_from('>iIIIIIBBBB', blob, o)
|
||||||
|
self.sections.append(Section(i, hdr, blob))
|
||||||
|
o += 28
|
||||||
|
# section name table follows the section headers
|
||||||
|
name_tbl_off = o
|
||||||
|
for s in self.sections:
|
||||||
|
if s.name_off >= 0:
|
||||||
|
s.name = read_cstr(blob, name_tbl_off + s.name_off)
|
||||||
|
|
||||||
|
def loader(self):
|
||||||
|
for s in self.sections:
|
||||||
|
if s.kind == 4:
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_cstr(b, o):
|
||||||
|
e = o
|
||||||
|
while e < len(b) and b[e] != 0:
|
||||||
|
e += 1
|
||||||
|
return b[o:e].decode('mac_roman', 'replace')
|
||||||
|
|
||||||
|
def unpack_pidata(raw, unpacked_size):
|
||||||
|
"""Decompress PEF pattern-initialized data (RLE opcode stream)."""
|
||||||
|
out = bytearray()
|
||||||
|
i = 0
|
||||||
|
n = len(raw)
|
||||||
|
def argcount():
|
||||||
|
nonlocal i
|
||||||
|
val = 0
|
||||||
|
while True:
|
||||||
|
b = raw[i]; i += 1
|
||||||
|
val = (val << 7) | (b & 0x7f)
|
||||||
|
if not (b & 0x80):
|
||||||
|
break
|
||||||
|
return val
|
||||||
|
while i < n:
|
||||||
|
op = raw[i]; i += 1
|
||||||
|
opcode = op >> 5
|
||||||
|
count5 = op & 0x1f
|
||||||
|
cnt = count5 if count5 else argcount()
|
||||||
|
if opcode == 0: # Zero
|
||||||
|
out.extend(b'\x00' * cnt)
|
||||||
|
elif opcode == 1: # blockCopy
|
||||||
|
out.extend(raw[i:i+cnt]); i += cnt
|
||||||
|
elif opcode == 2: # repeatedBlock
|
||||||
|
rpt = argcount()
|
||||||
|
block = raw[i:i+cnt]; i += cnt
|
||||||
|
for _ in range(rpt + 1):
|
||||||
|
out.extend(block)
|
||||||
|
elif opcode == 3: # interleaveRepeatBlockWithBlockCopy
|
||||||
|
cs = argcount() # custom size
|
||||||
|
rc = argcount() # repeat count
|
||||||
|
common = raw[i:i+cnt]; i += cnt
|
||||||
|
for _ in range(rc):
|
||||||
|
out.extend(common)
|
||||||
|
out.extend(raw[i:i+cs]); i += cs
|
||||||
|
out.extend(common)
|
||||||
|
elif opcode == 4: # interleaveRepeatBlockWithZero
|
||||||
|
cs = argcount()
|
||||||
|
rc = argcount()
|
||||||
|
for _ in range(rc):
|
||||||
|
out.extend(b'\x00' * cnt)
|
||||||
|
out.extend(raw[i:i+cs]); i += cs
|
||||||
|
out.extend(b'\x00' * cnt)
|
||||||
|
else:
|
||||||
|
raise ValueError("bad PIDATA opcode %d at %d" % (opcode, i))
|
||||||
|
return bytes(out[:unpacked_size])
|
||||||
|
|
||||||
|
# ---- Loader section parsing -------------------------------------------------
|
||||||
|
|
||||||
|
def parse_loader(sec):
|
||||||
|
b = sec.data()
|
||||||
|
(main_section, main_offset, init_section, init_offset,
|
||||||
|
term_section, term_offset, imported_lib_count, total_imported_sym_count,
|
||||||
|
reloc_section_count, reloc_instr_offset, loader_strings_offset,
|
||||||
|
export_hash_offset, export_hash_table_power, exported_symbol_count) = \
|
||||||
|
struct.unpack_from('>iIiIiIIIIIIIII', b, 0)
|
||||||
|
info = dict(main_section=main_section, main_offset=main_offset,
|
||||||
|
init_section=init_section, init_offset=init_offset,
|
||||||
|
term_section=term_section, term_offset=term_offset,
|
||||||
|
imported_lib_count=imported_lib_count,
|
||||||
|
total_imported_sym_count=total_imported_sym_count,
|
||||||
|
reloc_section_count=reloc_section_count,
|
||||||
|
reloc_instr_offset=reloc_instr_offset,
|
||||||
|
loader_strings_offset=loader_strings_offset,
|
||||||
|
export_hash_offset=export_hash_offset,
|
||||||
|
export_hash_table_power=export_hash_table_power,
|
||||||
|
exported_symbol_count=exported_symbol_count)
|
||||||
|
# Imported library table: starts at offset 56
|
||||||
|
libs = []
|
||||||
|
o = 56
|
||||||
|
for _ in range(imported_lib_count):
|
||||||
|
(name_off, old_impl_ver, curr_ver, imp_sym_count,
|
||||||
|
first_imp_sym, options, rsvdA, rsvdB) = struct.unpack_from('>IIIIIBBH', b, o)
|
||||||
|
libs.append(dict(name_off=name_off, imp_sym_count=imp_sym_count,
|
||||||
|
first_imp_sym=first_imp_sym, options=options))
|
||||||
|
o += 24
|
||||||
|
# Imported symbol table follows lib table
|
||||||
|
imp_syms = []
|
||||||
|
for _ in range(total_imported_sym_count):
|
||||||
|
val = u32(b, o); o += 4
|
||||||
|
sym_class = val >> 24
|
||||||
|
name_off = val & 0x00FFFFFF
|
||||||
|
imp_syms.append((sym_class, name_off))
|
||||||
|
strtab = info['loader_strings_offset']
|
||||||
|
for lib in libs:
|
||||||
|
lib['name'] = read_cstr(b, strtab + lib['name_off'])
|
||||||
|
syms = []
|
||||||
|
for k in range(lib['first_imp_sym'], lib['first_imp_sym'] + lib['imp_sym_count']):
|
||||||
|
cls, noff = imp_syms[k]
|
||||||
|
syms.append((cls, read_cstr(b, strtab + noff)))
|
||||||
|
lib['symbols'] = syms
|
||||||
|
# Exported symbols (loader export hash). Layout:
|
||||||
|
# export hash table (2^power u32 slots) at export_hash_offset
|
||||||
|
# then key table (u32 per exported symbol)
|
||||||
|
# then exported symbol table (10 bytes each: class/name u32, value u32, sect u16)
|
||||||
|
exports = []
|
||||||
|
hash_slots = 1 << export_hash_table_power
|
||||||
|
key_tab_off = export_hash_offset + hash_slots * 4
|
||||||
|
sym_tab_off = key_tab_off + exported_symbol_count * 4
|
||||||
|
for k in range(exported_symbol_count):
|
||||||
|
eo = sym_tab_off + k * 10
|
||||||
|
clsname = u32(b, eo)
|
||||||
|
value = u32(b, eo + 4)
|
||||||
|
sect = u16(b, eo + 8)
|
||||||
|
sym_class = clsname >> 24
|
||||||
|
name_off = clsname & 0x00FFFFFF
|
||||||
|
name = read_cstr(b, strtab + name_off)
|
||||||
|
exports.append(dict(name=name, sym_class=sym_class, value=value, section=sect))
|
||||||
|
return info, libs, exports
|
||||||
|
|
||||||
|
SYM_CLASS = {0: 'code(TVect)', 1: 'data', 2: 'TVector', 3: 'TOC/Glue',
|
||||||
|
4: 'linker/glue', 0x40: 'weak?'}
|
||||||
|
|
||||||
|
# ---- commands ---------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_header(pef):
|
||||||
|
print("PEF container: arch=%s formatVersion=%d timestamp=0x%08x currentVer=0x%08x"
|
||||||
|
% (pef.arch.decode('ascii','replace'), pef.format_version, pef.timestamp, pef.current_ver))
|
||||||
|
print("sectionCount=%d instantiatedSectionCount=%d" % (pef.section_count, pef.inst_section_count))
|
||||||
|
print()
|
||||||
|
print("idx name kind default_addr total unpacked packed fileoff align")
|
||||||
|
for s in pef.sections:
|
||||||
|
print("%3d %-14s %-19s 0x%08x %-9d %-9d %-9d 0x%06x %d" % (
|
||||||
|
s.idx, (s.name or '-'), "%d:%s" % (s.kind, SECTION_KINDS.get(s.kind, '?')),
|
||||||
|
s.default_addr, s.total_size, s.unpacked_size, s.packed_size,
|
||||||
|
s.container_off, s.align))
|
||||||
|
|
||||||
|
def cmd_loader(pef):
|
||||||
|
lsec = pef.loader()
|
||||||
|
if not lsec:
|
||||||
|
print("no loader section"); return
|
||||||
|
info, libs, exports = parse_loader(lsec)
|
||||||
|
print("=== Loader header ===")
|
||||||
|
for k, v in info.items():
|
||||||
|
if isinstance(v, int) and v > 9 and k.endswith(('offset','section','count')) is False:
|
||||||
|
print(" %-28s 0x%x (%d)" % (k, v, v))
|
||||||
|
else:
|
||||||
|
print(" %-28s %s" % (k, v))
|
||||||
|
ms = info['main_section']
|
||||||
|
print(" ENTRY main: section %d offset 0x%x" % (info['main_section'], info['main_offset']) if ms>=0 else " (no main)")
|
||||||
|
print(" ENTRY init: section %d offset 0x%x" % (info['init_section'], info['init_offset']))
|
||||||
|
print(" ENTRY term: section %d offset 0x%x" % (info['term_section'], info['term_offset']))
|
||||||
|
print()
|
||||||
|
print("=== Imported libraries (%d) ===" % len(libs))
|
||||||
|
for lib in libs:
|
||||||
|
print("\n-- %s (%d symbols) --" % (lib['name'], lib['imp_sym_count']))
|
||||||
|
for j,(cls,name) in enumerate(lib['symbols']):
|
||||||
|
print(" [%4d] cls=%s %s" % (lib['first_imp_sym']+j, SYM_CLASS.get(cls, hex(cls)), name))
|
||||||
|
print()
|
||||||
|
print("=== Exported symbols (%d) ===" % len(exports))
|
||||||
|
for e in exports:
|
||||||
|
print(" %-40s cls=%s value=0x%x sect=%d" % (e['name'], SYM_CLASS.get(e['sym_class'], hex(e['sym_class'])), e['value'], e['section']))
|
||||||
|
|
||||||
|
def iter_strings(data, minlen=4):
|
||||||
|
cur = bytearray()
|
||||||
|
start = 0
|
||||||
|
printable = set(bytes(string.printable, 'ascii')) - set(b'\t\n\r\x0b\x0c')
|
||||||
|
printable |= set(b' ')
|
||||||
|
for i, ch in enumerate(data):
|
||||||
|
if ch in printable:
|
||||||
|
if not cur:
|
||||||
|
start = i
|
||||||
|
cur.append(ch)
|
||||||
|
else:
|
||||||
|
if len(cur) >= minlen:
|
||||||
|
yield start, cur.decode('ascii', 'replace')
|
||||||
|
cur = bytearray()
|
||||||
|
if len(cur) >= minlen:
|
||||||
|
yield start, cur.decode('ascii', 'replace')
|
||||||
|
|
||||||
|
def cmd_strings(pef, minlen=4):
|
||||||
|
for s in pef.sections:
|
||||||
|
if s.kind in (0,1,2,3):
|
||||||
|
data = s.data()
|
||||||
|
print("### section %d (%s) base=0x%x ###" % (s.idx, SECTION_KINDS.get(s.kind), s.default_addr))
|
||||||
|
for off, st in iter_strings(data, minlen):
|
||||||
|
print("0x%08x %s" % (s.default_addr + off, st))
|
||||||
|
|
||||||
|
def cmd_dump(pef, secidx, off, length):
|
||||||
|
data = pef.sections[secidx].data()
|
||||||
|
chunk = data[off:off+length]
|
||||||
|
for i in range(0, len(chunk), 16):
|
||||||
|
row = chunk[i:i+16]
|
||||||
|
hexs = ' '.join('%02x' % c for c in row)
|
||||||
|
asci = ''.join(chr(c) if 32 <= c < 127 else '.' for c in row)
|
||||||
|
print('%08x %-48s %s' % (off+i, hexs, asci))
|
||||||
|
|
||||||
|
def cmd_dis(pef, secidx=0, start=0, count=200):
|
||||||
|
import capstone
|
||||||
|
sec = pef.sections[secidx]
|
||||||
|
data = sec.data()
|
||||||
|
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
||||||
|
md.detail = False
|
||||||
|
base = sec.default_addr
|
||||||
|
for insn in md.disasm(data[start:start+count*4], base+start):
|
||||||
|
print("0x%08x %-8s %s" % (insn.address, insn.mnemonic, insn.op_str))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
path = sys.argv[1]
|
||||||
|
cmd = sys.argv[2] if len(sys.argv) > 2 else 'header'
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
blob = f.read()
|
||||||
|
pef = PEF(blob)
|
||||||
|
if cmd == 'header': cmd_header(pef)
|
||||||
|
elif cmd == 'loader': cmd_loader(pef)
|
||||||
|
elif cmd == 'strings':
|
||||||
|
minlen = int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
||||||
|
cmd_strings(pef, minlen)
|
||||||
|
elif cmd == 'dump':
|
||||||
|
cmd_dump(pef, int(sys.argv[3]), int(sys.argv[4],0), int(sys.argv[5],0))
|
||||||
|
elif cmd == 'dis':
|
||||||
|
secidx = int(sys.argv[3]) if len(sys.argv) > 3 else 0
|
||||||
|
start = int(sys.argv[4],0) if len(sys.argv) > 4 else 0
|
||||||
|
count = int(sys.argv[5],0) if len(sys.argv) > 5 else 200
|
||||||
|
cmd_dis(pef, secidx, start, count)
|
||||||
|
else:
|
||||||
|
print("unknown cmd", cmd)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ppcxref.py -- TOC-aware PPC annotator for the Console 4.10 PEF.
|
||||||
|
|
||||||
|
Resolves r2(TOC)-relative loads to data addresses and, when those point at
|
||||||
|
C strings, annotates the disassembly. Also builds a string cross-reference:
|
||||||
|
which code addresses reference which strings.
|
||||||
|
|
||||||
|
r2 (TOC base) = data section offset 0x8000 (from the main transition vector).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
py -3.13 ppcxref.py annotate <start> <count> # annotated disasm
|
||||||
|
py -3.13 ppcxref.py xref <substring> # find code refs to strings matching substring
|
||||||
|
py -3.13 ppcxref.py toc <hexoff> # what does TOC[-off(r2)] point to
|
||||||
|
"""
|
||||||
|
import sys, struct, capstone
|
||||||
|
sys.path.insert(0, __import__('os').path.dirname(__file__))
|
||||||
|
from pefparse import PEF
|
||||||
|
|
||||||
|
BIN = r"C:\VWE\TeslaRel410\410console\4_10-console-extracted\Console 4.10"
|
||||||
|
TOC_BASE = 0x8000 # r2 value = data section offset
|
||||||
|
|
||||||
|
pef = PEF(open(BIN,'rb').read())
|
||||||
|
CODE = pef.sections[0].data()
|
||||||
|
DATA = pef.sections[1].data()
|
||||||
|
|
||||||
|
def d32(off):
|
||||||
|
if off < 0 or off+4 > len(DATA): return None
|
||||||
|
return struct.unpack_from('>I', DATA, off)[0]
|
||||||
|
|
||||||
|
def cstr_at(off, maxlen=80):
|
||||||
|
if off < 0 or off >= len(DATA): return None
|
||||||
|
e = off
|
||||||
|
while e < len(DATA) and DATA[e] != 0 and (e-off) < maxlen:
|
||||||
|
c = DATA[e]
|
||||||
|
if c < 0x09 or (c > 0x0d and c < 0x20) or c > 0x7e:
|
||||||
|
# allow only printable; stop otherwise
|
||||||
|
if c != 0xc9: # ellipsis
|
||||||
|
break
|
||||||
|
e += 1
|
||||||
|
s = DATA[off:e]
|
||||||
|
try:
|
||||||
|
txt = s.decode('mac_roman')
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
# require mostly printable and length>=3
|
||||||
|
printable = sum(1 for c in s if 0x20 <= c <= 0x7e)
|
||||||
|
if len(s) >= 3 and printable >= max(3, int(len(s)*0.8)):
|
||||||
|
return txt
|
||||||
|
return None
|
||||||
|
|
||||||
|
def toc_target(disp):
|
||||||
|
"""disp is signed offset from r2. Returns (data_off, pointed_value, string_or_None)."""
|
||||||
|
off = TOC_BASE + disp
|
||||||
|
val = d32(off)
|
||||||
|
s = None
|
||||||
|
if val is not None:
|
||||||
|
s = cstr_at(val)
|
||||||
|
return off, val, s
|
||||||
|
|
||||||
|
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
||||||
|
md.detail = True
|
||||||
|
|
||||||
|
def annotate(start, count):
|
||||||
|
base = 0
|
||||||
|
for insn in md.disasm(CODE[start:start+count*4], base+start):
|
||||||
|
note = ''
|
||||||
|
# detect r2-relative: op_str contains "r2)" with a displacement
|
||||||
|
ops = insn.op_str
|
||||||
|
# capstone PPC prints e.g. "r4, -0x6c9c(r2)" or "r3, r2, -0x6848"
|
||||||
|
disp = None
|
||||||
|
if '(r2)' in ops:
|
||||||
|
# form: rX, disp(r2)
|
||||||
|
try:
|
||||||
|
d = ops.split(',')[-1].strip()
|
||||||
|
d = d[:d.index('(r2)')]
|
||||||
|
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
|
||||||
|
except Exception:
|
||||||
|
disp = None
|
||||||
|
if disp is not None:
|
||||||
|
o, val, s = toc_target(disp)
|
||||||
|
if val is not None:
|
||||||
|
note = "; TOC[0x%x]=*0x%x=0x%x" % (o, o, val)
|
||||||
|
if s: note += ' "%s"' % s
|
||||||
|
elif 'r2,' in ops and insn.mnemonic in ('addi','addic','subi'):
|
||||||
|
# form: rX, r2, disp -> address = TOC_BASE+disp (pointer to data directly)
|
||||||
|
try:
|
||||||
|
d = ops.split(',')[-1].strip()
|
||||||
|
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
|
||||||
|
except Exception:
|
||||||
|
disp = None
|
||||||
|
if disp is not None:
|
||||||
|
addr = TOC_BASE + disp
|
||||||
|
s = cstr_at(addr)
|
||||||
|
note = "; &data[0x%x]" % addr
|
||||||
|
if s: note += ' "%s"' % s
|
||||||
|
elif insn.mnemonic == 'lis':
|
||||||
|
# high-half load, often followed by addi to form an absolute data ptr
|
||||||
|
note = ''
|
||||||
|
print("0x%08x %-8s %-28s %s" % (insn.address, insn.mnemonic, insn.op_str, note))
|
||||||
|
|
||||||
|
def build_string_table():
|
||||||
|
"""All C strings in data with >=4 printable chars -> {addr: text}."""
|
||||||
|
tbl = {}
|
||||||
|
i = 0
|
||||||
|
n = len(DATA)
|
||||||
|
while i < n:
|
||||||
|
if 0x20 <= DATA[i] <= 0x7e:
|
||||||
|
j = i
|
||||||
|
while j < n and (0x20 <= DATA[j] <= 0x7e):
|
||||||
|
j += 1
|
||||||
|
if j - i >= 4:
|
||||||
|
tbl[i] = DATA[i:j].decode('ascii','replace')
|
||||||
|
i = j
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
return tbl
|
||||||
|
|
||||||
|
def xref(substr):
|
||||||
|
# find data offsets of strings containing substr
|
||||||
|
strtbl = build_string_table()
|
||||||
|
targets = {a:t for a,t in strtbl.items() if substr.lower() in t.lower()}
|
||||||
|
if not targets:
|
||||||
|
print("no strings match", substr); return
|
||||||
|
print("=== target strings ===")
|
||||||
|
for a,t in sorted(targets.items()):
|
||||||
|
print(" 0x%06x %s" % (a, t))
|
||||||
|
# Build TOC reverse map: which TOC slots hold pointers into these strings
|
||||||
|
# (pointer may target start OR interior). Map value->slot.
|
||||||
|
tocslots = {}
|
||||||
|
for slot in range(0, len(DATA), 4):
|
||||||
|
v = d32(slot)
|
||||||
|
if v in targets:
|
||||||
|
tocslots.setdefault(v, []).append(slot)
|
||||||
|
# Also direct addi r2 references use TOC_BASE+disp == string addr directly
|
||||||
|
print("\n=== code references ===")
|
||||||
|
# Disassemble whole code, look for r2-relative producing an address in targets
|
||||||
|
for insn in md.disasm(CODE, 0):
|
||||||
|
ops = insn.op_str
|
||||||
|
hit = None
|
||||||
|
if '(r2)' in ops:
|
||||||
|
try:
|
||||||
|
d = ops.split(',')[-1].strip(); d = d[:d.index('(r2)')]
|
||||||
|
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
|
||||||
|
o = TOC_BASE+disp; v = d32(o)
|
||||||
|
if v in targets: hit = (v, targets[v], 'TOC')
|
||||||
|
except Exception: pass
|
||||||
|
elif 'r2,' in ops and insn.mnemonic in ('addi','subi'):
|
||||||
|
try:
|
||||||
|
d = ops.split(',')[-1].strip()
|
||||||
|
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
|
||||||
|
addr = TOC_BASE+disp
|
||||||
|
if addr in targets: hit = (addr, targets[addr], 'direct')
|
||||||
|
except Exception: pass
|
||||||
|
if hit:
|
||||||
|
print(" 0x%08x %-8s %-24s -> %s \"%s\"" % (insn.address, insn.mnemonic, ops, hit[2], hit[1]))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
if cmd == 'annotate':
|
||||||
|
annotate(int(sys.argv[2],0), int(sys.argv[3],0))
|
||||||
|
elif cmd == 'xref':
|
||||||
|
xref(sys.argv[2])
|
||||||
|
elif cmd == 'toc':
|
||||||
|
o,v,s = toc_target(int(sys.argv[2],0))
|
||||||
|
print("TOC off=0x%x val=0x%x str=%s" % (o, v if v else 0, s))
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
rsrcparse.py -- classic Mac OS resource-fork parser.
|
||||||
|
Reads a resource fork (raw, or embedded in an AppleDouble ._ sidecar) and
|
||||||
|
lists resource types/IDs/names. Optionally dumps a given type.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
py -3.13 rsrcparse.py <._file> [--base 0x52] map
|
||||||
|
py -3.13 rsrcparse.py <._file> [--base 0x52] dump <TYPE> [id]
|
||||||
|
"""
|
||||||
|
import sys, struct
|
||||||
|
|
||||||
|
def u32(b,o): return struct.unpack_from('>I',b,o)[0]
|
||||||
|
def u16(b,o): return struct.unpack_from('>H',b,o)[0]
|
||||||
|
|
||||||
|
def appledouble_rfork_base(blob):
|
||||||
|
# AppleDouble: magic 0x00051607; entries describe forks. Entry id 2 = resource fork.
|
||||||
|
if u32(blob,0) != 0x00051607:
|
||||||
|
return 0, len(blob)
|
||||||
|
n = u16(blob, 24)
|
||||||
|
o = 26
|
||||||
|
for _ in range(n):
|
||||||
|
eid, off, length = struct.unpack_from('>III', blob, o); o += 12
|
||||||
|
if eid == 2:
|
||||||
|
return off, length
|
||||||
|
return 0, len(blob)
|
||||||
|
|
||||||
|
def parse(blob, base):
|
||||||
|
rf = blob[base:]
|
||||||
|
data_off, map_off, data_len, map_len = struct.unpack_from('>IIII', rf, 0)
|
||||||
|
m = map_off
|
||||||
|
type_list_off = u16(rf, m+24) # from map start
|
||||||
|
name_list_off = u16(rf, m+26)
|
||||||
|
tlo = m + type_list_off
|
||||||
|
ntypes = u16(rf, tlo) + 1
|
||||||
|
types = []
|
||||||
|
for i in range(ntypes):
|
||||||
|
eo = tlo + 2 + i*8
|
||||||
|
typ = rf[eo:eo+4].decode('mac_roman','replace')
|
||||||
|
count = u16(rf, eo+4) + 1
|
||||||
|
ref_off = u16(rf, eo+6)
|
||||||
|
refs = []
|
||||||
|
rlo = tlo + ref_off
|
||||||
|
for j in range(count):
|
||||||
|
ro = rlo + j*12
|
||||||
|
rid = struct.unpack_from('>h', rf, ro)[0]
|
||||||
|
noff = struct.unpack_from('>h', rf, ro+2)[0]
|
||||||
|
attr_dataoff = u32(rf, ro+4)
|
||||||
|
attrs = attr_dataoff >> 24
|
||||||
|
dataoff = attr_dataoff & 0x00FFFFFF
|
||||||
|
name = None
|
||||||
|
if noff != -1:
|
||||||
|
nlo = m + name_list_off + noff
|
||||||
|
ln = rf[nlo]
|
||||||
|
name = rf[nlo+1:nlo+1+ln].decode('mac_roman','replace')
|
||||||
|
# resource data: at data_off + dataoff, first 4 bytes = length
|
||||||
|
dstart = data_off + dataoff
|
||||||
|
dlen = u32(rf, dstart)
|
||||||
|
refs.append(dict(id=rid, name=name, attrs=attrs, data_start=base+dstart+4, dlen=dlen))
|
||||||
|
types.append((typ, refs))
|
||||||
|
return types
|
||||||
|
|
||||||
|
def cmd_map(blob, base):
|
||||||
|
types = parse(blob, base)
|
||||||
|
total = sum(len(r) for _,r in types)
|
||||||
|
print("resource types: %d, total resources: %d" % (len(types), total))
|
||||||
|
for typ, refs in sorted(types, key=lambda x:x[0]):
|
||||||
|
ids = ', '.join('%d%s' % (r['id'], ('("%s")'%r['name']) if r['name'] else '') for r in refs[:12])
|
||||||
|
more = '' if len(refs)<=12 else ' ...(%d total)'%len(refs)
|
||||||
|
print(" '%s' x%-4d : %s%s" % (typ, len(refs), ids, more))
|
||||||
|
|
||||||
|
def cmd_dump(blob, base, typ, wantid=None):
|
||||||
|
types = dict(parse(blob, base))
|
||||||
|
if typ not in types:
|
||||||
|
print("no type", typ); return
|
||||||
|
for r in types[typ]:
|
||||||
|
if wantid is not None and r['id'] != wantid: continue
|
||||||
|
data = blob[r['data_start']:r['data_start']+r['dlen']]
|
||||||
|
print("=== '%s' id=%d name=%s len=%d ===" % (typ, r['id'], r['name'], r['dlen']))
|
||||||
|
# hex + ascii
|
||||||
|
for i in range(0, min(len(data), 4096), 16):
|
||||||
|
row = data[i:i+16]
|
||||||
|
hexs=' '.join('%02x'%c for c in row)
|
||||||
|
asci=''.join(chr(c) if 32<=c<127 else '.' for c in row)
|
||||||
|
print(' %04x %-48s %s' % (i, hexs, asci))
|
||||||
|
|
||||||
|
def cmd_names(blob, base, typ):
|
||||||
|
types = dict(parse(blob, base))
|
||||||
|
if typ not in types:
|
||||||
|
print("no type", typ); return
|
||||||
|
for r in types[typ]:
|
||||||
|
print(" id=%-6d %s" % (r['id'], r['name'] or ''))
|
||||||
|
|
||||||
|
def cmd_strn(blob, base, wantid=None):
|
||||||
|
"""Decode STR# (list of Pascal strings)."""
|
||||||
|
types = dict(parse(blob, base))
|
||||||
|
for r in types.get('STR#', []):
|
||||||
|
if wantid is not None and r['id'] != wantid: continue
|
||||||
|
d = blob[r['data_start']:r['data_start']+r['dlen']]
|
||||||
|
n = u16(d,0); o=2; items=[]
|
||||||
|
for _ in range(n):
|
||||||
|
ln=d[o]; o+=1; items.append(d[o:o+ln].decode('mac_roman','replace')); o+=ln
|
||||||
|
print("STR# %d (%s): %s" % (r['id'], r['name'], ' | '.join(items)))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = sys.argv[1:]
|
||||||
|
path = args[0]; args = args[1:]
|
||||||
|
base = None
|
||||||
|
if args and args[0] == '--base':
|
||||||
|
base = int(args[1],0); args = args[2:]
|
||||||
|
blob = open(path,'rb').read()
|
||||||
|
if base is None:
|
||||||
|
base, _ = appledouble_rfork_base(blob)
|
||||||
|
cmd = args[0] if args else 'map'
|
||||||
|
if cmd == 'map': cmd_map(blob, base)
|
||||||
|
elif cmd == 'dump': cmd_dump(blob, base, args[1], int(args[2]) if len(args)>2 else None)
|
||||||
|
elif cmd == 'names': cmd_names(blob, base, args[1])
|
||||||
|
elif cmd == 'strn': cmd_strn(blob, base, int(args[1]) if len(args)>1 else None)
|
||||||
Reference in New Issue
Block a user