8 Commits
Author SHA1 Message Date
CydandClaude Fable 5 fd0c8c5196 vPOD: pod power controls, end-mission restart cycle, egg viewer polish
Adds pod-lifecycle controls to the vPOD window:

- Power Off / Power On: Power Off closes the TCP listener so the console can no
  longer connect, mimicking a pod with no game client running; Power On reopens
  it. The listener close/open is the faithful signal a console sees.
- End-mission graceful-exit + watchdog restart: on the end-mission command
  (StopMission) the game exits gracefully (listener closes, connection drops)
  and a watchdog relaunches it about 1.5 s later, coming back up in
  WaitingForEgg - the real pod's per-game cycle. A "Restart game after mission
  ends (watchdog)" checkbox (default on) toggles it; unchecked, the pod just
  returns to WaitingForEgg without exiting.
- Egg viewer: the last egg is kept across missions/restarts (no auto-clear) so
  it can be copied for dev use; a Clear button empties it on demand.
- Log pane defaults to half the window (egg lines are rarely wide); the split
  is set at load once the control has its real width.

Verified over real TCP: driving egg -> run -> end-mission makes the listener
drop then reopen with the pod back in WaitingForEgg (confirmed in vPOD's own
log: "Game exited gracefully" then "Watchdog restarted the game").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:43:44 -05:00
CydandClaude Fable 5 aff8bbf6e5 vPOD: gitignore its dist like the console/launcher packages
The console (Console\dist) and launcher (Launcher\dist) packages are build
artifacts rebuilt by their build scripts and gitignored per-project; vPOD's
package was inconsistently committed. Give vPOD its own .gitignore (/dist/) so
its pack.ps1 output is rebuilt on demand the same way, and drop the tracked
zip. Deployment is unaffected: the Apps.xml product still lists vPOD in Install
Product, and the operator selects the pack.ps1-built zip at install time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:58:59 -05:00
CydandClaude Fable 5 3fd637c58a Add vPOD: a virtual pod / game-client stand-in for testing the consoles
vPOD impersonates a Tesla game client (rpl4opt.exe / btl4.exe) so the Red
Planet and BattleTech operator consoles can be exercised without real cockpit
hardware. New net48 WinForms project under Console\vPOD:

- MungaPodServer: the server half of the Munga control protocol (TCP 1501).
  The vendored MungaSocket is client-only, so this reimplements the identical
  framing ([16-byte header][12-byte base + body], dispatched by
  ClientID+MessageID) for the listening side, reusing the vendored message
  classes' WriteTo/BinaryReader serialization.
- PodSimulator: the ApplicationState machine driven by the console's messages -
  answers StateQuery, reassembles the streamed egg and acknowledges it, and
  walks WaitingForEgg -> LoadingMission -> WaitingForLaunch -> RunningMission
  and back on Run/Stop/Abort/Suspend/Resume.
- VPodForm: live display of listening/connection status, the colour-coded
  ApplicationState, an egg viewer (fields + summary), and a newest-first
  protocol log. A Red Planet / BattleTech toggle changes which ApplicationID
  the pod reports, live, so one vPOD stands in for either game.
- PodArguments: parses the real client's launch flags (-net/-app/-lc/-mr/
  -host/-res).

Deployable from Manage Site -> Install Product: a catalog product in
RedPlanet\Apps.xml (Game Client / Live Camera / Mission Review entries) plus
pack.ps1, which builds dist\vPOD.zip laying out vPOD\vPOD.exe for the launcher
to extract to C:\Games\vPOD. CatalogTests updated to 5 products / 11 entries
with the four vPOD entry assertions (88/88 pass). TeslaConsole.csproj excludes
vPOD\** from its **/*.cs glob; the project is added to the solution.

Verified end-to-end over real TCP: a console-role client using the vendored
MungaSocket drives connect -> WaitingForEgg -> stream egg -> (ack) ->
WaitingForLaunch -> Run -> RunningMission -> Stop -> WaitingForEgg, with vPOD
reporting the correct state at each step. MungaGame (what the console's game
windows use) is a thin wrapper over MungaSocket, so this exercises the exact
wire behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:34:53 -05:00
CydandClaude Fable 5 18d787bdd5 BattleTech results printing, defaults dialog, and BT411 catalog (BT port phase 4)
Completes the BattleTech console feature to parity with Red Planet:

- BTPrintDocument: the per-pilot landscape score sheet (mech portrait,
  placing, kill/death/damage stats, randomized mission-highlights narrative,
  pilot-vs-pilot damage matrix, place-over-time chart), mirroring
  RPPrintDocument minus the football/lap/boost/score-zone concepts. Wired into
  BTGame (Auto Print checkbox + Print Last Mission button) and the shell's File
  menu (BT Mission Print Preview / Print BT Mission, loading .btm files).
- BTStrings(.xml): the BT mission-highlight narrative pools (damage tiers,
  kill, death-without-honor, recap), loaded from BattleTech\BTStrings.xml like
  RPStrings.
- BTDefaultsDialog: two-column (Free For All / No Return) editor for the BT
  operator defaults, reachable from Settings (Change/Import/Export BattleTech
  Defaults). Verified via UI Automation: opens with all 18 option combos
  populated.
- Apps.xml: the BattleTech 4.11 product (btl4.exe on the shared RP411 engine,
  same -net/-res/-lc/-mr command line; deploys to C:\Games\BT411) with Game
  Client / Live Camera / Mission Review launch entries. CatalogTests updated to
  4 products / 8 entries plus the three new BT entry assertions.

Role model spelling corrected to "noreturn": the Mac Console.ini tag and the
game's BTL4.RES role resource are dfltrole/NoReturn; the 4.10 console's eggs
emitted a broken "noretun" that cannot resolve against the RES. BTGoldenEggTests
and BTConfig.xml updated accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:04:03 -05:00
CydandClaude Fable 5 deaade4f72 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>
2026-07-07 22:43:32 -05:00
CydandClaude Fable 5 36166a95b0 BTGame pane + shell de-hardcode for multi-game pods (BT port phase 3)
BTGame mirrors RPGame - same GameStateData/state-machine/NetworkScan engine
driving each pod's MungaGame (take ownership, stream egg on WaitingForEgg,
Run/Abort/Stop, countdown) - with the BT differences: ApplicationID.BTL4, a
pilots grid carrying Mech/Camo/Patch/Badge/Experience, an Advanced Damage
checkbox in place of RP's Score Compression, and the Mech* in-match messages
recorded via BTMissionRecorder into BTMissionResults (.btm, gzip +
BinaryFormatter like .rpm; scores are 1000-based like RP). Free For All and
No Return share the pane; the mode selects the pilots' role. BTDefaults
persists per-mode operator defaults to BTDefaults.btd (no dialog yet).
RPGame's dead decompile members (sStopToIdleStates, IsAllPodsAppState) were
not carried over.

TeslaConsoleForm no longer assumes RP: the "AppID != 0 -> Pod Not Running RP"
check becomes a GameTag map (RPL4 -> RP, BTL4 -> BT, otherwise Unknown Pod
Application) feeding the per-game status strings, and the Games menu gains
BattleTech: Free For All / No Return.

Deferred to phase 4: mech/arena art, BT defaults dialog, BT score-sheet
print document, Apps.xml BT411 product, live-pod verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:29:20 -05:00
CydandClaude Fable 5 9148ce2cca BattleTech egg builder, catalog, and golden-egg DiffTests (BT port phases 1-2)
New TeslaConsole.BattleTech namespace mirroring TeslaConsole.RedPlanet's
mission layer: BTMission/BTFreeForAllMission (egg serializer; RP wire framing
and ordinal bitmaps reused verbatim), BTParticipant/BTPlayer/BTCamera (BT
participant fields: advancedDamage, experience, vehicleValue, patch, role),
and the BTConfig/BTScenario/BTMap/BTVehicle/BTWeather/BTRole catalog loaded
from BattleTech\BTConfig.xml (reconstructed from the Mac Console 4.10 ini BT
tree). Free For All and No Return share one mission class - both send
scenario=freeforall; the mode is the role assigned to each pilot.

BTGoldenEggTests (7 tests, recovered-only) diff the generated egg
field-by-field against two eggs captured from the original consoles
(cavern.egg, TESTARN.EGG): order-independent per-section compare with
font-rendered bitmap pixel rows excluded, ordinal art byte-exact vs
TESTARN.EGG, EggFileMessage framing reassembly, role de-dup, No Return role
semantics, and shipped-catalog contents. Invoker gains BTEggString/
BTEggFraming reflection cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:28:07 -05:00
CydandClaude Opus 4.8 0431c6840b install.bat: join Tesla workgroup + enable SMB1 client and server
- Join a site-wide workgroup (WORKGROUP=Tesla) at install time so every pod
  shares one browse list for the mw4files / c shares.
- Enable SMB1 server as well as client: the pod hosts those shares and Win10/11
  ship the SMB1-Server sub-feature off by default, so old SMB1 clients couldn't
  reach them. Now enables SMB1Protocol + -Client + -Server.
- Refresh the stale asset-layout comment in the header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:57:18 -05:00
57 changed files with 8964 additions and 20 deletions
+7
View File
@@ -22,3 +22,10 @@
Thumbs.db
desktop.ini
.DS_Store
__pycache__/
# 410console hand-off: the docs/spec/reference/tools are tracked; the raw Mac
# archive and its extraction are not (may be committed later for completeness)
/410console/410consoleArchive.sit
/410console/4_10-console-extracted/
/410console/.finf/
+554
View File
@@ -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", © 19951997 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** (012) 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", © 19951997 **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 10001011):
**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, 012**; 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 012 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
30023039). 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 012, 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
`0x3EBxx0x3EFxx` 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 012** (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] ← 1st4th 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]` 1st4th 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.)
+18
View File
@@ -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.
+309
View File
@@ -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()
+166
View File
@@ -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))
+118
View File
@@ -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)
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
BattleTech data catalog for TeslaConsole (mirror of RedPlanet\RPConfig.xml).
Reconstructed from the Mac Console 4.10 `Console.ini` BattleTech tree
(Adventure::Solo::BattleTech -> Location -> Scenario -> Time/Weather/
Experience/Vehicle/Emblem/DropZone/Camo/Patch) and the two console-generated
golden eggs (410console/battletech-port/reference/cavern.egg, TESTARN.EGG).
Wire (egg) mapping of the console fields:
scenario -> [mission] scenario= (BOTH Free For All and No Return send "freeforall")
map -> [mission] map=
time -> [mission] time=
weather -> [mission] weather= (temperature is a property of the weather entry)
experience-> participant experience=
vehicle -> participant vehicle= (the mech)
color -> participant color= (this is the CAMO list)
patch -> participant patch= (a color-name list, distinct from camo)
badge -> participant badge= (the faction EMBLEM)
role -> participant role=Role::<key>, plus a [Role::<key>] model=<model> block
Free For All and No Return differ ONLY by the pilot role (Default vs NoReturn);
both emit scenario=freeforall on the wire. Image assets are added in a later
phase (the image= attribute is optional; a missing one just shows the name).
Map-specific restrictions from the Mac ini that this flat model does NOT yet
enforce (candidates for a later refinement): Cavern is night-only; Ravines has
no "clear" weather (fog/soup only); Polar Missile Base uses 4 drop zones.
-->
<BattleTech>
<freeforall name="Free For All">
<!-- Pilot skill. key = egg value; name = operator-facing label (Mac STR# 307). -->
<experience key="novice" name="Standard" />
<experience key="standard" name="Veteran" />
<experience key="veteran" name="Expert" />
<!-- color= : mech camo (Mac BT::Standard::Camo::* tags). -->
<color key="Grey" name="Grey" />
<color key="White" name="White" />
<color key="Black" name="Black" />
<color key="Brown" name="Brown" />
<color key="Crimson" name="Crimson" />
<color key="Green" name="Green" />
<color key="Tan" name="Tan" />
<!-- patch= : team/badge color (Mac BT::Standard::Badge* tags). -->
<patch key="Red" name="Red" />
<patch key="Yellow" name="Yellow" />
<patch key="Blue" name="Blue" />
<patch key="Green" name="Green" />
<patch key="Grey" name="Grey" />
<patch key="Violet" name="Violet" />
<patch key="White" name="White" />
<patch key="Black" name="Black" />
<!-- badge= : faction emblem (Mac BT::5DropZone::* emblem tags / STR# 3006). -->
<badge key="VGL" name="VGL" />
<badge key="Davion" name="Davion" />
<badge key="Kurita" name="Kurita" />
<badge key="Liao" name="Liao" />
<badge key="Marik" name="Marik" />
<badge key="Steiner" name="Steiner" />
<badge key="None" name="None" />
<!-- role= : Free For All uses Default, No Return uses NoReturn. model= is the
egg value. The Mac ini tag is "noreturn" and the game's BTL4.RES role
resources are named dfltrole/NoReturn; the 4.10 console emitted a broken
"noretun" (a dropped character) in every egg on the production box, which
cannot resolve against the RES, so the correct spelling is used here. -->
<role key="Default" model="dfltrole" name="Default" />
<role key="NoReturn" model="noreturn" name="No Return" />
</freeforall>
<!-- Arenas. key = egg map value (Mac tag=); name = Mac STR# 3000 label. -->
<map key="arena1" name="Abandoned Arena" />
<map key="arena2" name="Destroyed Arena" />
<map key="polar3" name="Polar Missile Base" />
<map key="polar4" name="Helicopter Base" />
<map key="rav" name="Ravines" />
<map key="grass" name="Savannah" />
<map key="dbase" name="Badlands" />
<map key="cavern" name="Cavern" />
<!-- Mechs. key = egg vehicle value (Mac tag=); name = Mac STR# 3002 label. -->
<vehicle key="blkhawk" name="Blackhawk" />
<vehicle key="loki" name="Loki" />
<vehicle key="bhk1" name="Nova" />
<vehicle key="madcat" name="Mad Cat" />
<vehicle key="owens" name="Owens" />
<vehicle key="thor" name="Thor" />
<vehicle key="vulture" name="Vulture" />
<vehicle key="avatar" name="Avatar" />
<vehicle key="sunder" name="Sunder" />
<vehicle key="mad1" name="Timber Wolf" />
<vehicle key="ava1" name="Satyr" />
<vehicle key="lok1" name="Hellbringer" />
<vehicle key="thr1" name="Summoner" />
<vehicle key="vul1" name="Maddog" />
<vehicle key="own1" name="Kotori" />
<vehicle key="snd1" name="Denkou" />
<vehicle key="lok2" name="Mischief" />
<vehicle key="mad2" name="Zanin Neko" />
<!-- Time of day (Mac STR# 301 / BT TimeList). -->
<time key="morning" name="Morning" />
<time key="day" name="Day" />
<time key="evening" name="Evening" />
<time key="night" name="Night" />
<!-- Weather. temperature is carried on the weather entry (both known BT weathers
report 27 in the Mac ini); it becomes the egg [mission] temperature=. -->
<weather key="clear" name="Clear" temperature="27" />
<weather key="fog" name="Light Fog" temperature="27" />
<weather key="soup" name="Heavy Fog" temperature="27" />
</BattleTech>
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
BattleTech mission-highlight narrative strings (mirror of RedPlanet\RPStrings.xml).
Placeholders: {time} = mm:ss game time, {player} = pilot name, {vehicle} = mech
display name, {damager} = the other pilot involved.
-->
<BTStrings>
<LightDamage>{time} - Armor plating flakes off as {damager} lightly damages {player}'s mech.</LightDamage>
<LightDamage>{time} - {damager} lands a glancing hit on {player}.</LightDamage>
<LightDamage>{player}'s armor shrugs off a light volley from {damager}.</LightDamage>
<LightDamage>{player} laughs over the comm as {damager} can only scratch {player}'s paint.</LightDamage>
<ModerateDamage>{time} - Armor buckles as {damager} moderately damages {player}'s mech.</ModerateDamage>
<ModerateDamage>{player} is still in the fight as {damager} delivers a solid blow to {player}'s mech.</ModerateDamage>
<ModerateDamage>{damager} carves a respectable chunk out of {player}'s armor.</ModerateDamage>
<ModerateDamage>{time} - {damager} rocks {player}'s cockpit with a moderate impact.</ModerateDamage>
<HeavyDamage>{time} - Molten armor sprays as {damager} heavily damages {player}'s mech.</HeavyDamage>
<HeavyDamage>{player} fights the controls as {damager} nearly cores {player}'s mech.</HeavyDamage>
<HeavyDamage>{damager} unloads a devastating alpha strike into {player}'s mech.</HeavyDamage>
<HeavyDamage>{time} - Warning klaxons scream in {player}'s cockpit after a heavy impact from {damager}.</HeavyDamage>
<Kill>{time} - {damager} delivers a righteous kill to {player}.</Kill>
<Kill>{damager} stands victorious over the smoking wreckage of {player}'s {vehicle}.</Kill>
<Kill>{time} - {damager} cores {player}'s reactor. Glory to the victor.</Kill>
<Kill>The shattered remains of {player}'s {vehicle} litter the arena floor as {damager} salutes.</Kill>
<DeathWithoutHonor>{time} - {player} dies without honor as the {vehicle} destroys itself.</DeathWithoutHonor>
<DeathWithoutHonor>Shame falls upon {player}, whose {vehicle} is lost to no enemy's hand.</DeathWithoutHonor>
<DeathWithoutHonor>{time} - {player}'s {vehicle} overheats catastrophically. A death without honor.</DeathWithoutHonor>
<DeathWithoutHonor>{player} learns a hard lesson in mech management as the {vehicle} goes down on its own.</DeathWithoutHonor>
<Recap>...and at the end of the mission it's: {winners} followed by {looser}.</Recap>
</BTStrings>
+56
View File
@@ -53,6 +53,33 @@
hostType="MissionReview" />
</Product>
<!-- The bt411 Windows port (C:\VWE\BT411 repo; btl4.exe on the shared RP411
engine, same -net/-res/-lc/-mr command line). Deploys to C:\Games\BT411
like RP411 does to C:\Games\RP411. -->
<Product id="F4C957FD-72F7-4C5F-8971-28095007E8DF"
name="BattleTech 4.11"
menuText="BattleTech 4.11..."
hostTypeDialog="true">
<Launch key="F4C957FD-72F7-4C5F-8971-28095007E8DF"
displayName="BattleTech 4.11"
exe="C:\Games\BT411\btl4.exe"
args="-net 1501{res}"
autoRestart="true"
hostType="GameClient" />
<Launch key="D393711A-EDA0-48B2-82A0-89DF12B768AF"
displayName="BattleTech 4.11 LC"
exe="C:\Games\BT411\btl4.exe"
args="-net 1501{res} -lc"
autoRestart="true"
hostType="LiveCamera" />
<Launch key="2E9B8628-9C20-42FB-B070-E9C38D521082"
displayName="BattleTech 4.11 MR"
exe="C:\Games\BT411\btl4.exe"
args="-net 1501{res} -mr"
autoRestart="true"
hostType="MissionReview" />
</Product>
<Product id="CC8500ED-A653-45a7-BEF8-C332D30371A6"
name="BattleTech Firestorm"
menuText="BattleTech Firestorm..."
@@ -84,4 +111,33 @@
autoRestart="true"
hostType="None" />
</Product>
<!-- vPOD - virtual pod / game-client stand-in for testing the game consoles
(Console\vPOD). Speaks Munga on 1501 like a real rpl4opt.exe/btl4.exe and
reports either ApplicationID (toggled live in its window). Deploy it to a
pod exactly like a real client; the package (Console\vPOD\dist\vPOD.zip,
built by pack.ps1) extracts to C:\Games\vPOD. -->
<Product id="0041C870-6E5E-4F3B-9782-F94F2F76F21D"
name="vPOD (Virtual Pod)"
menuText="vPOD (Virtual Pod)..."
hostTypeDialog="true">
<Launch key="0041C870-6E5E-4F3B-9782-F94F2F76F21D"
displayName="vPOD"
exe="C:\Games\vPOD\vPOD.exe"
args="-net 1501{res}"
autoRestart="true"
hostType="GameClient" />
<Launch key="EA0D4129-8950-428D-8399-E6A77D2D566A"
displayName="vPOD LC"
exe="C:\Games\vPOD\vPOD.exe"
args="-net 1501{res} -lc"
autoRestart="true"
hostType="LiveCamera" />
<Launch key="FC7CE34E-F4FE-4218-84CD-B13A6FA58E57"
displayName="vPOD MR"
exe="C:\Games\vPOD\vPOD.exe"
args="-net 1501{res} -mr"
autoRestart="true"
hostType="MissionReview" />
</Product>
</AppCatalog>
@@ -0,0 +1,35 @@
using System;
using System.Text;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A spectator / mission-review station. Mirrors <c>RPCamera</c>: a non-combatant
/// participant that occupies a pilot slot but is not a mech. No BT camera golden
/// egg exists, so the field set follows the Red Planet camera block (minus the
/// RP-only football <c>team=</c> field) and should be confirmed against a live BT
/// pod when one is available.
/// </summary>
[Serializable]
internal class BTCamera : BTParticipant
{
public BTCamera(string podHostAddress, HostType hostType)
: base(podHostAddress, hostType)
{
if (hostType != HostType.MissionReviewHostType)
{
throw new ArgumentOutOfRangeException("hostType");
}
}
protected sealed override void AppendParticipantSpecificData(int index, StringBuilder sb)
{
sb.Append("loadzones=0\n");
sb.Append("name=Camera\n");
sb.AppendFormat("bitmapindex={0}\n", index);
sb.Append("vehicle=camera\n");
sb.Append("badge=None\n");
sb.Append("dropzone=one\n");
sb.Append("color=Black\n");
}
}
+117
View File
@@ -0,0 +1,117 @@
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Loads and exposes the BattleTech data catalog (BattleTech\BTConfig.xml),
/// mirroring <c>TeslaConsole.RedPlanet.RPConfig</c>. The single BT scenario is
/// "Free For All"; No Return reuses the same catalog and differs only by role.
/// </summary>
public static class BTConfig
{
private static readonly BTScenario mFreeForAll;
private static readonly Dictionary<string, BTMap> mMaps;
private static readonly Dictionary<string, BTVehicle> mVehicles;
private static readonly Dictionary<string, string> mTimesOfDay;
private static readonly Dictionary<string, BTWeather> mWeather;
public static BTScenario FreeForAll => mFreeForAll;
public static Dictionary<string, BTMap> Maps => mMaps;
public static Dictionary<string, BTVehicle> Vehicles => mVehicles;
public static Dictionary<string, string> TimesOfDay => mTimesOfDay;
public static Dictionary<string, BTWeather> Weather => mWeather;
static BTConfig()
{
mMaps = new Dictionary<string, BTMap>();
mVehicles = new Dictionary<string, BTVehicle>();
mTimesOfDay = new Dictionary<string, string>();
mWeather = new Dictionary<string, BTWeather>();
XmlNode freeForAllNode = null;
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "BattleTech\\BTConfig.xml"));
foreach (XmlNode childNode in xmlDocument.DocumentElement.ChildNodes)
{
switch (childNode.Name)
{
case "freeforall":
freeForAllNode = childNode;
break;
case "map":
{
BTMap bTMap = new BTMap(childNode);
mMaps.Add(bTMap.Key, bTMap);
break;
}
case "vehicle":
{
BTVehicle bTVehicle = new BTVehicle(childNode);
mVehicles.Add(bTVehicle.Key, bTVehicle);
break;
}
case "time":
AddKeyNameNode(childNode, mTimesOfDay);
break;
case "weather":
{
BTWeather bTWeather = new BTWeather(childNode);
mWeather.Add(bTWeather.Key, bTWeather);
break;
}
}
}
mFreeForAll = new BTScenario(freeForAllNode);
}
internal static void AddKeyNameNode(XmlNode node, Dictionary<string, string> list)
{
list.Add(node.Attributes["key"].InnerText, node.Attributes["name"].InnerText);
}
public static string MapNameOrKey(string mapKey)
{
if (!mMaps.ContainsKey(mapKey))
{
return mapKey;
}
return mMaps[mapKey].Name;
}
public static string VehicleNameOrKey(string vehicleKey)
{
if (!mVehicles.ContainsKey(vehicleKey))
{
return vehicleKey;
}
return mVehicles[vehicleKey].Name;
}
public static string TimeOfDayNameOrKey(string timeOfDayKey)
{
if (!mTimesOfDay.ContainsKey(timeOfDayKey))
{
return timeOfDayKey;
}
return mTimesOfDay[timeOfDayKey];
}
public static string WeatherNameOrKey(string weatherKey)
{
if (!mWeather.ContainsKey(weatherKey))
{
return weatherKey;
}
return mWeather[weatherKey].Name;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A mech took damage (<c>MechDamagedMessage</c>). Carries the Red Planet
/// damage fields plus BT's damage-matrix extras: which zone was hit, whether it
/// was destroyed, and the weapon that did it.
/// </summary>
[Serializable]
internal class BTDamagedEvent : BTMissionEvent
{
public readonly BTPlayer Damager;
public readonly int DamageLoss;
public readonly int PointsTransfered;
public readonly int DamageZoneIndex;
public readonly bool DamageZoneDestroyed;
public readonly int WeaponIndex;
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[2] { ReportingPilot, Damager };
public BTDamagedEvent(BTPlayer reportingPilot, TimeSpan missionTime, BTPlayer damager, int damageLoss, int pointsTransfered, int damageZoneIndex, bool damageZoneDestroyed, int weaponIndex)
: base(reportingPilot, missionTime)
{
Damager = damager;
DamageLoss = damageLoss;
PointsTransfered = pointsTransfered;
DamageZoneIndex = damageZoneIndex;
DamageZoneDestroyed = damageZoneDestroyed;
WeaponIndex = weaponIndex;
}
private BTDamagedEvent()
{
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A mech died without a killer to credit — overheating, terrain, or
/// self-destruction (<c>MechDeathWithoutHonorMessage</c>).
/// </summary>
[Serializable]
internal class BTDeathWithoutHonorEvent : BTMissionEvent
{
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[1] { ReportingPilot };
public BTDeathWithoutHonorEvent(BTPlayer reportingPilot, TimeSpan missionTime)
: base(reportingPilot, missionTime)
{
}
private BTDeathWithoutHonorEvent()
{
}
}
@@ -0,0 +1,347 @@
using System;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Operator defaults for the two BattleTech game modes, persisted to
/// BTDefaults.btd in the common app-data directory (mirrors
/// <c>RPDefaults</c>/.rpd). Both modes share one defaults shape — they differ
/// only by role, which comes from the mode, not from a default. There is no
/// defaults dialog yet (RPDefaultsDialog's counterpart is a later phase); the
/// file is import/export round-trippable in the meantime.
/// </summary>
internal static class BTDefaults
{
public class BattleDefaults
{
private readonly string mNodeName;
private bool mAdvancedDamage = true;
private TimeSpan mMissionLength = new TimeSpan(0, 10, 0);
private string mMapKey;
private string mVehicleKey;
private string mTimeOfDayKey;
private string mWeatherKey;
private string mExperienceKey;
private string mCamoKey;
private string mPatchKey;
private string mEmblemKey;
private int mLaunchDelay = 15;
public bool AdvancedDamage
{
get
{
return mAdvancedDamage;
}
set
{
mAdvancedDamage = value;
}
}
public TimeSpan MissionLength
{
get
{
return mMissionLength;
}
set
{
mMissionLength = value;
}
}
public string MapKey
{
get
{
return mMapKey;
}
set
{
mMapKey = value;
}
}
public string VehicleKey
{
get
{
return mVehicleKey;
}
set
{
mVehicleKey = value;
}
}
public string TimeOfDayKey
{
get
{
return mTimeOfDayKey;
}
set
{
mTimeOfDayKey = value;
}
}
public string WeatherKey
{
get
{
return mWeatherKey;
}
set
{
mWeatherKey = value;
}
}
public string ExperienceKey
{
get
{
return mExperienceKey;
}
set
{
mExperienceKey = value;
}
}
public string CamoKey
{
get
{
return mCamoKey;
}
set
{
mCamoKey = value;
}
}
public string PatchKey
{
get
{
return mPatchKey;
}
set
{
mPatchKey = value;
}
}
public string EmblemKey
{
get
{
return mEmblemKey;
}
set
{
mEmblemKey = value;
}
}
public int LaunchDelay
{
get
{
return mLaunchDelay;
}
set
{
mLaunchDelay = value;
}
}
internal BattleDefaults(string nodeName)
{
mNodeName = nodeName;
}
public void Save(XmlNode parrentNode)
{
XmlNode xmlNode = parrentNode.AppendChild(parrentNode.OwnerDocument.CreateElement(mNodeName));
xmlNode.AppendChild(xmlNode.OwnerDocument.CreateElement("AdvancedDamage")).InnerText = mAdvancedDamage.ToString();
xmlNode.AppendChild(xmlNode.OwnerDocument.CreateElement("MissionLength")).InnerText = mMissionLength.ToString();
SaveKey(xmlNode, "MapKey", mMapKey);
SaveKey(xmlNode, "VehicleKey", mVehicleKey);
SaveKey(xmlNode, "TimeOfDayKey", mTimeOfDayKey);
SaveKey(xmlNode, "WeatherKey", mWeatherKey);
SaveKey(xmlNode, "ExperienceKey", mExperienceKey);
SaveKey(xmlNode, "CamoKey", mCamoKey);
SaveKey(xmlNode, "PatchKey", mPatchKey);
SaveKey(xmlNode, "EmblemKey", mEmblemKey);
xmlNode.AppendChild(xmlNode.OwnerDocument.CreateElement("LaunchDelay")).InnerText = mLaunchDelay.ToString();
}
private static void SaveKey(XmlNode parentNode, string elementName, string value)
{
if (!string.IsNullOrEmpty(value))
{
parentNode.AppendChild(parentNode.OwnerDocument.CreateElement(elementName)).InnerText = value;
}
}
public void Parse(XmlNode scenarioNode)
{
foreach (XmlNode childNode in scenarioNode.ChildNodes)
{
switch (childNode.Name)
{
case "AdvancedDamage":
{
if (bool.TryParse(childNode.InnerText, out var result2))
{
mAdvancedDamage = result2;
}
break;
}
case "MissionLength":
{
if (TimeSpan.TryParse(childNode.InnerText, out var result3))
{
mMissionLength = result3;
}
break;
}
case "MapKey":
ParseKey(childNode, ref mMapKey);
break;
case "VehicleKey":
ParseKey(childNode, ref mVehicleKey);
break;
case "TimeOfDayKey":
ParseKey(childNode, ref mTimeOfDayKey);
break;
case "WeatherKey":
ParseKey(childNode, ref mWeatherKey);
break;
case "ExperienceKey":
ParseKey(childNode, ref mExperienceKey);
break;
case "CamoKey":
ParseKey(childNode, ref mCamoKey);
break;
case "PatchKey":
ParseKey(childNode, ref mPatchKey);
break;
case "EmblemKey":
ParseKey(childNode, ref mEmblemKey);
break;
case "LaunchDelay":
{
if (int.TryParse(childNode.InnerText, out var result))
{
mLaunchDelay = result;
}
break;
}
}
}
}
private static void ParseKey(XmlNode node, ref string field)
{
if (!string.IsNullOrEmpty(node.InnerText))
{
field = node.InnerText;
}
}
}
public const string FreeForAllNode = "FreeForAllDefaults";
public const string NoReturnNode = "NoReturnDefaults";
private static readonly string sDefaultsFilePath;
private static readonly BattleDefaults sFreeForAll;
private static readonly BattleDefaults sNoReturn;
public static BattleDefaults FreeForAll => sFreeForAll;
public static BattleDefaults NoReturn => sNoReturn;
static BTDefaults()
{
sDefaultsFilePath = Path.Combine(Program.GetCommonAppDataDirectory(), "BTDefaults.btd");
sFreeForAll = new BattleDefaults("FreeForAllDefaults");
sNoReturn = new BattleDefaults("NoReturnDefaults");
if (File.Exists(sDefaultsFilePath))
{
try
{
Import(sDefaultsFilePath);
}
catch (Exception)
{
MessageBox.Show("The BattleTech defaults file appears to be corrupt. Please delete it or export a new one to replace it.", "Error Loading BattleTech Defaults!", MessageBoxButtons.OK);
}
}
}
public static void Import(string fileName)
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(fileName);
foreach (XmlNode childNode in xmlDocument.DocumentElement.ChildNodes)
{
switch (childNode.Name)
{
case "FreeForAllDefaults":
sFreeForAll.Parse(childNode);
break;
case "NoReturnDefaults":
sNoReturn.Parse(childNode);
break;
}
}
}
public static void Save()
{
try
{
Export(sDefaultsFilePath);
}
catch (Exception)
{
MessageBox.Show("The BattleTech defaults file could not be saved. These defaults will only be remembered until the application is closed.", "Error Saving BattleTech Defaults!", MessageBoxButtons.OK);
}
}
public static void Export(string filePath)
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.AppendChild(xmlDocument.CreateElement("BTDefaults"));
sFreeForAll.Save(xmlDocument.DocumentElement);
sNoReturn.Save(xmlDocument.DocumentElement);
string directoryName = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
}
xmlDocument.Save(filePath);
}
}
@@ -0,0 +1,441 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using TeslaConsole.Properties;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The BattleTech operator-defaults editor: one column per game mode (Free For
/// All / No Return), mirroring <c>RPDefaultsDialog</c>'s Death Race / Football
/// layout. OK persists to <see cref="BTDefaults" /> (BTDefaults.btd). Both
/// modes share the single BT catalog; the rows are the BT option set (mech,
/// camo, patch, badge, experience, advanced damage) instead of RP's.
/// </summary>
public class BTDefaultsDialog : Form
{
private IContainer components;
private TableLayoutPanel mMainTable;
private Label mFfaHeaderLabel;
private Label mNoReturnHeaderLabel;
private Label mMapLabel;
private Label mWeatherLabel;
private Label mTimeOfDayLabel;
private Label mAdvancedDamageLabel;
private Label mMissionLengthLabel;
private Label mMechLabel;
private Label mCamoLabel;
private Label mPatchLabel;
private Label mBadgeLabel;
private Label mExperienceLabel;
private Label mLaunchDelayLabel;
private ComboBox mFfaMap;
private ComboBox mNoReturnMap;
private ComboBox mFfaWeather;
private ComboBox mNoReturnWeather;
private ComboBox mFfaTimeOfDay;
private ComboBox mNoReturnTimeOfDay;
private ComboBox mFfaAdvancedDamage;
private ComboBox mNoReturnAdvancedDamage;
private MaskedTextBox mFfaMissionLength;
private MaskedTextBox mNoReturnMissionLength;
private ComboBox mFfaVehicle;
private ComboBox mNoReturnVehicle;
private ComboBox mFfaCamo;
private ComboBox mNoReturnCamo;
private ComboBox mFfaPatch;
private ComboBox mNoReturnPatch;
private ComboBox mFfaBadge;
private ComboBox mNoReturnBadge;
private ComboBox mFfaExperience;
private ComboBox mNoReturnExperience;
private NumericUpDown mFfaLaunchDelay;
private NumericUpDown mNoReturnLaunchDelay;
private TableLayoutPanel mButtonsTable;
private Button mCancel;
private Button mOK;
private static BTDefaultsDialog mSingletonDialog;
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
public static void ShowSingleton()
{
if (mSingletonDialog == null || mSingletonDialog.IsDisposed)
{
mSingletonDialog = new BTDefaultsDialog();
}
mSingletonDialog.Show();
mSingletonDialog.Focus();
mSingletonDialog.WindowState = FormWindowState.Normal;
}
private BTDefaultsDialog()
{
InitializeComponent();
base.Icon = Resources.swirl;
BTScenario scenario = BTConfig.FreeForAll;
BuildColumn(scenario, BTDefaults.FreeForAll, mFfaMap, mFfaWeather, mFfaTimeOfDay, mFfaAdvancedDamage, mFfaMissionLength, mFfaVehicle, mFfaCamo, mFfaPatch, mFfaBadge, mFfaExperience, mFfaLaunchDelay);
BuildColumn(scenario, BTDefaults.NoReturn, mNoReturnMap, mNoReturnWeather, mNoReturnTimeOfDay, mNoReturnAdvancedDamage, mNoReturnMissionLength, mNoReturnVehicle, mNoReturnCamo, mNoReturnPatch, mNoReturnBadge, mNoReturnExperience, mNoReturnLaunchDelay);
}
private static void BuildColumn(BTScenario scenario, BTDefaults.BattleDefaults defaults, ComboBox map, ComboBox weather, ComboBox timeOfDay, ComboBox advancedDamage, MaskedTextBox missionLength, ComboBox vehicle, ComboBox camo, ComboBox patch, ComboBox badge, ComboBox experience, NumericUpDown launchDelay)
{
BuildNamedOption(map, scenario.Maps.Keys, scenario.Maps.Values, defaults.MapKey);
BuildNamedOption(weather, scenario.Weather.Keys, scenario.Weather.Values, defaults.WeatherKey);
BuildGenericOption(timeOfDay, scenario.TimesOfDay, defaults.TimeOfDayKey);
BuildOnOffOptions(advancedDamage, defaults.AdvancedDamage);
BuildMissionLengthControl(missionLength, defaults.MissionLength);
BuildNamedOption(vehicle, scenario.Vehicles.Keys, scenario.Vehicles.Values, defaults.VehicleKey);
BuildGenericOption(camo, scenario.Colors, defaults.CamoKey);
BuildGenericOption(patch, scenario.Patches, defaults.PatchKey);
BuildGenericOption(badge, scenario.Badges, defaults.EmblemKey);
BuildGenericOption(experience, scenario.Experiences, defaults.ExperienceKey);
launchDelay.Value = defaults.LaunchDelay;
}
private static void BuildOnOffOptions(ComboBox comboBox, bool defaultValue)
{
comboBox.DisplayMember = "Value";
comboBox.ValueMember = "Key";
comboBox.Items.Add(new KeyValuePair<bool, string>(key: false, "Off"));
comboBox.Items.Add(new KeyValuePair<bool, string>(key: true, "On"));
comboBox.SelectedIndex = (defaultValue ? 1 : 0);
}
private static void BuildMissionLengthControl(MaskedTextBox maskedTextBox, TimeSpan defaultValue)
{
maskedTextBox.Mask = "00:00:00";
maskedTextBox.ValidatingType = typeof(TimeSpan);
maskedTextBox.Text = defaultValue.ToString();
}
/// <summary>
/// Fills a combo from a keyed catalog of objects whose Name property is the
/// display text and whose ToString returns it (BTMap/BTVehicle/BTWeather).
/// </summary>
private static void BuildNamedOption<T>(ComboBox comboBox, IEnumerable<string> keys, IEnumerable<T> values, string defaultKey)
{
int selectedIndex = 0;
int num = 0;
comboBox.DisplayMember = "Name";
comboBox.ValueMember = "Key";
IEnumerator<string> keyEnumerator = keys.GetEnumerator();
foreach (T value in values)
{
keyEnumerator.MoveNext();
comboBox.Items.Add(value);
if (keyEnumerator.Current == defaultKey)
{
selectedIndex = num;
}
num++;
}
comboBox.SelectedIndex = selectedIndex;
}
private static void BuildGenericOption(ComboBox comboBox, IEnumerable<KeyValuePair<string, string>> options, string defaultKey)
{
int selectedIndex = 0;
int num = 0;
comboBox.DisplayMember = "Value";
comboBox.ValueMember = "Key";
foreach (KeyValuePair<string, string> option in options)
{
comboBox.Items.Add(option);
if (option.Key == defaultKey)
{
selectedIndex = num;
}
num++;
}
comboBox.SelectedIndex = selectedIndex;
}
private void mCancel_Click(object sender, EventArgs e)
{
Close();
}
private void mOK_Click(object sender, EventArgs e)
{
SaveColumn(BTDefaults.FreeForAll, mFfaMap, mFfaWeather, mFfaTimeOfDay, mFfaAdvancedDamage, mFfaMissionLength, mFfaVehicle, mFfaCamo, mFfaPatch, mFfaBadge, mFfaExperience, mFfaLaunchDelay);
SaveColumn(BTDefaults.NoReturn, mNoReturnMap, mNoReturnWeather, mNoReturnTimeOfDay, mNoReturnAdvancedDamage, mNoReturnMissionLength, mNoReturnVehicle, mNoReturnCamo, mNoReturnPatch, mNoReturnBadge, mNoReturnExperience, mNoReturnLaunchDelay);
BTDefaults.Save();
Hide();
}
private static void SaveColumn(BTDefaults.BattleDefaults defaults, ComboBox map, ComboBox weather, ComboBox timeOfDay, ComboBox advancedDamage, MaskedTextBox missionLength, ComboBox vehicle, ComboBox camo, ComboBox patch, ComboBox badge, ComboBox experience, NumericUpDown launchDelay)
{
defaults.MapKey = ((BTMap)map.SelectedItem).Key;
defaults.WeatherKey = ((BTWeather)weather.SelectedItem).Key;
defaults.TimeOfDayKey = ExtractSelectedKey<string>(timeOfDay);
defaults.AdvancedDamage = ExtractSelectedKey<bool>(advancedDamage);
defaults.MissionLength = (TimeSpan)missionLength.ValidateText();
defaults.VehicleKey = ((BTVehicle)vehicle.SelectedItem).Key;
defaults.CamoKey = ExtractSelectedKey<string>(camo);
defaults.PatchKey = ExtractSelectedKey<string>(patch);
defaults.EmblemKey = ExtractSelectedKey<string>(badge);
defaults.ExperienceKey = ExtractSelectedKey<string>(experience);
defaults.LaunchDelay = (int)launchDelay.Value;
}
private static T ExtractSelectedKey<T>(ComboBox comboBox)
{
if (comboBox.SelectedItem is KeyValuePair<T, string>)
{
return ((KeyValuePair<T, string>)comboBox.SelectedItem).Key;
}
return (T)comboBox.SelectedItem;
}
private void InitializeComponent()
{
this.mMainTable = new System.Windows.Forms.TableLayoutPanel();
this.mFfaHeaderLabel = new System.Windows.Forms.Label();
this.mNoReturnHeaderLabel = new System.Windows.Forms.Label();
this.mMapLabel = new System.Windows.Forms.Label();
this.mWeatherLabel = new System.Windows.Forms.Label();
this.mTimeOfDayLabel = new System.Windows.Forms.Label();
this.mAdvancedDamageLabel = new System.Windows.Forms.Label();
this.mMissionLengthLabel = new System.Windows.Forms.Label();
this.mMechLabel = new System.Windows.Forms.Label();
this.mCamoLabel = new System.Windows.Forms.Label();
this.mPatchLabel = new System.Windows.Forms.Label();
this.mBadgeLabel = new System.Windows.Forms.Label();
this.mExperienceLabel = new System.Windows.Forms.Label();
this.mLaunchDelayLabel = new System.Windows.Forms.Label();
this.mFfaMap = new System.Windows.Forms.ComboBox();
this.mNoReturnMap = new System.Windows.Forms.ComboBox();
this.mFfaWeather = new System.Windows.Forms.ComboBox();
this.mNoReturnWeather = new System.Windows.Forms.ComboBox();
this.mFfaTimeOfDay = new System.Windows.Forms.ComboBox();
this.mNoReturnTimeOfDay = new System.Windows.Forms.ComboBox();
this.mFfaAdvancedDamage = new System.Windows.Forms.ComboBox();
this.mNoReturnAdvancedDamage = new System.Windows.Forms.ComboBox();
this.mFfaMissionLength = new System.Windows.Forms.MaskedTextBox();
this.mNoReturnMissionLength = new System.Windows.Forms.MaskedTextBox();
this.mFfaVehicle = new System.Windows.Forms.ComboBox();
this.mNoReturnVehicle = new System.Windows.Forms.ComboBox();
this.mFfaCamo = new System.Windows.Forms.ComboBox();
this.mNoReturnCamo = new System.Windows.Forms.ComboBox();
this.mFfaPatch = new System.Windows.Forms.ComboBox();
this.mNoReturnPatch = new System.Windows.Forms.ComboBox();
this.mFfaBadge = new System.Windows.Forms.ComboBox();
this.mNoReturnBadge = new System.Windows.Forms.ComboBox();
this.mFfaExperience = new System.Windows.Forms.ComboBox();
this.mNoReturnExperience = new System.Windows.Forms.ComboBox();
this.mFfaLaunchDelay = new System.Windows.Forms.NumericUpDown();
this.mNoReturnLaunchDelay = new System.Windows.Forms.NumericUpDown();
this.mButtonsTable = new System.Windows.Forms.TableLayoutPanel();
this.mCancel = new System.Windows.Forms.Button();
this.mOK = new System.Windows.Forms.Button();
this.mMainTable.SuspendLayout();
this.mButtonsTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)this.mFfaLaunchDelay).BeginInit();
((System.ComponentModel.ISupportInitialize)this.mNoReturnLaunchDelay).BeginInit();
base.SuspendLayout();
this.mMainTable.ColumnCount = 3;
this.mMainTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.mMainTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50f));
this.mMainTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50f));
this.mMainTable.Controls.Add(this.mFfaHeaderLabel, 1, 0);
this.mMainTable.Controls.Add(this.mNoReturnHeaderLabel, 2, 0);
this.mMainTable.Controls.Add(this.mMapLabel, 0, 1);
this.mMainTable.Controls.Add(this.mFfaMap, 1, 1);
this.mMainTable.Controls.Add(this.mNoReturnMap, 2, 1);
this.mMainTable.Controls.Add(this.mWeatherLabel, 0, 2);
this.mMainTable.Controls.Add(this.mFfaWeather, 1, 2);
this.mMainTable.Controls.Add(this.mNoReturnWeather, 2, 2);
this.mMainTable.Controls.Add(this.mTimeOfDayLabel, 0, 3);
this.mMainTable.Controls.Add(this.mFfaTimeOfDay, 1, 3);
this.mMainTable.Controls.Add(this.mNoReturnTimeOfDay, 2, 3);
this.mMainTable.Controls.Add(this.mAdvancedDamageLabel, 0, 4);
this.mMainTable.Controls.Add(this.mFfaAdvancedDamage, 1, 4);
this.mMainTable.Controls.Add(this.mNoReturnAdvancedDamage, 2, 4);
this.mMainTable.Controls.Add(this.mMissionLengthLabel, 0, 5);
this.mMainTable.Controls.Add(this.mFfaMissionLength, 1, 5);
this.mMainTable.Controls.Add(this.mNoReturnMissionLength, 2, 5);
this.mMainTable.Controls.Add(this.mMechLabel, 0, 6);
this.mMainTable.Controls.Add(this.mFfaVehicle, 1, 6);
this.mMainTable.Controls.Add(this.mNoReturnVehicle, 2, 6);
this.mMainTable.Controls.Add(this.mCamoLabel, 0, 7);
this.mMainTable.Controls.Add(this.mFfaCamo, 1, 7);
this.mMainTable.Controls.Add(this.mNoReturnCamo, 2, 7);
this.mMainTable.Controls.Add(this.mPatchLabel, 0, 8);
this.mMainTable.Controls.Add(this.mFfaPatch, 1, 8);
this.mMainTable.Controls.Add(this.mNoReturnPatch, 2, 8);
this.mMainTable.Controls.Add(this.mBadgeLabel, 0, 9);
this.mMainTable.Controls.Add(this.mFfaBadge, 1, 9);
this.mMainTable.Controls.Add(this.mNoReturnBadge, 2, 9);
this.mMainTable.Controls.Add(this.mExperienceLabel, 0, 10);
this.mMainTable.Controls.Add(this.mFfaExperience, 1, 10);
this.mMainTable.Controls.Add(this.mNoReturnExperience, 2, 10);
this.mMainTable.Controls.Add(this.mLaunchDelayLabel, 0, 11);
this.mMainTable.Controls.Add(this.mFfaLaunchDelay, 1, 11);
this.mMainTable.Controls.Add(this.mNoReturnLaunchDelay, 2, 11);
this.mMainTable.Controls.Add(this.mButtonsTable, 0, 12);
this.mMainTable.Dock = System.Windows.Forms.DockStyle.Fill;
this.mMainTable.Location = new System.Drawing.Point(3, 3);
this.mMainTable.Name = "mMainTable";
this.mMainTable.RowCount = 13;
for (int i = 0; i < 12; i++)
{
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
}
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100f));
this.mMainTable.Size = new System.Drawing.Size(478, 384);
this.mMainTable.TabIndex = 0;
this.mFfaHeaderLabel.Anchor = System.Windows.Forms.AnchorStyles.None;
this.mFfaHeaderLabel.AutoSize = true;
this.mFfaHeaderLabel.Name = "mFfaHeaderLabel";
this.mFfaHeaderLabel.Text = "Free For All";
this.mNoReturnHeaderLabel.Anchor = System.Windows.Forms.AnchorStyles.None;
this.mNoReturnHeaderLabel.AutoSize = true;
this.mNoReturnHeaderLabel.Name = "mNoReturnHeaderLabel";
this.mNoReturnHeaderLabel.Text = "No Return";
ConfigureRowLabel(this.mMapLabel, "mMapLabel", "Map:");
ConfigureRowLabel(this.mWeatherLabel, "mWeatherLabel", "Weather:");
ConfigureRowLabel(this.mTimeOfDayLabel, "mTimeOfDayLabel", "Time Of Day:");
ConfigureRowLabel(this.mAdvancedDamageLabel, "mAdvancedDamageLabel", "Advanced Damage:");
ConfigureRowLabel(this.mMissionLengthLabel, "mMissionLengthLabel", "Mission Length:");
ConfigureRowLabel(this.mMechLabel, "mMechLabel", "Mech:");
ConfigureRowLabel(this.mCamoLabel, "mCamoLabel", "Camo:");
ConfigureRowLabel(this.mPatchLabel, "mPatchLabel", "Patch:");
ConfigureRowLabel(this.mBadgeLabel, "mBadgeLabel", "Badge:");
ConfigureRowLabel(this.mExperienceLabel, "mExperienceLabel", "Experience:");
ConfigureRowLabel(this.mLaunchDelayLabel, "mLaunchDelayLabel", "Autotranslocate Delay:");
ConfigureOptionCombo(this.mFfaMap, "mFfaMap");
ConfigureOptionCombo(this.mNoReturnMap, "mNoReturnMap");
ConfigureOptionCombo(this.mFfaWeather, "mFfaWeather");
ConfigureOptionCombo(this.mNoReturnWeather, "mNoReturnWeather");
ConfigureOptionCombo(this.mFfaTimeOfDay, "mFfaTimeOfDay");
ConfigureOptionCombo(this.mNoReturnTimeOfDay, "mNoReturnTimeOfDay");
ConfigureOptionCombo(this.mFfaAdvancedDamage, "mFfaAdvancedDamage");
ConfigureOptionCombo(this.mNoReturnAdvancedDamage, "mNoReturnAdvancedDamage");
ConfigureOptionCombo(this.mFfaVehicle, "mFfaVehicle");
ConfigureOptionCombo(this.mNoReturnVehicle, "mNoReturnVehicle");
ConfigureOptionCombo(this.mFfaCamo, "mFfaCamo");
ConfigureOptionCombo(this.mNoReturnCamo, "mNoReturnCamo");
ConfigureOptionCombo(this.mFfaPatch, "mFfaPatch");
ConfigureOptionCombo(this.mNoReturnPatch, "mNoReturnPatch");
ConfigureOptionCombo(this.mFfaBadge, "mFfaBadge");
ConfigureOptionCombo(this.mNoReturnBadge, "mNoReturnBadge");
ConfigureOptionCombo(this.mFfaExperience, "mFfaExperience");
ConfigureOptionCombo(this.mNoReturnExperience, "mNoReturnExperience");
this.mFfaMissionLength.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
this.mFfaMissionLength.Name = "mFfaMissionLength";
this.mNoReturnMissionLength.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
this.mNoReturnMissionLength.Name = "mNoReturnMissionLength";
this.mFfaLaunchDelay.Dock = System.Windows.Forms.DockStyle.Fill;
this.mFfaLaunchDelay.Name = "mFfaLaunchDelay";
this.mNoReturnLaunchDelay.Dock = System.Windows.Forms.DockStyle.Fill;
this.mNoReturnLaunchDelay.Name = "mNoReturnLaunchDelay";
this.mButtonsTable.ColumnCount = 2;
this.mMainTable.SetColumnSpan(this.mButtonsTable, 3);
this.mButtonsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100f));
this.mButtonsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.mButtonsTable.Controls.Add(this.mCancel, 1, 0);
this.mButtonsTable.Controls.Add(this.mOK, 0, 0);
this.mButtonsTable.Dock = System.Windows.Forms.DockStyle.Fill;
this.mButtonsTable.Margin = new System.Windows.Forms.Padding(0);
this.mButtonsTable.Name = "mButtonsTable";
this.mButtonsTable.RowCount = 1;
this.mButtonsTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100f));
this.mButtonsTable.Size = new System.Drawing.Size(478, 44);
this.mButtonsTable.TabIndex = 27;
this.mCancel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
this.mCancel.Name = "mCancel";
this.mCancel.Size = new System.Drawing.Size(75, 22);
this.mCancel.TabIndex = 1;
this.mCancel.Text = "&Cancel";
this.mCancel.UseVisualStyleBackColor = true;
this.mCancel.Click += new System.EventHandler(mCancel_Click);
this.mOK.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
this.mOK.Name = "mOK";
this.mOK.Size = new System.Drawing.Size(75, 22);
this.mOK.TabIndex = 0;
this.mOK.Text = "&OK";
this.mOK.UseVisualStyleBackColor = true;
this.mOK.Click += new System.EventHandler(mOK_Click);
base.AcceptButton = this.mOK;
base.AutoScaleDimensions = new System.Drawing.SizeF(6f, 13f);
base.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
base.ClientSize = new System.Drawing.Size(484, 390);
base.Controls.Add(this.mMainTable);
base.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
base.MaximizeBox = false;
base.Name = "BTDefaultsDialog";
base.Padding = new System.Windows.Forms.Padding(3);
this.Text = "BattleTech Defaults";
this.mMainTable.ResumeLayout(false);
this.mMainTable.PerformLayout();
this.mButtonsTable.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)this.mFfaLaunchDelay).EndInit();
((System.ComponentModel.ISupportInitialize)this.mNoReturnLaunchDelay).EndInit();
base.ResumeLayout(false);
}
private static void ConfigureRowLabel(Label label, string name, string text)
{
label.Anchor = AnchorStyles.Right;
label.AutoSize = true;
label.Name = name;
label.Text = text;
}
private static void ConfigureOptionCombo(ComboBox comboBox, string name)
{
comboBox.Anchor = AnchorStyles.Left | AnchorStyles.Right;
comboBox.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox.FormattingEnabled = true;
comboBox.Name = name;
}
}
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The BattleTech battle mission. Both operator game modes — Free For All and
/// No Return — are produced by this class and send <c>scenario=freeforall</c> on
/// the wire (confirmed by both golden eggs); the two modes differ only in the
/// role assigned to each <see cref="BTPlayer" />.
/// </summary>
[Serializable]
internal class BTFreeForAllMission : BTMission
{
public const string ScenarioTag = "freeforall";
private readonly List<BTPlayer> mPlayers = new List<BTPlayer>();
public List<BTPlayer> BattlePlayers => mPlayers;
public override IEnumerable<BTPlayer> Players => mPlayers.ToArray();
public override int PlayerCount => mPlayers.Count;
public BTFreeForAllMission(string mapKey, string timeOfDayKey, string weatherKey, int temperature, TimeSpan gameLength)
: base(ScenarioTag, mapKey, timeOfDayKey, weatherKey, temperature, gameLength)
{
}
protected override void AppendMissionSpecificData(StringBuilder egg)
{
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>A mech was destroyed (<c>MechKilledMessage</c>).</summary>
[Serializable]
internal class BTKilledEvent : BTMissionEvent
{
public readonly BTPlayer Killer;
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[2] { ReportingPilot, Killer };
public BTKilledEvent(BTPlayer reportingPilot, TimeSpan missionTime, BTPlayer killer)
: base(reportingPilot, missionTime)
{
Killer = killer;
}
private BTKilledEvent()
{
}
}
+44
View File
@@ -0,0 +1,44 @@
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
public class BTMap
{
private readonly string mKey;
private readonly string mName;
private readonly string mImagePath;
public string Key => mKey;
public string Name => mName;
public Image Image
{
get
{
if (mImagePath == null)
{
return null;
}
return ImageCache.GetImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), mImagePath));
}
}
internal BTMap(XmlNode mapNode)
{
XmlAttribute xmlAttribute = mapNode.Attributes["image"];
mKey = mapNode.Attributes["key"].InnerText;
mName = mapNode.Attributes["name"].InnerText;
mImagePath = xmlAttribute?.InnerText;
}
public override string ToString()
{
return mName;
}
}
@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using System.Text;
using Munga.Net;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech mission and its egg serializer. Mirrors
/// <c>TeslaConsole.RedPlanet.RPMission</c>: the wire framing
/// (<see cref="ToEggFileMessages" />) and the plasma bitmap / ordinal sections
/// are identical to Red Planet. BattleTech differs only in the egg body — see
/// <see cref="ToEggString" /> (adventure name, non-zero temperature, no score
/// compression, BT participant fields and the <c>[Role::*]</c> blocks).
/// </summary>
[Serializable]
internal abstract class BTMission
{
private readonly string mScenarioKey;
private readonly string mMapKey;
private readonly string mTimeOfDayKey;
private readonly string mWeatherKey;
private readonly int mTemperature;
private readonly TimeSpan mGameLength;
private readonly List<BTCamera> mCameras = new List<BTCamera>();
public string ScenarioKey => mScenarioKey;
public string MapKey => mMapKey;
public string TimeOfDayKey => mTimeOfDayKey;
public string WeatherKey => mWeatherKey;
public int Temperature => mTemperature;
public TimeSpan GameLength => mGameLength;
public abstract IEnumerable<BTPlayer> Players { get; }
public abstract int PlayerCount { get; }
public List<BTCamera> Cameras => mCameras;
protected BTMission(string scenarioKey, string mapKey, string timeOfDayKey, string weatherKey, int temperature, TimeSpan gameLength)
{
if (scenarioKey == null)
{
throw new ArgumentNullException("scenarioKey");
}
if (mapKey == null)
{
throw new ArgumentNullException("mapKey");
}
if (timeOfDayKey == null)
{
throw new ArgumentNullException("timeOfDayKey");
}
if (weatherKey == null)
{
throw new ArgumentNullException("weatherKey");
}
if (gameLength <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException("gameLength");
}
mScenarioKey = scenarioKey;
mMapKey = mapKey;
mTimeOfDayKey = timeOfDayKey;
mWeatherKey = weatherKey;
mTemperature = temperature;
mGameLength = gameLength;
}
public EggFileMessage[] ToEggFileMessages()
{
string text = ToEggString();
text = text.Replace("\r\n", "\0");
text = text.Replace('\n', '\0');
byte[] bytes = Encoding.ASCII.GetBytes(text);
EggFileMessage[] array = new EggFileMessage[(bytes.Length + 999) / 1000];
byte[] array2 = new byte[1000];
for (int i = 0; i < array.Length; i++)
{
int num = i * 1000;
int num2 = bytes.Length - num;
if (num2 > 1000)
{
num2 = 1000;
}
Buffer.BlockCopy(bytes, num, array2, 0, num2);
array[i] = new EggFileMessage(i, bytes.Length, num2, array2);
}
return array;
}
public string ToEggString()
{
StringBuilder stringBuilder = new StringBuilder();
List<string> list = new List<string>();
stringBuilder.Append("[mission]\n");
stringBuilder.Append("adventure=BattleTech\n");
stringBuilder.AppendFormat("map={0}\n", mMapKey);
stringBuilder.AppendFormat("scenario={0}\n", mScenarioKey);
stringBuilder.AppendFormat("time={0}\n", mTimeOfDayKey);
stringBuilder.AppendFormat("weather={0}\n", mWeatherKey);
stringBuilder.AppendFormat("temperature={0}\n", mTemperature);
stringBuilder.AppendFormat("length={0}\n", mGameLength.TotalSeconds);
stringBuilder.Append("[pilots]\n");
foreach (BTPlayer player in Players)
{
stringBuilder.AppendFormat("pilot={0}\n", player.PodHostAddress);
}
foreach (BTCamera camera in Cameras)
{
stringBuilder.AppendFormat("pilot={0}\n", camera.PodHostAddress);
}
int num = 1;
List<BTRole> roles = new List<BTRole>();
foreach (BTPlayer player2 in Players)
{
player2.AppendParticipant(num++, stringBuilder);
list.Add(player2.Name);
if (!roles.Exists((BTRole r) => r.Key == player2.Role.Key))
{
roles.Add(player2.Role);
}
}
foreach (BTCamera camera2 in Cameras)
{
camera2.AppendParticipant(num++, stringBuilder);
list.Add("Camera");
}
AppendMissionSpecificData(stringBuilder);
stringBuilder.Append("[largebitmap]\n");
foreach (string item in list)
{
stringBuilder.AppendFormat("bitmap=BitMap::Large::{0}\n", item);
}
stringBuilder.Append("[smallbitmap]\n");
foreach (string item2 in list)
{
stringBuilder.AppendFormat("bitmap=BitMap::Small::{0}\n", item2);
}
foreach (string item3 in list)
{
stringBuilder.AppendFormat("[BitMap::Large::{0}]\n", item3);
stringBuilder.Append(PlasmaBitmaps.GenerateString(128, 32, item3));
stringBuilder.Append("width=8\n");
stringBuilder.AppendFormat("[BitMap::Small::{0}]\n", item3);
stringBuilder.Append(PlasmaBitmaps.GenerateString(64, 16, item3));
stringBuilder.Append("width=4\n");
}
foreach (BTRole role in roles)
{
stringBuilder.AppendFormat("[Role::{0}]\n", role.Key);
stringBuilder.AppendFormat("model={0}\n", role.Model);
}
AppendOrdinals(stringBuilder);
return stringBuilder.ToString();
}
protected abstract void AppendMissionSpecificData(StringBuilder egg);
private static void AppendOrdinals(StringBuilder sb)
{
sb.Append("[ordinals]\n");
sb.Append("bitmap=Ordinal::BitMap::1\n");
sb.Append("bitmap=Ordinal::BitMap::2\n");
sb.Append("bitmap=Ordinal::BitMap::3\n");
sb.Append("bitmap=Ordinal::BitMap::4\n");
sb.Append("[Ordinal::BitMap::1]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=000038000003C000001FF00000000F00\n");
sb.Append("bitmap=0000F8000003C000003FF80000000F00\n");
sb.Append("bitmap=0001F8000003C00000707C0000000F00\n");
sb.Append("bitmap=0001F8000003C00000603C0000000F00\n");
sb.Append("bitmap=00007801FC0FF00000003C3DF807FF00\n");
sb.Append("bitmap=00007803FE0FF00000003C3FFC0FFF00\n");
sb.Append("bitmap=00007803C703C00000003C3E3C1F0F00\n");
sb.Append("bitmap=000078078303C0000000783C1E1E0F00\n");
sb.Append("bitmap=000078078003C0000000783C1E1E0F00\n");
sb.Append("bitmap=00007807C003C0000000F03C1E1E0F00\n");
sb.Append("bitmap=00007807F003C0000001E03C1E1E0F00\n");
sb.Append("bitmap=00007803FC03C0000003C03C1E1E0F00\n");
sb.Append("bitmap=00007801FE03C0000007803C1E1E0F00\n");
sb.Append("bitmap=000078007F03C000000F003C1E1E0F00\n");
sb.Append("bitmap=000078001F03C000001E003C1E1E0F00\n");
sb.Append("bitmap=000078000F03C000003C003C1E1E0F00\n");
sb.Append("bitmap=000078060F03C0000078003C1E1E0F00\n");
sb.Append("bitmap=000078071E03E0000078003C1E1F1F00\n");
sb.Append("bitmap=00007803FE01F000007FFC3C1E0FFF00\n");
sb.Append("bitmap=00007801FC00F000007FFC3C1E07EF00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
sb.Append("[Ordinal::BitMap::2]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=001FFF0000003C0000003C03C0780000\n");
sb.Append("bitmap=001FFF0000003C0000007C03C0780000\n");
sb.Append("bitmap=00000E0000003C000000FC03C0780000\n");
sb.Append("bitmap=00003C0000003C000001BC03C0780000\n");
sb.Append("bitmap=0000700F3E1FFC000003BC0FF07BF000\n");
sb.Append("bitmap=0001E00F7E3FFC0000073C0FF07FF800\n");
sb.Append("bitmap=0003800FFE7C3C0000063C03C07C7800\n");
sb.Append("bitmap=0007F80FFE783C00000C3C03C0783C00\n");
sb.Append("bitmap=0007FE0F80783C0000183C03C0783C00\n");
sb.Append("bitmap=00001E0F00783C0000383C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000703C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C00007FFF03C0783C00\n");
sb.Append("bitmap=00000F0F00783C00007FFF03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=00180F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=001C1F0F007C7C0000003C03E0783C00\n");
sb.Append("bitmap=000FFE0F003FFC0000003C01F0783C00\n");
sb.Append("bitmap=0007FC0F001FBC0000003C00F0783C00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
sb.Append("[Ordinal::BitMap::3]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=001FFF03C07800000000FC03C0780000\n");
sb.Append("bitmap=001FFF03C07800000003FC03C0780000\n");
sb.Append("bitmap=001E0003C07800000007C003C0780000\n");
sb.Append("bitmap=001E0003C0780000000F0003C0780000\n");
sb.Append("bitmap=001E000FF07BF000000F000FF07BF000\n");
sb.Append("bitmap=001E000FF07FF800001E000FF07FF800\n");
sb.Append("bitmap=001E0003C07C7800001E0003C07C7800\n");
sb.Append("bitmap=001FFC03C0783C00001EFC03C0783C00\n");
sb.Append("bitmap=001FFE03C0783C00001FFE03C0783C00\n");
sb.Append("bitmap=00001F03C0783C00001F1F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00180F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=001C1F03E0783C00001F1F03E0783C00\n");
sb.Append("bitmap=000FFE01F0783C00000FFE01F0783C00\n");
sb.Append("bitmap=0007FC00F0783C000007FC00F0783C00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
sb.Append("[Ordinal::BitMap::4]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=001FFF03C07800000007FC03C0780000\n");
sb.Append("bitmap=001FFF03C0780000000FFE03C0780000\n");
sb.Append("bitmap=00000F03C0780000001F1F03C0780000\n");
sb.Append("bitmap=00000F03C0780000001E0F03C0780000\n");
sb.Append("bitmap=00000F0FF07BF000001E0F0FF07BF000\n");
sb.Append("bitmap=00001F0FF07FF800001E0F0FF07FF800\n");
sb.Append("bitmap=00001E03C07C7800001E0F03C07C7800\n");
sb.Append("bitmap=00003E03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00003C03C0783C00000F1E03C0783C00\n");
sb.Append("bitmap=00003C03C0783C000007FC03C0783C00\n");
sb.Append("bitmap=00007803C0783C000007FC03C0783C00\n");
sb.Append("bitmap=00007803C0783C00000F1E03C0783C00\n");
sb.Append("bitmap=00007803C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003E0783C00000F1E03E0783C00\n");
sb.Append("bitmap=0000F001F0783C00000FFE01F0783C00\n");
sb.Append("bitmap=0000F000F0783C000007FC00F0783C00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Base class for a recorded in-match BattleTech event. Mirrors the Red Planet
/// <c>MissionEvent</c>, typed over <see cref="BTPlayer" />.
/// </summary>
[Serializable]
internal abstract class BTMissionEvent
{
public readonly TimeSpan GameTime;
public readonly BTPlayer ReportingPilot;
public abstract IEnumerable<BTPlayer> InvolvedPilots { get; }
public BTMissionEvent(BTPlayer reportingPilot, TimeSpan missionTime)
{
ReportingPilot = reportingPilot;
GameTime = missionTime;
}
protected BTMissionEvent()
{
}
}
@@ -0,0 +1,99 @@
using System;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Records the in-match Munga event messages of one BattleTech mission into a
/// <see cref="BTMissionResults" />. Mirrors <c>RPMissionRecorder</c>; the event
/// set follows the BT messages in Munga Net.dll (Mech* / EndMission).
/// </summary>
internal class BTMissionRecorder
{
private BTMissionResults mMissionResults;
private bool mStartSet;
public BTMissionRecorder(BTMission mission)
{
mMissionResults = new BTMissionResults(mission);
}
public void SetMissionStart()
{
if (mStartSet)
{
throw new InvalidOperationException();
}
mMissionResults.mMissionStart = DateTime.Now;
mStartSet = true;
}
private void ValidatePlayer(BTPlayer player, string param)
{
if (player == null)
{
throw new ArgumentNullException(param);
}
foreach (BTPlayer player2 in mMissionResults.Mission.Players)
{
if (player2 == player)
{
return;
}
}
throw new ArgumentException(param);
}
private TimeSpan GetMissionTime()
{
if (!mStartSet)
{
return TimeSpan.Zero;
}
return DateTime.Now - mMissionResults.mMissionStart;
}
public void PlayerKilled(BTPlayer reportingPlayer, BTPlayer killer)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
ValidatePlayer(killer, "killer");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTKilledEvent(reportingPlayer, missionTime, killer));
}
public void PlayerDeathWithoutHonor(BTPlayer reportingPlayer)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTDeathWithoutHonorEvent(reportingPlayer, missionTime));
}
public void PlayerScoreUpdate(BTPlayer reportingPlayer, int score)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTScoreUpdateEvent(reportingPlayer, missionTime, score));
}
public void PlayerDamaged(BTPlayer reportingPlayer, BTPlayer damager, int damageLoss, int pointsTransfered, int damageZoneIndex, bool damageZoneDestroyed, int weaponIndex)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
ValidatePlayer(damager, "damager");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTDamagedEvent(reportingPlayer, missionTime, damager, damageLoss, pointsTransfered, damageZoneIndex, damageZoneDestroyed, weaponIndex));
}
public void PlayerFinalScore(BTPlayer reportingPlayer, int finalScore)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
mMissionResults.mFinalScores.Add(reportingPlayer, finalScore);
}
public BTMissionResults EndMission(TimeSpan missionLength)
{
mMissionResults.mActuallMissionTime = missionLength;
BTMissionResults result = mMissionResults;
mMissionResults = null;
return result;
}
}
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.IO;
using System.IO.Compression;
using System.Runtime.Serialization.Formatters.Binary;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The recorded outcome of one BattleTech mission, saved as a gzip'd
/// BinaryFormatter blob (.btm) exactly like the RP .rpm mechanism. Mirrors
/// <c>RPMissionResults</c> minus the football branches (BT battle modes have
/// no teams, so every ranking entry is a single pilot).
/// </summary>
[Serializable]
internal class BTMissionResults
{
private BTMission mMission;
internal DateTime mMissionStart;
internal TimeSpan mActuallMissionTime;
internal readonly List<BTMissionEvent> mEvents = new List<BTMissionEvent>();
internal readonly Dictionary<BTPlayer, int> mFinalScores = new Dictionary<BTPlayer, int>();
public BTMission Mission => mMission;
public DateTime MissionStart => mMissionStart;
public TimeSpan ActuallMissionTime => mActuallMissionTime;
public IEnumerable<BTMissionEvent> Events => mEvents;
internal BTMissionResults(BTMission mission)
{
mMission = mission;
}
public static string GetBTMissionsDirectory()
{
string text = Path.Combine(Program.GetCommonAppDataDirectory(), "BT Missions");
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
return text;
}
public static BTMissionResults Load(string file)
{
using GZipStream gZipStream = new GZipStream(File.OpenRead(file), CompressionMode.Decompress);
BinaryFormatter binaryFormatter = new BinaryFormatter();
BTMissionResults result = (BTMissionResults)binaryFormatter.Deserialize(gZipStream);
gZipStream.Close();
return result;
}
public void Save()
{
string path = Path.Combine(GetBTMissionsDirectory(), mMissionStart.ToString("yyyy-MM-dd HH-mm-ss.ffff") + ".btm");
using GZipStream gZipStream = new GZipStream(File.OpenWrite(path), CompressionMode.Compress);
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(gZipStream, this);
gZipStream.Close();
}
public int GetScore(BTPlayer player)
{
return GetScore(player, TimeSpan.MaxValue);
}
public int GetScore(BTPlayer player, TimeSpan gameTime)
{
if (gameTime == TimeSpan.MaxValue && mFinalScores.ContainsKey(player))
{
return mFinalScores[player];
}
int result = 1000;
TimeSpan timeSpan = TimeSpan.MinValue;
foreach (BTMissionEvent mEvent in mEvents)
{
if (mEvent is BTScoreUpdateEvent && mEvent.ReportingPilot == player && mEvent.GameTime > timeSpan && mEvent.GameTime <= gameTime)
{
result = ((BTScoreUpdateEvent)mEvent).Score;
timeSpan = mEvent.GameTime;
}
}
return result;
}
public int GetFinalPlace(BTPlayer player)
{
int num = 1;
int score = GetScore(player, TimeSpan.MaxValue);
foreach (BTPlayer player2 in mMission.Players)
{
if (player2 != player && GetScore(player2, TimeSpan.MaxValue) > score)
{
num++;
}
}
return num;
}
public BTPlayer[][] GetChartOrder()
{
return GetChartOrder(TimeSpan.MaxValue);
}
public BTPlayer[][] GetChartOrder(TimeSpan gameTime)
{
List<Tuple<BTPlayer[], int>> list = new List<Tuple<BTPlayer[], int>>();
foreach (BTPlayer player in Mission.Players)
{
list.Add(new Tuple<BTPlayer[], int>(new BTPlayer[1] { player }, GetScore(player, gameTime)));
}
for (int i = 0; i < list.Count - 1; i++)
{
for (int j = i + 1; j < list.Count; j++)
{
if (list[i].B < list[j].B || (list[i].B == list[j].B && string.CompareOrdinal(list[i].A[0].Name, list[j].A[0].Name) > 0))
{
Tuple<BTPlayer[], int> value = list[i];
list[i] = list[j];
list[j] = value;
}
}
}
List<BTPlayer[]> list2 = new List<BTPlayer[]>();
foreach (Tuple<BTPlayer[], int> item in list)
{
list2.Add(item.A);
}
return list2.ToArray();
}
public PrintDocument GetPrintDocument()
{
return new BTPrintDocument(this);
}
}
@@ -0,0 +1,31 @@
using System;
using System.Text;
namespace TeslaConsole.BattleTech;
[Serializable]
internal abstract class BTParticipant
{
private readonly string mPodHostAddress;
private readonly HostType mHostType;
public string PodHostAddress => mPodHostAddress;
public HostType HostType => mHostType;
protected BTParticipant(string podHostAddress, HostType hostType)
{
mPodHostAddress = podHostAddress;
mHostType = hostType;
}
public void AppendParticipant(int index, StringBuilder sb)
{
sb.AppendFormat("[{0}]\n", mPodHostAddress);
sb.AppendFormat("hostType={0}\n", (int)mHostType);
AppendParticipantSpecificData(index, sb);
}
protected abstract void AppendParticipantSpecificData(int index, StringBuilder sb);
}
+122
View File
@@ -0,0 +1,122 @@
using System;
using System.Text;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech combatant (mech pilot). Mirrors <c>RPPlayer</c> but carries the
/// BT-only participant fields (advanced damage model, pilot experience, mech
/// value, faction emblem, camo, patch colour and role). Free For All and No
/// Return use the same player type and differ only by the assigned
/// <see cref="Role" />.
/// </summary>
[Serializable]
internal class BTPlayer : BTParticipant
{
private readonly string mName;
private readonly string mVehicleKey;
private readonly string mCamoKey;
private readonly string mPatchKey;
private readonly string mEmblemKey;
private readonly string mExperienceKey;
private readonly BTRole mRole;
private readonly bool mAdvancedDamage;
private readonly string mDropZoneKey;
private readonly int mVehicleValue;
public string Name => mName;
public string VehicleKey => mVehicleKey;
public string CamoKey => mCamoKey;
public string PatchKey => mPatchKey;
public string EmblemKey => mEmblemKey;
public string ExperienceKey => mExperienceKey;
public BTRole Role => mRole;
public bool AdvancedDamage => mAdvancedDamage;
public string DropZoneKey => mDropZoneKey;
public int VehicleValue => mVehicleValue;
public BTPlayer(string podHostAddress, string name, string vehicleKey, string camoKey, string patchKey, string emblemKey, string experienceKey, BTRole role, bool advancedDamage = true, string dropZoneKey = "one", int vehicleValue = 0)
: base(podHostAddress, HostType.GameMachineHostType)
{
if (podHostAddress == null)
{
throw new ArgumentNullException("podHostAddress");
}
if (name == null)
{
throw new ArgumentNullException("name");
}
if (vehicleKey == null)
{
throw new ArgumentNullException("vehicleKey");
}
if (camoKey == null)
{
throw new ArgumentNullException("camoKey");
}
if (patchKey == null)
{
throw new ArgumentNullException("patchKey");
}
if (emblemKey == null)
{
throw new ArgumentNullException("emblemKey");
}
if (experienceKey == null)
{
throw new ArgumentNullException("experienceKey");
}
if (role == null)
{
throw new ArgumentNullException("role");
}
if (dropZoneKey == null)
{
throw new ArgumentNullException("dropZoneKey");
}
mName = name;
mVehicleKey = vehicleKey;
mCamoKey = camoKey;
mPatchKey = patchKey;
mEmblemKey = emblemKey;
mExperienceKey = experienceKey;
mRole = role;
mAdvancedDamage = advancedDamage;
mDropZoneKey = dropZoneKey;
mVehicleValue = vehicleValue;
}
protected sealed override void AppendParticipantSpecificData(int index, StringBuilder sb)
{
sb.AppendFormat("advancedDamage={0}\n", mAdvancedDamage ? "1" : "0");
sb.Append("loadzones=1\n");
sb.AppendFormat("name={0}\n", mName);
sb.AppendFormat("bitmapindex={0}\n", index);
sb.AppendFormat("experience={0}\n", mExperienceKey);
sb.AppendFormat("vehicle={0}\n", mVehicleKey);
sb.AppendFormat("vehicleValue={0}\n", mVehicleValue);
sb.AppendFormat("badge={0}\n", mEmblemKey);
sb.AppendFormat("dropzone={0}\n", mDropZoneKey);
sb.AppendFormat("color={0}\n", mCamoKey);
sb.AppendFormat("patch={0}\n", mPatchKey);
sb.AppendFormat("role=Role::{0}\n", mRole.Key);
}
}
@@ -0,0 +1,748 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Printing;
using System.Text;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The BattleTech score-sheet print document: one landscape page per pilot with
/// the mech portrait, placing, kill/death/damage stats, a randomized
/// mission-highlights narrative (<see cref="BTStrings" />), the pilot-vs-pilot
/// damage matrix, and the place-over-time chart. A close mirror of
/// <c>RPPrintDocument</c> minus the football branches and the RP-only
/// lap/boost/score-zone concepts; the chart marks kills and deaths only.
/// </summary>
public class BTPrintDocument : PrintDocument
{
private const int sThinBorderSize = 3;
private const int sThickBorderSize = 14;
private const int sChampherSize = 14;
private const double sSmallRectWidthDropFraction = 0.5721493440968718;
private const double sSmallRectHeightDropFraction = 245.0 / 372.0;
private const string sSansSerifFontName = "Helvetica LT Std";
private const string sSerifFontName = "Courier New";
private readonly BTMissionResults mMissionResults;
private readonly BTPlayer[][] mFinalRanking;
private readonly BTPlayer[] mPrintOrder;
private int mCurrentPlayer;
private Dictionary<BTPlayer, List<Point>> mPlayerLines;
private int[] mChartRows;
private Dictionary<TimeSpan, int> mChartCols;
internal BTPrintDocument(BTMissionResults missionResults)
{
mMissionResults = missionResults;
bool noReturn = false;
foreach (BTPlayer player in mMissionResults.Mission.Players)
{
if (player.Role.Key == "NoReturn")
{
noReturn = true;
break;
}
}
base.DocumentName = string.Format("BattleTech {0} mission at {1} on {2}", noReturn ? "No Return" : "Free For All", mMissionResults.MissionStart.ToLongTimeString(), mMissionResults.MissionStart.ToLongDateString());
mFinalRanking = mMissionResults.GetChartOrder();
List<BTPlayer> list = new List<BTPlayer>();
for (int i = 0; i < mFinalRanking.Length; i++)
{
list.Add(mFinalRanking[i][0]);
}
for (int j = 0; j < mFinalRanking.Length; j++)
{
for (int k = 1; k < mFinalRanking[j].Length; k++)
{
list.Add(mFinalRanking[j][k]);
}
}
mPrintOrder = list.ToArray();
}
protected override void OnQueryPageSettings(QueryPageSettingsEventArgs e)
{
e.PageSettings.Margins = new Margins(50, 50, 50, 50);
e.PageSettings.Landscape = true;
base.OnQueryPageSettings(e);
}
protected override void OnBeginPrint(PrintEventArgs e)
{
mCurrentPlayer = 0;
base.OnBeginPrint(e);
}
protected override void OnPrintPage(PrintPageEventArgs e)
{
BTPlayer bTPlayer = mPrintOrder[mCurrentPlayer];
List<BTMissionEvent> list = new List<BTMissionEvent>();
foreach (BTMissionEvent @event in mMissionResults.Events)
{
if (Contains(@event.InvolvedPilots, bTPlayer))
{
list.Add(@event);
}
}
for (int i = 0; i < list.Count - 1; i++)
{
for (int j = i + 1; j < list.Count; j++)
{
if (list[i].GameTime > list[j].GameTime)
{
BTMissionEvent value = list[i];
list[i] = list[j];
list[j] = value;
}
}
}
int score = mMissionResults.GetScore(bTPlayer);
int finalPlace = mMissionResults.GetFinalPlace(bTPlayer);
Size size = new Size(e.PageBounds.Width - 100, e.PageBounds.Height - 100);
if (size.Width > e.MarginBounds.Width)
{
size.Width = e.MarginBounds.Width;
}
if (size.Height > e.MarginBounds.Height)
{
size.Height = e.MarginBounds.Height;
}
Rectangle rectangle = new Rectangle((e.MarginBounds.Width - size.Width) / 2 + e.MarginBounds.Left, (e.MarginBounds.Height - size.Height) / 2 + e.MarginBounds.Top, size.Width, size.Height);
Rectangle inputRect = rectangle;
inputRect.Inflate(-14, -14);
Rectangle rectangle2 = rectangle;
rectangle2.Inflate(-3, -3);
Rectangle inputRect2 = rectangle2;
inputRect2.Inflate(-14, -14);
Rectangle rectangle3 = rectangle2;
rectangle3.Inflate(-7, -7);
Rectangle inputRect3 = rectangle3;
inputRect3.Inflate(-14, -14);
Rectangle rectangle4 = rectangle2;
rectangle4.Inflate(-14, -14);
Rectangle inputRect4 = rectangle4;
inputRect4.Inflate(-14, -14);
Rectangle rectangle5 = rectangle4;
rectangle5.Inflate(-3, -3);
Rectangle inputRect5 = rectangle5;
inputRect5.Inflate(-14, -14);
int widthDrop = (int)(0.5721493440968718 * (double)rectangle3.Width + 0.5);
int heightDrop = (int)(245.0 / 372.0 * (double)rectangle3.Height + 0.5);
Rectangle rectangle6 = RectDrop(rectangle, widthDrop, heightDrop);
Rectangle rectangle7 = RectDrop(inputRect, widthDrop, heightDrop);
Rectangle rectangle8 = RectDrop(rectangle2, widthDrop, heightDrop);
Rectangle rectangle9 = RectDrop(inputRect2, widthDrop, heightDrop);
Rectangle rectangle10 = RectDrop(rectangle3, widthDrop, heightDrop);
Rectangle rectangle11 = RectDrop(inputRect3, widthDrop, heightDrop);
Rectangle rectangle12 = RectDrop(rectangle4, widthDrop, heightDrop);
Rectangle rectangle13 = RectDrop(inputRect4, widthDrop, heightDrop);
Rectangle rectangle14 = RectDrop(rectangle5, widthDrop, heightDrop);
Rectangle rectangle15 = RectDrop(inputRect5, widthDrop, heightDrop);
using (GraphicsPath graphicsPath = new GraphicsPath())
{
graphicsPath.AddLine(rectangle.Left, inputRect.Bottom, rectangle.Left, inputRect.Top);
graphicsPath.AddLine(inputRect.Left, rectangle.Top, inputRect.Right, rectangle.Top);
graphicsPath.AddLine(rectangle3.Right, rectangle3.Top, inputRect3.Left, rectangle3.Top);
graphicsPath.AddLine(rectangle3.Left, inputRect3.Top, rectangle3.Left, rectangle3.Bottom);
graphicsPath.StartFigure();
graphicsPath.AddLine(rectangle3.Left, inputRect5.Bottom, rectangle5.Left, inputRect5.Bottom);
graphicsPath.AddLine(inputRect5.Left, rectangle5.Bottom, inputRect5.Right, rectangle5.Bottom);
graphicsPath.AddLine(rectangle5.Right, inputRect5.Bottom, rectangle5.Right, rectangle3.Top);
graphicsPath.AddLine(rectangle3.Right, rectangle3.Top, rectangle3.Right, inputRect3.Bottom);
graphicsPath.AddLine(inputRect3.Right, rectangle3.Bottom, rectangle3.Left, rectangle3.Bottom);
graphicsPath.StartFigure();
graphicsPath.AddLine(rectangle10.Left, rectangle15.Bottom, rectangle14.Left, rectangle15.Bottom);
graphicsPath.AddLine(rectangle15.Left, rectangle14.Bottom, rectangle15.Right, rectangle14.Bottom);
graphicsPath.AddLine(rectangle14.Right, rectangle15.Bottom, rectangle14.Right, rectangle10.Top);
graphicsPath.AddLine(rectangle10.Right, rectangle10.Top, rectangle10.Right, rectangle11.Bottom);
graphicsPath.AddLine(rectangle11.Right, rectangle10.Bottom, rectangle10.Left, rectangle10.Bottom);
e.Graphics.FillPath(Brushes.LightGray, graphicsPath);
}
using (GraphicsPath graphicsPath2 = new GraphicsPath())
{
graphicsPath2.AddLine(rectangle2.Left, inputRect2.Bottom, rectangle.Left, inputRect.Bottom);
graphicsPath2.AddLine(inputRect.Left, rectangle.Bottom, inputRect.Right, rectangle.Bottom);
graphicsPath2.AddLine(rectangle.Right, inputRect.Bottom, rectangle.Right, inputRect.Top);
graphicsPath2.AddLine(inputRect.Right, rectangle.Top, inputRect2.Right, rectangle2.Top);
graphicsPath2.AddLine(inputRect2.Right, rectangle3.Top, rectangle3.Right, inputRect3.Top);
graphicsPath2.AddLine(rectangle3.Right, inputRect3.Bottom, inputRect3.Right, rectangle3.Bottom);
graphicsPath2.AddLine(inputRect2.Left, rectangle3.Bottom, rectangle3.Left, inputRect2.Bottom);
graphicsPath2.StartFigure();
graphicsPath2.AddLine(rectangle12.Left, rectangle13.Bottom, rectangle14.Left, rectangle15.Bottom);
graphicsPath2.AddLine(rectangle14.Left, rectangle15.Top, rectangle15.Left, rectangle14.Top);
graphicsPath2.AddLine(rectangle15.Right, rectangle14.Top, rectangle14.Right, rectangle15.Top);
graphicsPath2.AddLine(rectangle12.Right, rectangle13.Top, rectangle10.Right, rectangle13.Top);
graphicsPath2.AddLine(rectangle10.Right, rectangle10.Top, rectangle11.Left, rectangle10.Top);
graphicsPath2.AddLine(rectangle10.Left, rectangle11.Top, rectangle10.Left, rectangle13.Bottom);
graphicsPath2.StartFigure();
graphicsPath2.AddLine(rectangle4.Left, inputRect4.Bottom, rectangle5.Left, inputRect5.Bottom);
graphicsPath2.AddLine(rectangle5.Left, rectangle6.Bottom + 14, inputRect5.Left, rectangle6.Bottom);
graphicsPath2.AddLine(rectangle7.Right, rectangle6.Bottom, rectangle6.Right, rectangle7.Bottom);
graphicsPath2.AddLine(rectangle6.Right, inputRect5.Top, rectangle6.Right + 14, rectangle5.Top);
graphicsPath2.AddLine(inputRect5.Right, rectangle5.Top, rectangle5.Right, inputRect5.Top);
graphicsPath2.AddLine(rectangle4.Right, inputRect4.Top, rectangle3.Right, inputRect4.Top);
graphicsPath2.AddLine(rectangle3.Right, inputRect3.Top, inputRect3.Right, rectangle3.Top);
graphicsPath2.AddLine(rectangle10.Right, rectangle3.Top, rectangle10.Right, rectangle11.Bottom);
graphicsPath2.AddLine(rectangle11.Right, rectangle10.Bottom, inputRect3.Left, rectangle10.Bottom);
graphicsPath2.AddLine(rectangle3.Left, rectangle10.Bottom, rectangle3.Left, inputRect4.Bottom);
e.Graphics.FillPath(Brushes.DarkGray, graphicsPath2);
}
using (GraphicsPath graphicsPath3 = new GraphicsPath())
{
graphicsPath3.AddLine(rectangle2.Left, inputRect2.Bottom, rectangle2.Left, inputRect2.Top);
graphicsPath3.AddLine(inputRect2.Left, rectangle2.Top, inputRect2.Right, rectangle2.Top);
graphicsPath3.AddLine(rectangle2.Right, inputRect2.Top, rectangle2.Right, inputRect2.Bottom);
graphicsPath3.AddLine(inputRect2.Right, rectangle2.Bottom, inputRect2.Left, rectangle2.Bottom);
graphicsPath3.StartFigure();
graphicsPath3.AddLine(rectangle12.Left, rectangle13.Bottom, rectangle12.Left, rectangle13.Top);
graphicsPath3.AddLine(rectangle13.Left, rectangle12.Top, rectangle13.Right, rectangle12.Top);
graphicsPath3.AddLine(rectangle12.Right, rectangle13.Top, rectangle12.Right, rectangle13.Bottom);
graphicsPath3.AddLine(rectangle13.Right, rectangle12.Bottom, rectangle13.Left, rectangle12.Bottom);
graphicsPath3.StartFigure();
graphicsPath3.AddLine(rectangle4.Left, inputRect4.Bottom, rectangle4.Left, rectangle8.Bottom + 14);
graphicsPath3.AddLine(rectangle13.Left, rectangle8.Bottom, rectangle9.Right, rectangle8.Bottom);
graphicsPath3.AddLine(rectangle8.Right, rectangle9.Bottom, rectangle8.Right, inputRect4.Top);
graphicsPath3.AddLine(rectangle8.Right + 14, rectangle4.Top, inputRect4.Right, rectangle4.Top);
graphicsPath3.AddLine(rectangle4.Right, inputRect4.Top, rectangle4.Right, inputRect4.Bottom);
graphicsPath3.AddLine(inputRect4.Right, rectangle4.Bottom, inputRect4.Left, rectangle4.Bottom);
e.Graphics.FillPath(Brushes.Gray, graphicsPath3);
}
using Font font3 = new Font("Helvetica LT Std", 12f);
using Font font = new Font("Helvetica LT Std", 8f);
Rectangle rectangle16 = rectangle14;
rectangle16.X += 175;
rectangle16.Width -= 178;
rectangle16.Y += 58;
rectangle16.Height -= 86;
BTVehicle bTVehicle = (BTConfig.Vehicles.ContainsKey(bTPlayer.VehicleKey) ? BTConfig.Vehicles[bTPlayer.VehicleKey] : null);
Image image = bTVehicle?.Image;
if (image != null)
{
e.Graphics.DrawImage(image, GetCenteredScaledRect(rectangle16, image.Size));
}
else
{
StringFormat stringFormat = new StringFormat();
stringFormat.LineAlignment = StringAlignment.Center;
stringFormat.Alignment = StringAlignment.Center;
using (StringFormat format = stringFormat)
{
Rectangle rectangle17 = rectangle16;
rectangle17.Inflate(-20, -20);
e.Graphics.DrawString("The tech crew's holo-imager is down for repairs. Recon imagery could not be obtained for this mech.", font, Brushes.Black, rectangle17, format);
}
e.Graphics.DrawRectangle(Pens.Black, rectangle16);
}
using (Font font2 = new Font("Helvetica LT Std", 18f, FontStyle.Bold))
{
using StringFormat stringFormat2 = new StringFormat();
stringFormat2.Alignment = StringAlignment.Center;
stringFormat2.LineAlignment = StringAlignment.Far;
e.Graphics.DrawString(bTPlayer.Name, font2, Brushes.Black, rectangle14.Left + rectangle14.Width / 2, rectangle16.Top - 10, stringFormat2);
}
using (StringFormat stringFormat3 = new StringFormat())
{
stringFormat3.Alignment = StringAlignment.Center;
e.Graphics.DrawString((bTVehicle != null) ? bTVehicle.Name : bTPlayer.VehicleKey, font3, Brushes.Black, rectangle16.Left + rectangle16.Width / 2, rectangle16.Bottom + 5, stringFormat3);
}
int num = rectangle15.Left - 7;
Rectangle rectangle18 = new Rectangle(num, rectangle16.Top, rectangle16.Left - num, rectangle15.Bottom - rectangle16.Top);
using (Font font4 = new Font("Helvetica LT Std", 16f, FontStyle.Bold))
{
using StringFormat stringFormat4 = new StringFormat();
stringFormat4.Alignment = StringAlignment.Center;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine(string.Format("{0}{1} Place", finalPlace, finalPlace switch
{
3 => "rd",
2 => "nd",
1 => "st",
_ => "th",
}));
e.Graphics.DrawString(stringBuilder.ToString(), font4, Brushes.Black, rectangle18, stringFormat4);
}
using (StringFormat stringFormat5 = new StringFormat())
{
stringFormat5.LineAlignment = StringAlignment.Far;
stringFormat5.SetTabStops(0f, new float[1] { 80f });
int deaths = 0;
int kills = 0;
GetPlayerStats(out deaths, out kills, bTPlayer, list);
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.AppendLine($"Score:\t{score}");
stringBuilder2.AppendLine();
stringBuilder2.AppendLine($"Deaths:\t{deaths}");
stringBuilder2.AppendLine();
stringBuilder2.AppendLine($"Kills:\t{kills}");
e.Graphics.DrawString(stringBuilder2.ToString(), font, Brushes.Black, rectangle18, stringFormat5);
}
int num2 = rectangle5.Top + 40;
Rectangle rectangle19 = new Rectangle(rectangle6.Right + 5, num2, rectangle5.Right - rectangle6.Right - 10, rectangle6.Bottom - 5 - num2);
using (StringFormat stringFormat6 = new StringFormat())
{
stringFormat6.Alignment = StringAlignment.Center;
e.Graphics.DrawString("Mission Highlights", font3, Brushes.Black, (rectangle19.Right + rectangle19.Left) / 2, rectangle5.Top + 15, stringFormat6);
}
using (Font font5 = new Font("Courier New", 10f))
{
string text = mMissionResults.MissionStart.ToString() + " ";
string s = text;
List<string> list2 = new List<string>();
BTPlayer[][] array = mFinalRanking;
foreach (BTPlayer[] array2 in array)
{
list2.Add(array2[0].Name);
}
string text2 = BTStrings.Recap(list2.ToArray());
Random random = new Random();
List<BTMissionEvent> list3 = new List<BTMissionEvent>(list);
SortedList<TimeSpan, string> sortedList = new SortedList<TimeSpan, string>();
string text3 = text + text2;
while (e.Graphics.MeasureString(text3, font5, rectangle19.Width).Height < (float)rectangle19.Height)
{
s = text3;
if (list3.Count < 1)
{
break;
}
int index = random.Next(list3.Count);
BTMissionEvent bTMissionEvent = list3[index];
list3.RemoveAt(index);
string text4 = null;
if (bTMissionEvent is BTDamagedEvent)
{
BTDamagedEvent bTDamagedEvent = (BTDamagedEvent)bTMissionEvent;
if (bTDamagedEvent.DamageLoss > 0)
{
text4 = ((bTDamagedEvent.DamageLoss < 10) ? BTStrings.LightDamage(bTDamagedEvent.GameTime, bTDamagedEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTDamagedEvent.ReportingPilot.VehicleKey), bTDamagedEvent.Damager.Name) : ((bTDamagedEvent.DamageLoss >= 100) ? BTStrings.HeavyDamage(bTDamagedEvent.GameTime, bTDamagedEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTDamagedEvent.ReportingPilot.VehicleKey), bTDamagedEvent.Damager.Name) : BTStrings.ModerateDamage(bTDamagedEvent.GameTime, bTDamagedEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTDamagedEvent.ReportingPilot.VehicleKey), bTDamagedEvent.Damager.Name)));
}
}
else if (bTMissionEvent is BTKilledEvent)
{
BTKilledEvent bTKilledEvent = (BTKilledEvent)bTMissionEvent;
text4 = ((bTKilledEvent.ReportingPilot != bTKilledEvent.Killer) ? BTStrings.Kill(bTKilledEvent.GameTime, bTKilledEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTKilledEvent.ReportingPilot.VehicleKey), bTKilledEvent.Killer.Name) : BTStrings.DeathWithoutHonor(bTKilledEvent.GameTime, bTKilledEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTKilledEvent.ReportingPilot.VehicleKey)));
}
else if (bTMissionEvent is BTDeathWithoutHonorEvent)
{
text4 = BTStrings.DeathWithoutHonor(bTMissionEvent.GameTime, bTMissionEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTMissionEvent.ReportingPilot.VehicleKey));
}
if (text4 != null)
{
TimeSpan gameTime;
for (gameTime = bTMissionEvent.GameTime; sortedList.ContainsKey(gameTime); gameTime += TimeSpan.FromTicks(1L))
{
}
sortedList.Add(gameTime, text4);
}
StringBuilder stringBuilder3 = new StringBuilder(text);
foreach (KeyValuePair<TimeSpan, string> item in sortedList)
{
if (stringBuilder3.Length > 0)
{
stringBuilder3.Append(" ");
}
stringBuilder3.Append(item.Value);
}
if (stringBuilder3.Length > 0)
{
stringBuilder3.Append(" ");
}
stringBuilder3.Append(text2);
text3 = stringBuilder3.ToString();
}
e.Graphics.DrawString(s, font5, Brushes.Black, rectangle19);
}
int num3 = rectangle6.Bottom + 5;
Rectangle rectangle20 = new Rectangle(inputRect5.Left, num3, inputRect5.Width, rectangle5.Bottom - num3);
Rectangle rectangle21 = new Rectangle(rectangle20.Right - 250, rectangle20.Top, 250, rectangle20.Height - 20);
Image image2 = (BTConfig.Maps.ContainsKey(mMissionResults.Mission.MapKey) ? BTConfig.Maps[mMissionResults.Mission.MapKey] : null)?.Image;
if (image2 != null)
{
e.Graphics.DrawImage(image2, GetCenteredScaledRect(rectangle21, image2.Size));
}
else
{
StringFormat stringFormat7 = new StringFormat();
stringFormat7.LineAlignment = StringAlignment.Center;
stringFormat7.Alignment = StringAlignment.Center;
using (StringFormat format2 = stringFormat7)
{
Rectangle rectangle22 = rectangle21;
rectangle22.Inflate(-20, -20);
e.Graphics.DrawString("The recon satellite is currently on a transitional orbit. Satellite imagery could not be obtained for this arena.", font, Brushes.Black, rectangle22, format2);
}
e.Graphics.DrawRectangle(Pens.Black, rectangle21);
}
Rectangle rect = new Rectangle(rectangle20.Left, rectangle20.Top, rectangle21.Left - 5 - rectangle20.Left, 92);
Dictionary<BTPlayer, int[]> dictionary = new Dictionary<BTPlayer, int[]>();
foreach (BTPlayer player in mMissionResults.Mission.Players)
{
if (player != bTPlayer)
{
dictionary.Add(player, new int[4]);
}
}
foreach (BTMissionEvent event2 in mMissionResults.Events)
{
if (event2 is BTKilledEvent)
{
BTKilledEvent bTKilledEvent2 = (BTKilledEvent)event2;
if (bTKilledEvent2.ReportingPilot != bTKilledEvent2.Killer)
{
if (bTKilledEvent2.Killer == bTPlayer)
{
dictionary[bTKilledEvent2.ReportingPilot][3]++;
}
else if (bTKilledEvent2.ReportingPilot == bTPlayer)
{
dictionary[bTKilledEvent2.Killer][2]++;
}
}
}
else
{
if (!(event2 is BTDamagedEvent))
{
continue;
}
BTDamagedEvent bTDamagedEvent2 = (BTDamagedEvent)event2;
if (bTDamagedEvent2.ReportingPilot != bTDamagedEvent2.Damager)
{
if (bTDamagedEvent2.Damager == bTPlayer)
{
dictionary[bTDamagedEvent2.ReportingPilot][1] += bTDamagedEvent2.DamageLoss;
}
else if (bTDamagedEvent2.ReportingPilot == bTPlayer)
{
dictionary[bTDamagedEvent2.Damager][0] += bTDamagedEvent2.DamageLoss;
}
}
}
}
using (Pen pen = new Pen(Color.Gray, 2f))
{
using Font font6 = new Font("Helvetica LT Std", 6f, FontStyle.Regular);
using StringFormat stringFormat8 = new StringFormat();
e.Graphics.DrawRectangle(Pens.Gray, rect);
int num4 = 120;
int num5 = 14;
int num6 = rect.Left + 1;
int num7 = rect.Left + num4;
int num8 = (rect.Width - num4) / Math.Max(1, dictionary.Count);
if (dictionary.Count == 0)
{
e.Graphics.DrawLine(pen, num7, rect.Top, num7, rect.Bottom);
}
int num9 = rect.Bottom;
for (int l = 0; l < 5; l++)
{
num9 -= num5;
e.Graphics.DrawLine(Pens.Gray, rect.Left, num9, rect.Right, num9);
}
int num10 = rect.Top + 1;
stringFormat8.LineAlignment = StringAlignment.Near;
e.Graphics.DrawString("Pilot", font6, Brushes.Black, num6, num10, stringFormat8);
int num11 = rect.Bottom - num5 / 2;
int num12 = num11 - num5;
int num13 = num12 - num5;
int num14 = num13 - num5;
int num15 = num14 - num5;
stringFormat8.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString("Score", font6, Brushes.Black, num6, num15, stringFormat8);
e.Graphics.DrawString("Damage You Received From", font6, Brushes.Black, num6, num14, stringFormat8);
e.Graphics.DrawString("Damage You Inflicted On", font6, Brushes.Black, num6, num13, stringFormat8);
e.Graphics.DrawString("Times You Were Killed By", font6, Brushes.Black, num6, num12, stringFormat8);
e.Graphics.DrawString("Times You Killed", font6, Brushes.Black, num6, num11, stringFormat8);
int num16 = num7;
foreach (KeyValuePair<BTPlayer, int[]> item2 in dictionary)
{
int num17 = num16 + 1;
e.Graphics.DrawLine(pen, num16, rect.Top, num16, rect.Bottom);
stringFormat8.LineAlignment = StringAlignment.Near;
e.Graphics.DrawString(item2.Key.Name, font6, Brushes.Black, num17, num10, stringFormat8);
stringFormat8.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString(mMissionResults.GetScore(item2.Key).ToString(), font6, Brushes.Black, num17, num15, stringFormat8);
e.Graphics.DrawString(item2.Value[0].ToString(), font6, Brushes.Black, num17, num14, stringFormat8);
e.Graphics.DrawString(item2.Value[1].ToString(), font6, Brushes.Black, num17, num13, stringFormat8);
e.Graphics.DrawString(item2.Value[2].ToString(), font6, Brushes.Black, num17, num12, stringFormat8);
e.Graphics.DrawString(item2.Value[3].ToString(), font6, Brushes.Black, num17, num11, stringFormat8);
num16 += num8;
}
}
int num18 = rectangle20.Left + 60;
int num19 = rect.Bottom + 120;
Rectangle rectangle23 = new Rectangle(num18, num19, rect.Right - 75 - num18, rectangle20.Bottom - 85 - num19);
if (mPlayerLines == null)
{
mPlayerLines = new Dictionary<BTPlayer, List<Point>>();
BTPlayer[][] array = mFinalRanking;
foreach (BTPlayer[] array3 in array)
{
BTPlayer[] array4 = array3;
foreach (BTPlayer key in array4)
{
mPlayerLines.Add(key, new List<Point>());
}
}
mChartRows = new int[mPlayerLines.Count];
float num20 = (float)rectangle23.Height / (float)mPlayerLines.Count;
for (int n = 0; n < mChartRows.Length; n++)
{
mChartRows[n] = (int)(num20 * (float)n + 0.5f) + rectangle23.Top;
}
mChartCols = new Dictionary<TimeSpan, int>();
TimeSpan timeSpan = mMissionResults.ActuallMissionTime.Subtract(TimeSpan.FromSeconds(10.0));
TimeSpan timeSpan2 = TimeSpan.FromSeconds(15.0);
for (TimeSpan timeSpan3 = timeSpan2; timeSpan3 < timeSpan; timeSpan3 += timeSpan2)
{
mChartCols.Add(timeSpan3, (int)((float)timeSpan3.Ticks / (float)mMissionResults.ActuallMissionTime.Ticks * (float)rectangle23.Width + 0.5f) + rectangle23.Left);
}
int num21 = 0;
array = mFinalRanking;
foreach (BTPlayer[] array5 in array)
{
BTPlayer[] array4 = array5;
foreach (BTPlayer key2 in array4)
{
mPlayerLines[key2].Add(new Point(rectangle23.Left, mChartRows[num21++]));
}
}
foreach (KeyValuePair<TimeSpan, int> mChartCol in mChartCols)
{
num21 = 0;
array = mMissionResults.GetChartOrder(mChartCol.Key);
foreach (BTPlayer[] array6 in array)
{
BTPlayer[] array4 = array6;
foreach (BTPlayer key3 in array4)
{
mPlayerLines[key3].Add(new Point(mChartCol.Value, mChartRows[num21++]));
}
}
}
num21 = 0;
array = mFinalRanking;
foreach (BTPlayer[] array7 in array)
{
BTPlayer[] array4 = array7;
foreach (BTPlayer key4 in array4)
{
mPlayerLines[key4].Add(new Point(rectangle23.Right, mChartRows[num21++]));
}
}
}
using (Font font7 = new Font("Helvetica LT Std", 8f))
{
using (StringFormat stringFormat9 = new StringFormat())
{
stringFormat9.LineAlignment = StringAlignment.Center;
foreach (KeyValuePair<BTPlayer, List<Point>> mPlayerLine in mPlayerLines)
{
if (mPlayerLine.Key != bTPlayer)
{
DrawPlayerLine(e.Graphics, mPlayerLine.Key, mPlayerLine.Value.ToArray(), rectangle23.Right + 5, Pens.LightGray, Brushes.LightGray, font7, stringFormat9);
}
}
DrawPlayerLine(e.Graphics, bTPlayer, mPlayerLines[bTPlayer].ToArray(), rectangle23.Right + 5, Pens.Black, Brushes.Black, font7, stringFormat9);
}
using Pen pen2 = new Pen(Color.Black, 1.5f);
using (Font font8 = new Font("Courier New", 8f))
{
int num22 = 5;
int num23 = 3;
e.Graphics.DrawLines(pen2, new Point[3]
{
new Point(rectangle23.Left, rectangle23.Top),
new Point(rectangle23.Left, rectangle23.Bottom),
new Point(rectangle23.Right, rectangle23.Bottom)
});
using StringFormat stringFormat10 = new StringFormat();
stringFormat10.Alignment = StringAlignment.Far;
stringFormat10.LineAlignment = StringAlignment.Center;
int num24 = 0;
BTPlayer[][] array = mFinalRanking;
foreach (BTPlayer[] array8 in array)
{
int num25 = mChartRows[num24];
e.Graphics.DrawLine(pen2, rectangle23.Left - num22, num25, rectangle23.Left + num22, num25);
Graphics graphics = e.Graphics;
int num26 = ++num24;
graphics.DrawString(num26.ToString(), font8, Brushes.Black, rectangle23.Left - 10, num25, stringFormat10);
_ = array8;
}
int num27 = rectangle23.Bottom + 23;
stringFormat10.Alignment = StringAlignment.Center;
stringFormat10.LineAlignment = StringAlignment.Near;
e.Graphics.DrawString("0", font8, Brushes.Black, rectangle23.Left, num27, stringFormat10);
foreach (KeyValuePair<TimeSpan, int> mChartCol2 in mChartCols)
{
if (mChartCol2.Key.Seconds == 0)
{
e.Graphics.DrawLine(pen2, mChartCol2.Value, rectangle23.Bottom - num22, mChartCol2.Value, rectangle23.Bottom + num22);
e.Graphics.DrawString(mChartCol2.Key.Minutes.ToString(), font8, Brushes.Black, mChartCol2.Value, num27, stringFormat10);
}
else
{
e.Graphics.DrawLine(pen2, mChartCol2.Value, rectangle23.Bottom - num23, mChartCol2.Value, rectangle23.Bottom);
}
}
e.Graphics.DrawLine(pen2, rectangle23.Right, rectangle23.Bottom - num22, rectangle23.Right, rectangle23.Bottom + num22);
e.Graphics.DrawString(((int)(mMissionResults.ActuallMissionTime.TotalMinutes + 0.5)).ToString(), font8, Brushes.Black, rectangle23.Right, num27, stringFormat10);
e.Graphics.DrawString("Time", font, Brushes.Black, (rectangle23.Left + rectangle23.Right) / 2, rectangle23.Bottom + 68, stringFormat10);
stringFormat10.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString("P\r\nl\r\na\r\nc\r\ne", font, Brushes.Black, rectangle23.Left - 54, (rectangle23.Bottom + rectangle23.Top) / 2, stringFormat10);
int num28 = rectangle23.Top - 70;
int num29 = rectangle23.Left + 10;
int num30 = (rectangle23.Right + rectangle23.Left) / 2 - 30;
stringFormat10.Alignment = StringAlignment.Near;
DrawKillMarker(e.Graphics, pen2, num29, num28, 15f);
e.Graphics.DrawString("Kill", font7, Brushes.Black, num29 + 15, num28, stringFormat10);
DrawDeathMarker(e.Graphics, pen2, num30, num28, 15f);
e.Graphics.DrawString("Death", font7, Brushes.Black, num30 + 15, num28, stringFormat10);
}
foreach (BTMissionEvent event3 in mMissionResults.Events)
{
if (event3 is BTKilledEvent)
{
BTKilledEvent bTKilledEvent3 = (BTKilledEvent)event3;
if (bTKilledEvent3.Killer != bTKilledEvent3.ReportingPilot)
{
DrawKillMarker(e.Graphics, (bTKilledEvent3.ReportingPilot == bTPlayer || bTKilledEvent3.Killer == bTPlayer) ? Pens.Black : Pens.LightGray, GetMarkerPosition(bTKilledEvent3.GameTime, mPlayerLines[bTKilledEvent3.Killer], rectangle23), 10f);
}
DrawDeathMarker(e.Graphics, (bTKilledEvent3.ReportingPilot == bTPlayer || bTKilledEvent3.Killer == bTPlayer) ? Pens.Black : Pens.LightGray, GetMarkerPosition(bTKilledEvent3.GameTime, mPlayerLines[bTKilledEvent3.ReportingPilot], rectangle23), 10f);
}
else if (event3 is BTDeathWithoutHonorEvent)
{
DrawDeathMarker(e.Graphics, (event3.ReportingPilot == bTPlayer) ? Pens.Black : Pens.LightGray, GetMarkerPosition(event3.GameTime, mPlayerLines[event3.ReportingPilot], rectangle23), 10f);
}
}
}
e.HasMorePages = ++mCurrentPlayer < mPrintOrder.Length;
base.OnPrintPage(e);
}
private PointF GetMarkerPosition(TimeSpan time, List<Point> chartLine, RectangleF chartArea)
{
TimeSpan timeSpan = TimeSpan.Zero;
TimeSpan timeSpan2 = mMissionResults.ActuallMissionTime;
int num = 0;
foreach (KeyValuePair<TimeSpan, int> mChartCol in mChartCols)
{
timeSpan2 = mChartCol.Key;
if (mChartCol.Key < time)
{
num++;
timeSpan = timeSpan2;
continue;
}
break;
}
if (timeSpan == timeSpan2)
{
timeSpan2 = mMissionResults.ActuallMissionTime;
}
float num2 = ((float)time.Ticks - (float)timeSpan.Ticks) / ((float)timeSpan2.Ticks - (float)timeSpan.Ticks);
PointF pointF = chartLine[num];
PointF pointF2 = chartLine[num + 1];
return new PointF((pointF2.X - pointF.X) * num2 + pointF.X, (pointF2.Y - pointF.Y) * num2 + pointF.Y);
}
private static void DrawKillMarker(Graphics graphics, Pen pen, PointF point, float size)
{
DrawKillMarker(graphics, pen, point.X, point.Y, size);
}
private static void DrawKillMarker(Graphics graphics, Pen pen, float x, float y, float size)
{
float y2 = y + size / 2f;
graphics.DrawPolygon(pen, new PointF[3]
{
new PointF(x - size / 2f, y2),
new PointF(x + size / 2f, y2),
new PointF(x, y - size / 2f)
});
}
private static void DrawDeathMarker(Graphics graphics, Pen pen, PointF point, float size)
{
DrawDeathMarker(graphics, pen, point.X, point.Y, size);
}
private static void DrawDeathMarker(Graphics graphics, Pen pen, float x, float y, float size)
{
graphics.DrawRectangle(pen, x - size / 2f, y - size / 2f, size, size);
}
private static void DrawPlayerLine(Graphics graphics, BTPlayer player, Point[] points, int playerNameX, Pen pen, Brush brush, Font font, StringFormat format)
{
graphics.DrawLines(pen, points);
graphics.DrawString(player.Name, font, brush, playerNameX, points[points.Length - 1].Y, format);
}
private static void GetPlayerStats(out int deaths, out int kills, BTPlayer player, IEnumerable<BTMissionEvent> relaventEventsSorted)
{
deaths = 0;
kills = 0;
foreach (BTMissionEvent item in relaventEventsSorted)
{
if (item.ReportingPilot == player)
{
if (item is BTKilledEvent || item is BTDeathWithoutHonorEvent)
{
deaths++;
}
}
else if (item is BTKilledEvent && ((BTKilledEvent)item).Killer == player)
{
kills++;
}
}
}
private static Rectangle RectDrop(Rectangle inputRect, int widthDrop, int heightDrop)
{
return new Rectangle(inputRect.X, inputRect.Y, inputRect.Width - widthDrop, inputRect.Height - heightDrop);
}
private static Rectangle GetCenteredScaledRect(Rectangle bounds, Size originalSize)
{
float num = (float)bounds.Width / (float)bounds.Height;
float num2 = (float)originalSize.Width / (float)originalSize.Height;
Size size = bounds.Size;
if (num > num2)
{
size.Width = (int)((float)size.Height * num2 + 0.5f);
}
else
{
size.Height = (int)((float)size.Width / num2 + 0.5f);
}
return new Rectangle(bounds.X + (bounds.Width - size.Width) / 2, bounds.Y + (bounds.Height - size.Height) / 2, size.Width, size.Height);
}
private static bool Contains(IEnumerable<BTPlayer> players, BTPlayer player)
{
foreach (BTPlayer player2 in players)
{
if (player2 == player)
{
return true;
}
}
return false;
}
}
+48
View File
@@ -0,0 +1,48 @@
using System;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech pilot role. Each participant references one role
/// (<c>role=Role::&lt;Key&gt;</c>); the egg then carries a matching
/// <c>[Role::&lt;Key&gt;] model=&lt;Model&gt;</c> block. Free For All uses the
/// Default role, No Return uses the NoReturn role. Serializable because it is
/// reachable from a saved <see cref="BTMissionResults" /> via the mission's
/// players (mirrors the .rpm save mechanism).
/// </summary>
[Serializable]
public class BTRole
{
private readonly string mKey;
private readonly string mModel;
private readonly string mName;
public string Key => mKey;
public string Model => mModel;
public string Name => mName;
internal BTRole(XmlNode roleNode)
{
mKey = roleNode.Attributes["key"].InnerText;
mModel = roleNode.Attributes["model"].InnerText;
XmlAttribute xmlAttribute = roleNode.Attributes["name"];
mName = ((xmlAttribute != null) ? xmlAttribute.InnerText : mKey);
}
public BTRole(string key, string model, string name)
{
mKey = key;
mModel = model;
mName = name;
}
public override string ToString()
{
return mName;
}
}
@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech scenario (game mode). Mirrors the Red Planet
/// <c>Scenario</c>/<c>DeathRaceScenario</c> pattern: it inherits the shared
/// map/vehicle/time/weather catalogs from <see cref="BTConfig" /> (minus any
/// <c>&lt;invalid&gt;</c> exclusions) and adds the BT-specific option lists
/// (experience, camo, patch, emblem, role).
/// </summary>
public class BTScenario
{
private readonly string mKey;
private readonly string mName;
private readonly Dictionary<string, BTMap> mMaps = new Dictionary<string, BTMap>();
private readonly Dictionary<string, BTVehicle> mVehicles = new Dictionary<string, BTVehicle>();
private readonly Dictionary<string, string> mTimesOfDay = new Dictionary<string, string>();
private readonly Dictionary<string, BTWeather> mWeather = new Dictionary<string, BTWeather>();
private readonly Dictionary<string, string> mExperiences = new Dictionary<string, string>();
private readonly Dictionary<string, string> mColors = new Dictionary<string, string>();
private readonly Dictionary<string, string> mPatches = new Dictionary<string, string>();
private readonly Dictionary<string, string> mBadges = new Dictionary<string, string>();
private readonly Dictionary<string, BTRole> mRoles = new Dictionary<string, BTRole>();
public string Key => mKey;
public string Name => mName;
public Dictionary<string, BTMap> Maps => mMaps;
public Dictionary<string, BTVehicle> Vehicles => mVehicles;
public Dictionary<string, string> TimesOfDay => mTimesOfDay;
public Dictionary<string, BTWeather> Weather => mWeather;
public Dictionary<string, string> Experiences => mExperiences;
public Dictionary<string, string> Colors => mColors;
public Dictionary<string, string> Patches => mPatches;
public Dictionary<string, string> Badges => mBadges;
public Dictionary<string, BTRole> Roles => mRoles;
internal BTScenario(XmlNode scenarioNode)
{
mKey = scenarioNode.Name;
mName = scenarioNode.Attributes["name"].InnerText;
List<KeyValuePair<string, string>> invalid = new List<KeyValuePair<string, string>>();
foreach (XmlNode childNode in scenarioNode.ChildNodes)
{
switch (childNode.Name)
{
case "invalid":
invalid.Add(new KeyValuePair<string, string>(childNode.Attributes["type"].InnerText, childNode.Attributes["key"].InnerText));
break;
case "experience":
BTConfig.AddKeyNameNode(childNode, mExperiences);
break;
case "color":
BTConfig.AddKeyNameNode(childNode, mColors);
break;
case "patch":
BTConfig.AddKeyNameNode(childNode, mPatches);
break;
case "badge":
BTConfig.AddKeyNameNode(childNode, mBadges);
break;
case "role":
{
BTRole role = new BTRole(childNode);
mRoles.Add(role.Key, role);
break;
}
}
}
foreach (BTMap value in BTConfig.Maps.Values)
{
if (!IsInvalid("map", value.Key, invalid))
{
mMaps.Add(value.Key, value);
}
}
foreach (BTVehicle value2 in BTConfig.Vehicles.Values)
{
if (!IsInvalid("vehicle", value2.Key, invalid))
{
mVehicles.Add(value2.Key, value2);
}
}
foreach (KeyValuePair<string, string> item in BTConfig.TimesOfDay)
{
if (!IsInvalid("time", item.Key, invalid))
{
mTimesOfDay.Add(item.Key, item.Value);
}
}
foreach (BTWeather value3 in BTConfig.Weather.Values)
{
if (!IsInvalid("weather", value3.Key, invalid))
{
mWeather.Add(value3.Key, value3);
}
}
}
private static bool IsInvalid(string type, string value, List<KeyValuePair<string, string>> invalid)
{
foreach (KeyValuePair<string, string> item in invalid)
{
if (item.Key == type && item.Value == value)
{
return true;
}
}
return false;
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>A pilot's running score changed (<c>MechScoreUpdateMessage</c>).</summary>
[Serializable]
internal class BTScoreUpdateEvent : BTMissionEvent
{
public readonly int Score;
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[1] { ReportingPilot };
public BTScoreUpdateEvent(BTPlayer reportingPilot, TimeSpan missionTime, int score)
: base(reportingPilot, missionTime)
{
Score = score;
}
private BTScoreUpdateEvent()
{
}
}
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The BattleTech mission-highlight narrative strings, loaded from
/// BattleTech\BTStrings.xml. Mirrors <c>TeslaConsole.RPStrings</c>: each
/// category is a pool of templates and one is picked at random with the
/// {time}/{player}/{vehicle}/{damager} placeholders substituted. The RP-only
/// categories (score/boost/lap) have no BT counterpart; DeathWithoutHonor
/// replaces SelfKill.
/// </summary>
internal class BTStrings
{
private static bool sInitalized = false;
private static List<string> sLightDamage = new List<string>();
private static List<string> sModerateDamage = new List<string>();
private static List<string> sHeavyDamage = new List<string>();
private static List<string> sKill = new List<string>();
private static List<string> sDeathWithoutHonor = new List<string>();
private static List<string> sRecap = new List<string>();
private static Random sRandom = new Random();
private static void Initalize()
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "BattleTech\\BTStrings.xml"));
if (xmlDocument.DocumentElement.Name != "BTStrings")
{
throw new XmlException("Document element was not a BTStrings node.");
}
foreach (XmlNode childNode in xmlDocument.DocumentElement.ChildNodes)
{
switch (childNode.Name)
{
case "LightDamage":
if (!sLightDamage.Contains(childNode.InnerText))
{
sLightDamage.Add(childNode.InnerText);
}
break;
case "ModerateDamage":
if (!sModerateDamage.Contains(childNode.InnerText))
{
sModerateDamage.Add(childNode.InnerText);
}
break;
case "HeavyDamage":
if (!sHeavyDamage.Contains(childNode.InnerText))
{
sHeavyDamage.Add(childNode.InnerText);
}
break;
case "Kill":
if (!sKill.Contains(childNode.InnerText))
{
sKill.Add(childNode.InnerText);
}
break;
case "DeathWithoutHonor":
if (!sDeathWithoutHonor.Contains(childNode.InnerText))
{
sDeathWithoutHonor.Add(childNode.InnerText);
}
break;
case "Recap":
if (!sRecap.Contains(childNode.InnerText))
{
sRecap.Add(childNode.InnerText);
}
break;
}
}
sInitalized = true;
}
private static string GetRandomStringAndReplace(List<string> list, TimeSpan time, string player, string vehicle, string damager)
{
if (!sInitalized)
{
Initalize();
}
if (list.Count == 0)
{
return null;
}
return list[sRandom.Next(list.Count)].Replace("{time}", GetTimeString(time)).Replace("{player}", player).Replace("{vehicle}", vehicle)
.Replace("{damager}", damager);
}
public static string GetTimeString(TimeSpan time)
{
int num = (int)(time.TotalSeconds + 0.5);
int num2 = num / 60;
return $"{num2:D2}:{num % 60:D2}";
}
public static string LightDamage(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sLightDamage, time, player, vehicle, damager);
}
public static string ModerateDamage(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sModerateDamage, time, player, vehicle, damager);
}
public static string HeavyDamage(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sHeavyDamage, time, player, vehicle, damager);
}
public static string Kill(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sKill, time, player, vehicle, damager);
}
public static string DeathWithoutHonor(TimeSpan time, string player, string vehicle)
{
return GetRandomStringAndReplace(sDeathWithoutHonor, time, player, vehicle, "");
}
public static string Recap(string[] playerRanking)
{
StringBuilder stringBuilder = new StringBuilder(playerRanking[0]);
for (int i = 1; i < playerRanking.Length - 1; i++)
{
stringBuilder.AppendFormat(", {0}", playerRanking[i]);
}
return GetRandomStringAndReplace(sRecap, TimeSpan.Zero, "", "", "").Replace("{winners}", stringBuilder.ToString()).Replace("{looser}", (playerRanking.Length > 1) ? playerRanking[playerRanking.Length - 1] : "nobody");
}
}
@@ -0,0 +1,44 @@
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
public class BTVehicle
{
private readonly string mKey;
private readonly string mName;
private readonly string mImagePath;
public string Key => mKey;
public string Name => mName;
public Image Image
{
get
{
if (mImagePath == null)
{
return null;
}
return ImageCache.GetImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), mImagePath));
}
}
internal BTVehicle(XmlNode vehicleNode)
{
XmlAttribute xmlAttribute = vehicleNode.Attributes["image"];
mKey = vehicleNode.Attributes["key"].InnerText;
mName = vehicleNode.Attributes["name"].InnerText;
mImagePath = xmlAttribute?.InnerText;
}
public override string ToString()
{
return mName;
}
}
@@ -0,0 +1,36 @@
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech weather option. Unlike Red Planet (where weather is a plain
/// key/name pair), a BT weather entry also carries the ambient temperature that
/// becomes the egg's <c>[mission] temperature=</c> field.
/// </summary>
public class BTWeather
{
private readonly string mKey;
private readonly string mName;
private readonly int mTemperature;
public string Key => mKey;
public string Name => mName;
public int Temperature => mTemperature;
internal BTWeather(XmlNode weatherNode)
{
mKey = weatherNode.Attributes["key"].InnerText;
mName = weatherNode.Attributes["name"].InnerText;
XmlAttribute xmlAttribute = weatherNode.Attributes["temperature"];
mTemperature = ((xmlAttribute != null) ? int.Parse(xmlAttribute.InnerText) : 0);
}
public override string ToString()
{
return mName;
}
}
+6
View File
@@ -30,6 +30,8 @@
<Content Include="RedPlanet\RPConfig.xml" CopyToOutputDirectory="PreserveNewest" />
<Content Include="RedPlanet\RPStrings.xml" CopyToOutputDirectory="PreserveNewest" />
<Content Include="RedPlanet\Apps.xml" CopyToOutputDirectory="PreserveNewest" />
<Content Include="BattleTech\BTConfig.xml" CopyToOutputDirectory="PreserveNewest" />
<Content Include="BattleTech\BTStrings.xml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
@@ -43,6 +45,10 @@
<Compile Remove="tests\**" />
<None Remove="tests\**" />
<Content Remove="tests\**" />
<!-- vPOD is its own deployable exe under vPOD\; exclude it from the console build. -->
<Compile Remove="vPOD\**" />
<None Remove="vPOD\**" />
<Content Remove="vPOD\**" />
</ItemGroup>
<ItemGroup>
+191 -6
View File
@@ -5,6 +5,7 @@ using System.Drawing.Drawing2D;
using System.Drawing.Printing;
using System.Windows.Forms;
using Munga.Net;
using TeslaConsole.BattleTech;
using TeslaConsole.RedPlanet;
using WeifenLuo.WinFormsUI.Docking;
@@ -38,6 +39,10 @@ public class TeslaConsoleForm : Form
private ToolStripMenuItem mRpMartianFootball;
private ToolStripMenuItem mBtFreeForAll;
private ToolStripMenuItem mBtNoReturn;
private ToolStripMenuItem mPlasmaFontTool;
private ToolStripMenuItem mRpMissionPrintPreview;
@@ -46,6 +51,12 @@ public class TeslaConsoleForm : Form
private ToolStripMenuItem mPrintRpMission;
private ToolStripMenuItem mBtMissionPrintPreview;
private ToolStripMenuItem mPrintBtMission;
private OpenFileDialog mBTMissionOpenDialog;
private ToolStripSeparator mSeperator1;
private PrintDialog mRPPrintDialog;
@@ -68,10 +79,20 @@ public class TeslaConsoleForm : Form
private ToolStripMenuItem exportRedPlanetDefaultsToolStripMenuItem;
private ToolStripMenuItem battleTechDefaultsToolStripMenuItem;
private ToolStripMenuItem importBattleTechDefaultsToolStripMenuItem;
private ToolStripMenuItem exportBattleTechDefaultsToolStripMenuItem;
private OpenFileDialog mRPDefaultsOpenDialog;
private SaveFileDialog mRPDefaultsSaveDialog;
private OpenFileDialog mBTDefaultsOpenDialog;
private SaveFileDialog mBTDefaultsSaveDialog;
private ToolStripSeparator toolStripSeparator1;
private ToolStripMenuItem enableCustomBitmapsToolStripMenuItem;
@@ -86,10 +107,12 @@ public class TeslaConsoleForm : Form
{
InitializeComponent();
mRpMissionPrintPreview.Visible = false;
mBtMissionPrintPreview.Visible = false;
mSeperator1.Visible = false;
mExceptionsMenu.Visible = false;
enableCustomBitmapsToolStripMenuItem.Checked = PlasmaBitmaps.EnableCustomBitmaps;
mRPMissionOpenDialog.InitialDirectory = RPMissionResults.GetRPMissionsDirectory();
mBTMissionOpenDialog.InitialDirectory = BTMissionResults.GetBTMissionsDirectory();
TeslaConsole.Site.LoadFromStore();
PodManager.StartConfigurationServer();
}
@@ -136,15 +159,16 @@ public class TeslaConsoleForm : Form
pod.MungaGame.SetStatus(ImageCache.PodQuestion16, "Waiting For State Update...");
break;
}
if (pod.MungaGame.AppID.Value != 0)
string gameTag = GameTag(pod.MungaGame.AppID.Value);
if (gameTag == null)
{
pod.MungaGame.SetStatus(ImageCache.PodBad16, "Pod Not Running RP");
pod.MungaGame.SetStatus(ImageCache.PodBad16, "Unknown Pod Application");
break;
}
switch (pod.MungaGame.AppState.Value)
{
case ApplicationState.InitializingState:
pod.MungaGame.SetStatus(ImageCache.PodQuestion16, "RP Initalizing");
pod.MungaGame.SetStatus(ImageCache.PodQuestion16, gameTag + " Initalizing");
break;
case ApplicationState.WaitingForEgg:
pod.MungaGame.SetStatus(ImageCache.PodOnline16, "Waiting For A Game");
@@ -202,6 +226,26 @@ public class TeslaConsoleForm : Form
}
}
/// <summary>
/// Short display tag for a pod's reported application, or null when the
/// console has no game module for it. RPL4 is Red Planet, BTL4 is
/// BattleTech; NDL4 (the never-released air-combat game) has no console
/// support. The "Initalizing" spelling in the status strings is preserved
/// from the original console.
/// </summary>
private static string GameTag(ApplicationID appID)
{
switch (appID)
{
case ApplicationID.RPL4:
return "RP";
case ApplicationID.BTL4:
return "BT";
default:
return null;
}
}
private void redPlanetDeathRaceToolStripMenuItem_Click(object sender, EventArgs e)
{
CreateNewRPGame(footballMode: false);
@@ -219,6 +263,23 @@ public class TeslaConsoleForm : Form
rPGame.Show(mMainDockPanel, DockState.Document);
}
private void battleTechFreeForAllToolStripMenuItem_Click(object sender, EventArgs e)
{
CreateNewBTGame(noReturnMode: false);
}
private void battleTechNoReturnToolStripMenuItem_Click(object sender, EventArgs e)
{
CreateNewBTGame(noReturnMode: true);
}
private void CreateNewBTGame(bool noReturnMode)
{
BTGame bTGame = new BTGame(TeslaConsole.Site.Active, noReturnMode);
bTGame.MdiParent = this;
bTGame.Show(mMainDockPanel, DockState.Document);
}
private void plasmaEditorToolStripMenuItem_Click(object sender, EventArgs e)
{
new PlasmaEditor().Show();
@@ -280,6 +341,52 @@ public class TeslaConsoleForm : Form
}
}
private PrintDocument GetBTMissionPrintDocument()
{
if (mBTMissionOpenDialog.ShowDialog() == DialogResult.OK)
{
try
{
BTMissionResults bTMissionResults = BTMissionResults.Load(mBTMissionOpenDialog.FileName);
return bTMissionResults.GetPrintDocument();
}
catch (Exception ex)
{
MessageBox.Show("The selected document could not be loaded because it appears to be corrupt.", "Error Loading Mission Data", MessageBoxButtons.OK);
Program.LogException(ex, $"Error Loading BT Mission Results (\"{mBTMissionOpenDialog.FileName}\").");
}
}
return null;
}
private void btPrintPreviewToolStripMenuItem_Click(object sender, EventArgs e)
{
PrintDocument bTMissionPrintDocument = GetBTMissionPrintDocument();
if (bTMissionPrintDocument != null)
{
if (mRPPrintPreviewDialog.IsDisposed)
{
mRPPrintPreviewDialog = new PrintPreviewDialog();
}
mRPPrintPreviewDialog.Document = bTMissionPrintDocument;
mRPPrintPreviewDialog.Show();
mRPPrintPreviewDialog.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
}
}
private void printBtMissionToolStripMenuItem_Click(object sender, EventArgs e)
{
PrintDocument bTMissionPrintDocument = GetBTMissionPrintDocument();
if (bTMissionPrintDocument != null)
{
mRPPrintDialog.Document = bTMissionPrintDocument;
if (mRPPrintDialog.ShowDialog() == DialogResult.OK)
{
mRPPrintDialog.Document.Print();
}
}
}
private void TeslaConsoleForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (mSitePanel != null && !mSitePanel.IsDisposed)
@@ -331,6 +438,43 @@ public class TeslaConsoleForm : Form
}
}
private void changeBattleTechDefaultsToolStripMenuItem_Click(object sender, EventArgs e)
{
BTDefaultsDialog.ShowSingleton();
}
private void importBattleTechDefaultsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (mBTDefaultsOpenDialog.ShowDialog() == DialogResult.OK)
{
try
{
BTDefaults.Import(mBTDefaultsOpenDialog.FileName);
}
catch (Exception)
{
MessageBox.Show("This BattleTech defaults file appears to be corrupt.", "Error Loading BattleTech Defaults!", MessageBoxButtons.OK);
return;
}
BTDefaults.Save();
}
}
private void exportBattleTechDefaultsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (mBTDefaultsSaveDialog.ShowDialog() == DialogResult.OK)
{
try
{
BTDefaults.Export(mBTDefaultsSaveDialog.FileName);
}
catch (Exception)
{
MessageBox.Show("The BattleTech defaults file could not be exported.", "Error Exporting BattleTech Defaults!", MessageBoxButtons.OK);
}
}
}
private void enableCustomBitmapsToolStripMenuItem_Click(object sender, EventArgs e)
{
enableCustomBitmapsToolStripMenuItem.Checked = (PlasmaBitmaps.EnableCustomBitmaps = !PlasmaBitmaps.EnableCustomBitmaps);
@@ -394,6 +538,8 @@ public class TeslaConsoleForm : Form
this.mSitePanelSeparator = new System.Windows.Forms.ToolStripSeparator();
this.mRpMissionPrintPreview = new System.Windows.Forms.ToolStripMenuItem();
this.mPrintRpMission = new System.Windows.Forms.ToolStripMenuItem();
this.mBtMissionPrintPreview = new System.Windows.Forms.ToolStripMenuItem();
this.mPrintBtMission = new System.Windows.Forms.ToolStripMenuItem();
this.mSeperator1 = new System.Windows.Forms.ToolStripSeparator();
this.mPlasmaEditor = new System.Windows.Forms.ToolStripMenuItem();
this.mPlasmaFontTool = new System.Windows.Forms.ToolStripMenuItem();
@@ -404,11 +550,16 @@ public class TeslaConsoleForm : Form
this.redPlanetDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.importRedPlanetDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportRedPlanetDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.battleTechDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.importBattleTechDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportBattleTechDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.enableCustomBitmapsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.mGamesMenu = new System.Windows.Forms.ToolStripMenuItem();
this.mRpDeathRace = new System.Windows.Forms.ToolStripMenuItem();
this.mRpMartianFootball = new System.Windows.Forms.ToolStripMenuItem();
this.mBtFreeForAll = new System.Windows.Forms.ToolStripMenuItem();
this.mBtNoReturn = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
this.mExceptionsMenu = new System.Windows.Forms.ToolStripMenuItem();
@@ -417,8 +568,11 @@ public class TeslaConsoleForm : Form
this.mRPPrintPreviewDialog = new System.Windows.Forms.PrintPreviewDialog();
this.mRPPrintDialog = new System.Windows.Forms.PrintDialog();
this.mRPMissionOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mBTMissionOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mRPDefaultsOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mRPDefaultsSaveDialog = new System.Windows.Forms.SaveFileDialog();
this.mBTDefaultsOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mBTDefaultsSaveDialog = new System.Windows.Forms.SaveFileDialog();
this.mMenu.SuspendLayout();
base.SuspendLayout();
this.mMainDockPanel.ActiveAutoHideContent = null;
@@ -480,7 +634,7 @@ public class TeslaConsoleForm : Form
this.mMenu.Size = new System.Drawing.Size(1168, 24);
this.mMenu.TabIndex = 3;
this.mMenu.Text = "menuStrip1";
this.mFileMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[10] { this.mShowSitePanelMenuItem, this.mSitePanelSeparator, this.mRpMissionPrintPreview, this.mPrintRpMission, this.mSeperator1, this.mPlasmaEditor, this.mPlasmaFontTool, this.mPlasmaBitmapDecoder, this.mSeperator2, this.mExit });
this.mFileMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[12] { this.mShowSitePanelMenuItem, this.mSitePanelSeparator, this.mRpMissionPrintPreview, this.mPrintRpMission, this.mBtMissionPrintPreview, this.mPrintBtMission, this.mSeperator1, this.mPlasmaEditor, this.mPlasmaFontTool, this.mPlasmaBitmapDecoder, this.mSeperator2, this.mExit });
this.mFileMenu.Name = "mFileMenu";
this.mFileMenu.Size = new System.Drawing.Size(37, 20);
this.mFileMenu.Text = "File";
@@ -498,6 +652,14 @@ public class TeslaConsoleForm : Form
this.mPrintRpMission.Size = new System.Drawing.Size(204, 22);
this.mPrintRpMission.Text = "Print RP Mission";
this.mPrintRpMission.Click += new System.EventHandler(printToolStripMenuItem_Click);
this.mBtMissionPrintPreview.Name = "mBtMissionPrintPreview";
this.mBtMissionPrintPreview.Size = new System.Drawing.Size(204, 22);
this.mBtMissionPrintPreview.Text = "BT Mission Print Preview";
this.mBtMissionPrintPreview.Click += new System.EventHandler(btPrintPreviewToolStripMenuItem_Click);
this.mPrintBtMission.Name = "mPrintBtMission";
this.mPrintBtMission.Size = new System.Drawing.Size(204, 22);
this.mPrintBtMission.Text = "Print BT Mission";
this.mPrintBtMission.Click += new System.EventHandler(printBtMissionToolStripMenuItem_Click);
this.mSeperator1.Name = "mSeperator1";
this.mSeperator1.Size = new System.Drawing.Size(201, 6);
this.mPlasmaEditor.Name = "mPlasmaEditor";
@@ -518,7 +680,7 @@ public class TeslaConsoleForm : Form
this.mExit.Size = new System.Drawing.Size(204, 22);
this.mExit.Text = "Exit";
this.mExit.Click += new System.EventHandler(mExit_Click);
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[5] { this.redPlanetDefaultsToolStripMenuItem, this.importRedPlanetDefaultsToolStripMenuItem, this.exportRedPlanetDefaultsToolStripMenuItem, this.toolStripSeparator1, this.enableCustomBitmapsToolStripMenuItem });
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[8] { this.redPlanetDefaultsToolStripMenuItem, this.importRedPlanetDefaultsToolStripMenuItem, this.exportRedPlanetDefaultsToolStripMenuItem, this.battleTechDefaultsToolStripMenuItem, this.importBattleTechDefaultsToolStripMenuItem, this.exportBattleTechDefaultsToolStripMenuItem, this.toolStripSeparator1, this.enableCustomBitmapsToolStripMenuItem });
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "Settings";
@@ -534,13 +696,25 @@ public class TeslaConsoleForm : Form
this.exportRedPlanetDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.exportRedPlanetDefaultsToolStripMenuItem.Text = "&Export Red Planet Defaults";
this.exportRedPlanetDefaultsToolStripMenuItem.Click += new System.EventHandler(exportRedPlanetDefaultsToolStripMenuItem_Click);
this.battleTechDefaultsToolStripMenuItem.Name = "battleTechDefaultsToolStripMenuItem";
this.battleTechDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.battleTechDefaultsToolStripMenuItem.Text = "Change &BattleTech Defaults";
this.battleTechDefaultsToolStripMenuItem.Click += new System.EventHandler(changeBattleTechDefaultsToolStripMenuItem_Click);
this.importBattleTechDefaultsToolStripMenuItem.Name = "importBattleTechDefaultsToolStripMenuItem";
this.importBattleTechDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.importBattleTechDefaultsToolStripMenuItem.Text = "Import BattleTech Defaults";
this.importBattleTechDefaultsToolStripMenuItem.Click += new System.EventHandler(importBattleTechDefaultsToolStripMenuItem_Click);
this.exportBattleTechDefaultsToolStripMenuItem.Name = "exportBattleTechDefaultsToolStripMenuItem";
this.exportBattleTechDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.exportBattleTechDefaultsToolStripMenuItem.Text = "Export BattleTech Defaults";
this.exportBattleTechDefaultsToolStripMenuItem.Click += new System.EventHandler(exportBattleTechDefaultsToolStripMenuItem_Click);
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(217, 6);
this.enableCustomBitmapsToolStripMenuItem.Name = "enableCustomBitmapsToolStripMenuItem";
this.enableCustomBitmapsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.enableCustomBitmapsToolStripMenuItem.Text = "Enable Custom Bitmaps";
this.enableCustomBitmapsToolStripMenuItem.Click += new System.EventHandler(enableCustomBitmapsToolStripMenuItem_Click);
this.mGamesMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[2] { this.mRpDeathRace, this.mRpMartianFootball });
this.mGamesMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[4] { this.mRpDeathRace, this.mRpMartianFootball, this.mBtFreeForAll, this.mBtNoReturn });
this.mGamesMenu.Name = "mGamesMenu";
this.mGamesMenu.Size = new System.Drawing.Size(55, 20);
this.mGamesMenu.Text = "Games";
@@ -552,6 +726,14 @@ public class TeslaConsoleForm : Form
this.mRpMartianFootball.Size = new System.Drawing.Size(223, 22);
this.mRpMartianFootball.Text = "Red Planet: Martian Football";
this.mRpMartianFootball.Click += new System.EventHandler(redPlanetMartianFootballToolStripMenuItem_Click);
this.mBtFreeForAll.Name = "mBtFreeForAll";
this.mBtFreeForAll.Size = new System.Drawing.Size(223, 22);
this.mBtFreeForAll.Text = "BattleTech: Free For All";
this.mBtFreeForAll.Click += new System.EventHandler(battleTechFreeForAllToolStripMenuItem_Click);
this.mBtNoReturn.Name = "mBtNoReturn";
this.mBtNoReturn.Size = new System.Drawing.Size(223, 22);
this.mBtNoReturn.Text = "BattleTech: No Return";
this.mBtNoReturn.Click += new System.EventHandler(battleTechNoReturnToolStripMenuItem_Click);
this.aboutToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[1] { this.aboutToolStripMenuItem1 });
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(52, 20);
@@ -581,8 +763,11 @@ public class TeslaConsoleForm : Form
this.mRPPrintPreviewDialog.Visible = false;
this.mRPPrintDialog.UseEXDialog = true;
this.mRPMissionOpenDialog.Filter = "RP Mission (*.rpm)|*.rpm";
this.mBTMissionOpenDialog.Filter = "BT Mission (*.btm)|*.btm";
this.mRPDefaultsOpenDialog.Filter = "RP Defaults (*.rpd)|*.rpd";
this.mRPDefaultsSaveDialog.Filter = "RP Defaults (*.rpd)|*.rpd";
this.mBTDefaultsOpenDialog.Filter = "BT Defaults (*.btd)|*.btd";
this.mBTDefaultsSaveDialog.Filter = "BT Defaults (*.btd)|*.btd";
base.AutoScaleDimensions = new System.Drawing.SizeF(6f, 13f);
base.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
base.ClientSize = new System.Drawing.Size(1168, 591);
@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Xunit;
namespace TeslaConsole.DiffTests
{
/// <summary>
/// Golden-egg tests for the new TeslaConsole.BattleTech mission builder. BattleTech
/// is absent from the original exe, so like CatalogTests these run against the
/// recovered build only; the baselines are two eggs captured from the original
/// consoles (BattleTech\cavern.egg, BattleTech\TESTARN.EGG — see the port spec in
/// 410console/battletech-port/).
///
/// The comparison is per-section and order-independent: the pod parses the egg
/// INI-style, and the two golden eggs themselves disagree with each other on
/// participant field order. Pixel rows of the font-rendered name bitmaps are
/// excluded — the original consoles rendered them with a different font, and
/// cavern.egg's ordinal art is Mac-rendered too (the spec's known false
/// byte-match). TESTARN.EGG's ordinal art, however, is identical to the
/// RP-inherited rows, so that one IS compared byte-exactly.
/// </summary>
public class BTGoldenEggTests : IClassFixture<DifferentialFixture>
{
private readonly DifferentialFixture _fx;
public BTGoldenEggTests(DifferentialFixture fx) => _fx = fx;
// Mission args: mapKey, timeOfDayKey, weatherKey, temperature, lengthSeconds,
// then per pilot: podHost, name, vehicle, camo, patch, emblem, experience,
// dropzone, roleKey, roleModel (matches Invoker.BuildBTMission).
private static readonly string[] CavernArgs =
{
"cavern", "night", "clear", "27", "120",
"200.0.0.113", "cyd", "madcat", "Grey", "Red", "VGL", "veteran", "one", "Default", "dfltrole",
};
private static readonly string[] TestarnArgs =
{
"arena1", "day", "clear", "27", "600",
"200.0.0.113", "cyd", "madcat", "White", "Yellow", "VGL", "veteran", "one", "Default", "dfltrole",
};
private string Generate(string[] args)
{
string egg = _fx.Recovered.Run("BTEggString", args);
Assert.StartsWith("[mission]", egg); // not an "EXC:<Type>" result
return egg;
}
private static string ReadGolden(string fileName)
{
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "BattleTech", fileName);
Assert.True(File.Exists(path), "Golden egg not found: " + path);
// On disk the egg is the wire form: NUL-delimited ASCII.
return Encoding.ASCII.GetString(File.ReadAllBytes(path));
}
// ---- egg parsing ----
private static Dictionary<string, List<string>> ParseEgg(string text)
{
var sections = new Dictionary<string, List<string>>();
List<string> current = null;
foreach (var raw in text.Split(new[] { '\0', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
// TESTARN.EGG was captured with a stray \r before each NUL delimiter.
var line = raw.TrimEnd('\r');
if (line.Length == 0)
continue;
if (line.StartsWith("[", StringComparison.Ordinal))
{
string name = line.Trim('[', ']');
Assert.False(sections.ContainsKey(name), "Duplicate egg section: " + name);
current = new List<string>();
sections.Add(name, current);
}
else
{
Assert.NotNull(current); // a key=value before any section header
current.Add(line);
}
}
return sections;
}
/// <summary>Sections whose bitmap= lines are pixel rows, not references.</summary>
private static bool IsPixelSection(string name)
=> name.StartsWith("BitMap::", StringComparison.Ordinal) ||
name.StartsWith("Ordinal::BitMap::", StringComparison.Ordinal);
private static Dictionary<string, List<string>> DropPixelRows(Dictionary<string, List<string>> egg)
=> egg.ToDictionary(
s => s.Key,
s => IsPixelSection(s.Key)
? s.Value.Where(l => !l.StartsWith("bitmap=", StringComparison.Ordinal)).ToList()
: s.Value);
private static void AssertFieldsMatchGolden(string generated, string golden,
params string[] acceptedExtraGoldenSections)
{
var gen = DropPixelRows(ParseEgg(generated));
var gold = DropPixelRows(ParseEgg(golden));
var missing = gold.Keys.Except(gen.Keys).Except(acceptedExtraGoldenSections)
.OrderBy(s => s, StringComparer.Ordinal).ToArray();
var extra = gen.Keys.Except(gold.Keys)
.OrderBy(s => s, StringComparer.Ordinal).ToArray();
Assert.True(missing.Length == 0,
"Golden egg sections missing from the generated egg: " + string.Join(", ", missing));
Assert.True(extra.Length == 0,
"Generated egg sections absent from the golden egg: " + string.Join(", ", extra));
foreach (var name in gen.Keys)
{
var g = gen[name].OrderBy(s => s, StringComparer.Ordinal).ToArray();
var r = gold[name].OrderBy(s => s, StringComparer.Ordinal).ToArray();
Assert.True(g.SequenceEqual(r),
"[" + name + "] fields differ.\n golden: " + string.Join(" | ", r) +
"\n generated: " + string.Join(" | ", g));
}
}
// ---- golden-egg field equivalence ----
[Fact]
public void Cavern_Egg_Fields_Match_Golden()
=> AssertFieldsMatchGolden(Generate(CavernArgs), ReadGolden("cavern.egg"));
[Fact]
public void Testarn_Egg_Fields_Match_Golden()
{
// The original console emitted a [Role::*] block for every role in its
// catalog; ours emits only the roles the pilots reference, so TESTARN's
// unreferenced [Role::NoReturn] is an accepted difference (extra blocks
// are harmless to the pod's INI-style parser; missing referenced ones
// would not be).
AssertFieldsMatchGolden(Generate(TestarnArgs), ReadGolden("TESTARN.EGG"),
"Role::NoReturn");
}
[Fact]
public void Ordinal_Bitmap_Art_Matches_Testarn_Byte_Exactly()
{
// TESTARN.EGG's 1st4th place art is identical to the RP ordinals the BT
// builder reuses, so here the pixel rows are compared exactly and in order.
// (cavern.egg is excluded on purpose: its ordinal art is Mac-font-rendered
// and legitimately differs — the spec's known false byte-match.)
var gen = ParseEgg(Generate(TestarnArgs));
var gold = ParseEgg(ReadGolden("TESTARN.EGG"));
for (int n = 1; n <= 4; n++)
{
string name = "Ordinal::BitMap::" + n;
Assert.True(gen.ContainsKey(name), "Generated egg lacks [" + name + "]");
Assert.Equal(gold[name], gen[name]);
}
}
// ---- wire framing ----
[Fact]
public void Egg_Wire_Framing_Chunks_And_Reassembles()
{
string result = _fx.Recovered.Run("BTEggFraming", CavernArgs);
var m = Regex.Match(result, @"^messages=(\d+);total=(\d+);framing=True;reassembled=True$");
Assert.True(m.Success, "Unexpected framing result: " + result);
int messages = int.Parse(m.Groups[1].Value);
int total = int.Parse(m.Groups[2].Value);
Assert.Equal((total + 999) / 1000, messages);
Assert.True(total > 5000, "Suspiciously small egg (" + total + " bytes)");
}
// ---- behavior pinned by the resolved spec questions ----
[Fact]
public void Role_Blocks_Are_Deduplicated_Across_Pilots()
{
var args = CavernArgs.Concat(new[]
{
"200.0.0.114", "kai", "loki", "Black", "Blue", "Davion", "novice", "two", "Default", "dfltrole",
}).ToArray();
string egg = Generate(args);
int roleRefs = Regex.Matches(egg, Regex.Escape("role=Role::Default\n")).Count;
int roleBlocks = Regex.Matches(egg, Regex.Escape("[Role::Default]\n")).Count;
Assert.Equal(2, roleRefs);
Assert.Equal(1, roleBlocks);
}
[Fact]
public void NoReturn_Mode_Sends_Freeforall_Scenario_With_NoReturn_Role()
{
// No Return is not a separate scenario on the wire: both golden eggs say
// scenario=freeforall, and the modes differ only by the pilots' role.
// The model is "noreturn" (the Mac ini tag, resolvable against the
// game's BTL4.RES "NoReturn" resource) — NOT the golden eggs' "noretun",
// which was a 4.10 console bug that dropped a character.
var args = (string[])CavernArgs.Clone();
args[13] = "NoReturn";
args[14] = "noreturn";
var egg = ParseEgg(Generate(args));
Assert.Contains("scenario=freeforall", egg["mission"]);
Assert.Contains("role=Role::NoReturn", egg["200.0.0.113"]);
Assert.True(egg.ContainsKey("Role::NoReturn"), "Missing [Role::NoReturn] block");
Assert.Contains("model=noreturn", egg["Role::NoReturn"]);
Assert.False(egg.ContainsKey("Role::Default"), "Unreferenced [Role::Default] emitted");
}
// ---- the shipped catalog ----
[Fact]
public void BTConfig_Catalog_Ships_With_Build_And_Has_Expected_Counts()
{
// Verifies the csproj Content include actually lands BTConfig.xml next to
// the exe (BTConfig loads it from there at runtime) and that the catalog
// carries the full Mac Console.ini BT tree.
string path = Path.Combine(Path.GetDirectoryName(AssemblyPaths.RecoveredExe),
"BattleTech", "BTConfig.xml");
Assert.True(File.Exists(path), "BTConfig.xml not copied next to the build: " + path);
var doc = new XmlDocument();
doc.Load(path);
XmlElement root = doc.DocumentElement;
Assert.Equal("BattleTech", root.Name);
Assert.Equal(8, root.SelectNodes("map").Count);
Assert.Equal(18, root.SelectNodes("vehicle").Count);
Assert.Equal(4, root.SelectNodes("time").Count);
Assert.Equal(3, root.SelectNodes("weather").Count);
foreach (XmlNode weather in root.SelectNodes("weather"))
Assert.Equal("27", weather.Attributes["temperature"].InnerText);
XmlNode ffa = root.SelectSingleNode("freeforall");
Assert.NotNull(ffa);
Assert.Equal(3, ffa.SelectNodes("experience").Count);
Assert.Equal(7, ffa.SelectNodes("color").Count);
Assert.Equal(8, ffa.SelectNodes("patch").Count);
Assert.Equal(7, ffa.SelectNodes("badge").Count);
Assert.Equal(2, ffa.SelectNodes("role").Count);
Assert.Equal("dfltrole", ffa.SelectSingleNode("role[@key='Default']").Attributes["model"].InnerText);
Assert.Equal("noreturn", ffa.SelectSingleNode("role[@key='NoReturn']").Attributes["model"].InnerText);
}
}
}
@@ -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
@@ -25,8 +25,8 @@ namespace TeslaConsole.DiffTests
=> _fx.Recovered.Run("CatalogEntry", new[] { _catalog, launchKey, w, h });
[Fact]
public void Catalog_Has_Three_Products_And_Five_Entries()
=> Assert.Equal("products=3;entries=5",
public void Catalog_Has_Five_Products_And_Eleven_Entries()
=> Assert.Equal("products=5;entries=11",
_fx.Recovered.Run("CatalogSummary", new[] { _catalog }));
[Fact]
@@ -73,5 +73,58 @@ namespace TeslaConsole.DiffTests
=> Assert.Equal(
@"BattleTech Firestorm|cc8500ed-a653-45a7-bef8-c332d30371a6|C:\Games\MW4\launcher.exe||C:\Games\MW4|True",
Entry("CC8500ED-A653-45a7-BEF8-C332D30371A6"));
// BattleTech 4.11 is a new product (no original counterpart); these pin the
// catalog entries the console registers on BT pods.
[Fact]
public void BattleTech_GameClient_Matches_Expected()
=> Assert.Equal(
@"BattleTech 4.11|f4c957fd-72f7-4c5f-8971-28095007e8df|C:\Games\BT411\btl4.exe|-net 1501|C:\Games\BT411|True",
Entry("F4C957FD-72F7-4C5F-8971-28095007E8DF"));
[Fact]
public void BattleTech_GameClient_With_Resolution_Matches_Expected()
=> Assert.Equal(
@"BattleTech 4.11|f4c957fd-72f7-4c5f-8971-28095007e8df|C:\Games\BT411\btl4.exe|-net 1501 -res 1024 768|C:\Games\BT411|True",
Entry("F4C957FD-72F7-4C5F-8971-28095007E8DF", "1024", "768"));
[Fact]
public void BattleTech_LiveCamera_Matches_Expected()
=> Assert.Equal(
@"BattleTech 4.11 LC|d393711a-eda0-48b2-82a0-89df12b768af|C:\Games\BT411\btl4.exe|-net 1501 -lc|C:\Games\BT411|True",
Entry("D393711A-EDA0-48B2-82A0-89DF12B768AF"));
[Fact]
public void BattleTech_MissionReview_Matches_Expected()
=> Assert.Equal(
@"BattleTech 4.11 MR|2e9b8628-9c20-42fb-b070-e9c38d521082|C:\Games\BT411\btl4.exe|-net 1501 -mr|C:\Games\BT411|True",
Entry("2E9B8628-9C20-42FB-B070-E9C38D521082"));
// vPOD (Virtual Pod) — the game-client stand-in for testing the consoles.
[Fact]
public void VPod_GameClient_Matches_Expected()
=> Assert.Equal(
@"vPOD|0041c870-6e5e-4f3b-9782-f94f2f76f21d|C:\Games\vPOD\vPOD.exe|-net 1501|C:\Games\vPOD|True",
Entry("0041C870-6E5E-4F3B-9782-F94F2F76F21D"));
[Fact]
public void VPod_GameClient_With_Resolution_Matches_Expected()
=> Assert.Equal(
@"vPOD|0041c870-6e5e-4f3b-9782-f94f2f76f21d|C:\Games\vPOD\vPOD.exe|-net 1501 -res 1024 768|C:\Games\vPOD|True",
Entry("0041C870-6E5E-4F3B-9782-F94F2F76F21D", "1024", "768"));
[Fact]
public void VPod_LiveCamera_Matches_Expected()
=> Assert.Equal(
@"vPOD LC|ea0d4129-8950-428d-8399-e6a77d2d566a|C:\Games\vPOD\vPOD.exe|-net 1501 -lc|C:\Games\vPOD|True",
Entry("EA0D4129-8950-428D-8399-E6A77D2D566A"));
[Fact]
public void VPod_MissionReview_Matches_Expected()
=> Assert.Equal(
@"vPOD MR|fc7ce34e-f4fe-4218-84cd-b13a6fa58e57|C:\Games\vPOD\vPOD.exe|-net 1501 -mr|C:\Games\vPOD|True",
Entry("FC7CE34E-F4FE-4218-84CD-B13A6FA58E57"));
}
}
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Xml;
namespace TeslaConsole.DiffTests
@@ -194,6 +195,12 @@ namespace TeslaConsole.DiffTests
case "CatalogEntry":
return CatalogEntry(args[0], args[1], args[2], args[3]);
case "BTEggString":
return BTEggString(args);
case "BTEggFraming":
return BTEggFraming(args);
default:
throw new ArgumentException("Unknown case: " + caseName);
}
@@ -345,6 +352,88 @@ namespace TeslaConsole.DiffTests
return $"{disp}|{lkey:D}|{exe}|{margs}|{wd}|{ar}";
}
/// <summary>
/// Builds a TeslaConsole.BattleTech.BTFreeForAllMission from flat args:
/// [0]=mapKey [1]=timeOfDayKey [2]=weatherKey [3]=temperature [4]=lengthSeconds,
/// then one pilot per 10 args: podHost, name, vehicle, camo, patch, emblem,
/// experience, dropzone, roleKey, roleModel. The BT types are internal to the
/// exe, so construction goes through reflection like every other case.
/// </summary>
private object BuildBTMission(string[] args)
{
var missionType = T("TeslaConsole.BattleTech.BTFreeForAllMission");
object mission = Activator.CreateInstance(missionType, new object[]
{
args[0], args[1], args[2],
int.Parse(args[3], CultureInfo.InvariantCulture),
TimeSpan.FromSeconds(double.Parse(args[4], CultureInfo.InvariantCulture)),
});
var roleType = T("TeslaConsole.BattleTech.BTRole");
var playerType = T("TeslaConsole.BattleTech.BTPlayer");
object players = missionType.GetProperty("BattlePlayers").GetValue(mission, null);
MethodInfo add = players.GetType().GetMethod("Add");
for (int i = 5; i < args.Length; i += 10)
{
object role = Activator.CreateInstance(roleType,
new object[] { args[i + 8], args[i + 9], args[i + 8] });
object player = Activator.CreateInstance(playerType, new object[]
{
args[i], args[i + 1], args[i + 2], args[i + 3], args[i + 4],
args[i + 5], args[i + 6], role,
/* advancedDamage */ true, /* dropZoneKey */ args[i + 7],
/* vehicleValue */ 0,
});
add.Invoke(players, new[] { player });
}
return mission;
}
private string BTEggString(string[] args)
{
object mission = BuildBTMission(args);
return (string)mission.GetType().GetMethod("ToEggString").Invoke(mission, null);
}
/// <summary>
/// Exercises BTMission.ToEggFileMessages and verifies the RP-inherited wire
/// framing: newlines become NULs, ASCII bytes go out in 1000-byte chunks with
/// contiguous sequence numbers, and the chunks reassemble byte-exactly to the
/// ToEggString content. Returns "messages=N;total=L;framing=B;reassembled=B".
/// </summary>
private string BTEggFraming(string[] args)
{
object mission = BuildBTMission(args);
Type mt = mission.GetType();
string egg = (string)mt.GetMethod("ToEggString").Invoke(mission, null);
byte[] expected = Encoding.ASCII.GetBytes(
egg.Replace("\r\n", "\0").Replace('\n', '\0'));
var messages = (Array)mt.GetMethod("ToEggFileMessages").Invoke(mission, null);
if (messages.Length == 0)
return "no-messages";
Type et = messages.GetValue(0).GetType();
FieldInfo seq = et.GetField("SequenceNumber");
FieldInfo total = et.GetField("NotationFileLength");
FieldInfo len = et.GetField("ThisMessageLength");
FieldInfo data = et.GetField("NotationData");
bool framing = true;
var reassembled = new MemoryStream();
for (int i = 0; i < messages.Length; i++)
{
object m = messages.GetValue(i);
int chunk = (int)len.GetValue(m);
framing &= (int)seq.GetValue(m) == i;
framing &= (int)total.GetValue(m) == expected.Length;
framing &= chunk == (i < messages.Length - 1 ? 1000 : expected.Length - 1000 * i);
reassembled.Write((byte[])data.GetValue(m), 0, chunk);
}
bool bytesMatch = reassembled.ToArray().SequenceEqual(expected);
return $"messages={messages.Length};total={expected.Length};framing={framing};reassembled={bytesMatch}";
}
/// <summary>
/// Deterministic black/white bitmap whose pixels depend only on (x, y, seed),
/// so both assemblies receive identical input pixels to pack.
+19 -1
View File
@@ -27,7 +27,25 @@ proprietary dependencies — still resolves its references for metadata inspecti
and property/event accessor methods are excluded — the README at the repo root
notes those legitimately differ between a decompilation and the lost sources.
2. **Behavioral output** (`BehavioralEquivalenceTests`)
2. **Recovered-only characterization** (`CatalogTests`, `BTGoldenEggTests`)
Features that were *added* in the reconstruction have no counterpart in the
original exe, so these run against the recovered build only:
- `CatalogTests` — the data-driven product catalog reproduces the exact
`LaunchData` the old hardcoded code emitted.
- `BTGoldenEggTests` — the new `TeslaConsole.BattleTech` mission builder is
diffed field-by-field against two golden eggs captured from the original
consoles ([`BattleTech/cavern.egg`](BattleTech/cavern.egg),
[`BattleTech/TESTARN.EGG`](BattleTech/TESTARN.EGG)). The comparison is
per-section and order-independent (the pod parses eggs INI-style; the two
golden eggs themselves disagree on field order). Font-rendered name-bitmap
pixel rows are excluded, but TESTARN's ordinal art — identical to the
RP-inherited rows — is compared byte-exactly. Also covers the
`EggFileMessage` wire framing (NUL-delimited ASCII, 1000-byte chunks,
byte-exact reassembly), role-block de-duplication, the No Return mode
(same `scenario=freeforall`, different role), and the shipped
`BattleTech\BTConfig.xml` catalog contents.
3. **Behavioral output** (`BehavioralEquivalenceTests`)
The same deterministic, dependency-free methods are invoked in *both* assemblies
over a battery of inputs and the results must match byte-for-byte:
- `RPStrings.GetTimeString` (mm:ss formatting + 0.5 s rounding)
@@ -31,6 +31,11 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<!-- BattleTech golden eggs captured from the original consoles (see BTGoldenEggTests). -->
<Content Include="BattleTech\*.egg" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!--
Build the reconstruction before the tests so we always compare against a fresh
+7
View File
@@ -0,0 +1,7 @@
# Build output
[Bb]in/
[Oo]bj/
# Packaged install output (pack.ps1) — rebuilt on demand, like the console and
# launcher packages; the operator selects the zip at Install Product time.
/dist/
+254
View File
@@ -0,0 +1,254 @@
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Munga.Net;
namespace VPod;
/// <summary>
/// The server side of the Munga control protocol. A real game client is the
/// Munga server (the console <c>Connect()</c>s to pod:1501); the vendored
/// <see cref="MungaSocket" /> only implements the client half, so this
/// reimplements the identical framing for the listening side.
///
/// Wire format (mirrors MungaSocket.Send/Receive, little-endian):
/// [16-byte NetworkPacketHeader][MungaMessage: 4=len 4=id 4=flags + body]
/// where the message length field counts the 12-byte base but not the header.
/// Messages are dispatched to the vendored typed classes by ClientID+MessageID.
/// </summary>
internal sealed class MungaPodServer
{
/// <summary>A message received from the console, plus the header it arrived under.</summary>
public sealed class Incoming
{
public NetworkPacketHeader Header;
public MungaMessage Message;
}
private readonly int mPort;
private TcpListener mListener;
private Thread mAcceptThread;
private volatile bool mRunning;
private readonly object mClientLock = new object();
private TcpClient mClient;
private NetworkStream mStream;
public event Action<Incoming> MessageReceived;
public event Action<string> ConnectionChanged; // remote endpoint string, or null when dropped
public event Action<string> Log;
public bool IsListening => mRunning;
public bool IsConnected
{
get
{
lock (mClientLock)
{
return mClient != null && mClient.Connected;
}
}
}
public MungaPodServer(int port)
{
mPort = port;
}
public void Start()
{
if (mRunning)
{
return;
}
mListener = new TcpListener(IPAddress.Any, mPort);
mListener.Start();
mRunning = true;
mAcceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "vPOD-accept" };
mAcceptThread.Start();
Log?.Invoke($"Listening on TCP {mPort} (all interfaces).");
}
public void Stop()
{
mRunning = false;
try { mListener?.Stop(); } catch { }
DropClient();
Log?.Invoke("Stopped listening.");
}
private void AcceptLoop()
{
while (mRunning)
{
TcpClient client;
try
{
client = mListener.AcceptTcpClient();
}
catch
{
break; // listener stopped
}
// One console at a time: replace any prior connection.
DropClient();
lock (mClientLock)
{
mClient = client;
mClient.NoDelay = true;
mStream = mClient.GetStream();
}
string remote = client.Client.RemoteEndPoint?.ToString() ?? "?";
Log?.Invoke($"Console connected from {remote}.");
ConnectionChanged?.Invoke(remote);
ReceiveLoop(client);
Log?.Invoke("Console disconnected.");
ConnectionChanged?.Invoke(null);
}
}
private void ReceiveLoop(TcpClient client)
{
try
{
NetworkStream stream = client.GetStream();
while (mRunning && client.Connected)
{
Incoming incoming = ReadMessage(stream);
if (incoming == null)
{
break;
}
MessageReceived?.Invoke(incoming);
}
}
catch (Exception ex)
{
Log?.Invoke("Receive error: " + ex.Message);
}
finally
{
DropClient();
}
}
private static Incoming ReadMessage(NetworkStream stream)
{
byte[] headerBytes = ReadExact(stream, NetworkPacketHeader.PacketHeaderSize); // 16
if (headerBytes == null)
{
return null;
}
NetworkPacketHeader header = new NetworkPacketHeader(headerBytes);
byte[] baseBytes = ReadExact(stream, MungaMessage.BaseMessageSize); // 12
if (baseBytes == null)
{
return null;
}
MungaMessage probe = new MungaMessage(new BinaryReader(new MemoryStream(baseBytes)), header.ClientID);
int total = probe.MessageLength; // includes the 12-byte base
byte[] full = new byte[total];
Buffer.BlockCopy(baseBytes, 0, full, 0, MungaMessage.BaseMessageSize);
if (total > MungaMessage.BaseMessageSize)
{
byte[] body = ReadExact(stream, total - MungaMessage.BaseMessageSize);
if (body == null)
{
return null;
}
Buffer.BlockCopy(body, 0, full, MungaMessage.BaseMessageSize, body.Length);
}
BinaryReader reader = new BinaryReader(new MemoryStream(full));
MungaMessage message = Dispatch(header.ClientID, probe.MessageID, reader);
return new Incoming { Header = header, Message = message };
}
/// <summary>Maps a (ClientID, MessageID) to the vendored typed message. Mirrors MungaSocket.Receive.</summary>
private static MungaMessage Dispatch(ClientID clientId, int messageId, BinaryReader reader)
{
switch (clientId)
{
case ClientID.NetworkManagerClientID:
switch (messageId)
{
case 3: return new EggFileMessage(reader);
case 4: return new AcknowledgeEggFileMessage(reader);
}
break;
case ClientID.ApplicationClientID:
switch (messageId)
{
case 3: return new StateQueryMessage(reader);
case 4: return new CheckLoadMessage(reader);
case 5: return new RunMissionMessage(reader);
case 6: return new StopMissionMessage(reader);
case 8: return new SuspendMissionMessage(reader);
case 9: return new ResumeMissionMessage(reader);
case 10: return new LoadMissionMessage(reader);
case 11: return new AbortMissionMessage(reader);
case 12: return new LightsOutMissionMessage(reader);
}
break;
}
return null; // unknown/ignored message id
}
/// <summary>Sends a pod-&gt;console message on the current connection (no-op if disconnected).</summary>
public void Send(MungaMessage message, int gameId, int fromHost)
{
lock (mClientLock)
{
if (mStream == null || mClient == null || !mClient.Connected)
{
return;
}
try
{
NetworkPacketHeader header = new NetworkPacketHeader(message.ClientID, gameId, fromHost, Environment.TickCount);
MemoryStream ms = new MemoryStream();
BinaryWriter writer = new BinaryWriter(ms);
header.WriteTo(writer);
message.WriteTo(writer);
byte[] bytes = ms.ToArray();
mStream.Write(bytes, 0, bytes.Length);
mStream.Flush();
}
catch (Exception ex)
{
Log?.Invoke("Send error: " + ex.Message);
}
}
}
private void DropClient()
{
lock (mClientLock)
{
try { mStream?.Dispose(); } catch { }
try { mClient?.Close(); } catch { }
mStream = null;
mClient = null;
}
}
private static byte[] ReadExact(NetworkStream stream, int count)
{
byte[] buffer = new byte[count];
int offset = 0;
while (offset < count)
{
int read = stream.Read(buffer, offset, count - offset);
if (read == 0)
{
return null; // connection closed
}
offset += read;
}
return buffer;
}
}
+97
View File
@@ -0,0 +1,97 @@
using System;
using Munga.Net;
namespace VPod;
/// <summary>
/// The launch options a real game client understands, parsed from the command
/// line the console/launcher starts it with. vPOD accepts the same shape so it
/// is a drop-in stand-in:
/// <c>-net &lt;port&gt;</c> Munga control port (default 1501)
/// <c>-lc</c> live-camera role (cosmetic here; state model is identical)
/// <c>-mr</c> mission-review role
/// <c>-res W H</c> resolution (ignored)
/// <c>-app rp|bt</c> which ApplicationID to report (RP by default; also
/// switchable live in the UI)
/// <c>-host &lt;id&gt;</c> the responding host id reported in state responses
/// </summary>
internal sealed class PodArguments
{
public int Port { get; private set; } = MungaSocket.ConsolePort; // 1501
public ApplicationID Application { get; private set; } = ApplicationID.RPL4;
public HostType HostType { get; private set; } = HostType.GameMachineHostType;
public int HostId { get; private set; } = 1;
public static PodArguments Parse(string[] args)
{
PodArguments result = new PodArguments();
for (int i = 0; i < args.Length; i++)
{
string token = args[i].ToLowerInvariant();
switch (token)
{
case "-net":
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int port))
{
result.Port = port;
i++;
}
break;
case "-lc":
result.HostType = HostType.MissionReviewHostType;
break;
case "-mr":
result.HostType = HostType.MissionReviewHostType;
break;
case "-res":
i += 2; // width height, ignored
break;
case "-app":
if (i + 1 < args.Length)
{
result.Application = ParseApp(args[i + 1]);
i++;
}
break;
case "-host":
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int host))
{
result.HostId = host;
i++;
}
break;
}
}
return result;
}
private static ApplicationID ParseApp(string value)
{
switch (value.ToLowerInvariant())
{
case "bt":
case "btl4":
case "battletech":
return ApplicationID.BTL4;
case "nd":
case "ndl4":
return ApplicationID.NDL4;
default:
return ApplicationID.RPL4;
}
}
}
/// <summary>
/// Mirror of the console's HostType (GameMachine=0, MissionReview=2, Console=3).
/// Duplicated here so vPOD does not depend on the console assembly.
/// </summary>
internal enum HostType
{
GameMachineHostType = 0,
MissionReviewHostType = 2,
ConsoleHostType = 3
}
+333
View File
@@ -0,0 +1,333 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Munga.Net;
namespace VPod;
/// <summary>
/// Emulates a game client's ApplicationState machine over the Munga control
/// channel, driving it from the messages the console sends. This is the brain of
/// vPOD: it answers state queries, reassembles the streamed egg, and walks the
/// pod through the load/run/stop lifecycle the console's game window expects.
///
/// The console (RPGame/BTGame NetworkScan) gates purely on the reported
/// ApplicationState:
/// WaitingForEgg -- ready; console streams the egg here
/// (egg complete) -> LoadingMission -> WaitingForLaunch (console's "Loaded")
/// RunMission -> LaunchingMission -> RunningMission (console's "Running")
/// StopMission -> EndingMission -> WaitingForEgg (mission over/reset)
/// AbortMission -> AbortingMission -> WaitingForEgg
/// Intermediate states are shown for realism; the console only waits on the gate
/// states, so a short delay between them is harmless.
/// </summary>
internal sealed class PodSimulator
{
private const int NetworkManagerFromHost = 0;
private readonly MungaPodServer mServer;
private readonly object mLock = new object();
private ApplicationState mState = ApplicationState.InitializingState;
private ApplicationID mApplicationId;
private int mHostId;
private int mTransitionDelayMs = 400;
private bool mRestartOnEndMission = true;
// Egg reassembly
private byte[] mEggBuffer;
private int mEggTotal;
private int mEggReceived;
private readonly HashSet<int> mEggChunks = new HashSet<int>();
private string mEggText;
// Cancels a pending delayed transition when a newer one supersedes it.
private int mTransitionToken;
private Timer mTransitionTimer;
public event Action<ApplicationState> StateChanged;
public event Action<string> EggReceived; // full egg text (NUL-normalized)
public event Action EggProgress; // a chunk arrived (mid-transfer)
public event Action<string> Log;
public event Action EndMissionExit; // game exited on end mission; watchdog should restart it
public PodSimulator(MungaPodServer server, ApplicationID applicationId, int hostId)
{
mServer = server;
mApplicationId = applicationId;
mHostId = hostId;
}
public ApplicationState State { get { lock (mLock) { return mState; } } }
public ApplicationID ApplicationId
{
get { lock (mLock) { return mApplicationId; } }
set
{
lock (mLock) { mApplicationId = value; }
Log?.Invoke("Reported application changed to " + value + ".");
}
}
public int HostId { get { lock (mLock) { return mHostId; } } }
public int TransitionDelayMs
{
get { lock (mLock) { return mTransitionDelayMs; } }
set { lock (mLock) { mTransitionDelayMs = Math.Max(0, value); } }
}
/// <summary>
/// When true (the faithful default), the end-mission command makes the game
/// exit and its watchdog restart it (the form drops and reopens the listener).
/// When false, the pod simply returns to WaitingForEgg without exiting.
/// </summary>
public bool RestartOnEndMission
{
get { lock (mLock) { return mRestartOnEndMission; } }
set { lock (mLock) { mRestartOnEndMission = value; } }
}
public string EggText { get { lock (mLock) { return mEggText; } } }
public int EggPercent
{
get
{
lock (mLock)
{
if (mEggTotal <= 0) return 0;
return (int)(100L * mEggReceived / mEggTotal);
}
}
}
/// <summary>Bring the pod online: Initializing -> WaitingForEgg.</summary>
public void PowerOn()
{
SetState(ApplicationState.InitializingState);
ScheduleTransition(ApplicationState.WaitingForEgg, ResetEggOnArrive: true);
}
/// <summary>Operator reset: drop any mission and return to WaitingForEgg immediately.</summary>
public void Reset()
{
lock (mLock)
{
ClearEggLocked();
CancelPendingLocked();
}
SetState(ApplicationState.WaitingForEgg);
Log?.Invoke("Reset to WaitingForEgg.");
}
public void HandleMessage(MungaPodServer.Incoming incoming)
{
MungaMessage msg = incoming?.Message;
if (msg == null)
{
return;
}
switch (msg)
{
case StateQueryMessage _:
RespondState();
break;
case EggFileMessage egg:
HandleEggChunk(egg);
break;
case RunMissionMessage _:
Log?.Invoke("<- RunMissionMessage");
if (State == ApplicationState.WaitingForLaunch)
{
SetState(ApplicationState.LaunchingMission);
mServer.Send(new ReadyToRunMessage(HostId), 0, HostId);
ScheduleTransition(ApplicationState.RunningMission);
}
break;
case StopMissionMessage _:
Log?.Invoke("<- StopMissionMessage (end mission)");
if (State == ApplicationState.RunningMission || State == ApplicationState.SuspendingMission)
{
SetState(ApplicationState.EndingMission);
bool restart;
lock (mLock) { restart = mRestartOnEndMission; }
if (restart)
{
// Faithful pod behaviour: the game exe exits gracefully on the
// end-mission command, then its watchdog restarts it. The form
// simulates the exit + restart by dropping and reopening the
// listener; the pod comes back up in WaitingForEgg.
EndMissionExit?.Invoke();
}
else
{
ScheduleTransition(ApplicationState.WaitingForEgg, ResetEggOnArrive: true);
}
}
break;
case AbortMissionMessage _:
Log?.Invoke("<- AbortMissionMessage");
SetState(ApplicationState.AbortingMission);
mServer.Send(new AbortMissionResponseMessage(HostId), 0, HostId);
ScheduleTransition(ApplicationState.WaitingForEgg, ResetEggOnArrive: true);
break;
case SuspendMissionMessage _:
Log?.Invoke("<- SuspendMissionMessage");
if (State == ApplicationState.RunningMission)
{
SetState(ApplicationState.SuspendingMission);
}
break;
case ResumeMissionMessage _:
Log?.Invoke("<- ResumeMissionMessage");
if (State == ApplicationState.SuspendingMission)
{
SetState(ApplicationState.ResumingMission);
ScheduleTransition(ApplicationState.RunningMission);
}
break;
case LoadMissionMessage _:
Log?.Invoke("<- LoadMissionMessage");
if (State == ApplicationState.WaitingForEgg)
{
SetState(ApplicationState.LoadingMission);
ScheduleTransition(ApplicationState.WaitingForLaunch);
}
break;
case CheckLoadMessage _:
Log?.Invoke("<- CheckLoadMessage");
break;
case LightsOutMissionMessage _:
Log?.Invoke("<- LightsOutMissionMessage");
break;
}
}
private void RespondState()
{
ApplicationState state;
ApplicationID app;
int host;
lock (mLock)
{
state = mState;
app = mApplicationId;
host = mHostId;
}
mServer.Send(new StateResponseMessage(host, state, app), 0, host);
}
private void HandleEggChunk(EggFileMessage egg)
{
bool complete = false;
string text = null;
lock (mLock)
{
if (mEggBuffer == null || mEggTotal != egg.NotationFileLength)
{
mEggTotal = egg.NotationFileLength;
mEggBuffer = new byte[mEggTotal];
mEggReceived = 0;
mEggChunks.Clear();
}
int offset = egg.SequenceNumber * 1000;
int len = egg.ThisMessageLength;
if (offset >= 0 && offset + len <= mEggBuffer.Length && mEggChunks.Add(egg.SequenceNumber))
{
Buffer.BlockCopy(egg.NotationData, 0, mEggBuffer, offset, len);
mEggReceived += len;
}
if (mEggReceived >= mEggTotal && mEggTotal > 0)
{
// On the wire the egg is NUL-delimited ASCII; show it line-per-field.
text = Encoding.ASCII.GetString(mEggBuffer).Replace('\0', '\n');
mEggText = text;
complete = true;
}
}
EggProgress?.Invoke();
if (complete)
{
Log?.Invoke($"Egg received ({mEggTotal} bytes, {mEggChunks.Count} chunks). Acknowledging.");
mServer.Send(new AcknowledgeEggFileMessage(), NetworkManagerFromHost, HostId);
EggReceived?.Invoke(text);
// WaitingForEgg -> LoadingMission -> WaitingForLaunch
SetState(ApplicationState.LoadingMission);
ScheduleTransition(ApplicationState.WaitingForLaunch);
}
}
private void SetState(ApplicationState state)
{
bool changed;
lock (mLock)
{
changed = mState != state;
mState = state;
}
if (changed)
{
Log?.Invoke("State -> " + state);
StateChanged?.Invoke(state);
}
}
private void ScheduleTransition(ApplicationState target, bool ResetEggOnArrive = false)
{
int token;
int delay;
lock (mLock)
{
token = ++mTransitionToken;
delay = mTransitionDelayMs;
mTransitionTimer?.Dispose();
mTransitionTimer = new Timer(delegate
{
bool proceed;
lock (mLock)
{
proceed = token == mTransitionToken;
if (proceed && ResetEggOnArrive)
{
ClearEggLocked();
}
}
if (proceed)
{
SetState(target);
}
}, null, delay, Timeout.Infinite);
}
}
private void CancelPendingLocked()
{
mTransitionToken++;
mTransitionTimer?.Dispose();
mTransitionTimer = null;
}
private void ClearEggLocked()
{
mEggBuffer = null;
mEggTotal = 0;
mEggReceived = 0;
mEggChunks.Clear();
mEggText = null;
}
}
+16
View File
@@ -0,0 +1,16 @@
using System;
using System.Windows.Forms;
namespace VPod;
internal static class Program
{
[STAThread]
private static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
PodArguments options = PodArguments.Parse(args);
Application.Run(new VPodForm(options));
}
}
+69
View File
@@ -0,0 +1,69 @@
# vPOD — virtual pod / game-client stand-in
A test tool that impersonates a Tesla game client (Red Planet's `rpl4opt.exe`
or BattleTech's `btl4.exe`) so the operator **consoles can be exercised without
real cockpit hardware**. It speaks the Munga command/control protocol as a
server on TCP 1501 — the console connects to it exactly as it would a real pod —
emulates the pod `ApplicationState` machine, reassembles the streamed egg, and
shows everything on a live display.
## What it does
- **Listens on TCP 1501** (configurable) and answers the console's
`StateQuery` with a `StateResponse`, reporting the game (`ApplicationID`) and
the current `ApplicationState`.
- **Walks the mission lifecycle** the console drives it through:
`WaitingForEgg → LoadingMission → WaitingForLaunch → LaunchingMission →
RunningMission → …`, reacting to the egg stream and to Run / Stop / Abort /
Suspend / Resume messages, and acknowledging the egg.
- **End-mission graceful exit + watchdog restart** — on the console's end-mission
command the "game exe" exits (the listener closes, the console's connection
drops), then a watchdog relaunches it a moment later and it comes back up in
`WaitingForEgg`. This is the real pod's per-game cycle (`autoRestart`); the
*Restart game after mission ends (watchdog)* checkbox (on by default) toggles
it — unchecked, the pod just returns to `WaitingForEgg` without exiting.
- **Power On / Power Off** — Power Off closes the TCP listener so the console
cannot connect, mimicking a pod with no game client running; Power On reopens
it. **Reset** returns a live pod to `WaitingForEgg`.
- **Reassembles and shows the egg** the console streams (the `EggFileMessage`
chunks), one field per line, with a summary line (adventure / map / scenario /
pilot count). The last egg is **kept** across missions/restarts (so it can be
copied for dev use) until the **Clear** button empties the viewer.
- **Game toggle** — a Red Planet ⇄ BattleTech switch on the window changes which
`ApplicationID` the pod reports, live, so one vPOD can stand in for either
game. (`-app rp|bt` sets the initial choice.)
- A **newest-first protocol log** of the traffic.
## Running it
```
vPOD.exe [-net <port>] [-app rp|bt] [-lc|-mr] [-host <id>] [-res W H]
```
- `-net <port>` Munga control port (default **1501**).
- `-app rp|bt` which game to report initially (also switchable in the UI).
- `-lc` / `-mr` live-camera / mission-review role (cosmetic; the state model is
identical to a game machine).
- `-host <id>` responding host id reported in state responses (default 1).
- `-res W H` accepted and ignored (real clients take it; kept for drop-in
launch compatibility).
## Deploying from the console (Manage Site → Install Product)
vPOD is a catalog product (`RedPlanet\Apps.xml`, id `0041C870-…`) with Game
Client / Live Camera / Mission Review entries, so it appears in **Manage Site →
Install Product** like any game. Build the deployable package first:
```
pwsh -File pack.ps1 # produces dist\vPOD.zip
```
The zip lays out `vPOD\vPOD.exe` (+ `Munga Net.dll`) so the launcher extracts it
to `C:\Games\vPOD` and the catalog entry launches `C:\Games\vPOD\vPOD.exe`.
## Testing locally against the console
The default site ships a **`local` pod at 127.0.0.1**. Run vPOD on the console
machine, open a game window (e.g. *Games → Red Planet: Death Race*), and enable
the local pod — the console connects to `127.0.0.1:1501` (vPOD), and you can
drive Load → Run → Stop and watch vPOD's state and egg viewer follow along.
+514
View File
@@ -0,0 +1,514 @@
using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Munga.Net;
namespace VPod;
/// <summary>
/// The vPOD window: a live view of the simulated pod. Top panel shows the
/// listening/connection status and the current ApplicationState (colour-coded),
/// with the game toggle (Red Planet / BattleTech) that changes which
/// ApplicationID the pod reports. Below, the left pane is the egg viewer (the
/// last egg the console streamed, one field per line) and the right pane is a
/// scrolling protocol log.
/// </summary>
internal sealed class VPodForm : Form
{
private readonly PodArguments mOptions;
private readonly MungaPodServer mServer;
private readonly PodSimulator mSimulator;
private Label mListeningLabel;
private Label mConnectionLabel;
private Label mStateLabel;
private Label mEggSummaryLabel;
private RadioButton mRedPlanetRadio;
private RadioButton mBattleTechRadio;
private Button mPowerButton;
private Button mPowerOffButton;
private Button mResetButton;
private CheckBox mRestartCheckbox;
private Timer mRestartTimer;
private SplitContainer mSplit;
private TextBox mEggBox;
private TextBox mLogBox;
// How long the "watchdog" waits before relaunching the exited game.
private const int WatchdogRestartMs = 1500;
public VPodForm(PodArguments options)
{
mOptions = options;
mServer = new MungaPodServer(options.Port);
mSimulator = new PodSimulator(mServer, options.Application, options.HostId);
BuildUi();
mServer.Log += OnLog;
mServer.ConnectionChanged += OnConnectionChanged;
mServer.MessageReceived += OnMessageReceived;
mSimulator.Log += OnLog;
mSimulator.StateChanged += OnStateChanged;
mSimulator.EggReceived += OnEggReceived;
mSimulator.EggProgress += OnEggProgress;
mSimulator.EndMissionExit += OnEndMissionExit;
mRestartTimer = new Timer { Interval = WatchdogRestartMs };
mRestartTimer.Tick += OnRestartTimerTick;
Load += OnFormLoad;
FormClosing += OnFormClosing;
}
private void BuildUi()
{
Text = $"vPOD - virtual pod (port {mOptions.Port}, host {mOptions.HostId})";
ClientSize = new Size(920, 560);
MinimumSize = new Size(760, 460);
Font = new Font("Segoe UI", 9f);
// ---- status panel ----
GroupBox statusGroup = new GroupBox
{
Text = "Pod Status",
Dock = DockStyle.Top,
Height = 176,
Padding = new Padding(10)
};
mListeningLabel = new Label { AutoSize = true, Location = new Point(16, 26) };
mConnectionLabel = new Label { AutoSize = true, Location = new Point(16, 50) };
Label roleLabel = new Label
{
AutoSize = true,
Location = new Point(16, 74),
Text = "Role: " + RoleText(mOptions.HostType)
};
Label stateCaption = new Label
{
AutoSize = true,
Location = new Point(16, 106),
Text = "Application State:"
};
mStateLabel = new Label
{
AutoSize = true,
Location = new Point(130, 100),
Font = new Font("Segoe UI", 15f, FontStyle.Bold),
Text = "—"
};
// game toggle
GroupBox gameGroup = new GroupBox
{
Text = "Mimicking Game",
Location = new Point(560, 20),
Size = new Size(330, 130)
};
mRedPlanetRadio = new RadioButton
{
Text = "Red Planet (RPL4)",
Location = new Point(18, 28),
AutoSize = true,
Checked = mOptions.Application == ApplicationID.RPL4
};
mBattleTechRadio = new RadioButton
{
Text = "BattleTech (BTL4)",
Location = new Point(18, 56),
AutoSize = true,
Checked = mOptions.Application == ApplicationID.BTL4
};
mRedPlanetRadio.CheckedChanged += GameToggleChanged;
mBattleTechRadio.CheckedChanged += GameToggleChanged;
mPowerButton = new Button { Text = "Power On", Location = new Point(12, 90), Size = new Size(94, 28) };
mPowerOffButton = new Button { Text = "Power Off", Location = new Point(112, 90), Size = new Size(94, 28) };
mResetButton = new Button { Text = "Reset", Location = new Point(212, 90), Size = new Size(94, 28) };
mPowerButton.Click += PowerOnClicked;
mPowerOffButton.Click += PowerOffClicked;
mResetButton.Click += (s, e) => mSimulator.Reset();
gameGroup.Controls.Add(mRedPlanetRadio);
gameGroup.Controls.Add(mBattleTechRadio);
gameGroup.Controls.Add(mPowerButton);
gameGroup.Controls.Add(mPowerOffButton);
gameGroup.Controls.Add(mResetButton);
mRestartCheckbox = new CheckBox
{
Text = "Restart game after mission ends (watchdog)",
Location = new Point(16, 146),
AutoSize = true,
Checked = true
};
mRestartCheckbox.CheckedChanged += (s, e) => mSimulator.RestartOnEndMission = mRestartCheckbox.Checked;
statusGroup.Controls.Add(mListeningLabel);
statusGroup.Controls.Add(mConnectionLabel);
statusGroup.Controls.Add(roleLabel);
statusGroup.Controls.Add(stateCaption);
statusGroup.Controls.Add(mStateLabel);
statusGroup.Controls.Add(mRestartCheckbox);
statusGroup.Controls.Add(gameGroup);
// ---- egg viewer + log ----
// Split the egg viewer (left) and protocol log (right). The 50/50 default
// is applied in OnFormLoad once the control has its real width (setting it
// here would clamp to the control's default size).
mSplit = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical
};
SplitContainer split = mSplit;
GroupBox eggGroup = new GroupBox { Text = "Current Egg", Dock = DockStyle.Fill, Padding = new Padding(8) };
// The egg is kept after a mission/restart (not auto-cleared) so it can be
// copied for dev use; the Clear button empties the viewer on demand.
Panel eggHeader = new Panel { Dock = DockStyle.Top, Height = 30 };
Button clearEggButton = new Button { Text = "Clear", Dock = DockStyle.Right, Width = 70 };
mEggSummaryLabel = new Label
{
Dock = DockStyle.Fill,
Text = "No egg loaded.",
TextAlign = ContentAlignment.MiddleLeft
};
clearEggButton.Click += (s, e) =>
{
mEggBox.Clear();
mEggSummaryLabel.Text = "No egg loaded.";
};
eggHeader.Controls.Add(mEggSummaryLabel);
eggHeader.Controls.Add(clearEggButton);
mEggBox = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
WordWrap = false,
Font = new Font("Consolas", 9f),
BackColor = Color.White
};
eggGroup.Controls.Add(mEggBox);
eggGroup.Controls.Add(eggHeader);
GroupBox logGroup = new GroupBox { Text = "Protocol Log", Dock = DockStyle.Fill, Padding = new Padding(8) };
mLogBox = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
WordWrap = false,
Font = new Font("Consolas", 9f),
BackColor = Color.FromArgb(24, 24, 24),
ForeColor = Color.Gainsboro
};
logGroup.Controls.Add(mLogBox);
split.Panel1.Controls.Add(eggGroup);
split.Panel2.Controls.Add(logGroup);
Controls.Add(split);
Controls.Add(statusGroup);
}
private void OnFormLoad(object sender, EventArgs e)
{
// Now that the split has its real width, give the log half the window.
if (mSplit.Width > mSplit.Panel1MinSize + mSplit.Panel2MinSize)
{
mSplit.SplitterDistance = mSplit.Width / 2;
}
UpdateConnectionLabel(null);
UpdateStateLabel(mSimulator.State);
if (StartServer())
{
mSimulator.PowerOn();
SetPoweredState(on: true);
}
else
{
SetPoweredState(on: false);
}
}
private void OnFormClosing(object sender, FormClosingEventArgs e)
{
mServer.Stop();
}
private void PowerOnClicked(object sender, EventArgs e)
{
mRestartTimer.Stop(); // cancel any pending watchdog restart
if (!mServer.IsListening && !StartServer())
{
return; // couldn't bind the port; stay powered off
}
mSimulator.PowerOn();
SetPoweredState(on: true);
}
/// <summary>
/// Powers the pod off by closing the TCP listener, so the console can no
/// longer connect — the same condition as a pod with no game client running.
/// </summary>
private void PowerOffClicked(object sender, EventArgs e)
{
mRestartTimer.Stop();
mServer.Stop();
SetPoweredState(on: false);
}
/// <summary>
/// The game exited gracefully on the console's end-mission command. Simulate
/// the process exit by closing the listener (the console's connection drops),
/// then let the watchdog timer relaunch it.
/// </summary>
private void OnEndMissionExit()
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
OnLog("Game exited gracefully (end mission); watchdog will restart it...");
mServer.Stop();
UpdateListeningLabel(false);
UpdateConnectionLabel(null);
mStateLabel.Text = "Restarting...";
mStateLabel.ForeColor = Color.DarkOrange;
mPowerButton.Enabled = true;
mPowerOffButton.Enabled = false;
mResetButton.Enabled = false;
mRestartTimer.Stop();
mRestartTimer.Start();
}));
}
private void OnRestartTimerTick(object sender, EventArgs e)
{
mRestartTimer.Stop();
if (StartServer())
{
OnLog("Watchdog restarted the game.");
mSimulator.PowerOn();
SetPoweredState(on: true);
}
else
{
SetPoweredState(on: false);
}
}
/// <summary>Starts the listener and updates the status label. Returns false (with a message) if the port is unavailable.</summary>
private bool StartServer()
{
try
{
mServer.Start();
UpdateListeningLabel(true);
return true;
}
catch (Exception ex)
{
UpdateListeningLabel(false);
OnLog("FAILED to listen on port " + mOptions.Port + ": " + ex.Message);
MessageBox.Show(this,
"Could not listen on TCP port " + mOptions.Port + ".\n\n" + ex.Message +
"\n\nIs another pod or vPOD already using it?",
"vPOD", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
}
/// <summary>Reflects the on/off state in the buttons and, when off, the status labels.</summary>
private void SetPoweredState(bool on)
{
mPowerButton.Enabled = !on;
mPowerOffButton.Enabled = on;
mResetButton.Enabled = on;
if (!on)
{
UpdateListeningLabel(false);
UpdateConnectionLabel(null);
mStateLabel.Text = "Powered Off";
mStateLabel.ForeColor = Color.Gray;
}
}
private void GameToggleChanged(object sender, EventArgs e)
{
if (sender is RadioButton rb && !rb.Checked)
{
return; // only act on the newly-checked one
}
mSimulator.ApplicationId = mBattleTechRadio.Checked ? ApplicationID.BTL4 : ApplicationID.RPL4;
}
// ---- event handlers (marshal to UI thread) ----
private const int MaxLogLines = 500;
private void OnLog(string message)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
// Newest first: prepend, and cap the buffer so it can't grow without bound.
string line = $"[{DateTime.Now:HH:mm:ss}] {message}";
string existing = mLogBox.Text;
string combined = existing.Length > 0 ? line + "\r\n" + existing : line;
string[] lines = combined.Split(new[] { "\r\n" }, StringSplitOptions.None);
if (lines.Length > MaxLogLines)
{
combined = string.Join("\r\n", lines, 0, MaxLogLines);
}
mLogBox.Text = combined;
}));
}
private void OnConnectionChanged(string remote)
{
if (IsDisposed) return;
BeginInvoke((Action)(() => UpdateConnectionLabel(remote)));
}
private void OnMessageReceived(MungaPodServer.Incoming incoming)
{
// The simulator drives all protocol behaviour; the UI only logs
// non-query traffic (StateQuery is once-a-second noise).
if (!(incoming.Message is StateQueryMessage) && incoming.Message != null)
{
OnLog("<- " + incoming.Message.GetType().Name);
}
mSimulator.HandleMessage(incoming);
}
private void OnStateChanged(ApplicationState state)
{
if (IsDisposed) return;
// The egg viewer is intentionally NOT cleared here — the last egg stays
// visible (copyable) across missions/restarts until the Clear button.
BeginInvoke((Action)(() => UpdateStateLabel(state)));
}
private void OnEggReceived(string eggText)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
mEggBox.Text = eggText.Replace("\n", "\r\n");
mEggSummaryLabel.Text = SummarizeEgg(eggText);
}));
}
private void OnEggProgress()
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
int pct = mSimulator.EggPercent;
if (pct < 100 && pct > 0)
{
mEggSummaryLabel.Text = $"Receiving egg... {pct}%";
}
}));
}
// ---- label helpers ----
private void UpdateListeningLabel(bool listening)
{
mListeningLabel.Text = listening
? $"Listening on TCP {mOptions.Port} ●"
: $"Not listening on TCP {mOptions.Port}";
mListeningLabel.ForeColor = listening ? Color.ForestGreen : Color.Firebrick;
}
private void UpdateConnectionLabel(string remote)
{
if (string.IsNullOrEmpty(remote))
{
mConnectionLabel.Text = "Console: not connected";
mConnectionLabel.ForeColor = Color.Gray;
}
else
{
mConnectionLabel.Text = "Console: connected from " + remote;
mConnectionLabel.ForeColor = Color.ForestGreen;
}
}
private void UpdateStateLabel(ApplicationState state)
{
mStateLabel.Text = Prettify(state);
mStateLabel.ForeColor = ColorFor(state);
}
private static string RoleText(HostType hostType)
{
switch (hostType)
{
case HostType.MissionReviewHostType:
return "Camera / Mission Review";
case HostType.ConsoleHostType:
return "Console";
default:
return "Game Machine (player)";
}
}
private static string Prettify(ApplicationState state)
{
switch (state)
{
case ApplicationState.InitializingState: return "Initializing";
case ApplicationState.WaitingForEgg: return "Waiting For Egg";
case ApplicationState.LoadingMission: return "Loading Mission";
case ApplicationState.WaitingForLaunch: return "Waiting For Launch";
case ApplicationState.LaunchingMission: return "Launching Mission";
case ApplicationState.RunningMission: return "Running Mission";
case ApplicationState.EndingMission: return "Ending Mission";
case ApplicationState.StoppingMission: return "Stopping Mission";
case ApplicationState.SuspendingMission: return "Suspended";
case ApplicationState.ResumingMission: return "Resuming Mission";
case ApplicationState.AbortingMission: return "Aborting Mission";
case ApplicationState.CreatingMission: return "Creating Mission";
default: return state.ToString();
}
}
private static Color ColorFor(ApplicationState state)
{
switch (state)
{
case ApplicationState.WaitingForEgg: return Color.ForestGreen;
case ApplicationState.WaitingForLaunch: return Color.DarkGoldenrod;
case ApplicationState.RunningMission: return Color.RoyalBlue;
case ApplicationState.InitializingState: return Color.Gray;
default: return Color.DarkOrange;
}
}
private static string SummarizeEgg(string eggText)
{
string adventure = null, map = null, scenario = null;
int pilots = 0;
foreach (string raw in eggText.Split('\n'))
{
string line = raw.Trim();
if (line.StartsWith("adventure=")) adventure = line.Substring(10);
else if (line.StartsWith("map=")) map = line.Substring(4);
else if (line.StartsWith("scenario=")) scenario = line.Substring(9);
else if (line.StartsWith("pilot=")) pilots++;
}
StringBuilder sb = new StringBuilder();
sb.Append(adventure ?? "(egg)");
if (map != null) sb.Append(" • map=").Append(map);
if (scenario != null) sb.Append(" • ").Append(scenario);
sb.Append(" • ").Append(pilots).Append(pilots == 1 ? " pilot" : " pilots");
return sb.ToString();
}
}
+39
View File
@@ -0,0 +1,39 @@
# Builds the vPOD install package for the console's Manage Site -> Install Product.
#
# The launcher extracts the zip's entries directly into C:\Games\, so the archive
# root must contain a `vPOD\` folder holding vPOD.exe + its dependencies. The
# console's Apps.xml vPOD product then launches C:\Games\vPOD\vPOD.exe.
#
# Usage: pwsh -File pack.ps1 (Release, default)
# pwsh -File pack.ps1 -Config Debug
param([string]$Config = 'Release')
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$bin = Join-Path $root "bin\$Config\net48"
$distDir = Join-Path $root 'dist'
$stage = Join-Path $distDir 'vPOD' # -> C:\Games\vPOD on the pod
$zipPath = Join-Path $distDir 'vPOD.zip'
Write-Host "Building vPOD ($Config)..."
dotnet build (Join-Path $root 'vPOD.csproj') -c $Config --nologo -v minimal | Out-Null
if (-not (Test-Path (Join-Path $bin 'vPOD.exe'))) {
throw "Build output not found: $bin\vPOD.exe"
}
# Fresh stage dir.
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
New-Item -ItemType Directory -Force $stage | Out-Null
# The runtime payload: the exe + the vendored Munga Net.dll (copied local by the
# csproj) + config. No pdb/xml.
Get-ChildItem $bin -File |
Where-Object { $_.Extension -in '.exe', '.dll', '.config' } |
ForEach-Object { Copy-Item $_.FullName $stage }
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
Compress-Archive -Path $stage -DestinationPath $zipPath -Force
Write-Host "Packaged: $zipPath"
Get-ChildItem $stage | Select-Object Name, Length | Format-Table -AutoSize
+44
View File
@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
vPOD - a virtual pod / game-client stand-in for testing the Tesla game
consoles (Red Planet and BattleTech) without real cockpit hardware.
It speaks the Munga command/control protocol as a SERVER on TCP 1501 (the
console connects to it exactly as it would a real rpl4opt.exe / btl4.exe),
emulates the pod ApplicationState machine, reassembles the streamed egg,
and shows both on a live display. Deployable to a pod machine via the
console's Manage Site -> Install Product (see dist\ + RedPlanet\Apps.xml).
net48 to match the rest of the suite and the vendored Munga Net.dll.
-->
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<TargetFramework>net48</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<AssemblyName>vPOD</AssemblyName>
<RootNamespace>VPod</RootNamespace>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Version>1.0.0</Version>
<Product>vPOD</Product>
</PropertyGroup>
<ItemGroup>
<!-- net48 reference assemblies so this builds without a full targeting pack installed -->
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<!-- The Munga wire types (messages, header, enums). Copied next to vPOD.exe so
the deployable package is self-contained. -->
<Reference Include="Munga Net">
<HintPath>..\lib\Munga Net.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
</Project>
+26 -11
View File
@@ -7,13 +7,11 @@
::
:: TeslaLauncher\
:: install.bat <- this file
:: Service\
:: TeslaLauncherService.exe
:: Agent\
:: TeslaLauncherAgent.exe
:: oalinst.exe
:: dx9201006\
:: DXSETUP.exe
:: Service\TeslaLauncherService.exe
:: Agent\TeslaLauncherAgent.exe
:: dx9201006\DXSETUP.exe (DirectX June 2010 redist)
:: openal\oalinst.exe (OpenAL redist)
:: UltraVNC\UltraVNC_x64_Setup.exe (UltraVNC server + UltraVNC.inf)
:: =============================================================================
setlocal enabledelayedexpansion
@@ -40,6 +38,10 @@ set CONSOLE_PORT=53290
set AUTOLOGIN_USER=Firestorm
set AUTOLOGIN_PASS=thor6
:: Workgroup every pod on this site joins. Keep it the same across all pods so
:: the mw4files / c shares browse cleanly. Change once per site if needed.
set WORKGROUP=Tesla
:: ── Admin check ───────────────────────────────────────────────────────────────
net session >nul 2>&1
if %errorlevel% neq 0 (
@@ -157,10 +159,15 @@ if exist "%UVNC_SETUP%" (
start "" /wait "%UVNC_SETUP%" /verysilent /norestart /loadinf="%ROOT%UltraVNC\UltraVNC.inf"
)
:: Enable SMB1 client (required by game networking)
echo Enabling SMB1 file sharing...
dism /Online /Enable-Feature /FeatureName:SMB1Protocol /All /NoRestart >nul 2>&1
echo SMB1 enabled (reboot required to take effect).
:: Enable SMB1 client AND server (required by game networking). The pod both
:: reaches SMB1 shares (client) and hosts the mw4files / c shares below (server),
:: and Win10/11 ship both SMB1 sub-features off by default — so enable all three
:: (umbrella + client + server); each child pulls in the umbrella via /All.
echo Enabling SMB1 file sharing (client + server)...
dism /Online /Enable-Feature /FeatureName:SMB1Protocol /All /NoRestart >nul 2>&1
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Client /All /NoRestart >nul 2>&1
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Server /All /NoRestart >nul 2>&1
echo SMB1 client + server enabled (reboot required to take effect).
:: Create and share game-data folders (closed network — open to Everyone).
echo Creating network shares...
@@ -175,6 +182,14 @@ net share c /delete >nul 2>&1
net share c=C:\ /grant:Everyone,FULL >nul 2>&1
echo "Share \\%COMPUTERNAME%\c -> C:\ (Everyone: Full)"
:: Join the site workgroup so every pod shares one browse list. Add-Computer
:: throws if the machine is already in that workgroup, so the error is ignored.
:: Takes effect on the reboot at the end of this script (same as the hostname
:: SecureConfig applies on first boot).
echo Joining workgroup "%WORKGROUP%"...
powershell -NoProfile -Command "try { Add-Computer -WorkgroupName '%WORKGROUP%' -Force -ErrorAction Stop } catch { }" >nul 2>&1
echo Workgroup set to "%WORKGROUP%" (effective after reboot).
:: Enable DirectPlay (required by older games)
echo Enabling DirectPlay...
dism /Online /Enable-Feature /FeatureName:DirectPlay /All /NoRestart >nul 2>&1
+7
View File
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.Contract", "Contract\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.SecureConfig", "SecureConfig\Tesla.SecureConfig.csproj", "{070A6093-6C46-4A5B-A119-47ED195530E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vPOD", "Console\vPOD\vPOD.csproj", "{9EAC97A1-D71A-4AAB-9957-A79A1587D406}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -52,9 +54,14 @@ Global
{070A6093-6C46-4A5B-A119-47ED195530E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{070A6093-6C46-4A5B-A119-47ED195530E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{070A6093-6C46-4A5B-A119-47ED195530E1}.Release|Any CPU.Build.0 = Release|Any CPU
{9EAC97A1-D71A-4AAB-9957-A79A1587D406}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EAC97A1-D71A-4AAB-9957-A79A1587D406}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EAC97A1-D71A-4AAB-9957-A79A1587D406}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9EAC97A1-D71A-4AAB-9957-A79A1587D406}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{27C769F3-F07F-456E-ACB7-F4A4A21AB6D1} = {91E92ADD-9F67-41E4-B43C-CCB7B2D95F15}
{467AA87A-FBD4-45D7-B8F8-9336C95884D2} = {27C769F3-F07F-456E-ACB7-F4A4A21AB6D1}
{9EAC97A1-D71A-4AAB-9957-A79A1587D406} = {91E92ADD-9F67-41E4-B43C-CCB7B2D95F15}
EndGlobalSection
EndGlobal