Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91640dcbf2 | ||
|
|
eefb8054e0 | ||
|
|
8730b9b966 | ||
|
|
1d95a800fd | ||
|
|
f07df88acb | ||
|
|
13f8e0456b | ||
|
|
80ee1d26ea | ||
|
|
a5f25c8bb8 | ||
|
|
6a77fd0991 | ||
|
|
d30ce8bdbe | ||
|
|
cb7c655530 | ||
|
|
60893172c3 | ||
|
|
e87a8a22f1 | ||
|
|
e0a3c72370 | ||
|
|
1daaf4af78 | ||
|
|
fd0c8c5196 | ||
|
|
aff8bbf6e5 | ||
|
|
3fd637c58a | ||
|
|
18d787bdd5 | ||
|
|
deaade4f72 | ||
|
|
36166a95b0 | ||
|
|
9148ce2cca | ||
|
|
0431c6840b |
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: verify
|
||||
description: Drive the TeslaConsole WinForms app against vPOD to verify console changes end-to-end on this machine.
|
||||
---
|
||||
|
||||
# Verifying console changes against vPOD
|
||||
|
||||
Build (Debug is what the exes below point at):
|
||||
|
||||
```
|
||||
dotnet build Console/TeslaConsole.csproj
|
||||
dotnet build vPOD/vPOD.csproj
|
||||
```
|
||||
|
||||
Launch both (the machine's `C:\ProgramData\Tesla Console\local.siteconfig`
|
||||
already has squad `bay1` with a provisioned `vPOD` pod at 127.0.0.1; vPOD's
|
||||
session key persists in `%LocalAppData%\vPOD\TeslaKeyStore.key`):
|
||||
|
||||
```
|
||||
Start-Process vPOD\bin\Debug\net40\vPOD.exe -ArgumentList "-app","bt" # or rp
|
||||
Start-Process Console\bin\Debug\net40\TeslaConsole.exe
|
||||
```
|
||||
|
||||
(net40 since the XP11 port — both exes run on the machine's 4.8 runtime.)
|
||||
|
||||
Drive the UI with System.Windows.Automation (UIA) — WinForms exposes
|
||||
menus, buttons, checkboxes, and DataGridView rows/cells as real UIA elements:
|
||||
|
||||
- Menus: ExpandCollapsePattern on "Games", InvokePattern on the item.
|
||||
- Pilots grid cells are named like `Enabled Row 1`, `Pilot Row 1`; real mouse
|
||||
clicks at the cell's BoundingRectangle center behave exactly like a user
|
||||
(needed for CellEndEdit paths — programmatic value sets bypass them).
|
||||
Click another cell afterwards to commit a checkbox cell edit.
|
||||
- Type into a grid cell: click it, then `[System.Windows.Forms.SendKeys]::SendWait()`.
|
||||
- The WeifenLuo dock tabs are NOT UIA TabItems; switch documents with
|
||||
Ctrl+Tab sent to the focused window.
|
||||
- Screenshots: Graphics.CopyFromScreen of the element's BoundingRectangle
|
||||
(coordinates are physical pixels; fine to pass straight to SetCursorPos).
|
||||
|
||||
Observe behavior:
|
||||
|
||||
- Game connection endpoint: `Get-NetTCPConnection -OwningProcess <consolePid>
|
||||
-RemotePort 1501`. vPOD binds 0.0.0.0:1501, so any 127.x.x.x target lands on
|
||||
it — distinguish targets by the netstat RemoteAddress, not by whether vPOD
|
||||
answered.
|
||||
- Mission content: vPOD's Current Egg pane shows the decoded egg (player IPs,
|
||||
pilots, map) after clicking Load; its protocol log timestamps connects,
|
||||
disconnects, and state transitions.
|
||||
- Full lifecycle: enable a pod row + pilot name → Load → Run Mission → Stop
|
||||
Mission. With the watchdog checkbox on, vPOD drops the connection after a
|
||||
mission ends and comes back in WaitingForEgg — an observed reconnect there
|
||||
is normal.
|
||||
|
||||
Gotchas: enabling a pod row only requests the connection once the cell edit
|
||||
commits. A helper script with these UIA primitives from a previous session:
|
||||
scratchpad `uia.ps1` (Get-Window / Find-ByName / Click-Elem / Screenshot) —
|
||||
recreate from this recipe if gone. Close both apps when done
|
||||
(`CloseMainWindow`, then Stop-Process stragglers).
|
||||
@@ -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/
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
# Console 4.10 — Decompilation & Documentation
|
||||
|
||||
Reverse-engineering notes for the classic Mac OS **operator console** that
|
||||
commanded the networked Virtual World Entertainment (VWE) BattleTech / Red
|
||||
Planet pods. Part of the VWE Tesla restoration project.
|
||||
|
||||
- **Target dir:** `c:\VWE\TeslaRel410\410console\4_10-console-extracted\`
|
||||
- **Binary:** `Console 4.10` (728,064 bytes) — classic Mac OS **fat application**
|
||||
(68K `CODE` resources **+** PowerPC **PEF/CFM** fragment in the data fork).
|
||||
- **Internal identity:** type `APPL`, creator **`vwe4`**, version resource
|
||||
**`2.4.0b3` / "Console 2.4.0b3", © 1995–1997 FASA Interactive, Inc.**
|
||||
(The "4.10" in the folder name is the site/config release, not the app's own
|
||||
`vers`.)
|
||||
- **Analysis method:** custom Python PEF/PPC + resource-fork tooling (no Ghidra
|
||||
on this machine); capstone in PPC big-endian mode. See `410console/tools/`.
|
||||
- **Status:** read-only analysis. Nothing in the target was modified.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR — what the console does
|
||||
|
||||
The console is a **document-based Mac app** (Metrowerks **PowerPlant** framework,
|
||||
CodeWarrior C++). Each open document is a **Mission**. The operator picks an
|
||||
*Adventure* (Solo/Team × BattleTech/Red Planet), a *Location* (arena) and a
|
||||
*Scenario*, assigns *pilots/vehicles*, then drives a fleet of **cockpit pods**
|
||||
through a mission lifecycle. It talks to two networks:
|
||||
|
||||
1. **Pod command/control** — one **MacTCP TCP** stream per pod on **port 1501**
|
||||
(console `200.0.0.1`, pods `200.0.0.11`–`200.0.0.120`). This is the protocol
|
||||
that matters for the emulator. Commands (Load / Translocate / Stop / End /
|
||||
Reset) go out as small binary packets; pods stream back a one-byte
|
||||
**Cockpit State** (0–12) that the console records in `Console Log`.
|
||||
2. **4th Dimension (4D) database** — a second client link to a 4D server for the
|
||||
**pilot roster and mission scheduling** ("Get Next Departure", Add/Delete/
|
||||
Lookup Pilot, scoring). Evidenced by the `_4D_Select4DServer` /
|
||||
`_4D_Find4DServer` error strings; the wire protocol for 4D was **not**
|
||||
reverse-engineered (it is ACI's proprietary 4D client library).
|
||||
|
||||
The rest of this document backs every claim above with offsets, string refs,
|
||||
resource dumps and log excerpts.
|
||||
|
||||
---
|
||||
|
||||
## 1. File inventory & formats
|
||||
|
||||
| File | Size | Format / role |
|
||||
|---|---|---|
|
||||
| `Console 4.10` | 728 KB | The application. Fat binary: PEF/PPC in data fork, 68K `CODE` + all UI resources in the resource fork (see §5). |
|
||||
| `Console Log` | 5.3 MB | Runtime log, CR-terminated records. Ground truth for the pod protocol (§7). |
|
||||
| `Console.ini` | 92 KB | Live config: 586 `::`-namespaced sections — pods, adventures, locations, scenarios, vehicles (§8). |
|
||||
| `Console.ini copy` | 91 KB | Prior copy of the above. |
|
||||
| `Stationary.ini` | 98 KB | The **stationery/template** master config (Mac "stationery" = template document) used to seed a fresh `Console.ini`. |
|
||||
| `ini Folder/DB<City>/` | — | Per-venue config overrides: Atlanta, Chicago, Cincinnati, Denver, Detroit, Houston, Miami, Ontario, Pittsburgh, San Jose, Toronto, LaZerPark. |
|
||||
| `ConsoleSorter/` + `Console Log Sorter`, `Console Log.rpl` | 0 B here | A companion log-sorter tool (data forks empty in this extraction; only `Instructions` survived). |
|
||||
| `FRC.rsrc`, `KR.rsrc`, `US.rsrc` | 225 B each | Tiny locale resource stubs (French / Korean / US). |
|
||||
| `Fonts/` (`Killmark`, `Stencil`) | 0 B | Bitmap/suitcase fonts for kill-marker/stencil display (data forks empty here). |
|
||||
| `PlainText16 Folder/` | 173 KB | `PlainText` support (text traits). |
|
||||
| `.finf/` | 32 B each | Mac Finder-info sidecars from extraction (type/creator). |
|
||||
| `__MACOSX/._*` | — | **AppleDouble sidecars — these carry the resource forks.** `._Console 4.10` is 3.38 MB and holds the app's entire resource fork (§5). |
|
||||
|
||||
**Resource-fork location:** the app's resource fork was preserved inside
|
||||
`__MACOSX/._Console 4.10` (AppleDouble magic `0x00051607`, entry id 2 =
|
||||
resource fork at file offset `0x52`, length `0x3391CA`). Finder info at offset
|
||||
`0x32` reads `APPLvwe4` → **type `APPL`, creator `vwe4`**.
|
||||
|
||||
---
|
||||
|
||||
## 2. PEF container structure
|
||||
|
||||
First 16 bytes: `4A6F7921 70656666 70777063 00000001` = `Joy! peff pwpc` v1
|
||||
(classic Mac OS CFM PowerPC container, big-endian).
|
||||
|
||||
**Container header (40 bytes):**
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| tag1 / tag2 / arch | `Joy!` / `peff` / `pwpc` |
|
||||
| formatVersion | 1 |
|
||||
| dateTimeStamp | `0xAF4A5504` |
|
||||
| currentVersion | 0 |
|
||||
| sectionCount | 3 |
|
||||
| instSectionCount | 2 |
|
||||
|
||||
**Section table** (each header 28 bytes; bodies follow the name table):
|
||||
|
||||
| # | Kind | default | total | unpacked | packed | file off | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 0 | Code (0) | 0 | 625,536 | 625,536 | 625,536 | `0x003840` | PPC text (`0x98B80` bytes). |
|
||||
| 1 | PatternInitData (2) | 0 | 127,204 | 122,866 | 88,128 | `0x09C3C0` | RLE-packed data/TOC/strings; unpacks to `0x1DFF2`. |
|
||||
| 2 | Loader (4) | 0 | 0 | 0 | 14,272 | `0x000080` | Imports/exports/relocs/entry. |
|
||||
|
||||
**Entry points (from the Loader header):**
|
||||
|
||||
- **main** = data-section transition vector at offset `0x5154` → `[code 0x00082E5C, TOC 0x00008000]`.
|
||||
So `main` executes at **code `0x82E5C`** and the runtime **TOC/`r2` = data offset `0x8000`**.
|
||||
(This TOC value is the key that lets us resolve all `r2`-relative constant loads — see §6.)
|
||||
- **init** = none (`-1`), **term** = none (`-1`).
|
||||
- **exported symbols = 0** (nothing is exported; this is a leaf application, not a shared library).
|
||||
- imported libraries = 3, imported symbols = 448, reloc sections = 1.
|
||||
|
||||
Tool: `py -3.13 tools/pefparse.py "Console 4.10" header|loader|strings|dump|dis`.
|
||||
|
||||
---
|
||||
|
||||
## 3. CFM imports (448 symbols across 3 libraries) — interpreted
|
||||
|
||||
Only three shared libraries are imported. **Notably absent: OpenTransport,
|
||||
AppleTalk, and any TCP shared library** — so the console does *not* use Open
|
||||
Transport. Its IP networking is done the older way, through the **MacTCP `.IPP`
|
||||
driver via the Device Manager** (see the `PBControlAsync/Sync` + `PBOpenSync`
|
||||
imports and the `CTCPDriver`/`.IPP` strings in §6).
|
||||
|
||||
### MathLib (6)
|
||||
`num2dec, dec2num, str2dec, dec2str` (SANE-style decimal ↔ string) and
|
||||
`fetestexcept, feclearexcept` (FP exception flags). Used for numeric formatting
|
||||
of scores/stats.
|
||||
|
||||
### InterfaceLib (436) — the Mac Toolbox surface
|
||||
Grouped by subsystem (representative, not exhaustive):
|
||||
|
||||
- **QuickDraw / GWorld / PICT:** `InitGraf, OpenCPort, NewGWorld*(via UGWorld),
|
||||
LockPixels, CopyBits(implied), OpenCPicture, DrawPicture(implied), PlotCIcon,
|
||||
PlotIconID, RGBBackColor, FrameRoundRect, InvertRgn, ClipRect …` — draws the
|
||||
console windows, pod photos (PICT), mech/vehicle art, kill markers.
|
||||
- **Window Manager:** `NewCWindow, GetNewCWindow, ZoomWindow, SelectWindow,
|
||||
HiliteWindow, FindWindow, BeginUpdate/EndUpdate, DragWindow(implied) …`
|
||||
- **Menu Manager:** `NewMenu, InsertMenu(implied), DeleteMenu, DrawMenuBar,
|
||||
MenuKey, HiliteMenu, EnableItem, CountMItems, GetItemMark …`
|
||||
- **Control Manager:** `NewControl, TrackControl, FindControl, Draw1Control,
|
||||
SetControlValue/Min/Max, HiliteControl, SetControlAction …` (scrollers,
|
||||
buttons, the toolbar).
|
||||
- **List / Dialog / TextEdit:** `LGetSelect, LSetSelect, LSetCell, LGetCell`
|
||||
(List Manager — player/network tables), `CautionAlert, ParamText`,
|
||||
`TEInsert, TEActivate, TEGetHeight, TECut, TEUpdate` (mission notes / fields).
|
||||
- **File Manager:** `FSpOpenDF, FSpCreate, FSpDelete, FSRead, FSWrite, FSClose,
|
||||
PBOpenSync, PBReadSync, PBWriteSync, PBHCreateSync, PBGetEOFSync,
|
||||
PBSetEOFSync, FSpCreateResFile, FSpOpenResFile, HOpenResFile …` — reads
|
||||
`Console.ini`, writes `Console Log`, loads/saves mission **egg** files.
|
||||
- **Device Manager (→ MacTCP):** **`PBOpenSync`, `PBControlAsync`,
|
||||
`PBControlSync`** — open the `.IPP` driver and issue TCP control calls
|
||||
(TCPCreate/ActiveOpen/Send/Rcv/Close) — the pod network layer.
|
||||
- **Printing Manager:** `PrOpen, PrOpenDoc, PrOpenPage, PrCloseDoc, PrClose` —
|
||||
prints score sheets / mission highlights.
|
||||
- **Apple Event Manager:** `AEInstallEventHandler, AEProcessAppleEvent,
|
||||
AECreateAppleEvent, AESend, AEGetParamDesc, AEGetNthDesc, AEPutParamDesc,
|
||||
AECreateList, AEGetAttributePtr, AEResolve(+ObjectSupportLib) …` (see §9).
|
||||
- **Misc:** `Random, BlockMove, GetScrap, SysEnvirons, ExitToShell,
|
||||
NewRoutineDescriptor/NewFatRoutineDescriptor` (68K↔PPC mixed-mode glue).
|
||||
|
||||
### ObjectSupportLib (6) — Apple Event object model
|
||||
`AEResolve, AESetObjectCallbacks, AEInstallObjectAccessor, CreateObjSpecifier,
|
||||
AEObjectInit, CreateOffsetDescriptor` — the app is an AppleScript object-model
|
||||
server over its window/document objects (standard PowerPlant `UAppleEventsMgr`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Code organization (from the 27 named `CODE` segments)
|
||||
|
||||
Because this is a fat binary, the resource fork still carries the CodeWarrior
|
||||
**68K segment names**, which are a clean module map of the whole program
|
||||
(the PPC side is the same code, differently packaged):
|
||||
|
||||
```
|
||||
1 Main 10 Utilities 19 RP Panes
|
||||
2 Commanders 11 Libraries 20 Data Objects
|
||||
3 Connection 12 Debug 21 BT Data Objects
|
||||
4 Panes 13 TCP/IP 22 RP Data Objects
|
||||
5 Table 14 MUNGA 23 Printing
|
||||
6 Files 15 Console Menus 24 RP Printing
|
||||
7 Apple Events 16 Console Windows 25 BT Printing
|
||||
8 Array 17 Console Panes 26 Console Features
|
||||
9 Features 18 BT Panes
|
||||
```
|
||||
|
||||
- **Connection / TCP/IP** = the pod networking (`CTCPDriver`, `CTCPStream`,
|
||||
`CTCPEndpoint`, `CTCPAsyncCall`, `CTCPResolverCall`, `NetworkEndpoint`).
|
||||
- **MUNGA** = data marshalling (the mission/egg (de)serializer — "munge").
|
||||
- **BT / RP** split = per-game code (BattleTech vs Red Planet) for panes, data
|
||||
objects, and printing.
|
||||
|
||||
`.cp` source-module names recovered from the binary (the CodeWarrior debug
|
||||
string table) include, among ~140 files:
|
||||
`ConsoleApp, ConsoleDoc, ConsoleWindow, PrefsDoc, NetworkEndpoint,
|
||||
NetworkEndpointList, NetworkTable, NetworkWindow, CTCPDriver, CTCPStream,
|
||||
CTCPEndpoint, CTCPAsyncCall, CTCPResolverCall, MissionObject, MissionEvent,
|
||||
MissionEventQueue, MissionView, MissionGraphView, MissionGraphWindow,
|
||||
MissionClockView, Participant, TeamList, TeamPrtcpntList, PlayerTable,
|
||||
BTScenario, BTVehicle, BTLocation, RPScenario, RPVehicle, RPLocation, Vehicle,
|
||||
Weather, Experience, Adventure, ScoreSheetView, DamageMatrix, BTKillMarker` and
|
||||
the PowerPlant framework classes (`LApplication, LDocument, LWindow, LView,
|
||||
LPane, LTableView, LCommander, LListener/LBroadcaster(implied), UAppleEventsMgr,
|
||||
UGWorld, UMemoryMgr, UReanimator, UModalDialogs`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Resource fork (550 resources, 39 types)
|
||||
|
||||
Parsed from `__MACOSX/._Console 4.10` with `tools/rsrcparse.py`. Highlights:
|
||||
|
||||
- **`cfrg` (1)** — the Code Fragment resource that points the CFM loader at the
|
||||
PEF in the data fork (confirms the fat-binary layout).
|
||||
- **`vers` (2)** — `2.4.0b3`, "Console 2.4.0b3", © 1995–1997 **FASA Interactive, Inc.**
|
||||
- **`SIZE` / `BNDL` / `FREF` / `ICN#`…** — standard app packaging; `icm#/ics#`
|
||||
application icons.
|
||||
- **`ºMWC` (32000)** — Metrowerks CodeWarrior project stamp (confirms toolchain).
|
||||
|
||||
### Operator commands & UI
|
||||
- **Toolbar buttons** (`ICN#`/`icl4`/`icl8`, ids 1000–1011):
|
||||
**Get Next Departure, Translocate, Load Mission, Add Pilot, Delete Pilot, Lookup.**
|
||||
- **`MENU`/`Mcmd` (10)**: Apple, File (New / Close / Save / Save a Copy… / Page
|
||||
Setup… / Print / Quit), Edit, **Configuration**, **Adventure**, **BattleTech**,
|
||||
**Red Planet** + scenario menus (`BTScenario`, `RPScenario`).
|
||||
- **`WIND`/`PPob` (windows)**: **`RPMR ConsoleWindow`, `RPMF ConsoleWindow`,
|
||||
`BTFA ConsoleWindow`, `BTSB ConsoleWindow`** (the four game-mode console
|
||||
layouts), **Mission Graph**, **Toolbar**, **Network Diagnostic**, **Lookup**.
|
||||
- **`ALRT`/`DITL`**: About Box, Save-before-closing/quitting, Confirm Revert,
|
||||
Low Memory Warning, **TCP Warning**, Landscape/Portrait mode.
|
||||
|
||||
### Data/game resources
|
||||
- **`Mech`/`MWpn`/`MDgZ` (×24/×24/×22)** — BattleTech mechs + weapons + damage
|
||||
zones: *BlackHawk, Loki, MadCat, Owens, Thor, Vulture, Avatar, Sunder,
|
||||
TimberWolf, Satyr, Hellbringer, Summoner, …*
|
||||
- **`PICT` (×98)** — pod photos (*Carpe Diem, Calamity Jane, Man O' War,…*) and
|
||||
faction crests (*Davion, Kurita, Steiner, Liao, Marik, Comstar, VGL*). These
|
||||
match `pictID=` in `Console.ini`.
|
||||
- **`Txtr` (×57)** — named text traits (fonts/styles for each field, plasma
|
||||
screen, mission clock).
|
||||
|
||||
### String lists (`STR#`) — the semantic vocabulary
|
||||
61 `STR#` resources. The protocol-relevant ones:
|
||||
|
||||
- **`STR# 131 "Mission State"`** — the console-side (document) mission state
|
||||
descriptions, index 0..6:
|
||||
1. `Mission data is incomplete.`
|
||||
2. `The mission is ready to load.`
|
||||
3. `The mission is being loaded.`
|
||||
4. `The mission is ready for translocation.`
|
||||
5. `The mission is being translocated.`
|
||||
6. `The mission is in progress.`
|
||||
7. `The mission has been completed.`
|
||||
- **`STR# 130 "Network Endpoints"`** — the 50-name roster: cockpits (*Frequent
|
||||
Flyer, Privateer, Man O' War, Divine Wind, Carpe Diem, Gypsy, Puck, Icarus,
|
||||
Calamity Jane, Valkyrie, Pegasus,…*), cameras (*Alpha/Beta/Gamma/Delta/Epsilon
|
||||
Camera*), *Mission Review* stations, and PC/test targets (*BrownCartPC,
|
||||
GaugeCartPC, ZabswareTarget*). Index 0 = `Bogon Alert!` (invalid),
|
||||
`<Available>` = a free slot.
|
||||
- **`STR# 1002/1003 "Network Errors"`** — explicitly a **4D database** client:
|
||||
`_4D_Select4DServer`, `_4D_Find4DServer`, "connection to the database",
|
||||
"asynchronous mode query already in progress", "invalid connection handle".
|
||||
- **`STR# 1001 Database Errors`, `1004 Database Tasks`, `1005 General Errors`,
|
||||
`128 General Strings`** (`"%s Mission %s"`, `"Mission Time & Date: %s, %s"`,
|
||||
Show/Hide Toolbar, Show/Hide Network), **`300 Adventure Strings`**
|
||||
(`BattleTech`, `Red Planet`), plus large game tables (scenarios, drop zones,
|
||||
roles, kill/death/damage strings, textures, football positions, …).
|
||||
- **`eTbl` (3)** — error tables named *Database Errors*, *Network Errors*,
|
||||
*4D Open Errors* (again confirming the 4D backend).
|
||||
|
||||
---
|
||||
|
||||
## 6. Pod command/control protocol (MacTCP, port 1501)
|
||||
|
||||
This is the highest-value result. Evidence chain: binary strings →
|
||||
disassembly (TOC-resolved) → `Console Log` → `Console.ini`.
|
||||
|
||||
### 6.1 Transport
|
||||
- Custom C++ classes **`CTCPDriver` / `CTCPStream` / `CTCPEndpoint` /
|
||||
`CTCPAsyncCall` / `CTCPResolverCall`** wrap **MacTCP**. The literal driver
|
||||
name **`.IPP`** appears at data `0x1053F` right next to `CTCPDriver` — this is
|
||||
the classic MacTCP IP driver opened with `PBOpenSync`, then driven with
|
||||
`PBControlAsync/Sync`.
|
||||
- One TCP connection per pod. From `Console.ini` each
|
||||
`[NetworkEndpoint::Cockpit::*]` sets `classID=Ptcp`, `defaultPort=1501`,
|
||||
`localHostPort=1501`, and the pod's `addressIP` (`200.0.0.11`…`200.0.0.117`,
|
||||
etc.). Console host = `200.0.0.1`; pods `200.0.0.11`–`200.0.0.120` (from the
|
||||
log). The many `Ptcp` sub-keys (`openTimeout, listenTimeout, delay,
|
||||
throughput, reliability, precedence, fragment, timeToLive, security,
|
||||
queryTimeout`) are **MacTCP TCP/IP ULP parameters** passed to TCPActiveOpen /
|
||||
the IP header — further proof this is MacTCP, not OT.
|
||||
|
||||
### 6.2 Receive path — pod → console "Cockpit State"
|
||||
Message dispatch lives at **code `0x63B3C`** (inside the participant's
|
||||
message handler). The received message's low byte is the type:
|
||||
|
||||
```
|
||||
0x63B3C clrlwi r0, r31, 0x18 ; r0 = msg & 0xFF (message type)
|
||||
0x63B40 cmplwi r0, 0xC ; 0..12 valid
|
||||
0x63B44 bgt 0x63BEC ; >12 → default → "Unknown message type!!!"
|
||||
0x63B48 lwz r3, -0x6CA4(r2) ; TOC[data 0x135C] = jump table @ data 0x17B1C
|
||||
0x63B4C slwi r0, r0, 2
|
||||
0x63B50 lwzx r3, r3, r0 ; handler = jumptable[type]
|
||||
0x63B54 mtctr r3 ; bctr
|
||||
```
|
||||
|
||||
Every case calls **`SetCockpitState(participant+0x30C, type)` at `0x62720`**:
|
||||
|
||||
```
|
||||
0x62720 SetCockpitState:
|
||||
stb r4, 0x289(r6) ; participant->cockpitState = type (field at +0x289)
|
||||
li r4, 0xBBC ; bl BroadcastMessage ; notify the UI panes/graph
|
||||
```
|
||||
(`0x62778` is the matching getter: `lbz r3, 0x289(r3)`.) The console then logs
|
||||
`"<IP> Cockpit State: <type>"`. So **the pod's message-type byte is literally
|
||||
the Cockpit State number, 0–12**; type > 12 logs `"Unknown message type!!!"`
|
||||
(seen twice in the 5.3 MB log). The jump table at data `0x17B1C` (13 code
|
||||
pointers, default `0x63BEC`) is the physical proof of the 0–12 range.
|
||||
|
||||
### 6.3 Send path — console → pod commands
|
||||
Commands are transmitted by **`NetworkEndpoint::Send` at `0x629EC`**, called as
|
||||
`Send(endpoint, &msgHeaderWord, payloadObj, payloadLen)`. It frames a packet =
|
||||
**16-byte header + payload**:
|
||||
|
||||
```
|
||||
buf = alloc(len + 0x10)
|
||||
memcpy(buf+0x10, payloadObj, len) ; body
|
||||
buf[0..3] = *msgHeaderWord ; message-class word (observed 0x04000000)
|
||||
buf[4..7] = 0
|
||||
buf[8..11] = value from 0x3CB38 ; sequence / timestamp
|
||||
CTCPStream::Write(endpoint, buf, len+0x10) ; via 0x39748 → 0x3BD28
|
||||
```
|
||||
|
||||
The **payload object** is a small serialized struct whose **byte[0] = its size**
|
||||
and **byte[4] = the command code**. Two confirmed by disassembling the object
|
||||
constructors:
|
||||
|
||||
| Command | Ctor | payload size | **command code (byte[4])** | Verb logged | Sender |
|
||||
|---|---|---|---|---|---|
|
||||
| Translocate mission | `0x3EC74` | `0x0C` | **5** | `Translocating Mission…` | `0x67438` |
|
||||
| Stop mission | `0x3ECC8` | `0x10` | **6** | `Stopping Mission…` | `0x6755C` |
|
||||
|
||||
Other senders (same shape, codes not yet pinned): **Loading Mission**
|
||||
(`0x64xxx`/`Received egg file` `0x648C0`), **Ending Mission** `0x66464` (uses a
|
||||
variant send `0x5E474` that packs the destination IP from a 4-byte struct field
|
||||
`+0x10..0x13`), **Reset Mission** `0x6769C`. Command codes for these are
|
||||
recoverable with the same method (find the `li r0,N; stb r0,-0x10(r1)` that
|
||||
seeds object byte[4]) — see Open Questions.
|
||||
|
||||
### 6.4 Internal PowerPlant command/broadcast IDs
|
||||
The app's own command constants cluster in **`0xBBA`–`0xBDF`** (decimal
|
||||
3002–3039). Confirmed meanings:
|
||||
`0xBBC` = cockpit-state-changed broadcast, `0xBC5` = "received egg file",
|
||||
`0xBCB` = ending mission, `0xBBA/0xBBB` = handled in the participant
|
||||
`ListenToMessage` at `0x628D8`. (`0xBC1` is the most frequent; others 0xBCE–
|
||||
0xBDF map to menu/toolbar commands.)
|
||||
|
||||
### 6.5 Observed mission lifecycle (from the log)
|
||||
A full run for one pod, with the two state machines interleaved
|
||||
(**Cockpit State N** = pod→console; **verbs** = console actions):
|
||||
|
||||
```
|
||||
Cockpit State 2 (present/connected)
|
||||
Cockpit State 1 (ready)
|
||||
Loading Mission… → console loads the egg
|
||||
Cockpit State 6
|
||||
Cockpit State 7
|
||||
Received egg file → egg in hand; "Ready to translocate"
|
||||
Cockpit State 8
|
||||
Translocating Mission… → console sends egg/mission to pod (cmd code 5)
|
||||
Cockpit State 9
|
||||
Cockpit State 10 (in progress / running)
|
||||
Stopping Mission… (cmd code 6)
|
||||
Cockpit State 11
|
||||
Ending Mission…
|
||||
Cockpit State 12
|
||||
Cockpit at <IP> initiated close!!! → pod closed its TCP stream
|
||||
Cockpit State 3 (closed/disconnected)
|
||||
Reset Mission… → console rearms for the next departure
|
||||
Cockpit State 2 …
|
||||
```
|
||||
|
||||
Cockpit-state frequencies in the sample log: `1:3997 2:13258 3:12809
|
||||
6:3098 7:3093 8:3262 9:2852 10:2852 11:2990 12:2890` (states 0, 4, 5
|
||||
never appear in this log though the dispatch table has slots for them — likely
|
||||
transient/internal). Verb counts: `initiated close 24702, Loading 3129,
|
||||
Received egg 3124, Stopping 2996, Ending 2905, Translocating 2882, Reset 953`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Console Log format
|
||||
|
||||
- **Encoding:** Mac Roman. **Records are CR-terminated** (`0x0D`; classic Mac,
|
||||
**no LF** — 98,910 CRs for ~91,607 records in the sample). `0xC9` = "…"
|
||||
ellipsis inside the mission verbs (13,415 occurrences).
|
||||
- **Record grammar:**
|
||||
`M/D/YYYY` `HH:MM:SS`␠`AM|PM`␠`<message>` `CR`
|
||||
where `<message>` is usually `<pod-IP> <text>` (state/verb lines) or the
|
||||
IP-embedded form `Cockpit at <pod-IP> initiated close!!!`.
|
||||
- The timestamps in this capture read year **1956** — the logging Mac's clock
|
||||
was unset (default RTC), not a real date. Times of day are valid.
|
||||
- **72 distinct message templates**; the load-bearing ones:
|
||||
`"<IP> Cockpit State: <N>"`, `"Cockpit at <IP> initiated close!!!"`,
|
||||
`"<IP> Loading|Translocating|Stopping|Ending|Reset Mission…"`,
|
||||
`"<IP> Received egg file"`, `"<IP> Unknown message type!!!"`.
|
||||
- **Log writer:** each line is assembled by a string-append helper (`0x85324`,
|
||||
repeatedly) and flushed by `0x851C4`; the leading date/time comes from the
|
||||
global at TOC `-0x77CC(r2)` (data `0x834`). Field separator string is at data
|
||||
`0x17B8`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Console.ini schema
|
||||
|
||||
INI-style but with **no newlines** — sections are `[Name]` headers and
|
||||
`key=value` pairs run together; parse by splitting on `[`. Section names use a
|
||||
`::` **hierarchical namespace**. 586 sections total in the live file.
|
||||
|
||||
**Section families (by count):** `RP` 342, `BT` 196, `rpl4` 34,
|
||||
`NetworkEndpoint` 10, `Adventure` 3, `NetworkEndpointList` 1.
|
||||
|
||||
**Every object carries a 4-char `classID`** (an OSType tag naming its C++
|
||||
class). Confirmed: `Ptcp` (TCP endpoint), `Advn` (adventure), `RPLn` (Red
|
||||
Planet location); BT/RP scenario & vehicle classes follow the same pattern.
|
||||
|
||||
### 8.1 Network endpoints (the pods)
|
||||
```
|
||||
[NetworkEndpointList]
|
||||
networkEndpoint = NetworkEndpoint::Cockpit::Frequent Flyer
|
||||
networkEndpoint = NetworkEndpoint::Cockpit::Privateer
|
||||
… (10 active endpoints)
|
||||
|
||||
[NetworkEndpoint::Cockpit::Frequent Flyer]
|
||||
classID=Ptcp hostType=0 state=0 addressUse=0
|
||||
addressIP=200.0.0.11
|
||||
defaultPort=1501 localHostPort=1501 openInterval=40 openTimeout=10
|
||||
listenTimeout=0
|
||||
resourceID=130 resourceIndex=2 pictID=308 ; STR# 130 name + PICT photo
|
||||
; MacTCP TCP/IP ULP parameters:
|
||||
delay=1 throughput=1 reliability=1 precedence=0 fragment=0
|
||||
timeToLive=60 security=0 queryTimeout=60
|
||||
loadQueryInterval=1 inProgressQueryInterval=10
|
||||
```
|
||||
`resourceID=130` + `resourceIndex` selects the pod's display name from
|
||||
`STR# 130`; `pictID` selects its photo PICT.
|
||||
|
||||
### 8.2 Adventure → Location → Scenario tree (populates the menus)
|
||||
```
|
||||
[Adventure::Solo::Red Planet] classID=Advn adventureType=2 resourceID=300 resourceIndex=2 list=RP::Solo::LocationList
|
||||
[RP::Solo::LocationList] location=rpl4::Solo::Location::Yip's Yahoorama …
|
||||
[rpl4::Solo::Location::Wiseguy's Wake] classID=RPLn resourceID=4000 resourceIndex=3 tag=wise pictID=601 list=RP::Wiseguy's Wake::Solo::ScenarioList
|
||||
[BT::Cavern::ScenarioList] scenario=BT::Cavern::NoReturn scenario=BT::Cavern::FreeForAll
|
||||
[BT::Solo::LocationList] location=BT::Solo::Ravines / Arena1 / Polar4 / Cavern …
|
||||
```
|
||||
Four top-level adventures exist in the binary:
|
||||
`Adventure::{Solo,Team}::{BattleTech,Red Planet}` — matching the four console
|
||||
windows `BTFA/BTSB/RPMF/RPMR ConsoleWindow`.
|
||||
|
||||
### 8.3 Key inventory
|
||||
`classID, resourceID, resourceIndex, pictID, tag, list, scenario, location,
|
||||
default, value, max, time` (structural) and the game-object keys
|
||||
`vehicle/vehicel(sic)/vehicleClass, mechID, camo, color, texture, emblem,
|
||||
killMarker, experience, role, position, dropZone, weather, temperature,
|
||||
adventureType, language, scriptCode` plus the full `Ptcp` network key set.
|
||||
|
||||
`Stationary.ini` is the same schema used as a **template** to regenerate a
|
||||
site's `Console.ini`; the per-city `ini Folder/DB<City>` files are venue
|
||||
overrides.
|
||||
|
||||
---
|
||||
|
||||
## 9. Apple Events
|
||||
|
||||
The app installs standard AE handlers (`AEInstallEventHandler`) and is a full
|
||||
AppleScript **object-model server** (`ObjectSupportLib` + `UAppleEventsMgr.cp`).
|
||||
Its `aete` (id 0, "English") declares only the **standard suites** — Required
|
||||
(`reqd`: run/open/print/quit), Standard/Core (`CoRe`: close/get/set/make/save/
|
||||
data size), and Miscellaneous Standards (`misc`: revert/select) — over the
|
||||
generic `application`/`window`/`document` classes. `aedt` (3) = *Required Suite,
|
||||
Core Suite, Misc Standards*.
|
||||
|
||||
**Conclusion:** Apple Events are used for **OS/Finder integration**
|
||||
(open-documents = open mission/ini files, print score sheets, quit), **not** for
|
||||
commanding pods. Pod command is exclusively the MacTCP protocol in §6. (No
|
||||
custom application scripting suite is defined.)
|
||||
|
||||
---
|
||||
|
||||
## 10. Tooling written (`410console/tools/`)
|
||||
|
||||
Reusable, dependency-light (Python 3.13 + capstone):
|
||||
|
||||
- **`pefparse.py`** — PEF container parser + PIDATA (RLE) unpacker + loader
|
||||
(imports/exports/entry) + string extractor + capstone PPC disassembler.
|
||||
`header | loader | strings [minlen] | dump <sec> <off> <len> | dis <sec> <off> <count>`.
|
||||
- **`ppcxref.py`** — TOC-aware annotator (`r2` = data `0x8000`): resolves
|
||||
`r2`-relative constant loads to data addresses/strings and builds a
|
||||
string→code cross-reference. `annotate <start> <count> | xref <substr> | toc <off>`.
|
||||
- **`rsrcparse.py`** — classic Mac resource-fork parser (reads the AppleDouble
|
||||
`._` sidecar). `map | names <TYPE> | strn [id] | dump <TYPE> [id]`.
|
||||
|
||||
Example: `py -3.13 tools/rsrcparse.py "4_10-console-extracted/__MACOSX/._Console 4.10" strn 131`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Confidence & open questions
|
||||
|
||||
**High confidence (multiple corroborating sources):**
|
||||
- Fat PEF/PPC app, PowerPlant/CodeWarrior, FASA Interactive, creator `vwe4`,
|
||||
vers 2.4.0b3. *(PEF header + resource fork + strings.)*
|
||||
- MacTCP `.IPP`, port 1501, per-pod TCP, addressing scheme. *(imports +
|
||||
`CTCPDriver`/`.IPP` string + ini + log.)*
|
||||
- Cockpit State machine 0–12, pod→console, dispatch at `0x63B3C`, stored at
|
||||
Participant `+0x289`, logged verbatim. *(disasm + jump table + log stats.)*
|
||||
- Send framing = 16-byte header + payload; command payload byte[0]=size,
|
||||
byte[4]=code; Translocate=5, Stop=6. *(disasm of `0x629EC` + two ctors.)*
|
||||
- Mission lifecycle ordering. *(log, thousands of runs.)*
|
||||
- ini schema and 4D-database backend. *(ini + `STR#`/`eTbl` + error strings.)*
|
||||
|
||||
**Open questions / next steps:**
|
||||
1. **Finish the command-code table.** Pin byte[4] for Load, End, Reset,
|
||||
Get-Next-Departure (method: disassemble each sender's object ctor near
|
||||
`0x3EBxx–0x3EFxx` for `li r0,N; stb r0,-0x10(r1)`). Also decode the
|
||||
header word-2 (`0x3CB38`) — sequence counter vs. timestamp.
|
||||
2. **Header semantics.** Confirm whether the pod reads packet byte[0] (class=4)
|
||||
or a payload byte as its "message type", and map the console's **command**
|
||||
codes to the resulting **Cockpit State** transitions (5→9, 6→11 observed).
|
||||
3. **Egg transfer mechanism.** Determine whether the `.egg` mission bundle
|
||||
(note the `Egg.ini` string at data `0x98E9`, and the **MUNGA** segment)
|
||||
travels to the pod over the same 1501 stream during "Translocate", or via a
|
||||
separate transfer. `Received egg file` is logged **console-side** — clarify
|
||||
direction (console reads egg from disk vs. receives from DB/pod).
|
||||
4. **States 0/4/5.** Never seen in this log; identify their handlers
|
||||
(`jumptable[0]=0x63BEC` default, `[4]/[5]=0x63B78`) and meaning.
|
||||
5. **4D protocol.** Out of scope here (proprietary ACI 4D client); the console↔
|
||||
4D link governs scheduling ("Get Next Departure") and the pilot roster.
|
||||
6. **Relocations.** `pefparse` does not yet apply the Loader relocation opcode
|
||||
stream (`reloc_instr_offset`); constant addresses were resolved via the TOC
|
||||
value instead, which was sufficient. A full reloc pass would make every data
|
||||
pointer absolute for deeper xref.
|
||||
|
||||
---
|
||||
|
||||
### Appendix A — key addresses (code section, base 0)
|
||||
| Addr | Role |
|
||||
|---|---|
|
||||
| `0x82E5C` | program `main` (via TVector @ data `0x5154`) |
|
||||
| `0x63B3C` | received-message dispatch (msg&0xFF → jumptable @ data `0x17B1C`) |
|
||||
| `0x62720` | `SetCockpitState(participant, state)` → Participant `+0x289`, broadcast `0xBBC` |
|
||||
| `0x62778` | `GetCockpitState` (`lbz +0x289`) |
|
||||
| `0x629EC` | `NetworkEndpoint::Send` (16-byte header + payload) |
|
||||
| `0x39748`→`0x3BD28` | `CTCPStream::Write` |
|
||||
| `0x3EC74` / `0x3ECC8` | Translocate (code 5) / Stop (code 6) command-object ctors |
|
||||
| `0x66464` / `0x67438` / `0x6755C` / `0x6769C` / `0x648C0` | senders: Ending / Translocating / Stopping / Reset / Received-egg |
|
||||
| `0x85324` / `0x851C4` | log-line append / flush |
|
||||
| data `0x8000` | TOC base (`r2`) |
|
||||
| data `0x17B1C` | 13-entry cockpit-message jump table |
|
||||
|
||||
### Appendix B — reproduce
|
||||
```
|
||||
cd c:\VWE\TeslaRel410\410console
|
||||
py -3.13 tools\pefparse.py "4_10-console-extracted\Console 4.10" loader
|
||||
py -3.13 tools\ppcxref.py xref "Mission"
|
||||
py -3.13 tools\ppcxref.py annotate 0x629ec 0x60
|
||||
py -3.13 tools\rsrcparse.py "4_10-console-extracted\__MACOSX\._Console 4.10" map
|
||||
py -3.13 tools\rsrcparse.py "4_10-console-extracted\__MACOSX\._Console 4.10" strn 131
|
||||
```
|
||||
@@ -0,0 +1,271 @@
|
||||
# BattleTech → TeslaConsole port spec
|
||||
|
||||
**Hand-off package for the `TeslaSuite` repo.** Everything learned in the
|
||||
`TeslaRel410` repo about the Mac **4.10 operator console** and the path to add
|
||||
**BattleTech** support to the modernized **TeslaConsole**. Implement this over in
|
||||
`TeslaSuite` (that repo's Claude has the console's own memories and can build it).
|
||||
This folder is self-contained — the `reference/` files travel with it.
|
||||
|
||||
> Sibling doc: `410console/CONSOLE-4.10-DECOMP.md` (full decompilation of the Mac
|
||||
> `Console 4.10` PowerPC binary). This spec assumes it.
|
||||
|
||||
---
|
||||
|
||||
## 0. The situation in one paragraph
|
||||
|
||||
The Mac **`Console 4.10`** (decompiled — see the sibling doc) and the Windows
|
||||
**`TeslaConsole.exe`** (4.11.x, which `TeslaSuite/Console/TeslaConsole` is a .NET
|
||||
reconstruction of) are two generations of the *same* operator console. The Mac
|
||||
one drove **both** games — BattleTech and Red Planet. **The modernized
|
||||
TeslaConsole implements only Red Planet** (`TeslaConsole.RedPlanet`), and its
|
||||
shell is even hardcoded to it (`TeslaConsoleForm`: `AppID != 0 → "Pod Not Running
|
||||
RP"`, status strings `"RP Initalizing"`…). The task: **add a
|
||||
`TeslaConsole.BattleTech` game that mirrors `TeslaConsole.RedPlanet`**, and
|
||||
de-hardcode the shell to pick the game by `ApplicationID`. The transport layer is
|
||||
game-agnostic and reused unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 1. The shared spine (reused verbatim by BattleTech — do NOT re-implement)
|
||||
|
||||
Both games and both console generations use one command/control path:
|
||||
|
||||
- **Munga over TCP 1501**, one `MungaSocket` per pod (`Munga Net.dll`, vendored in
|
||||
`Console/lib/`). Console `200.0.0.1`, pods `200.0.0.11…`.
|
||||
- `MungaGame.cs` (in `TeslaConsole`, **game-agnostic**): connect pod:1501, poll
|
||||
`StateQueryMessage`→`StateResponseMessage(ApplicationID, ApplicationState)`
|
||||
every ~1 s, and on `ApplicationState.WaitingForEgg` stream the egg as 1000-byte
|
||||
`EggFileMessage[]` chunks, await `AcknowledgeEggFileMessage`, then run.
|
||||
- **`ApplicationState`** (the named "cockpit states"): `CreatingMission,
|
||||
LaunchingMission, LoadingMission, ResumingMission, RunningMission,
|
||||
SuspendingMission, WaitingForLaunch, WaitingForEgg, …`. In the Mac 4.10 console
|
||||
these were the **numeric Cockpit States 0–12** (decomp §6.2).
|
||||
- **`HostType`** (`TeslaConsole/HostType.cs`): `GameMachineHostType=0,
|
||||
MissionReviewHostType=2` (camera/spectator), `ConsoleHostType=3`.
|
||||
- Reused unchanged by BT: `MungaGame`, `Pod`, `PodManager`, `Site`, `SitePanel`,
|
||||
`PlasmaBitmaps`, `PlasmaFontTool`, the ordinals blocks, and the whole
|
||||
provisioning/launch path (that's the separate RPC channel on TCP 53290; game
|
||||
control is Munga 1501).
|
||||
|
||||
**Only the egg *content* and the per-game UI/data differ between RP and BT.**
|
||||
|
||||
---
|
||||
|
||||
## 2. The BattleTech egg format (authoritative)
|
||||
|
||||
Reconstructed from the console-generated **`reference/cavern.egg`** (freeforall,
|
||||
map cavern, night; pilot `cyd` in a `madcat`, veteran) and
|
||||
**`reference/TESTARN.EGG`**. This is what `BTMission.ToEggString()` must produce.
|
||||
|
||||
### Structure (section by section)
|
||||
```
|
||||
[mission] adventure=BattleTech map=cavern scenario=freeforall time=night
|
||||
weather=clear temperature=27 length=120
|
||||
[ordinals] ← 1st–4th place plasma bitmaps, 128×32, IDENTICAL to RP
|
||||
bitmap=Ordinal::BitMap::1 … [Ordinal::BitMap::1] …32 rows… x=128 y=32 width=8
|
||||
(…::2 ::3 ::4)
|
||||
[pilots]
|
||||
pilot=200.0.0.113 ← one per player + one per camera
|
||||
[200.0.0.113] ← one participant block per pilot line
|
||||
hostType=0 advancedDamage=1 loadzones=1 name=cyd bitmapindex=1
|
||||
experience=veteran vehicle=madcat vehicleValue=0 badge=VGL
|
||||
dropzone=one color=Grey patch=Red role=Role::Default
|
||||
[largebitmap]
|
||||
bitmap=BitMap::Large::cyd
|
||||
[BitMap::Large::cyd] …32 rows… x=128 y=32 width=8 ← pilot-name plasma bitmap
|
||||
[smallbitmap]
|
||||
bitmap=BitMap::Small::cyd
|
||||
[BitMap::Small::cyd] …16 rows… x=64 y=16 width=4
|
||||
[Role::Default] model=dfltrole ← one block per referenced role
|
||||
```
|
||||
|
||||
### BattleTech vs Red Planet — the field diff
|
||||
`RPMission.ToEggString()` / `RPPlayer.AppendParticipantSpecificData()` is the
|
||||
template. BT differs as follows:
|
||||
|
||||
| Field | Red Planet | BattleTech |
|
||||
|---|---|---|
|
||||
| `[mission] adventure=` | `Red Planet` | **`BattleTech`** |
|
||||
| `[mission] temperature=` | `0` | **nonzero** (e.g. `27`) |
|
||||
| `[mission] compression=` | present (`0`/`1`) | **absent** |
|
||||
| participant `advancedDamage=` | — | **present** (`1`) advanced damage model |
|
||||
| participant `experience=` | — | **present** — pilot skill (`novice`/`veteran`/`expert`) |
|
||||
| participant `vehicleValue=` | — | **present** (`0`) |
|
||||
| participant `patch=` | — | **present** (e.g. `Red`) |
|
||||
| participant `role=` | — | **present** — `Role::<key>`, needs a `[Role::<key>]` block |
|
||||
| participant `vehicle=` | RP racer (quark/bug…) | **a mech** (madcat/…) |
|
||||
| `[Role::*]` blocks | — | **present** — `model=<rolemodel>` (`Default`→`dfltrole`; TESTARN.EGG also has `NoReturn`) |
|
||||
| `ordinals`, `largebitmap`, `smallbitmap` | present | **identical** — reuse as-is |
|
||||
|
||||
Shared participant fields (same as RP): `hostType, loadzones, name, bitmapindex,
|
||||
dropzone, color, badge`.
|
||||
|
||||
### Wire encoding (mirror RP exactly)
|
||||
`RPMission` builds the string with `\n` between every `key=value`, then
|
||||
`ToEggFileMessages()` does `Replace("\r\n","\0").Replace('\n','\0')`, ASCII-
|
||||
encodes, and chunks into 1000-byte `EggFileMessage(index, totalLen, thisLen,
|
||||
buf)`. So on the wire the egg is **NUL-delimited**. `cavern.egg` on disk is the
|
||||
same NUL-delimited blob (renders as run-together fields). **BT reuses this
|
||||
verbatim** — only `ToEggString()` content changes.
|
||||
|
||||
### Section-order caveat (don't chase a false byte-match)
|
||||
`cavern.egg` was written by the *original* console; its section order (ordinals
|
||||
early, bitmap defs inlined) differs from the modernized `RPMission.ToEggString`
|
||||
order (bitmaps listed then defined, ordinals last). The pod parses the egg
|
||||
**INI-style by section name**, so order is almost certainly non-critical. **Match
|
||||
RP's builder structure** (it's known-good against real pods) rather than
|
||||
byte-matching `cavern.egg`; use `cavern.egg` as the **field/value** reference and
|
||||
verify a generated egg on a real BT pod. (If a byte-exact golden test is wanted,
|
||||
generate from BT classes and pin *that*, like the existing DiffTests do.)
|
||||
|
||||
---
|
||||
|
||||
## 3. The RedPlanet template → BattleTech counterparts
|
||||
|
||||
Mirror `Console/TeslaConsole.RedPlanet/` into `Console/TeslaConsole.BattleTech/`.
|
||||
The `.csproj` is SDK-style (globs `**/*.cs`), so new files under a new folder are
|
||||
picked up automatically; add `Content Include` lines for the BT XML (see §5).
|
||||
|
||||
| RedPlanet file | BattleTech counterpart | Notes |
|
||||
|---|---|---|
|
||||
| `RPParticipant.cs` (abstract) | `BTParticipant` | `[podIP]` + `hostType`; abstract `AppendParticipantSpecificData` |
|
||||
| `RPPlayer.cs` (abstract) | `BTPlayer` | adds the BT participant fields (§2 diff); `HostType.GameMachine` |
|
||||
| `RPRacePlayer` / `RPFootballPlayer` | `BTPlayer` concretes per scenario | e.g. `BTFreeForAllPlayer`, `BTTeamPlayer` |
|
||||
| `RPCamera.cs` | `BTCamera` | `HostType.MissionReview`, `vehicle=camera` |
|
||||
| `RPMission.cs` (abstract) | `BTMission` | `ToEggString()` per §2; `ToEggFileMessages()` reusable as-is; emit `[Role::*]` blocks for referenced roles |
|
||||
| `RPRaceMission` / `RPFootballMission` | `BTFreeForAllMission` / `BTTeamMission` | `AppendMissionSpecificData` for scenario extras |
|
||||
| `Scenario.cs` + `FootballScenario`/`DeathRaceScenario` | `Scenario` + BT scenarios | loaded from `BTConfig.xml`; `<invalid>` filters valid maps/vehicles |
|
||||
| `RPConfig.cs` + `RedPlanet/RPConfig.xml` | `BTConfig.cs` + `BattleTech/BTConfig.xml` | the data catalog (§5) |
|
||||
| `RPDefaults.cs` / `RPDefaultsDialog.cs` | `BTDefaults` / `BTDefaultsDialog` | operator defaults UI |
|
||||
| `RPMap.cs` / `RPVehicle.cs` | `BTMap` (arena) / `BTVehicle` (mech) | key/name/image; images under `images/battletech maps|vehicles/` |
|
||||
| `RPTeam.cs` | `BTTeam` (if BT team scenario) | — |
|
||||
| `MissionEvent` + `Scored/Killed/Damaged/Boost/ScoreUpdate` | BT `MissionEvent` + `Killed/Damaged/…` | BT has **DamageMatrix** + **KillMarker** (decomp §5 string lists); RP-only `Boost`/football events drop; confirm BT's set against the pod's Munga event messages |
|
||||
| `RPMissionResults.cs` / `RPMissionRecorder.cs` / `RPPrintDocument.cs` | `BT*` | `.rpm`-style saved results + printout |
|
||||
| `RPGame.cs` (the DockContent pane, ~66 KB) | `BTGame` | the big one: mission-config UI + the run/stop state loop driving each pod's `MungaGame` |
|
||||
| `TeslaConsole/RPStrings.cs` + `RedPlanet/RPStrings.xml` | `BTStrings` | localized strings |
|
||||
|
||||
`RPGame`'s engine (worth copying closely): a per-pod `GameStateData
|
||||
{EggMessageSent, RunMessageSent, AbortMessageSent, StopMessageSent,
|
||||
PodAppearsReset}`, an `RPGameState` machine, `sStopToIdleStates` (the
|
||||
`ApplicationState`s that mean "busy, must stop first"), `mEggFileMessages`,
|
||||
`mMissionRecorder`, `mMissionPlayers` (index→player, to decode incoming events),
|
||||
`mLastMissionResults`. BT reuses this shape unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 4. Shell de-hardcoding (`TeslaConsole`)
|
||||
|
||||
The shell currently assumes RP. To support both games:
|
||||
|
||||
- **`TeslaConsoleForm.cs` ~line 139**: `if (pod.MungaGame.AppID.Value != 0) →
|
||||
"Pod Not Running RP"`. `ApplicationID 0 = Red Planet`. Generalize: map the pod's
|
||||
reported `ApplicationID` to a game (RP vs BT) and pick the right game module +
|
||||
status strings ("RP Initalizing" → per-game). **OPEN: the `ApplicationID` value
|
||||
for BattleTech** — see §6.
|
||||
- Game selection where a game pane is created (Site/SitePanel/SiteManagement — how
|
||||
`RPGame` is instantiated today): allow creating a `BTGame` for BT pods.
|
||||
- Per-game status string table instead of the inline "RP …" literals.
|
||||
|
||||
---
|
||||
|
||||
## 5. The BattleTech catalog (`BTConfig.xml`) — seed + sources
|
||||
|
||||
Mirror `reference/RPConfig.xml.template`: a root element with per-scenario blocks
|
||||
carrying scenario-specific option lists, plus top-level `<map>`/`<vehicle>`/
|
||||
`<weather>`/`<time>` catalogs and `<invalid>` exclusions. For BT:
|
||||
|
||||
```xml
|
||||
<BattleTech>
|
||||
<freeforall name="Free For All">
|
||||
<experience key="novice" name="Novice"/>
|
||||
<experience key="veteran" name="Veteran"/>
|
||||
<experience key="expert" name="Expert"/>
|
||||
<color key="Grey" name="Grey"/> …
|
||||
<badge key="VGL" name="VGL"/> …
|
||||
<patch key="Red" name="Red"/> …
|
||||
<role key="Default" model="dfltrole"/>
|
||||
<role key="NoReturn" model="…"/>
|
||||
</freeforall>
|
||||
<!-- team/other scenarios from the Console.ini BT tree -->
|
||||
<map key="cavern" name="Cavern" image="images/battletech maps/Cavern.bmp"/>
|
||||
<map key="arena1" name="…"/>
|
||||
<vehicle key="madcat" name="Mad Cat" image="images/battletech vehicles/Mad Cat.bmp"/>
|
||||
<vehicle key="loki" name="Loki"/> …
|
||||
<time key="day" name="Day"/> <time key="night" name="Night"/>
|
||||
<weather key="clear" name="Clear"/> …
|
||||
</BattleTech>
|
||||
```
|
||||
|
||||
**Known values** (from this repo's eggs + reversing this session):
|
||||
- **scenario**: `freeforall` (confirmed). More in the Console.ini BT tree.
|
||||
- **map (arena)**: `cavern`, `arena1` (confirmed). Full list: Console.ini BT
|
||||
LocationList + game maps.
|
||||
- **vehicle (mech)**: `madcat` (Mad Cat). Others seen in this repo's mech data:
|
||||
`loki`, `thor`, `vulture`, `bhk1` (Black Hawk); OWN/PGN skeletons exist too.
|
||||
(Torso-twist limits found this session: MadCat ±130°, Loki/Thor 110°, Vulture
|
||||
90°; some skeletons — BLH/OWN/PGN — have no torso joint.)
|
||||
- **experience**: `veteran` (confirmed); `novice`/`expert` are the other BTL4
|
||||
pilot levels.
|
||||
- **color**: `Grey` (confirmed). **badge**: `VGL`. **patch**: `Red`.
|
||||
- **role**: `Default` (`model=dfltrole`), `NoReturn` (from TESTARN.EGG).
|
||||
- **time**: `day`, `night`. **weather**: `clear`. **temperature**: numeric (27).
|
||||
**length**: seconds (120).
|
||||
|
||||
**Where to get the exhaustive catalog (in this repo):**
|
||||
- `410console/4_10-console-extracted/Console.ini` — the Mac console's full
|
||||
`[Adventure::…::BattleTech]` → LocationList → scenario/vehicle tree (minified;
|
||||
parse by `::`-namespaced sections; decomp §8.2 shows the RP tree shape).
|
||||
- `ALPHA_1/REL410/BT/` — the game itself: `GAUGE/L4GAUGE.CFG`, `*.RES` resources,
|
||||
`*.SKL` skeletons (mech list), and the `.egg` files.
|
||||
- `CODE/BT/` — BattleTech game source (mech/weapon/damage classes; the
|
||||
DamageMatrix/KillMarker semantics for the event model).
|
||||
|
||||
---
|
||||
|
||||
## 6. Open questions — resolve in the TeslaSuite repo
|
||||
|
||||
1. **`ApplicationID` for BattleTech.** RP is `0`. A BT pod (running `BTL4OPT`)
|
||||
reports its own value in `StateResponseMessage.Application`. Get it by
|
||||
decompiling `Console/lib/Munga Net.dll`'s `ApplicationID` enum, or by reading a
|
||||
live BT pod's state response. Needed for §4 shell game-selection.
|
||||
2. **Full BT scenario list** + per-scenario `<invalid>` maps/vehicles — from the
|
||||
Console.ini BT tree.
|
||||
3. **BT in-match event messages.** RP receives `Scored/Killed/Damaged/Boost/
|
||||
ScoreUpdate` `MungaMessage`s during a game. Confirm BT's set (BT models
|
||||
DamageMatrix + KillMarker) against `Munga Net.dll` / a live BT match.
|
||||
4. **Image assets** for BT mechs/arenas (RP ships `images/red planet …`; BT needs
|
||||
`images/battletech …`). Sourceable from the game `.RES`/gauge art or recreated.
|
||||
5. **Whether BT reuses RP's exact plasma bitmaps/ordinals** — `cavern.egg` shows
|
||||
YES (identical `[ordinals]` + `[BitMap::Large|Small::*]` mechanism, same
|
||||
128×32 / 64×16). Reuse `PlasmaBitmaps`/`PlasmaFontTool` unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- **Golden-egg check:** construct the `cavern.egg` scenario in BT classes
|
||||
(freeforall/cavern/night/clear/27/120, pilot `cyd`/`madcat`/`veteran`/`VGL`/
|
||||
`Grey`/`Red`/`Role::Default`) and diff `ToEggString()` field-by-field against
|
||||
`reference/cavern.egg`. Values must match; section order may not (see §2 caveat).
|
||||
- **Live check:** feed a generated egg to a real BT pod over Munga 1501 and drive
|
||||
the state machine `WaitingForEgg → … → RunningMission`.
|
||||
- Add BT cases to `Console/tests/TeslaConsole.DiffTests` alongside the RP ones.
|
||||
|
||||
---
|
||||
|
||||
## 8. `reference/` contents (travels with this spec)
|
||||
|
||||
- `cavern.egg` — console-generated BattleTech egg (the golden field reference).
|
||||
- `TESTARN.EGG` — 2nd BT egg (shows multiple `[Role::*]` blocks).
|
||||
- `RPConfig.xml.template` — the RP catalog XML to mirror as `BTConfig.xml`.
|
||||
- `L4PLASMA.HPP` — the pod plasma-display class (128×32) the egg bitmaps target.
|
||||
|
||||
## 9. The plasma tie-in (context)
|
||||
|
||||
The console **authors** the pod's plasma-scoreboard graphics: the egg's
|
||||
`[BitMap::Large::<pilot>]` (128×32) / `[BitMap::Small::<pilot>]` (64×16) and the
|
||||
`[ordinals]` 1st–4th place bitmaps are rendered on the pod's **plasma display**
|
||||
(`L4PLASMA.HPP`: `plasmaWidth=128, plasmaHeight=32`, serial out on DOS COM2). So
|
||||
console → egg → pod plasma panel. `PlasmaBitmaps`/`PlasmaFontTool` in TeslaConsole
|
||||
generate them; BT reuses both. (In `TeslaRel410` the pod side of this is emulated
|
||||
— see that repo's plasma-display notes.)
|
||||
@@ -0,0 +1,109 @@
|
||||
# Addendum: mission-close / egress-hold — decoded + disassembled (2026-07-10)
|
||||
|
||||
From the TeslaRel410 side. Verified against a live DOSBox BTL4OPT pod run
|
||||
BY THIS .NET CONSOLE over the host bridge (it works). Full writeup:
|
||||
TeslaRel410/emulator/NET-NOTES.md.
|
||||
|
||||
> **ISSUE CLOSED 2026-07-10 (won't-fix, operator decision).** The floor
|
||||
> egress lamps no longer exist on any surviving cockpit, so the hold has
|
||||
> no restoration value. FINAL, corrected finding (supersedes the "~30s"
|
||||
> phrasing below): the pod's post-mission egress window is a compiled
|
||||
> **~3 seconds** — the game exits at its stage-1 timer (Step-0 proven:
|
||||
> a no-loop single-shot boot still held 3.9s then the game self-exited),
|
||||
> and the deeper 30s timer is never reached. Byte-identical across all
|
||||
> four BTL4OPT builds, so nothing regressed. **Console takeaway is
|
||||
> unchanged and simple: send StopMission(RunBattleTech) promptly at your
|
||||
> timer end; the pod handles its own (short) close. No egress delay to
|
||||
> implement.** The rest of this file is preserved for the record.
|
||||
|
||||
## Bottom line for TeslaConsole
|
||||
|
||||
**The customer-egress window is a HARDCODED ~30s timer inside the GAME,
|
||||
not the console.** You do NOT need to implement the egress delay. Just
|
||||
send `Application__StopMissionMessage` promptly at mission end with
|
||||
`RunBattleTechExitCodeID` (=3), and the pod runs the whole authentic
|
||||
sequence itself: egress lamps ON -> ~30s hold -> lamps OFF -> exit ->
|
||||
GO.BAT relaunch. The console's only timing responsibility is not to send
|
||||
StopMission too EARLY (let the mission actually end first).
|
||||
|
||||
## The sequence (game side)
|
||||
|
||||
1. Mission end -> game fades to black hold, sends the wrap-up
|
||||
(ConsolePlayerMechScoreUpdate / ConsoleBTTeamScoreUpdate /
|
||||
RankAndScore). Its StateResponse to your 1/s StateQuery flips state
|
||||
0 -> 2 (mission-over/hold). It waits INDEFINITELY here for you.
|
||||
**CAUTION — "mission end" is NOT the pod's own clock.** Bench-confirmed
|
||||
2026-07-09 (12s mission via this console): the pod does NOT self-end;
|
||||
the egg `length=` only drives the cockpit clock DISPLAY, which hits 0
|
||||
and counts back UP while play continues. The console is the game's
|
||||
sole timekeeper — this hold state is only ever entered off the
|
||||
console's close (or an in-game end condition), so in practice steps
|
||||
1-2 collapse: the console's StopMission at ITS timer end triggers the
|
||||
fade/wrap-up/lamps directly (see step 3).
|
||||
2. You send `Application__StopMissionMessage(ExitCodeID)`.
|
||||
3. `StopMissionMessageHandler` (BTL4OPT.EXE @0x47b864, disassembled):
|
||||
- calls mission-shutdown/EndMission (0x44eeb4)
|
||||
- turns the RIO egress lamps ON (routine @0x47bba8, flag=1; lamps
|
||||
0x16/0x17/0x1e = the floor/entry cluster; the "LightsOut" string
|
||||
@0x4fd5ac names this family)
|
||||
- schedules the lights-out+exit for `now + 30.0*timebase + 0.5`
|
||||
(30.0f constant @0x47b8e4) -- the customer-egress hold
|
||||
4. ~30s later the timer fires -> lamps swept off -> game exits with the
|
||||
exit code -> pod's BAT loop dispatches it (RunBattleTech = relaunch).
|
||||
|
||||
## Console message set + exit codes (from CODE/RP/MUNGA/APPMSG.HPP)
|
||||
|
||||
Messages: StateQuery, CheckLoad, RunMission, **StopMission**, KeyCommand,
|
||||
SuspendMission, ResumeMission, LoadMission, Abort.
|
||||
|
||||
`ExitCodeID` (the game's exit status = what the pod's BAT loop runs next):
|
||||
```
|
||||
Null=0, Abort, RunRedPlanet, RunBattleTech, RunSinglePlayerRedPlanet,
|
||||
RunSinglePlayerBattleTech, DisplayMainTestPattern, DisplayAuxTestPattern,
|
||||
TestPlasmaDisplay, ResetRIO, RunAudioTest, RunNortonDiskDoctor,
|
||||
CheckDiskUsage, RefreshRedPlanet, RefreshBattleTech, ChangeScreenMode,
|
||||
SoftwareReset, ClearCrashlog, KillSpoolFile, RunRedPlanetCamera,
|
||||
RunBattleTechCamera, RunRedPlanetMissionReview, RunBattleTechMissionReview
|
||||
```
|
||||
= a whole remote-operations menu the console UI could expose (RIO reset,
|
||||
test patterns, plasma test, disk tools, camera/mission-review launches).
|
||||
|
||||
## Notes / open
|
||||
|
||||
- A configurable *pre*-StopMission delay is still fine to expose, but the
|
||||
~30s customer window itself is the game's and is authentic at 30s.
|
||||
- Measured lamp-on-to-sweep was only ~3.4s in two live runs; that sweep
|
||||
was likely the teardown GeneralReset, with the real 30s timer firing
|
||||
past the tap window. To confirm on the TeslaRel410 side: one mission
|
||||
with a 90s+ RIO serial tap past mission end (btdis2.py found the
|
||||
constant; a live tap confirms wall-time).
|
||||
- Bonus authenticity: the floor lamp is ALSO entry lighting -- on from
|
||||
game-ready until the mission drops (customer climbs in lit).
|
||||
- Bench 2026-07-09 (12s mission): pod clock counted UP past 0 with play
|
||||
continuing until the console's stop — confirms the console is the sole
|
||||
timekeeper and step 1 never fires off the pod's own clock. If a
|
||||
self-end path exists in the binary it wasn't reachable with our egg.
|
||||
- Cross-ref: the BT411 up-port source (engine/MUNGA/APP.cpp ~1634) has a
|
||||
TWO-stage StopMissionMessageHandler (stop#1 while Running -> EndingMission
|
||||
+ lamps; stop#2 while Ending -> Stop()/exit; +30s LightsOut = lamps off
|
||||
only). The shipping BTL4OPT.EXE disassembly above (one stop -> lamps +
|
||||
scheduled exit at +30s) is authoritative for the DOSBox pod; the up-port
|
||||
evidently diverges — worth knowing if BT411-built binaries ever drive a
|
||||
real pod.
|
||||
- A console-side "egress hold delay" (wait between console timer zero and
|
||||
the StopMission send) was prototyped in TeslaConsole 2026-07-09 and
|
||||
REVERTED: since the pod plays until stopped, that window is overtime
|
||||
play, not lamps-on time. The game's own 30s window is the authentic
|
||||
egress hold.
|
||||
- CROSS-BUILD DISASM (TeslaRel410 side, 2026-07-10, stopmission_cmp3.py):
|
||||
the StopMission close is BYTE-IDENTICAL across all four BTL4OPT builds
|
||||
-- BTLIVE (May-96), BTRAVINE (Sep-96), BTDAVE, Rel410 -- so no build
|
||||
regressed it (this settles the operator's "did Rel410 cut the hold?"
|
||||
question: no). Each build's dispatched outer handler (Rel410 @0x47c2c4)
|
||||
is the re-entrant two-stage one and carries BOTH a 3.0f and (via the
|
||||
inner @0x47b864) a 30.0f constant. Confirms the up-port's two-stage
|
||||
structure at the binary level. Whether 30s is honored live is still
|
||||
open: our RIO tap saw only ~3.4s of lamp-on (the 3.0f stage) before the
|
||||
exe recycled -- the 30s LightsOut may be pre-empted by teardown in the
|
||||
loop-conf flow. Not console-actionable either way (prompt StopMission is
|
||||
correct); noted for completeness.
|
||||
@@ -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
|
||||
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pefparse.py -- Minimal parser/disassembler for classic Mac OS CFM
|
||||
Preferred Executable Format (PEF) PowerPC containers ('Joy!peff pwpc').
|
||||
|
||||
Written for the VWE Tesla restoration project to decompile "Console 4.10".
|
||||
Big-endian PowerPC. Pure-python; only external dep is capstone (optional,
|
||||
for the `dis` command).
|
||||
|
||||
PEF reference: Apple "Mac OS Runtime Architectures", ch. Preferred
|
||||
Executable Format. Container header (40 bytes), then N section headers
|
||||
(28 bytes each), then the section name table, then section bodies.
|
||||
|
||||
Usage:
|
||||
py -3.13 pefparse.py <file> header # container + section headers
|
||||
py -3.13 pefparse.py <file> loader # loader section: imports/exports/entry
|
||||
py -3.13 pefparse.py <file> strings # printable strings in code+data
|
||||
py -3.13 pefparse.py <file> dis [secidx] [start] [count] # disassemble
|
||||
py -3.13 pefparse.py <file> dump <secidx> <off> <len> # hex dump section body
|
||||
"""
|
||||
import sys, struct, string
|
||||
|
||||
def u32(b, o): return struct.unpack_from('>I', b, o)[0]
|
||||
def u16(b, o): return struct.unpack_from('>H', b, o)[0]
|
||||
def s32(b, o): return struct.unpack_from('>i', b, o)[0]
|
||||
def u8(b, o): return b[o]
|
||||
|
||||
SECTION_KINDS = {
|
||||
0: 'Code', 1: 'UnpackedData', 2: 'PatternInitData', 3: 'Constant',
|
||||
4: 'Loader', 5: 'Debug', 6: 'ExecutableData', 7: 'Exception', 8: 'Traceback',
|
||||
}
|
||||
|
||||
class Section:
|
||||
def __init__(self, idx, hdr, container):
|
||||
(self.name_off, self.default_addr, self.total_size, self.unpacked_size,
|
||||
self.packed_size, self.container_off, self.kind, self.share,
|
||||
self.align, self.rsvd) = hdr
|
||||
self.idx = idx
|
||||
self.name = None
|
||||
self._container = container
|
||||
|
||||
def raw(self):
|
||||
"""Raw (possibly packed) bytes of the section body as stored."""
|
||||
return self._container[self.container_off:self.container_off + self.packed_size]
|
||||
|
||||
def data(self):
|
||||
"""Unpacked section body. PatternInitData(2) is RLE-decompressed."""
|
||||
raw = self.raw()
|
||||
if self.kind == 2:
|
||||
return unpack_pidata(raw, self.unpacked_size)
|
||||
return raw
|
||||
|
||||
class PEF:
|
||||
def __init__(self, blob):
|
||||
self.blob = blob
|
||||
assert blob[0:4] == b'Joy!', "not a PEF (missing Joy!)"
|
||||
assert blob[4:8] == b'peff', "not a PEF (missing peff)"
|
||||
self.arch = blob[8:12]
|
||||
self.format_version = u32(blob, 12)
|
||||
self.timestamp = u32(blob, 16)
|
||||
self.old_def_ver = u32(blob, 20)
|
||||
self.old_imp_ver = u32(blob, 24)
|
||||
self.current_ver = u32(blob, 28)
|
||||
self.section_count = u16(blob, 32)
|
||||
self.inst_section_count = u16(blob, 34)
|
||||
self.sections = []
|
||||
o = 40
|
||||
for i in range(self.section_count):
|
||||
hdr = struct.unpack_from('>iIIIIIBBBB', blob, o)
|
||||
self.sections.append(Section(i, hdr, blob))
|
||||
o += 28
|
||||
# section name table follows the section headers
|
||||
name_tbl_off = o
|
||||
for s in self.sections:
|
||||
if s.name_off >= 0:
|
||||
s.name = read_cstr(blob, name_tbl_off + s.name_off)
|
||||
|
||||
def loader(self):
|
||||
for s in self.sections:
|
||||
if s.kind == 4:
|
||||
return s
|
||||
return None
|
||||
|
||||
def read_cstr(b, o):
|
||||
e = o
|
||||
while e < len(b) and b[e] != 0:
|
||||
e += 1
|
||||
return b[o:e].decode('mac_roman', 'replace')
|
||||
|
||||
def unpack_pidata(raw, unpacked_size):
|
||||
"""Decompress PEF pattern-initialized data (RLE opcode stream)."""
|
||||
out = bytearray()
|
||||
i = 0
|
||||
n = len(raw)
|
||||
def argcount():
|
||||
nonlocal i
|
||||
val = 0
|
||||
while True:
|
||||
b = raw[i]; i += 1
|
||||
val = (val << 7) | (b & 0x7f)
|
||||
if not (b & 0x80):
|
||||
break
|
||||
return val
|
||||
while i < n:
|
||||
op = raw[i]; i += 1
|
||||
opcode = op >> 5
|
||||
count5 = op & 0x1f
|
||||
cnt = count5 if count5 else argcount()
|
||||
if opcode == 0: # Zero
|
||||
out.extend(b'\x00' * cnt)
|
||||
elif opcode == 1: # blockCopy
|
||||
out.extend(raw[i:i+cnt]); i += cnt
|
||||
elif opcode == 2: # repeatedBlock
|
||||
rpt = argcount()
|
||||
block = raw[i:i+cnt]; i += cnt
|
||||
for _ in range(rpt + 1):
|
||||
out.extend(block)
|
||||
elif opcode == 3: # interleaveRepeatBlockWithBlockCopy
|
||||
cs = argcount() # custom size
|
||||
rc = argcount() # repeat count
|
||||
common = raw[i:i+cnt]; i += cnt
|
||||
for _ in range(rc):
|
||||
out.extend(common)
|
||||
out.extend(raw[i:i+cs]); i += cs
|
||||
out.extend(common)
|
||||
elif opcode == 4: # interleaveRepeatBlockWithZero
|
||||
cs = argcount()
|
||||
rc = argcount()
|
||||
for _ in range(rc):
|
||||
out.extend(b'\x00' * cnt)
|
||||
out.extend(raw[i:i+cs]); i += cs
|
||||
out.extend(b'\x00' * cnt)
|
||||
else:
|
||||
raise ValueError("bad PIDATA opcode %d at %d" % (opcode, i))
|
||||
return bytes(out[:unpacked_size])
|
||||
|
||||
# ---- Loader section parsing -------------------------------------------------
|
||||
|
||||
def parse_loader(sec):
|
||||
b = sec.data()
|
||||
(main_section, main_offset, init_section, init_offset,
|
||||
term_section, term_offset, imported_lib_count, total_imported_sym_count,
|
||||
reloc_section_count, reloc_instr_offset, loader_strings_offset,
|
||||
export_hash_offset, export_hash_table_power, exported_symbol_count) = \
|
||||
struct.unpack_from('>iIiIiIIIIIIIII', b, 0)
|
||||
info = dict(main_section=main_section, main_offset=main_offset,
|
||||
init_section=init_section, init_offset=init_offset,
|
||||
term_section=term_section, term_offset=term_offset,
|
||||
imported_lib_count=imported_lib_count,
|
||||
total_imported_sym_count=total_imported_sym_count,
|
||||
reloc_section_count=reloc_section_count,
|
||||
reloc_instr_offset=reloc_instr_offset,
|
||||
loader_strings_offset=loader_strings_offset,
|
||||
export_hash_offset=export_hash_offset,
|
||||
export_hash_table_power=export_hash_table_power,
|
||||
exported_symbol_count=exported_symbol_count)
|
||||
# Imported library table: starts at offset 56
|
||||
libs = []
|
||||
o = 56
|
||||
for _ in range(imported_lib_count):
|
||||
(name_off, old_impl_ver, curr_ver, imp_sym_count,
|
||||
first_imp_sym, options, rsvdA, rsvdB) = struct.unpack_from('>IIIIIBBH', b, o)
|
||||
libs.append(dict(name_off=name_off, imp_sym_count=imp_sym_count,
|
||||
first_imp_sym=first_imp_sym, options=options))
|
||||
o += 24
|
||||
# Imported symbol table follows lib table
|
||||
imp_syms = []
|
||||
for _ in range(total_imported_sym_count):
|
||||
val = u32(b, o); o += 4
|
||||
sym_class = val >> 24
|
||||
name_off = val & 0x00FFFFFF
|
||||
imp_syms.append((sym_class, name_off))
|
||||
strtab = info['loader_strings_offset']
|
||||
for lib in libs:
|
||||
lib['name'] = read_cstr(b, strtab + lib['name_off'])
|
||||
syms = []
|
||||
for k in range(lib['first_imp_sym'], lib['first_imp_sym'] + lib['imp_sym_count']):
|
||||
cls, noff = imp_syms[k]
|
||||
syms.append((cls, read_cstr(b, strtab + noff)))
|
||||
lib['symbols'] = syms
|
||||
# Exported symbols (loader export hash). Layout:
|
||||
# export hash table (2^power u32 slots) at export_hash_offset
|
||||
# then key table (u32 per exported symbol)
|
||||
# then exported symbol table (10 bytes each: class/name u32, value u32, sect u16)
|
||||
exports = []
|
||||
hash_slots = 1 << export_hash_table_power
|
||||
key_tab_off = export_hash_offset + hash_slots * 4
|
||||
sym_tab_off = key_tab_off + exported_symbol_count * 4
|
||||
for k in range(exported_symbol_count):
|
||||
eo = sym_tab_off + k * 10
|
||||
clsname = u32(b, eo)
|
||||
value = u32(b, eo + 4)
|
||||
sect = u16(b, eo + 8)
|
||||
sym_class = clsname >> 24
|
||||
name_off = clsname & 0x00FFFFFF
|
||||
name = read_cstr(b, strtab + name_off)
|
||||
exports.append(dict(name=name, sym_class=sym_class, value=value, section=sect))
|
||||
return info, libs, exports
|
||||
|
||||
SYM_CLASS = {0: 'code(TVect)', 1: 'data', 2: 'TVector', 3: 'TOC/Glue',
|
||||
4: 'linker/glue', 0x40: 'weak?'}
|
||||
|
||||
# ---- commands ---------------------------------------------------------------
|
||||
|
||||
def cmd_header(pef):
|
||||
print("PEF container: arch=%s formatVersion=%d timestamp=0x%08x currentVer=0x%08x"
|
||||
% (pef.arch.decode('ascii','replace'), pef.format_version, pef.timestamp, pef.current_ver))
|
||||
print("sectionCount=%d instantiatedSectionCount=%d" % (pef.section_count, pef.inst_section_count))
|
||||
print()
|
||||
print("idx name kind default_addr total unpacked packed fileoff align")
|
||||
for s in pef.sections:
|
||||
print("%3d %-14s %-19s 0x%08x %-9d %-9d %-9d 0x%06x %d" % (
|
||||
s.idx, (s.name or '-'), "%d:%s" % (s.kind, SECTION_KINDS.get(s.kind, '?')),
|
||||
s.default_addr, s.total_size, s.unpacked_size, s.packed_size,
|
||||
s.container_off, s.align))
|
||||
|
||||
def cmd_loader(pef):
|
||||
lsec = pef.loader()
|
||||
if not lsec:
|
||||
print("no loader section"); return
|
||||
info, libs, exports = parse_loader(lsec)
|
||||
print("=== Loader header ===")
|
||||
for k, v in info.items():
|
||||
if isinstance(v, int) and v > 9 and k.endswith(('offset','section','count')) is False:
|
||||
print(" %-28s 0x%x (%d)" % (k, v, v))
|
||||
else:
|
||||
print(" %-28s %s" % (k, v))
|
||||
ms = info['main_section']
|
||||
print(" ENTRY main: section %d offset 0x%x" % (info['main_section'], info['main_offset']) if ms>=0 else " (no main)")
|
||||
print(" ENTRY init: section %d offset 0x%x" % (info['init_section'], info['init_offset']))
|
||||
print(" ENTRY term: section %d offset 0x%x" % (info['term_section'], info['term_offset']))
|
||||
print()
|
||||
print("=== Imported libraries (%d) ===" % len(libs))
|
||||
for lib in libs:
|
||||
print("\n-- %s (%d symbols) --" % (lib['name'], lib['imp_sym_count']))
|
||||
for j,(cls,name) in enumerate(lib['symbols']):
|
||||
print(" [%4d] cls=%s %s" % (lib['first_imp_sym']+j, SYM_CLASS.get(cls, hex(cls)), name))
|
||||
print()
|
||||
print("=== Exported symbols (%d) ===" % len(exports))
|
||||
for e in exports:
|
||||
print(" %-40s cls=%s value=0x%x sect=%d" % (e['name'], SYM_CLASS.get(e['sym_class'], hex(e['sym_class'])), e['value'], e['section']))
|
||||
|
||||
def iter_strings(data, minlen=4):
|
||||
cur = bytearray()
|
||||
start = 0
|
||||
printable = set(bytes(string.printable, 'ascii')) - set(b'\t\n\r\x0b\x0c')
|
||||
printable |= set(b' ')
|
||||
for i, ch in enumerate(data):
|
||||
if ch in printable:
|
||||
if not cur:
|
||||
start = i
|
||||
cur.append(ch)
|
||||
else:
|
||||
if len(cur) >= minlen:
|
||||
yield start, cur.decode('ascii', 'replace')
|
||||
cur = bytearray()
|
||||
if len(cur) >= minlen:
|
||||
yield start, cur.decode('ascii', 'replace')
|
||||
|
||||
def cmd_strings(pef, minlen=4):
|
||||
for s in pef.sections:
|
||||
if s.kind in (0,1,2,3):
|
||||
data = s.data()
|
||||
print("### section %d (%s) base=0x%x ###" % (s.idx, SECTION_KINDS.get(s.kind), s.default_addr))
|
||||
for off, st in iter_strings(data, minlen):
|
||||
print("0x%08x %s" % (s.default_addr + off, st))
|
||||
|
||||
def cmd_dump(pef, secidx, off, length):
|
||||
data = pef.sections[secidx].data()
|
||||
chunk = data[off:off+length]
|
||||
for i in range(0, len(chunk), 16):
|
||||
row = chunk[i:i+16]
|
||||
hexs = ' '.join('%02x' % c for c in row)
|
||||
asci = ''.join(chr(c) if 32 <= c < 127 else '.' for c in row)
|
||||
print('%08x %-48s %s' % (off+i, hexs, asci))
|
||||
|
||||
def cmd_dis(pef, secidx=0, start=0, count=200):
|
||||
import capstone
|
||||
sec = pef.sections[secidx]
|
||||
data = sec.data()
|
||||
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
||||
md.detail = False
|
||||
base = sec.default_addr
|
||||
for insn in md.disasm(data[start:start+count*4], base+start):
|
||||
print("0x%08x %-8s %s" % (insn.address, insn.mnemonic, insn.op_str))
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
cmd = sys.argv[2] if len(sys.argv) > 2 else 'header'
|
||||
with open(path, 'rb') as f:
|
||||
blob = f.read()
|
||||
pef = PEF(blob)
|
||||
if cmd == 'header': cmd_header(pef)
|
||||
elif cmd == 'loader': cmd_loader(pef)
|
||||
elif cmd == 'strings':
|
||||
minlen = int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
||||
cmd_strings(pef, minlen)
|
||||
elif cmd == 'dump':
|
||||
cmd_dump(pef, int(sys.argv[3]), int(sys.argv[4],0), int(sys.argv[5],0))
|
||||
elif cmd == 'dis':
|
||||
secidx = int(sys.argv[3]) if len(sys.argv) > 3 else 0
|
||||
start = int(sys.argv[4],0) if len(sys.argv) > 4 else 0
|
||||
count = int(sys.argv[5],0) if len(sys.argv) > 5 else 200
|
||||
cmd_dis(pef, secidx, start, count)
|
||||
else:
|
||||
print("unknown cmd", cmd)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ppcxref.py -- TOC-aware PPC annotator for the Console 4.10 PEF.
|
||||
|
||||
Resolves r2(TOC)-relative loads to data addresses and, when those point at
|
||||
C strings, annotates the disassembly. Also builds a string cross-reference:
|
||||
which code addresses reference which strings.
|
||||
|
||||
r2 (TOC base) = data section offset 0x8000 (from the main transition vector).
|
||||
|
||||
Usage:
|
||||
py -3.13 ppcxref.py annotate <start> <count> # annotated disasm
|
||||
py -3.13 ppcxref.py xref <substring> # find code refs to strings matching substring
|
||||
py -3.13 ppcxref.py toc <hexoff> # what does TOC[-off(r2)] point to
|
||||
"""
|
||||
import sys, struct, capstone
|
||||
sys.path.insert(0, __import__('os').path.dirname(__file__))
|
||||
from pefparse import PEF
|
||||
|
||||
BIN = r"C:\VWE\TeslaRel410\410console\4_10-console-extracted\Console 4.10"
|
||||
TOC_BASE = 0x8000 # r2 value = data section offset
|
||||
|
||||
pef = PEF(open(BIN,'rb').read())
|
||||
CODE = pef.sections[0].data()
|
||||
DATA = pef.sections[1].data()
|
||||
|
||||
def d32(off):
|
||||
if off < 0 or off+4 > len(DATA): return None
|
||||
return struct.unpack_from('>I', DATA, off)[0]
|
||||
|
||||
def cstr_at(off, maxlen=80):
|
||||
if off < 0 or off >= len(DATA): return None
|
||||
e = off
|
||||
while e < len(DATA) and DATA[e] != 0 and (e-off) < maxlen:
|
||||
c = DATA[e]
|
||||
if c < 0x09 or (c > 0x0d and c < 0x20) or c > 0x7e:
|
||||
# allow only printable; stop otherwise
|
||||
if c != 0xc9: # ellipsis
|
||||
break
|
||||
e += 1
|
||||
s = DATA[off:e]
|
||||
try:
|
||||
txt = s.decode('mac_roman')
|
||||
except Exception:
|
||||
return None
|
||||
# require mostly printable and length>=3
|
||||
printable = sum(1 for c in s if 0x20 <= c <= 0x7e)
|
||||
if len(s) >= 3 and printable >= max(3, int(len(s)*0.8)):
|
||||
return txt
|
||||
return None
|
||||
|
||||
def toc_target(disp):
|
||||
"""disp is signed offset from r2. Returns (data_off, pointed_value, string_or_None)."""
|
||||
off = TOC_BASE + disp
|
||||
val = d32(off)
|
||||
s = None
|
||||
if val is not None:
|
||||
s = cstr_at(val)
|
||||
return off, val, s
|
||||
|
||||
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
||||
md.detail = True
|
||||
|
||||
def annotate(start, count):
|
||||
base = 0
|
||||
for insn in md.disasm(CODE[start:start+count*4], base+start):
|
||||
note = ''
|
||||
# detect r2-relative: op_str contains "r2)" with a displacement
|
||||
ops = insn.op_str
|
||||
# capstone PPC prints e.g. "r4, -0x6c9c(r2)" or "r3, r2, -0x6848"
|
||||
disp = None
|
||||
if '(r2)' in ops:
|
||||
# form: rX, disp(r2)
|
||||
try:
|
||||
d = ops.split(',')[-1].strip()
|
||||
d = d[:d.index('(r2)')]
|
||||
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
|
||||
except Exception:
|
||||
disp = None
|
||||
if disp is not None:
|
||||
o, val, s = toc_target(disp)
|
||||
if val is not None:
|
||||
note = "; TOC[0x%x]=*0x%x=0x%x" % (o, o, val)
|
||||
if s: note += ' "%s"' % s
|
||||
elif 'r2,' in ops and insn.mnemonic in ('addi','addic','subi'):
|
||||
# form: rX, r2, disp -> address = TOC_BASE+disp (pointer to data directly)
|
||||
try:
|
||||
d = ops.split(',')[-1].strip()
|
||||
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
|
||||
except Exception:
|
||||
disp = None
|
||||
if disp is not None:
|
||||
addr = TOC_BASE + disp
|
||||
s = cstr_at(addr)
|
||||
note = "; &data[0x%x]" % addr
|
||||
if s: note += ' "%s"' % s
|
||||
elif insn.mnemonic == 'lis':
|
||||
# high-half load, often followed by addi to form an absolute data ptr
|
||||
note = ''
|
||||
print("0x%08x %-8s %-28s %s" % (insn.address, insn.mnemonic, insn.op_str, note))
|
||||
|
||||
def build_string_table():
|
||||
"""All C strings in data with >=4 printable chars -> {addr: text}."""
|
||||
tbl = {}
|
||||
i = 0
|
||||
n = len(DATA)
|
||||
while i < n:
|
||||
if 0x20 <= DATA[i] <= 0x7e:
|
||||
j = i
|
||||
while j < n and (0x20 <= DATA[j] <= 0x7e):
|
||||
j += 1
|
||||
if j - i >= 4:
|
||||
tbl[i] = DATA[i:j].decode('ascii','replace')
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
return tbl
|
||||
|
||||
def xref(substr):
|
||||
# find data offsets of strings containing substr
|
||||
strtbl = build_string_table()
|
||||
targets = {a:t for a,t in strtbl.items() if substr.lower() in t.lower()}
|
||||
if not targets:
|
||||
print("no strings match", substr); return
|
||||
print("=== target strings ===")
|
||||
for a,t in sorted(targets.items()):
|
||||
print(" 0x%06x %s" % (a, t))
|
||||
# Build TOC reverse map: which TOC slots hold pointers into these strings
|
||||
# (pointer may target start OR interior). Map value->slot.
|
||||
tocslots = {}
|
||||
for slot in range(0, len(DATA), 4):
|
||||
v = d32(slot)
|
||||
if v in targets:
|
||||
tocslots.setdefault(v, []).append(slot)
|
||||
# Also direct addi r2 references use TOC_BASE+disp == string addr directly
|
||||
print("\n=== code references ===")
|
||||
# Disassemble whole code, look for r2-relative producing an address in targets
|
||||
for insn in md.disasm(CODE, 0):
|
||||
ops = insn.op_str
|
||||
hit = None
|
||||
if '(r2)' in ops:
|
||||
try:
|
||||
d = ops.split(',')[-1].strip(); d = d[:d.index('(r2)')]
|
||||
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
|
||||
o = TOC_BASE+disp; v = d32(o)
|
||||
if v in targets: hit = (v, targets[v], 'TOC')
|
||||
except Exception: pass
|
||||
elif 'r2,' in ops and insn.mnemonic in ('addi','subi'):
|
||||
try:
|
||||
d = ops.split(',')[-1].strip()
|
||||
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
|
||||
addr = TOC_BASE+disp
|
||||
if addr in targets: hit = (addr, targets[addr], 'direct')
|
||||
except Exception: pass
|
||||
if hit:
|
||||
print(" 0x%08x %-8s %-24s -> %s \"%s\"" % (insn.address, insn.mnemonic, ops, hit[2], hit[1]))
|
||||
|
||||
if __name__ == '__main__':
|
||||
cmd = sys.argv[1]
|
||||
if cmd == 'annotate':
|
||||
annotate(int(sys.argv[2],0), int(sys.argv[3],0))
|
||||
elif cmd == 'xref':
|
||||
xref(sys.argv[2])
|
||||
elif cmd == 'toc':
|
||||
o,v,s = toc_target(int(sys.argv[2],0))
|
||||
print("TOC off=0x%x val=0x%x str=%s" % (o, v if v else 0, s))
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
rsrcparse.py -- classic Mac OS resource-fork parser.
|
||||
Reads a resource fork (raw, or embedded in an AppleDouble ._ sidecar) and
|
||||
lists resource types/IDs/names. Optionally dumps a given type.
|
||||
|
||||
Usage:
|
||||
py -3.13 rsrcparse.py <._file> [--base 0x52] map
|
||||
py -3.13 rsrcparse.py <._file> [--base 0x52] dump <TYPE> [id]
|
||||
"""
|
||||
import sys, struct
|
||||
|
||||
def u32(b,o): return struct.unpack_from('>I',b,o)[0]
|
||||
def u16(b,o): return struct.unpack_from('>H',b,o)[0]
|
||||
|
||||
def appledouble_rfork_base(blob):
|
||||
# AppleDouble: magic 0x00051607; entries describe forks. Entry id 2 = resource fork.
|
||||
if u32(blob,0) != 0x00051607:
|
||||
return 0, len(blob)
|
||||
n = u16(blob, 24)
|
||||
o = 26
|
||||
for _ in range(n):
|
||||
eid, off, length = struct.unpack_from('>III', blob, o); o += 12
|
||||
if eid == 2:
|
||||
return off, length
|
||||
return 0, len(blob)
|
||||
|
||||
def parse(blob, base):
|
||||
rf = blob[base:]
|
||||
data_off, map_off, data_len, map_len = struct.unpack_from('>IIII', rf, 0)
|
||||
m = map_off
|
||||
type_list_off = u16(rf, m+24) # from map start
|
||||
name_list_off = u16(rf, m+26)
|
||||
tlo = m + type_list_off
|
||||
ntypes = u16(rf, tlo) + 1
|
||||
types = []
|
||||
for i in range(ntypes):
|
||||
eo = tlo + 2 + i*8
|
||||
typ = rf[eo:eo+4].decode('mac_roman','replace')
|
||||
count = u16(rf, eo+4) + 1
|
||||
ref_off = u16(rf, eo+6)
|
||||
refs = []
|
||||
rlo = tlo + ref_off
|
||||
for j in range(count):
|
||||
ro = rlo + j*12
|
||||
rid = struct.unpack_from('>h', rf, ro)[0]
|
||||
noff = struct.unpack_from('>h', rf, ro+2)[0]
|
||||
attr_dataoff = u32(rf, ro+4)
|
||||
attrs = attr_dataoff >> 24
|
||||
dataoff = attr_dataoff & 0x00FFFFFF
|
||||
name = None
|
||||
if noff != -1:
|
||||
nlo = m + name_list_off + noff
|
||||
ln = rf[nlo]
|
||||
name = rf[nlo+1:nlo+1+ln].decode('mac_roman','replace')
|
||||
# resource data: at data_off + dataoff, first 4 bytes = length
|
||||
dstart = data_off + dataoff
|
||||
dlen = u32(rf, dstart)
|
||||
refs.append(dict(id=rid, name=name, attrs=attrs, data_start=base+dstart+4, dlen=dlen))
|
||||
types.append((typ, refs))
|
||||
return types
|
||||
|
||||
def cmd_map(blob, base):
|
||||
types = parse(blob, base)
|
||||
total = sum(len(r) for _,r in types)
|
||||
print("resource types: %d, total resources: %d" % (len(types), total))
|
||||
for typ, refs in sorted(types, key=lambda x:x[0]):
|
||||
ids = ', '.join('%d%s' % (r['id'], ('("%s")'%r['name']) if r['name'] else '') for r in refs[:12])
|
||||
more = '' if len(refs)<=12 else ' ...(%d total)'%len(refs)
|
||||
print(" '%s' x%-4d : %s%s" % (typ, len(refs), ids, more))
|
||||
|
||||
def cmd_dump(blob, base, typ, wantid=None):
|
||||
types = dict(parse(blob, base))
|
||||
if typ not in types:
|
||||
print("no type", typ); return
|
||||
for r in types[typ]:
|
||||
if wantid is not None and r['id'] != wantid: continue
|
||||
data = blob[r['data_start']:r['data_start']+r['dlen']]
|
||||
print("=== '%s' id=%d name=%s len=%d ===" % (typ, r['id'], r['name'], r['dlen']))
|
||||
# hex + ascii
|
||||
for i in range(0, min(len(data), 4096), 16):
|
||||
row = data[i:i+16]
|
||||
hexs=' '.join('%02x'%c for c in row)
|
||||
asci=''.join(chr(c) if 32<=c<127 else '.' for c in row)
|
||||
print(' %04x %-48s %s' % (i, hexs, asci))
|
||||
|
||||
def cmd_names(blob, base, typ):
|
||||
types = dict(parse(blob, base))
|
||||
if typ not in types:
|
||||
print("no type", typ); return
|
||||
for r in types[typ]:
|
||||
print(" id=%-6d %s" % (r['id'], r['name'] or ''))
|
||||
|
||||
def cmd_strn(blob, base, wantid=None):
|
||||
"""Decode STR# (list of Pascal strings)."""
|
||||
types = dict(parse(blob, base))
|
||||
for r in types.get('STR#', []):
|
||||
if wantid is not None and r['id'] != wantid: continue
|
||||
d = blob[r['data_start']:r['data_start']+r['dlen']]
|
||||
n = u16(d,0); o=2; items=[]
|
||||
for _ in range(n):
|
||||
ln=d[o]; o+=1; items.append(d[o:o+ln].decode('mac_roman','replace')); o+=ln
|
||||
print("STR# %d (%s): %s" % (r['id'], r['name'], ' | '.join(items)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv[1:]
|
||||
path = args[0]; args = args[1:]
|
||||
base = None
|
||||
if args and args[0] == '--base':
|
||||
base = int(args[1],0); args = args[2:]
|
||||
blob = open(path,'rb').read()
|
||||
if base is None:
|
||||
base, _ = appledouble_rfork_base(blob)
|
||||
cmd = args[0] if args else 'map'
|
||||
if cmd == 'map': cmd_map(blob, base)
|
||||
elif cmd == 'dump': cmd_dump(blob, base, args[1], int(args[2]) if len(args)>2 else None)
|
||||
elif cmd == 'names': cmd_names(blob, base, args[1])
|
||||
elif cmd == 'strn': cmd_strn(blob, base, int(args[1]) if len(args)>1 else None)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCopyright("Copyright © 2009")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: Guid("581ca4b6-a91c-4d24-b9b5-207f3b5da379")]
|
||||
[assembly: AssemblyFileVersion("4.11.4.1")]
|
||||
[assembly: AssemblyFileVersion("4.11.4.3")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: AssemblyTitle("Tesla Console")]
|
||||
[assembly: AssemblyDescription("All code and UI property of Virtual World Entertainment.\r\n\r\nDeveloped by Elsewhen Studios, LLC in association with VGCorps, LLC.\r\n\r\nElsewhen Studios and the Elsewhen Wormhole are trademarks of Elsewhen Studios, LLC\r\n\r\nIncludes the WeifenLuo DockingPane library. Copyright © 2007 Weifen Luo (email: weifenluo@yahoo.com). Licensed under the MIT License - details can be found in WeifenLuo.txt included in this installation.")]
|
||||
[assembly: AssemblyVersion("4.11.4.1")]
|
||||
[assembly: AssemblyVersion("4.11.4.3")]
|
||||
|
||||
@@ -39,13 +39,14 @@ The console references these assemblies. Most are vendored as binaries under
|
||||
Two of these are no longer vendored binaries — they are built from source and
|
||||
shared across the suite:
|
||||
|
||||
- `TeslaConsoleLaunchLib` ← `../Contract/Tesla.Contract.csproj`, a net48 project: the
|
||||
single source of truth for the Console↔Launcher RPC contract (wire types, the
|
||||
- `TeslaConsoleLaunchLib` ← `../Contract/Tesla.Contract.csproj` (net40, like the
|
||||
whole suite since XP11): the single source of truth for the
|
||||
Console↔Launcher RPC contract (wire types, the
|
||||
`PodManagerConnection` client, and the framed-JSON `PodRpc` protocol), shared with the
|
||||
Launcher Service. The assembly keeps the
|
||||
Launcher. The assembly keeps the
|
||||
`TeslaConsoleLaunchLib` name so the original-exe baseline still resolves in the
|
||||
differential tests; the wire no longer embeds assembly names (see RPC note below).
|
||||
- `TeslaSecureConfiguration` ← `../SecureConfig/Tesla.SecureConfig.csproj` (net48),
|
||||
- `TeslaSecureConfiguration` ← `../SecureConfig/Tesla.SecureConfig.csproj` (net40),
|
||||
the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange).
|
||||
|
||||
The original `TeslaSecureConfiguration.dll` is retained under `lib/` as the baseline
|
||||
@@ -55,14 +56,16 @@ decompiled to source the same way if full-source builds are needed.
|
||||
|
||||
### Console ↔ Launcher RPC (no BinaryFormatter)
|
||||
|
||||
The pod-management channel (TCP 53290) runs **length-prefixed System.Text.Json**
|
||||
The pod-management channel (TCP 53290) runs **length-prefixed JSON**
|
||||
frames over the existing OFB-encrypted stream — see `Contract/PodRpcProtocol.cs`,
|
||||
shared verbatim by both ends. This replaced the original `BinaryFormatter` +
|
||||
shared verbatim by both ends (Newtonsoft.Json — net40 has no System.Text.Json).
|
||||
This replaced the original `BinaryFormatter` +
|
||||
serialized-`MethodBase` scheme (a remote-code-execution sink, and what had pinned the
|
||||
Launcher to an old runtime); dispatch is now by method-name string. The Launcher
|
||||
Service/Agent target **net48**, same as the Console. Note the Console still uses
|
||||
`BinaryFormatter` for *local* disk persistence (`Site` config, mission results) — that
|
||||
is local file I/O on net48, not the network surface, and is intentionally left alone.
|
||||
Launcher to an old runtime); dispatch is now by method-name string. The Launcher and
|
||||
the Console both target **net40** (XP11: XP SP3 through Windows 11). Note the Console
|
||||
still uses `BinaryFormatter` for *local* disk persistence (`Site` config, mission
|
||||
results) — that is local file I/O, not the network surface, and is intentionally
|
||||
left alone.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -72,8 +75,10 @@ and still build and run.
|
||||
```
|
||||
TeslaConsole/
|
||||
*.cs, TeslaConsole.*/ decompiled source (by namespace)
|
||||
*.resx, app.ico embedded resources + icon
|
||||
TeslaConsole.csproj net48 project
|
||||
*.resx, app.ico string resources + icon
|
||||
assets/icons/ UI images (raw originals, embedded as manifest resources —
|
||||
the resx BinaryFormatter blobs cannot build for net40)
|
||||
TeslaConsole.csproj net40 project (XP11: runs on XP SP3 through Windows 11)
|
||||
RedPlanet/ runtime content (RPConfig.xml, RPStrings.xml) — copied to output
|
||||
images/ source art (pod art / maps / vehicles) — reference only
|
||||
installer_banner.bmp installer artwork — reference only
|
||||
@@ -84,14 +89,14 @@ TeslaConsole/
|
||||
## Building
|
||||
|
||||
Requirements: .NET SDK (6.0+) — the `Microsoft.NETFramework.ReferenceAssemblies`
|
||||
NuGet package supplies the net48 reference assemblies, so a standalone Framework
|
||||
NuGet package supplies the net40 reference assemblies, so a standalone Framework
|
||||
targeting pack is **not** required.
|
||||
|
||||
```
|
||||
dotnet build TeslaConsole.csproj -c Release
|
||||
```
|
||||
|
||||
Output: `bin/Release/net48/TeslaConsole.exe` (with `RedPlanet/` and all
|
||||
Output: `bin/Release/net40/TeslaConsole.exe` (with `RedPlanet/` and all
|
||||
dependency DLLs copied alongside it).
|
||||
|
||||
## Runtime content
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
|
||||
To add a product, append a <Product> with one <Launch>. exe must live under
|
||||
the install directory the package extracts to (currently C:\Games\...).
|
||||
|
||||
Key convention: generate one fresh Guid for the product id; the first
|
||||
<Launch> reuses it as its key, and each additional <Launch> increments the
|
||||
LAST HEX DIGIT of that Guid (+1, +2, ..., wrapping F->0). Do NOT append
|
||||
"-1"/"-2" to the string: keys are parsed as System.Guid and a suffixed
|
||||
string silently collapses to Guid.Empty. Exception: Red Planet 4.11's LC/MR
|
||||
keys predate the convention and are pinned to the original console's
|
||||
hardcoded Guids (SiteManagement.RPLCAppGuid/RPMRAppGuid + the diff tests);
|
||||
do not change them.
|
||||
-->
|
||||
<AppCatalog>
|
||||
<Product id="7D241B1F-AB6D-4e08-9C20-12294E743D94"
|
||||
@@ -53,6 +62,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="F4C957FD-72F7-4C5F-8971-28095007E8D0"
|
||||
displayName="BattleTech 4.11 LC"
|
||||
exe="C:\Games\BT411\btl4.exe"
|
||||
args="-net 1501{res} -lc"
|
||||
autoRestart="true"
|
||||
hostType="LiveCamera" />
|
||||
<Launch key="F4C957FD-72F7-4C5F-8971-28095007E8D1"
|
||||
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..."
|
||||
@@ -77,11 +113,61 @@
|
||||
name="RIOJoy"
|
||||
menuText="RIOJoy..."
|
||||
hostTypeDialog="false">
|
||||
<Launch key="FE83E212-45DF-48F9-848E-0B3CEE0692A3"
|
||||
<Launch key="87FBC2E6-6359-4EF4-96A5-DF157823CFF6"
|
||||
displayName="RIOJoy"
|
||||
exe="C:\Games\RIOJoy\app\RioJoy.Tray.exe"
|
||||
args=""
|
||||
autoRestart="true"
|
||||
hostType="None" />
|
||||
</Product>
|
||||
|
||||
<!-- TeslaRel410 - the DOSBox-X preservation pods (C:\VWE\TeslaRel410 repo).
|
||||
The package (emulator\dist\TeslaPod410.zip, built by deploy\package.ps1)
|
||||
extracts to C:\Games (postinstall.bat + TeslaPod410\); pod-launch.exe is
|
||||
the single supervising entry point and the mode arg ("bt"/"rp") selects
|
||||
the game. Camera ship and live mission review are NOT separate modes in
|
||||
4.10 (the console assigns the role per IP via the egg hostType), so the
|
||||
LC/MR entries boot identically and differ only by catalog role. No {res}
|
||||
token: DOSBox output size is fixed per rig at install time. -->
|
||||
<Product id="135019C7-2C2F-4C38-96BE-C7DB39994AB0"
|
||||
name="TeslaRel410"
|
||||
menuText="TeslaRel410..."
|
||||
hostTypeDialog="true">
|
||||
<Launch key="135019C7-2C2F-4C38-96BE-C7DB39994AB0"
|
||||
displayName="BT4.10"
|
||||
exe="C:\Games\TeslaPod410\pod-launch.exe"
|
||||
args="bt"
|
||||
autoRestart="true"
|
||||
hostType="GameClient" />
|
||||
<Launch key="135019C7-2C2F-4C38-96BE-C7DB39994AB1"
|
||||
displayName="BT4.10 LC"
|
||||
exe="C:\Games\TeslaPod410\pod-launch.exe"
|
||||
args="bt"
|
||||
autoRestart="true"
|
||||
hostType="LiveCamera" />
|
||||
<Launch key="135019C7-2C2F-4C38-96BE-C7DB39994AB2"
|
||||
displayName="BT4.10 MR"
|
||||
exe="C:\Games\TeslaPod410\pod-launch.exe"
|
||||
args="bt"
|
||||
autoRestart="true"
|
||||
hostType="MissionReview" />
|
||||
<Launch key="135019C7-2C2F-4C38-96BE-C7DB39994AB3"
|
||||
displayName="RP4.10"
|
||||
exe="C:\Games\TeslaPod410\pod-launch.exe"
|
||||
args="rp"
|
||||
autoRestart="true"
|
||||
hostType="GameClient" />
|
||||
<Launch key="135019C7-2C2F-4C38-96BE-C7DB39994AB4"
|
||||
displayName="RP4.10 LC"
|
||||
exe="C:\Games\TeslaPod410\pod-launch.exe"
|
||||
args="rp"
|
||||
autoRestart="true"
|
||||
hostType="LiveCamera" />
|
||||
<Launch key="135019C7-2C2F-4C38-96BE-C7DB39994AB5"
|
||||
displayName="RP4.10 MR"
|
||||
exe="C:\Games\TeslaPod410\pod-launch.exe"
|
||||
args="rp"
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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,364 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// When set, the BattleTech game pages target each pod's DOSBox-X guest
|
||||
/// (last octet +100) instead of the pod itself. A machine-level setting for
|
||||
/// the emulator preservation pods, so it is stored once per defaults file
|
||||
/// rather than per mode.
|
||||
/// </summary>
|
||||
public static bool DosBoxAddressShift { get; set; }
|
||||
|
||||
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;
|
||||
case "DosBoxAddressShift":
|
||||
{
|
||||
if (bool.TryParse(childNode.InnerText, out var result))
|
||||
{
|
||||
DosBoxAddressShift = result;
|
||||
}
|
||||
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);
|
||||
xmlDocument.DocumentElement.AppendChild(xmlDocument.CreateElement("DosBoxAddressShift")).InnerText = DosBoxAddressShift.ToString();
|
||||
string directoryName = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(directoryName))
|
||||
{
|
||||
Directory.CreateDirectory(directoryName);
|
||||
}
|
||||
xmlDocument.Save(filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
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 Label mDosBoxLabel;
|
||||
|
||||
private CheckBox mDosBoxShift;
|
||||
|
||||
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);
|
||||
mDosBoxShift.Checked = BTDefaults.DosBoxAddressShift;
|
||||
}
|
||||
|
||||
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.DosBoxAddressShift = mDosBoxShift.Checked;
|
||||
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.mDosBoxLabel = new System.Windows.Forms.Label();
|
||||
this.mDosBoxShift = new System.Windows.Forms.CheckBox();
|
||||
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.mDosBoxLabel, 0, 12);
|
||||
this.mMainTable.Controls.Add(this.mDosBoxShift, 1, 12);
|
||||
this.mMainTable.Controls.Add(this.mButtonsTable, 0, 13);
|
||||
this.mMainTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.mMainTable.Location = new System.Drawing.Point(3, 3);
|
||||
this.mMainTable.Name = "mMainTable";
|
||||
this.mMainTable.RowCount = 14;
|
||||
for (int i = 0; i < 13; 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, 414);
|
||||
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:");
|
||||
ConfigureRowLabel(this.mDosBoxLabel, "mDosBoxLabel", "DOSBox Build:");
|
||||
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.mMainTable.SetColumnSpan(this.mDosBoxShift, 2);
|
||||
this.mDosBoxShift.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.mDosBoxShift.AutoSize = true;
|
||||
this.mDosBoxShift.Name = "mDosBoxShift";
|
||||
this.mDosBoxShift.Text = "Shift all pod IPs +100 (DOSBox-X preservation build)";
|
||||
this.mDosBoxShift.UseVisualStyleBackColor = true;
|
||||
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, 420);
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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::<Key></c>); the egg then carries a matching
|
||||
/// <c>[Role::<Key>] model=<Model></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><invalid></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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
@@ -17,6 +19,15 @@ internal class Resources
|
||||
|
||||
private static CultureInfo resourceCulture;
|
||||
|
||||
// XP11: the images that used to live in the .resx as BinaryFormatter blobs now
|
||||
// ship as raw embedded files (logical name "TeslaConsole.Icons.<file>", original
|
||||
// bytes — see Console\assets\icons\). Building those blobs requires
|
||||
// System.Resources.Extensions' DeserializingResourceReader at runtime, which is
|
||||
// net461+ and cannot load on the XP-compatible net40. Strings stay in the .resx.
|
||||
// Instances are cached like ResourceManager.GetObject cached them: one shared
|
||||
// object per name.
|
||||
private static readonly Dictionary<string, object> imageCache = new Dictionary<string, object>();
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
internal static ResourceManager ResourceManager
|
||||
{
|
||||
@@ -43,262 +54,108 @@ internal class Resources
|
||||
}
|
||||
}
|
||||
|
||||
internal static Bitmap Add
|
||||
internal static Bitmap EmbeddedBitmap(string file)
|
||||
{
|
||||
get
|
||||
lock (imageCache)
|
||||
{
|
||||
object @object = ResourceManager.GetObject("Add", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
object image;
|
||||
if (!imageCache.TryGetValue(file, out image))
|
||||
{
|
||||
// Deliberately not disposed: GDI+ decodes lazily (e.g. the animated
|
||||
// GIF's frames), so the stream must outlive the Bitmap. It is an
|
||||
// UnmanagedMemoryStream over the mapped assembly image — no handle.
|
||||
Stream stream = typeof(Resources).Assembly.GetManifestResourceStream("TeslaConsole.Icons." + file);
|
||||
image = new Bitmap(stream);
|
||||
imageCache.Add(file, image);
|
||||
}
|
||||
return (Bitmap)image;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Bitmap Blank
|
||||
internal static Icon EmbeddedIcon(string file)
|
||||
{
|
||||
get
|
||||
lock (imageCache)
|
||||
{
|
||||
object @object = ResourceManager.GetObject("Blank", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
object icon;
|
||||
if (!imageCache.TryGetValue(file, out icon))
|
||||
{
|
||||
using (Stream stream = typeof(Resources).Assembly.GetManifestResourceStream("TeslaConsole.Icons." + file))
|
||||
{
|
||||
icon = new Icon(stream);
|
||||
}
|
||||
imageCache.Add(file, icon);
|
||||
}
|
||||
return (Icon)icon;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Bitmap DeleteHS
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("DeleteHS", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap Add => EmbeddedBitmap("Add.png");
|
||||
|
||||
internal static Bitmap Error
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("Error", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap Blank => EmbeddedBitmap("Blank.png");
|
||||
|
||||
internal static Bitmap DeleteHS => EmbeddedBitmap("DeleteHS.png");
|
||||
|
||||
internal static Bitmap Error => EmbeddedBitmap("Error.bmp");
|
||||
|
||||
internal static string ErrorReportsDir => ResourceManager.GetString("ErrorReportsDir", resourceCulture);
|
||||
|
||||
internal static Bitmap install
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("install", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap install => EmbeddedBitmap("install.png");
|
||||
|
||||
internal static Bitmap openHS
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("openHS", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap openHS => EmbeddedBitmap("openHS.png");
|
||||
|
||||
internal static Bitmap Play
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("Play", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap Play => EmbeddedBitmap("Play.png");
|
||||
|
||||
internal static Bitmap PodBad16
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodBad16", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodBad16 => EmbeddedBitmap("PodBad16.png");
|
||||
|
||||
internal static Bitmap PodBad32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodBad32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodBad32 => EmbeddedBitmap("PodBad32.png");
|
||||
|
||||
internal static string PodBadImageKey => ResourceManager.GetString("PodBadImageKey", resourceCulture);
|
||||
|
||||
internal static Bitmap PodGo16
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodGo16", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodGo16 => EmbeddedBitmap("PodGo16.png");
|
||||
|
||||
internal static Bitmap PodGo32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodGo32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodGo32 => EmbeddedBitmap("PodGo32.png");
|
||||
|
||||
internal static string PodGoImageKey => ResourceManager.GetString("PodGoImageKey", resourceCulture);
|
||||
|
||||
internal static Bitmap PodOffline16
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodOffline16", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodOffline16 => EmbeddedBitmap("PodOffline16.png");
|
||||
|
||||
internal static Bitmap PodOffline32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodOffline32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodOffline32 => EmbeddedBitmap("PodOffline32.png");
|
||||
|
||||
internal static string PodOfflineImageKey => ResourceManager.GetString("PodOfflineImageKey", resourceCulture);
|
||||
|
||||
internal static Bitmap PodOfflineQuestion32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodOfflineQuestion32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodOfflineQuestion32 => EmbeddedBitmap("PodOfflineQuestion32.png");
|
||||
|
||||
internal static Bitmap PodOnline16
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodOnline16", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodOnline16 => EmbeddedBitmap("PodOnline16.png");
|
||||
|
||||
internal static Bitmap PodOnline32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodOnline32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodOnline32 => EmbeddedBitmap("PodOnline32.png");
|
||||
|
||||
internal static string PodOnlineImageKey => ResourceManager.GetString("PodOnlineImageKey", resourceCulture);
|
||||
|
||||
internal static Bitmap PodQuestion16
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodQuestion16", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodQuestion16 => EmbeddedBitmap("PodQuestion16.png");
|
||||
|
||||
internal static Bitmap PodQuestion32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodQuestion32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodQuestion32 => EmbeddedBitmap("PodQuestion32.png");
|
||||
|
||||
internal static string PodQuestionImageKey => ResourceManager.GetString("PodQuestionImageKey", resourceCulture);
|
||||
|
||||
internal static Bitmap PodRun16
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodRun16", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodRun16 => EmbeddedBitmap("PodRun16.png");
|
||||
|
||||
internal static Bitmap PodRun32
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("PodRun32", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap PodRun32 => EmbeddedBitmap("PodRun32.png");
|
||||
|
||||
internal static string PodRunImageKey => ResourceManager.GetString("PodRunImageKey", resourceCulture);
|
||||
|
||||
internal static Bitmap Power
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("Power", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap Power => EmbeddedBitmap("Power.png");
|
||||
|
||||
internal static Bitmap RefreshDoc
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("RefreshDoc", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap RefreshDoc => EmbeddedBitmap("RefreshDoc.png");
|
||||
|
||||
internal static Bitmap saveHS
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("saveHS", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap saveHS => EmbeddedBitmap("saveHS.png");
|
||||
|
||||
internal static Bitmap square_throbber
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("square_throbber", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap square_throbber => EmbeddedBitmap("square_throbber.gif");
|
||||
|
||||
internal static Bitmap Stop
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("Stop", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap Stop => EmbeddedBitmap("Stop.png");
|
||||
|
||||
internal static Icon swirl
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("swirl", resourceCulture);
|
||||
return (Icon)@object;
|
||||
}
|
||||
}
|
||||
internal static Icon swirl => EmbeddedIcon("swirl.ico");
|
||||
|
||||
internal static Bitmap WebRefreshHH
|
||||
{
|
||||
get
|
||||
{
|
||||
object @object = ResourceManager.GetObject("WebRefreshHH", resourceCulture);
|
||||
return (Bitmap)@object;
|
||||
}
|
||||
}
|
||||
internal static Bitmap WebRefreshHH => EmbeddedBitmap("WebRefreshHH.png");
|
||||
|
||||
internal Resources()
|
||||
{
|
||||
|
||||
@@ -352,6 +352,14 @@ internal static class RPDefaults
|
||||
|
||||
public static FootballDefaults Football => sFootball;
|
||||
|
||||
/// <summary>
|
||||
/// When set, the Red Planet game pages target each pod's DOSBox-X guest
|
||||
/// (last octet +100) instead of the pod itself. A machine-level setting for
|
||||
/// the emulator preservation pods, so it is stored once per defaults file
|
||||
/// rather than per scenario.
|
||||
/// </summary>
|
||||
public static bool DosBoxAddressShift { get; set; }
|
||||
|
||||
static RPDefaults()
|
||||
{
|
||||
sDefaultsFilePath = Path.Combine(Program.GetCommonAppDataDirectory(), "RPDefaults.rpd");
|
||||
@@ -384,6 +392,14 @@ internal static class RPDefaults
|
||||
case "FootballDefaults":
|
||||
sFootball.Parse(childNode);
|
||||
break;
|
||||
case "DosBoxAddressShift":
|
||||
{
|
||||
if (bool.TryParse(childNode.InnerText, out var result))
|
||||
{
|
||||
DosBoxAddressShift = result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,6 +422,7 @@ internal static class RPDefaults
|
||||
xmlDocument.AppendChild(xmlDocument.CreateElement("RPDefaults"));
|
||||
sDeathRace.Save(xmlDocument.DocumentElement);
|
||||
sFootball.Save(xmlDocument.DocumentElement);
|
||||
xmlDocument.DocumentElement.AppendChild(xmlDocument.CreateElement("DosBoxAddressShift")).InnerText = DosBoxAddressShift.ToString();
|
||||
string directoryName = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(directoryName))
|
||||
{
|
||||
|
||||
@@ -77,6 +77,10 @@ public class RPDefaultsDialog : Form
|
||||
|
||||
private NumericUpDown mFootballLaunchDelay;
|
||||
|
||||
private Label mDosBoxLabel;
|
||||
|
||||
private CheckBox mDosBoxShift;
|
||||
|
||||
private static RPDefaultsDialog mSingletonDialog;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -123,6 +127,8 @@ public class RPDefaultsDialog : Form
|
||||
this.label11 = new System.Windows.Forms.Label();
|
||||
this.mDeathraceLaunchDelay = new System.Windows.Forms.NumericUpDown();
|
||||
this.mFootballLaunchDelay = new System.Windows.Forms.NumericUpDown();
|
||||
this.mDosBoxLabel = new System.Windows.Forms.Label();
|
||||
this.mDosBoxShift = new System.Windows.Forms.CheckBox();
|
||||
this.mMainTable.SuspendLayout();
|
||||
this.mButtonsTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)this.mDeathraceLaunchDelay).BeginInit();
|
||||
@@ -141,7 +147,7 @@ public class RPDefaultsDialog : Form
|
||||
this.mMainTable.Controls.Add(this.label9, 0, 8);
|
||||
this.mMainTable.Controls.Add(this.label1, 1, 0);
|
||||
this.mMainTable.Controls.Add(this.label10, 2, 0);
|
||||
this.mMainTable.Controls.Add(this.mButtonsTable, 0, 10);
|
||||
this.mMainTable.Controls.Add(this.mButtonsTable, 0, 11);
|
||||
this.mMainTable.Controls.Add(this.label5, 0, 6);
|
||||
this.mMainTable.Controls.Add(this.mDeathRaceVehicle, 1, 6);
|
||||
this.mMainTable.Controls.Add(this.mFootballVehicle, 2, 6);
|
||||
@@ -162,10 +168,13 @@ public class RPDefaultsDialog : Form
|
||||
this.mMainTable.Controls.Add(this.mFootballWeather, 2, 2);
|
||||
this.mMainTable.Controls.Add(this.label11, 0, 9);
|
||||
this.mMainTable.Controls.Add(this.mDeathraceLaunchDelay, 1, 9);
|
||||
this.mMainTable.Controls.Add(this.mDosBoxLabel, 0, 10);
|
||||
this.mMainTable.Controls.Add(this.mDosBoxShift, 1, 10);
|
||||
this.mMainTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.mMainTable.Location = new System.Drawing.Point(3, 3);
|
||||
this.mMainTable.Name = "mMainTable";
|
||||
this.mMainTable.RowCount = 11;
|
||||
this.mMainTable.RowCount = 12;
|
||||
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
@@ -177,7 +186,7 @@ public class RPDefaultsDialog : Form
|
||||
this.mMainTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
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, 298);
|
||||
this.mMainTable.Size = new System.Drawing.Size(478, 328);
|
||||
this.mMainTable.TabIndex = 0;
|
||||
this.mFootballPosition.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
this.mFootballPosition.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
@@ -406,10 +415,26 @@ public class RPDefaultsDialog : Form
|
||||
this.mFootballLaunchDelay.Name = "mFootballLaunchDelay";
|
||||
this.mFootballLaunchDelay.Size = new System.Drawing.Size(173, 20);
|
||||
this.mFootballLaunchDelay.TabIndex = 32;
|
||||
this.mDosBoxLabel.Anchor = System.Windows.Forms.AnchorStyles.Right;
|
||||
this.mDosBoxLabel.AutoSize = true;
|
||||
this.mDosBoxLabel.Location = new System.Drawing.Point(35, 258);
|
||||
this.mDosBoxLabel.Name = "mDosBoxLabel";
|
||||
this.mDosBoxLabel.Size = new System.Drawing.Size(82, 13);
|
||||
this.mDosBoxLabel.TabIndex = 29;
|
||||
this.mDosBoxLabel.Text = "DOSBox Build:";
|
||||
this.mMainTable.SetColumnSpan(this.mDosBoxShift, 2);
|
||||
this.mDosBoxShift.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.mDosBoxShift.AutoSize = true;
|
||||
this.mDosBoxShift.Location = new System.Drawing.Point(123, 256);
|
||||
this.mDosBoxShift.Name = "mDosBoxShift";
|
||||
this.mDosBoxShift.Size = new System.Drawing.Size(268, 17);
|
||||
this.mDosBoxShift.TabIndex = 33;
|
||||
this.mDosBoxShift.Text = "Shift all pod IPs +100 (DOSBox-X preservation build)";
|
||||
this.mDosBoxShift.UseVisualStyleBackColor = true;
|
||||
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, 304);
|
||||
base.ClientSize = new System.Drawing.Size(484, 334);
|
||||
base.Controls.Add(this.mMainTable);
|
||||
base.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
base.MaximizeBox = false;
|
||||
@@ -457,6 +482,7 @@ public class RPDefaultsDialog : Form
|
||||
BuildGenericOption(mFootballPosition, RPConfig.Football.Positions, RPDefaults.Football.PositionKey);
|
||||
BuildDelayControl(mDeathraceLaunchDelay, RPDefaults.DeathRace.LaunchDelay);
|
||||
BuildDelayControl(mFootballLaunchDelay, RPDefaults.Football.LaunchDelay);
|
||||
mDosBoxShift.Checked = RPDefaults.DosBoxAddressShift;
|
||||
}
|
||||
|
||||
private static void BuildScoreCompressionOptions(ComboBox comboBox, bool defaultValue)
|
||||
@@ -577,6 +603,7 @@ public class RPDefaultsDialog : Form
|
||||
RPDefaults.Football.PositionKey = ExtractSelectedKey<string>(mFootballPosition);
|
||||
RPDefaults.DeathRace.LaunchDelay = (int)mDeathraceLaunchDelay.Value;
|
||||
RPDefaults.Football.LaunchDelay = (int)mFootballLaunchDelay.Value;
|
||||
RPDefaults.DosBoxAddressShift = mDosBoxShift.Checked;
|
||||
RPDefaults.Save();
|
||||
Hide();
|
||||
}
|
||||
|
||||
@@ -176,6 +176,8 @@ public class RPGame : DockContent
|
||||
|
||||
private Timer tmrAutoTranslocate;
|
||||
|
||||
private readonly bool mDosBoxAddressShift = RPDefaults.DosBoxAddressShift;
|
||||
|
||||
private string GameStatusText
|
||||
{
|
||||
set
|
||||
@@ -421,6 +423,11 @@ public class RPGame : DockContent
|
||||
return ((KeyValuePair<string, string>)cell.Value).Key;
|
||||
}
|
||||
|
||||
private string MissionAddress(Pod pod)
|
||||
{
|
||||
return (mDosBoxAddressShift ? DosBox.ShiftAddress(pod.IPAddress) : pod.IPAddress).ToString();
|
||||
}
|
||||
|
||||
private void NetworkScan(object sender, EventArgs e)
|
||||
{
|
||||
if (mCurrentState == RPGameState.Run && mRequestedState == RPGameState.Run && !mProcessingMissionEndStateChangeReq)
|
||||
@@ -791,7 +798,7 @@ public class RPGame : DockContent
|
||||
{
|
||||
RPTeam rPTeam = ((item.Cells[mColorColumn.Index].Value is RPTeam) ? ((RPTeam)item.Cells[mColorColumn.Index].Value) : RPConfig.Football.Teams[(string)item.Cells[mColorColumn.Index].Value]);
|
||||
string stringOrKey = GetStringOrKey(item.Cells[mBadgeColumn.Index]);
|
||||
RPFootballPlayer rPFootballPlayer = new RPFootballPlayer(pod.IPAddress.ToString(), value = (string)item.Cells[mPilotColumn.Index].Value, (item.Cells[mVehicleColumn.Index].Value is string) ? ((string)item.Cells[mVehicleColumn.Index].Value) : ((RPVehicle)item.Cells[mVehicleColumn.Index].Value).Key, rPTeam.Key, stringOrKey, (stringOrKey == "runner") ? rPTeam.RunnerColor : rPTeam.TeamColor);
|
||||
RPFootballPlayer rPFootballPlayer = new RPFootballPlayer(MissionAddress(pod), value = (string)item.Cells[mPilotColumn.Index].Value, (item.Cells[mVehicleColumn.Index].Value is string) ? ((string)item.Cells[mVehicleColumn.Index].Value) : ((RPVehicle)item.Cells[mVehicleColumn.Index].Value).Key, rPTeam.Key, stringOrKey, (stringOrKey == "runner") ? rPTeam.RunnerColor : rPTeam.TeamColor);
|
||||
if (!dictionary.ContainsKey(rPFootballPlayer.TeamKey))
|
||||
{
|
||||
dictionary[rPFootballPlayer.TeamKey] = new List<RPFootballPlayer>();
|
||||
@@ -800,7 +807,7 @@ public class RPGame : DockContent
|
||||
}
|
||||
else
|
||||
{
|
||||
RPRacePlayer rPRacePlayer = new RPRacePlayer(pod.IPAddress.ToString(), value = (string)item.Cells[mPilotColumn.Index].Value, (item.Cells[mVehicleColumn.Index].Value is string) ? ((string)item.Cells[mVehicleColumn.Index].Value) : ((RPVehicle)item.Cells[mVehicleColumn.Index].Value).Key, GetStringOrKey(item.Cells[mColorColumn.Index]), GetStringOrKey(item.Cells[mBadgeColumn.Index]));
|
||||
RPRacePlayer rPRacePlayer = new RPRacePlayer(MissionAddress(pod), value = (string)item.Cells[mPilotColumn.Index].Value, (item.Cells[mVehicleColumn.Index].Value is string) ? ((string)item.Cells[mVehicleColumn.Index].Value) : ((RPVehicle)item.Cells[mVehicleColumn.Index].Value).Key, GetStringOrKey(item.Cells[mColorColumn.Index]), GetStringOrKey(item.Cells[mBadgeColumn.Index]));
|
||||
((RPRaceMission)rPMission).RacePlayers.Add(rPRacePlayer);
|
||||
mMissionPlayers.Add(num++, rPRacePlayer);
|
||||
}
|
||||
@@ -808,7 +815,7 @@ public class RPGame : DockContent
|
||||
}
|
||||
else
|
||||
{
|
||||
rPMission.Cameras.Add(new RPCamera(pod.IPAddress.ToString(), pod.HostType));
|
||||
rPMission.Cameras.Add(new RPCamera(MissionAddress(pod), pod.HostType));
|
||||
}
|
||||
}
|
||||
if (mFootballMode)
|
||||
@@ -1187,6 +1194,7 @@ public class RPGame : DockContent
|
||||
{
|
||||
if (flag.Value)
|
||||
{
|
||||
((Pod)dataGridViewRow.Tag).MungaGame.DosBoxAddressShift = mDosBoxAddressShift;
|
||||
((Pod)dataGridViewRow.Tag).MungaGame.MakeRequested(this);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
<AssemblyName>TeslaConsole</AssemblyName>
|
||||
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>True</UseWindowsForms>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<!-- net40 (XP11): the newest .NET Framework that installs on Windows XP SP3;
|
||||
net40 assemblies load in-place on the 4.8 runtime in Win10/11, so this ONE
|
||||
exe covers XP SP3 through Windows 11 — same deal as the Launcher. The
|
||||
original console was net20, so the decompiled core needs nothing newer.
|
||||
WinForms comes via plain framework references (UseWindowsForms is not
|
||||
wired up for net40). -->
|
||||
<TargetFramework>net40</TargetFramework>
|
||||
<LangVersion>Preview</LangVersion>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
@@ -12,17 +17,21 @@
|
||||
<!-- Decompiled from the original net20 TeslaConsole.exe; legacy WinForms 2.0 sources -->
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<!-- .resx files embed BinaryFormatter-serialized bitmaps (non-string resources) -->
|
||||
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
|
||||
<!-- CS0649: decompiled WinForms designer 'components' fields are never assigned -->
|
||||
<NoWarn>$(NoWarn);CS0649</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- net48 reference assemblies so the project builds without a full targeting pack installed -->
|
||||
<!-- .NET Framework reference assemblies so the project builds without a full targeting pack installed -->
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<!-- Required to read the pre-serialized binary resources above -->
|
||||
<PackageReference Include="System.Resources.Extensions" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The UI images, embedded as their raw original bytes (extracted 1:1 from the
|
||||
old .resx BinaryFormatter blobs). Loaded by Properties.Resources.EmbeddedBitmap/
|
||||
EmbeddedIcon: the .resx blob route needs System.Resources.Extensions at
|
||||
runtime, which is net461+ and cannot load on net40/XP. -->
|
||||
<EmbeddedResource Include="assets\icons\*.*" LogicalName="TeslaConsole.Icons.%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -30,6 +39,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>
|
||||
@@ -46,6 +57,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Configuration.Install" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
|
||||
@@ -80,7 +80,9 @@ public static class AppRegistry
|
||||
|
||||
private static readonly Dictionary<Guid, ProductDefinition> mById = new Dictionary<Guid, ProductDefinition>();
|
||||
|
||||
public static IReadOnlyList<ProductDefinition> Products => mProducts;
|
||||
// IList, not IReadOnlyList: the read-only interfaces are net45+ and the
|
||||
// XP11 console targets net40. Callers only enumerate/index it.
|
||||
public static IList<ProductDefinition> Products => mProducts;
|
||||
|
||||
public static string CatalogPath =>
|
||||
Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "RedPlanet\\Apps.xml");
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Net;
|
||||
|
||||
namespace TeslaConsole;
|
||||
|
||||
/// <summary>
|
||||
/// Support for the DOSBox-X preservation pods. The emulator that runs the
|
||||
/// original DOS build of a game is bridged onto the pod network with its last
|
||||
/// octet enumerated 100 above the host pod's address, so when a game page is
|
||||
/// driving the emulated version it must target host+100 instead of the
|
||||
/// address stored in the site config. Only the game traffic shifts — the
|
||||
/// launcher and provisioning services still run on the host itself.
|
||||
/// </summary>
|
||||
internal static class DosBox
|
||||
{
|
||||
public const int AddressOffset = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the address with its last octet raised by <see cref="AddressOffset"/>.
|
||||
/// The octet wraps modulo 256; real site configs keep host octets at or
|
||||
/// below 155 so the shifted address stays inside the subnet.
|
||||
/// </summary>
|
||||
public static IPAddress ShiftAddress(IPAddress address)
|
||||
{
|
||||
byte[] bytes = address.GetAddressBytes();
|
||||
bytes[bytes.Length - 1] = (byte)(bytes[bytes.Length - 1] + AddressOffset);
|
||||
return new IPAddress(bytes);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ public class MungaGame
|
||||
|
||||
private MungaConnectionState mState;
|
||||
|
||||
private bool mDosBoxAddressShift;
|
||||
|
||||
private readonly List<object> mRequestors = new List<object>();
|
||||
|
||||
private Form mOwner;
|
||||
@@ -109,6 +111,37 @@ public class MungaGame
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When set, the game connection targets the pod's DOSBox-X guest
|
||||
/// (last octet +100) instead of the pod itself. Changing the value while
|
||||
/// connected drops the connection; it reconnects at the new address as
|
||||
/// long as the game is still requested.
|
||||
/// </summary>
|
||||
internal bool DosBoxAddressShift
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (mInternalLock)
|
||||
{
|
||||
return mDosBoxAddressShift;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (mInternalLock)
|
||||
{
|
||||
if (mDosBoxAddressShift != value)
|
||||
{
|
||||
mDosBoxAddressShift = value;
|
||||
if (mState != MungaConnectionState.Disconnected)
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOwned
|
||||
{
|
||||
get
|
||||
@@ -262,7 +295,7 @@ public class MungaGame
|
||||
State = MungaConnectionState.Connecting;
|
||||
ShutdownSocket();
|
||||
mSocket = new MungaSocket();
|
||||
mSocket.BeginConnect(mPod.IPAddress, 1501, delegate(IAsyncResult ar)
|
||||
mSocket.BeginConnect(mDosBoxAddressShift ? DosBox.ShiftAddress(mPod.IPAddress) : mPod.IPAddress, 1501, delegate(IAsyncResult ar)
|
||||
{
|
||||
lock (mInternalLock)
|
||||
{
|
||||
|
||||
@@ -617,6 +617,39 @@ public class PodInfo : Component
|
||||
asyncControlState.asyncOp.PostOperationCompleted(installProductDelegates.OnCompleted, arg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the management connection if needed. A connection the launcher has
|
||||
/// dropped still reports IsOpen (Connected is stale until an I/O fails), and
|
||||
/// the launcher drops sessions idle for ~30s — easily hit while the operator
|
||||
/// sits in the install dialogs — so probe an "open" connection with a Ping
|
||||
/// and reconnect when it turns out to be dead.
|
||||
/// </summary>
|
||||
private void EnsureConnectionAlive()
|
||||
{
|
||||
if (mConnection.IsOpen)
|
||||
{
|
||||
try
|
||||
{
|
||||
mConnection.Ping(DateTime.Now);
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
mConnection.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mConnection.IsOpen)
|
||||
{
|
||||
mConnection.Open(new IPEndPoint(mPod.IPAddress, ManagePort), mPod.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void InstallProductWorker(string filePath, SendOrPostCallback progressCallback, AsyncOperation asyncOp, SendOrPostCallback completionMethodDelegate)
|
||||
{
|
||||
Exception ex = null;
|
||||
@@ -624,10 +657,7 @@ public class PodInfo : Component
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (!mConnection.IsOpen)
|
||||
{
|
||||
mConnection.Open(new IPEndPoint(mPod.IPAddress, 53290), mPod.Key);
|
||||
}
|
||||
EnsureConnectionAlive();
|
||||
Guid guid = mConnection.InstallProduct(filePath);
|
||||
if (guid == Guid.Empty)
|
||||
{
|
||||
@@ -709,10 +739,7 @@ public class PodInfo : Component
|
||||
Exception ex = null;
|
||||
try
|
||||
{
|
||||
if (!mConnection.IsOpen)
|
||||
{
|
||||
mConnection.Open(new IPEndPoint(mPod.IPAddress, 53290), mPod.Key);
|
||||
}
|
||||
EnsureConnectionAlive();
|
||||
mConnection.UninstallApp(launchKey);
|
||||
mAllApps.Expire();
|
||||
}
|
||||
|
||||
@@ -1138,12 +1138,27 @@ public class SiteManagement : Form
|
||||
{
|
||||
Tuple<PodData, Guid> tuple = e.UserState as Tuple<PodData, Guid>;
|
||||
PodData a = tuple.A;
|
||||
foreach (LaunchData appData in BuildLaunchData(tuple.B, a))
|
||||
Exception error = e.Error;
|
||||
// Register the launch entries only when the transfer succeeded, and never
|
||||
// let a pod that dropped mid-install take the console down (AddApp throws
|
||||
// when the connection is gone).
|
||||
if (error == null)
|
||||
{
|
||||
a.Conn.AddApp(appData);
|
||||
try
|
||||
{
|
||||
foreach (LaunchData appData in BuildLaunchData(tuple.B, a))
|
||||
{
|
||||
a.Conn.AddApp(appData);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.LogException(ex, "Registering launch entries failed.");
|
||||
error = ex;
|
||||
}
|
||||
}
|
||||
a.OperationInProgress = false;
|
||||
a.Error = e.Error;
|
||||
a.Error = error;
|
||||
dgvPods.InvalidateCell(colOperationProgress.Index, mPods.IndexOf(a));
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ internal class SitePanel : DockContent
|
||||
this.tsbManageSite.Text = "Manage Site";
|
||||
this.tsbManageSite.Click += new System.EventHandler(tsbManageSite_Click);
|
||||
this.mPowerDropDown.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[5] { this.mRestartAllPods, this.mShutdownAllPods, this.mPowerSeparator, this.mRestartAllCheckedPods, this.mShutdownAllCheckedPods });
|
||||
this.mPowerDropDown.Image = (System.Drawing.Image)resources.GetObject("mPowerDropDown.Image");
|
||||
this.mPowerDropDown.Image = TeslaConsole.Properties.Resources.EmbeddedBitmap("SitePanel.mPowerDropDown.Image.png");
|
||||
this.mPowerDropDown.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.mPowerDropDown.Name = "mPowerDropDown";
|
||||
this.mPowerDropDown.Size = new System.Drawing.Size(69, 22);
|
||||
@@ -302,7 +302,7 @@ internal class SitePanel : DockContent
|
||||
this.lblPodName.Text = "Mistress Quickly";
|
||||
this.picRefresh.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
this.picRefresh.Cursor = System.Windows.Forms.Cursors.Hand;
|
||||
this.picRefresh.Image = (System.Drawing.Image)resources.GetObject("picRefresh.Image");
|
||||
this.picRefresh.Image = TeslaConsole.Properties.Resources.EmbeddedBitmap("SitePanel.picRefresh.Image.png");
|
||||
this.picRefresh.Location = new System.Drawing.Point(308, 3);
|
||||
this.picRefresh.Name = "picRefresh";
|
||||
this.picRefresh.Size = new System.Drawing.Size(32, 32);
|
||||
|
||||
@@ -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);
|
||||
@@ -576,19 +758,22 @@ public class TeslaConsoleForm : Form
|
||||
this.mRPPrintPreviewDialog.AutoScrollMinSize = new System.Drawing.Size(0, 0);
|
||||
this.mRPPrintPreviewDialog.ClientSize = new System.Drawing.Size(400, 300);
|
||||
this.mRPPrintPreviewDialog.Enabled = true;
|
||||
this.mRPPrintPreviewDialog.Icon = (System.Drawing.Icon)resources.GetObject("mRPPrintPreviewDialog.Icon");
|
||||
this.mRPPrintPreviewDialog.Icon = TeslaConsole.Properties.Resources.EmbeddedIcon("TeslaConsoleForm.mRPPrintPreviewDialog.Icon.ico");
|
||||
this.mRPPrintPreviewDialog.Name = "printPreviewDialog1";
|
||||
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);
|
||||
base.Controls.Add(this.mMainDockPanel);
|
||||
base.Controls.Add(this.mMenu);
|
||||
base.Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon");
|
||||
base.Icon = TeslaConsole.Properties.Resources.EmbeddedIcon("TeslaConsoleForm.$this.Icon.ico");
|
||||
base.IsMdiContainer = true;
|
||||
base.MainMenuStrip = this.mMenu;
|
||||
base.Name = "TeslaConsoleForm";
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 938 B |
|
After Width: | Height: | Size: 693 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 314 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 454 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 501 B |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 772 B |
|
After Width: | Height: | Size: 700 B |
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 138 KiB |
@@ -2,9 +2,10 @@
|
||||
:: =============================================================================
|
||||
:: TeslaConsole - Build ^& Package
|
||||
:: =============================================================================
|
||||
:: Publishes the console (net48, framework-dependent) into TeslaConsole\App and
|
||||
:: assembles the installable package (App\ + install.bat) next to it. net48 is
|
||||
:: in-box on Windows 10/11, so the target control PC needs no runtime install.
|
||||
:: Publishes the console (net40, framework-dependent - XP11: runs on XP SP3
|
||||
:: through Windows 11) into TeslaConsole\App and assembles the installable
|
||||
:: package (App\ + install.bat) next to it. The 4.x runtime is in-box on
|
||||
:: Windows 10/11; an XP-era control PC needs dotNetFx40_Full_x86_x64.exe once.
|
||||
::
|
||||
:: Requirements: .NET SDK (6.0+) to drive the build; internet for first restore.
|
||||
::
|
||||
@@ -24,7 +25,7 @@ set ZIP=%ROOT%dist\TeslaConsole-pkg.zip
|
||||
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo TeslaConsole - Build ^& Package (net48, framework-dependent)
|
||||
echo TeslaConsole - Build ^& Package (net40, framework-dependent)
|
||||
echo Output : %BUILD_DIR%
|
||||
echo ============================================================
|
||||
echo.
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace TeslaConsole.DiffTests
|
||||
/// <summary>
|
||||
/// Locates the two assemblies under comparison:
|
||||
/// * Original - original/TeslaConsole.exe (the lost-source reference baseline)
|
||||
/// * Recovered - bin/Release/net48/TeslaConsole.exe (freshly built reconstruction)
|
||||
/// * Recovered - bin/Release/net40/TeslaConsole.exe (freshly built reconstruction;
|
||||
/// net40 since XP11 — loads fine in this net48 test host, both are CLR4)
|
||||
/// </summary>
|
||||
public static class AssemblyPaths
|
||||
{
|
||||
@@ -39,8 +40,8 @@ namespace TeslaConsole.DiffTests
|
||||
|
||||
private static string FindRecoveredExe()
|
||||
{
|
||||
string release = Path.Combine(RepoRoot, "bin", "Release", "net48", "TeslaConsole.exe");
|
||||
string debug = Path.Combine(RepoRoot, "bin", "Debug", "net48", "TeslaConsole.exe");
|
||||
string release = Path.Combine(RepoRoot, "bin", "Release", "net40", "TeslaConsole.exe");
|
||||
string debug = Path.Combine(RepoRoot, "bin", "Debug", "net40", "TeslaConsole.exe");
|
||||
|
||||
// Test whichever build is freshest, so a stale config never silently wins.
|
||||
string best = null;
|
||||
|
||||
@@ -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 1st–4th 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
|
||||
@@ -47,7 +47,7 @@ namespace TeslaConsole.DiffTests
|
||||
Assert.Contains("TeslaConsole", _fx.Original.AssemblyFullName);
|
||||
Assert.Contains("TeslaConsole", _fx.Recovered.AssemblyFullName);
|
||||
Assert.Contains("4.11.3.37076", _fx.Original.AssemblyFullName);
|
||||
Assert.Contains("4.11.4.1", _fx.Recovered.AssemblyFullName);
|
||||
Assert.Contains("4.11.4.3", _fx.Recovered.AssemblyFullName);
|
||||
}
|
||||
|
||||
// ---- RPStrings.GetTimeString: mm:ss formatting with 0.5s rounding ----
|
||||
|
||||
@@ -25,15 +25,15 @@ 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_Fourteen_Entries()
|
||||
=> Assert.Equal("products=5;entries=14",
|
||||
_fx.Recovered.Run("CatalogSummary", new[] { _catalog }));
|
||||
|
||||
[Fact]
|
||||
public void RioJoy_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"RIOJoy|fe83e212-45df-48f9-848e-0b3cee0692a3|C:\Games\RIOJoy\app\RioJoy.Tray.exe||C:\Games\RIOJoy\app|True",
|
||||
Entry("FE83E212-45DF-48F9-848E-0B3CEE0692A3"));
|
||||
@"RIOJoy|87fbc2e6-6359-4ef4-96a5-df157823cff6|C:\Games\RIOJoy\app\RioJoy.Tray.exe||C:\Games\RIOJoy\app|True",
|
||||
Entry("87FBC2E6-6359-4EF4-96A5-DF157823CFF6"));
|
||||
|
||||
// Each expected string is "DisplayName|LaunchKey|Exe|Args|WorkingDirectory|AutoRestart"
|
||||
// and matches exactly what the old hardcoded PodInfo_InstallProductCompleted emitted.
|
||||
@@ -73,5 +73,79 @@ 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|f4c957fd-72f7-4c5f-8971-28095007e8d0|C:\Games\BT411\btl4.exe|-net 1501 -lc|C:\Games\BT411|True",
|
||||
Entry("F4C957FD-72F7-4C5F-8971-28095007E8D0"));
|
||||
|
||||
[Fact]
|
||||
public void BattleTech_MissionReview_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"BattleTech 4.11 MR|f4c957fd-72f7-4c5f-8971-28095007e8d1|C:\Games\BT411\btl4.exe|-net 1501 -mr|C:\Games\BT411|True",
|
||||
Entry("F4C957FD-72F7-4C5F-8971-28095007E8D1"));
|
||||
|
||||
// TeslaRel410 — the DOSBox-X preservation pods. All six entries launch
|
||||
// pod-launch.exe; the mode arg ("bt"/"rp") selects the game, LC/MR boot
|
||||
// identically (the console assigns the role via the egg hostType), and
|
||||
// {res} is intentionally absent (output size is fixed per rig).
|
||||
|
||||
[Fact]
|
||||
public void Rel410_BT_GameClient_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"BT4.10|135019c7-2c2f-4c38-96be-c7db39994ab0|C:\Games\TeslaPod410\pod-launch.exe|bt|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB0"));
|
||||
|
||||
[Fact]
|
||||
public void Rel410_BT_Resolution_Choice_Has_No_Effect()
|
||||
=> Assert.Equal(
|
||||
@"BT4.10|135019c7-2c2f-4c38-96be-c7db39994ab0|C:\Games\TeslaPod410\pod-launch.exe|bt|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB0", "1024", "768"));
|
||||
|
||||
[Fact]
|
||||
public void Rel410_BT_LiveCamera_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"BT4.10 LC|135019c7-2c2f-4c38-96be-c7db39994ab1|C:\Games\TeslaPod410\pod-launch.exe|bt|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB1"));
|
||||
|
||||
[Fact]
|
||||
public void Rel410_BT_MissionReview_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"BT4.10 MR|135019c7-2c2f-4c38-96be-c7db39994ab2|C:\Games\TeslaPod410\pod-launch.exe|bt|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB2"));
|
||||
|
||||
[Fact]
|
||||
public void Rel410_RP_GameClient_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"RP4.10|135019c7-2c2f-4c38-96be-c7db39994ab3|C:\Games\TeslaPod410\pod-launch.exe|rp|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB3"));
|
||||
|
||||
[Fact]
|
||||
public void Rel410_RP_LiveCamera_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"RP4.10 LC|135019c7-2c2f-4c38-96be-c7db39994ab4|C:\Games\TeslaPod410\pod-launch.exe|rp|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB4"));
|
||||
|
||||
[Fact]
|
||||
public void Rel410_RP_MissionReview_Matches_Expected()
|
||||
=> Assert.Equal(
|
||||
@"RP4.10 MR|135019c7-2c2f-4c38-96be-c7db39994ab5|C:\Games\TeslaPod410\pod-launch.exe|rp|C:\Games\TeslaPod410|True",
|
||||
Entry("135019C7-2C2F-4C38-96BE-C7DB39994AB5"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Tesla.Net;
|
||||
using Xunit;
|
||||
|
||||
@@ -29,8 +28,8 @@ namespace TeslaConsole.DiffTests
|
||||
var req = PodRpc.ReadRequest(ms);
|
||||
Assert.Equal("KillApp", req.Method);
|
||||
Assert.Equal(2, req.Args.Count);
|
||||
Assert.Equal(Key, req.Args[0].GetGuid());
|
||||
Assert.Equal(4242, req.Args[1].GetInt32());
|
||||
Assert.Equal(Key, req.Args[0].ToObject<Guid>(PodRpc.JsonOptions));
|
||||
Assert.Equal(4242, req.Args[1].ToObject<int>(PodRpc.JsonOptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -150,7 +149,7 @@ namespace TeslaConsole.DiffTests
|
||||
ms.Position = 0;
|
||||
var resp = PodRpc.ReadResponse(ms);
|
||||
Assert.Null(resp.Error);
|
||||
return resp.Result.Deserialize<T>(PodRpc.JsonOptions);
|
||||
return resp.Result.ToObject<T>(PodRpc.JsonOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -58,7 +76,9 @@ dotnet test tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj
|
||||
```
|
||||
|
||||
A project reference builds the reconstruction first, and the suite always tests
|
||||
the most recently built `bin/{Debug,Release}/net48/TeslaConsole.exe`.
|
||||
the most recently built `bin/{Debug,Release}/net40/TeslaConsole.exe` (net40 since
|
||||
the XP11 port; the net48 test host loads it fine — both are CLR4, so the whole
|
||||
process runs the net40/Newtonsoft stack that ships).
|
||||
|
||||
## Scope / limitations
|
||||
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Xml" />
|
||||
<!-- Zip building for the VPodLauncherServerTests InstallProduct round-trip -->
|
||||
<Reference Include="System.IO.Compression" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- BattleTech golden eggs captured from the original consoles (see BTGoldenEggTests). -->
|
||||
<Content Include="BattleTech\*.egg" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -43,11 +50,16 @@
|
||||
</ProjectReference>
|
||||
<!-- The source-built wire contract (emits TeslaConsoleLaunchLib.dll). Referenced
|
||||
directly so WireContractCompatTests can construct Tesla.Net types and compare
|
||||
their BinaryFormatter output against the original vendored DLL. -->
|
||||
their BinaryFormatter output against the original vendored DLL. net40 (like
|
||||
everything under test since XP11); loads fine in this net48 host — both are
|
||||
CLR4 — so the suite exercises the exact Newtonsoft stack that ships. -->
|
||||
<ProjectReference Include="..\..\..\Contract\Tesla.Contract.csproj" />
|
||||
<!-- The source-built secure-config (emits TeslaSecureConfiguration.dll), for
|
||||
SecureConfigCompatTests' byte-identity checks vs the original vendored DLL. -->
|
||||
<ProjectReference Include="..\..\..\SecureConfig\Tesla.SecureConfig.csproj" />
|
||||
<!-- vPOD's virtual launcher (server side of the pod RPC), exercised end-to-end
|
||||
against the real PodManagerConnection client in VPodLauncherServerTests. -->
|
||||
<ProjectReference Include="..\..\..\vPOD\vPOD.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using Tesla.Net;
|
||||
using VPod;
|
||||
using Xunit;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
{
|
||||
/// <summary>
|
||||
/// End-to-end loopback exercise of vPOD's virtual launcher: the REAL
|
||||
/// console-side client (PodManagerConnection) against vPOD's LauncherRpcServer
|
||||
/// on an ephemeral port. Covers the OFB/CONF handshake, the ILauncherService
|
||||
/// dispatch surface, and the out-of-band InstallProduct transfer — including
|
||||
/// the 99%-complete convention the console's install retry loop depends on.
|
||||
/// This validates both ends: the client the console ships and the server vPOD
|
||||
/// uses to stand in for a pod's TeslaLauncher service.
|
||||
/// </summary>
|
||||
public class VPodLauncherServerTests : IDisposable
|
||||
{
|
||||
private readonly string mDataDir;
|
||||
private readonly VirtualLauncher mLauncher;
|
||||
private readonly LauncherRpcServer mServer;
|
||||
private readonly PodManagerConnection mClient;
|
||||
private readonly byte[] mKey;
|
||||
private readonly int mPort;
|
||||
|
||||
public VPodLauncherServerTests()
|
||||
{
|
||||
mDataDir = Path.Combine(Path.GetTempPath(), "vpod-test-" + Guid.NewGuid().ToString("N"));
|
||||
// Isolated games root: vPOD's default is the real C:\Games (launcher parity).
|
||||
mLauncher = new VirtualLauncher(mDataDir, Path.Combine(mDataDir, "Games"));
|
||||
mKey = new byte[32];
|
||||
new Random(1234).NextBytes(mKey);
|
||||
mPort = GetFreePort();
|
||||
mServer = new LauncherRpcServer(mLauncher, mPort);
|
||||
mServer.Start(mKey);
|
||||
mClient = new PodManagerConnection();
|
||||
mClient.Open(new IPEndPoint(IPAddress.Loopback, mPort), mKey);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { mClient.Close(); } catch { }
|
||||
mServer.Stop();
|
||||
try { Directory.Delete(mDataDir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handshake_And_Ping_RoundTrip()
|
||||
{
|
||||
Assert.True(mClient.IsOpen);
|
||||
var now = new DateTime(2026, 7, 9, 12, 0, 0, DateTimeKind.Utc);
|
||||
Assert.Equal(now, mClient.Ping(now));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handshake_With_Wrong_Key_Is_Rejected()
|
||||
{
|
||||
var wrongKey = new byte[32];
|
||||
new Random(9999).NextBytes(wrongKey);
|
||||
using (var badClient = new PodManagerConnection())
|
||||
{
|
||||
Assert.Throws<IOException>(
|
||||
() => badClient.Open(new IPEndPoint(IPAddress.Loopback, mPort), wrongKey));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallApp_Appears_In_GetInstalledApps_And_FullUpdate()
|
||||
{
|
||||
var app = SampleApp("Red Planet 4.11");
|
||||
mClient.InstallApp(app);
|
||||
|
||||
var installed = mClient.GetInstalledApps();
|
||||
Assert.Single(installed);
|
||||
Assert.Equal(app.LaunchPair.LaunchKey, installed[0].LaunchPair.LaunchKey);
|
||||
Assert.Equal(app.ExeFile, installed[0].ExeFile);
|
||||
Assert.Equal(app.Arguments, installed[0].Arguments);
|
||||
Assert.True(installed[0].AutoRestart);
|
||||
|
||||
var launchable = mClient.GetLaunchableApps();
|
||||
Assert.Single(launchable);
|
||||
Assert.Equal("Red Planet 4.11", launchable[0].DisplayName);
|
||||
|
||||
var full = mClient.FullUpdate();
|
||||
Assert.Single(full.InstalledApps);
|
||||
Assert.Empty(full.LaunchedApps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LaunchApp_Simulates_Pids_And_Kill_Removes_Them()
|
||||
{
|
||||
var app = SampleApp("BattleTech 4.11");
|
||||
mClient.InstallApp(app);
|
||||
|
||||
int pid = mClient.LaunchApp(app.LaunchPair.LaunchKey);
|
||||
Assert.True(pid > 0);
|
||||
var launched = mClient.GetLaunchedApps();
|
||||
Assert.Single(launched);
|
||||
Assert.Equal(pid, launched[0].ProcessId);
|
||||
Assert.Equal(app.LaunchPair.LaunchKey, launched[0].LaunchKey);
|
||||
|
||||
mClient.KillApp(app.LaunchPair.LaunchKey, pid);
|
||||
Assert.Empty(mClient.GetLaunchedApps());
|
||||
|
||||
mClient.LaunchApp(app.LaunchPair.LaunchKey);
|
||||
mClient.LaunchApp(app.LaunchPair.LaunchKey);
|
||||
mClient.KillAllApps();
|
||||
Assert.Empty(mClient.GetLaunchedApps());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VolumeLevel_RoundTrips()
|
||||
{
|
||||
mClient.VolumeLevel = 0.25f;
|
||||
Assert.Equal(0.25f, mClient.VolumeLevel);
|
||||
Assert.Equal(0.25f, mClient.FullUpdate().VolumeLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallProduct_Streams_Extracts_And_Completes_At_99()
|
||||
{
|
||||
string zipPath = MakeTestZip("TestGame", "readme.txt", "hello vpod");
|
||||
|
||||
Guid callId = mClient.InstallProduct(zipPath);
|
||||
Assert.NotEqual(Guid.Empty, callId);
|
||||
|
||||
var progress = PollUntilCompleted(callId);
|
||||
// 99, not 100: the console's InstallProductWorker only breaks its
|
||||
// 3-attempt retry loop on exactly 99.
|
||||
Assert.Equal(99, progress.PercentComplete);
|
||||
Assert.Equal("Complete", progress.Status);
|
||||
|
||||
string extracted = Path.Combine(mDataDir, "Games", "TestGame", "readme.txt");
|
||||
Assert.True(File.Exists(extracted), "expected extracted file at " + extracted);
|
||||
Assert.Equal("hello vpod", File.ReadAllText(extracted));
|
||||
|
||||
// The main connection survived the concurrent out-of-band transfer.
|
||||
Assert.True(mClient.IsOpen);
|
||||
var now = DateTime.UtcNow;
|
||||
mClient.Ping(now);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UninstallApp_Removes_Registration_And_Product_Directory()
|
||||
{
|
||||
string zipPath = MakeTestZip("TestGame", "game.exe", "not really an exe");
|
||||
Guid callId = mClient.InstallProduct(zipPath);
|
||||
PollUntilCompleted(callId);
|
||||
|
||||
string productDir = Path.Combine(mDataDir, "Games", "TestGame");
|
||||
var app = new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair { LaunchKey = Guid.NewGuid(), DisplayName = "Test Game" },
|
||||
WorkingDirectory = productDir,
|
||||
ExeFile = Path.Combine(productDir, "game.exe"),
|
||||
Arguments = "",
|
||||
AutoRestart = false
|
||||
};
|
||||
mClient.InstallApp(app);
|
||||
Assert.Single(mClient.GetInstalledApps());
|
||||
Assert.True(Directory.Exists(productDir));
|
||||
|
||||
mClient.UninstallApp(app.LaunchPair.LaunchKey);
|
||||
Assert.Empty(mClient.GetInstalledApps());
|
||||
Assert.False(Directory.Exists(productDir), "product directory should be cleaned up");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shutdown_Raises_ShutdownRequested()
|
||||
{
|
||||
bool? restartArg = null;
|
||||
using (var signalled = new ManualResetEventSlim())
|
||||
{
|
||||
mLauncher.ShutdownRequested += restart =>
|
||||
{
|
||||
restartArg = restart;
|
||||
signalled.Set();
|
||||
};
|
||||
mClient.Shutdown(doRestart: true);
|
||||
Assert.True(signalled.Wait(TimeSpan.FromSeconds(5)), "ShutdownRequested was not raised");
|
||||
Assert.True(restartArg);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearStore_Wipes_Apps_And_Requests_Reprovisioning()
|
||||
{
|
||||
mClient.InstallApp(SampleApp("Doomed App"));
|
||||
using (var signalled = new ManualResetEventSlim())
|
||||
{
|
||||
mLauncher.ReprovisionRequested += signalled.Set;
|
||||
mClient.ClearStore(); // one-way: no response frame
|
||||
Assert.True(signalled.Wait(TimeSpan.FromSeconds(5)), "ReprovisionRequested was not raised");
|
||||
}
|
||||
Assert.Empty(mLauncher.GetInstalledApps());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealLaunch_Starts_And_Kills_Real_Processes()
|
||||
{
|
||||
mLauncher.RealLaunch = true;
|
||||
var app = new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair { LaunchKey = Guid.NewGuid(), DisplayName = "Real Pinger" },
|
||||
WorkingDirectory = Environment.SystemDirectory,
|
||||
ExeFile = Path.Combine(Environment.SystemDirectory, "ping.exe"),
|
||||
Arguments = "-n 60 127.0.0.1",
|
||||
AutoRestart = false
|
||||
};
|
||||
mClient.InstallApp(app);
|
||||
|
||||
int pid = mClient.LaunchApp(app.LaunchPair.LaunchKey);
|
||||
Assert.True(pid > 0);
|
||||
using (var real = Process.GetProcessById(pid)) // throws if not actually running
|
||||
{
|
||||
Assert.False(real.HasExited);
|
||||
var launched = mClient.GetLaunchedApps();
|
||||
Assert.Single(launched);
|
||||
Assert.Equal(pid, launched[0].ProcessId);
|
||||
|
||||
mClient.KillAllOfType(app.LaunchPair.LaunchKey);
|
||||
Assert.True(real.WaitForExit(5000), "real process was not terminated");
|
||||
}
|
||||
Assert.Empty(mClient.GetLaunchedApps());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealLaunch_Watchdog_Restarts_AutoRestart_Apps_That_Exit_On_Their_Own()
|
||||
{
|
||||
mLauncher.RealLaunch = true;
|
||||
mLauncher.RealAutoRestart = true;
|
||||
var app = new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair { LaunchKey = Guid.NewGuid(), DisplayName = "Short Pinger" },
|
||||
WorkingDirectory = Environment.SystemDirectory,
|
||||
ExeFile = Path.Combine(Environment.SystemDirectory, "ping.exe"),
|
||||
Arguments = "-n 3 127.0.0.1", // exits on its own after ~2 s
|
||||
AutoRestart = true
|
||||
};
|
||||
mClient.InstallApp(app);
|
||||
int firstPid = mClient.LaunchApp(app.LaunchPair.LaunchKey);
|
||||
Assert.True(firstPid > 0);
|
||||
|
||||
// The app exits by itself; the watchdog must bring up a NEW pid
|
||||
// (exit ~2 s + the Agent's 2 s restart delay).
|
||||
int restartedPid = 0;
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(20);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
var launched = mClient.GetLaunchedApps();
|
||||
if (launched.Length == 1 && launched[0].ProcessId != firstPid)
|
||||
{
|
||||
restartedPid = launched[0].ProcessId;
|
||||
break;
|
||||
}
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
Assert.True(restartedPid > 0, "watchdog did not restart the exited app");
|
||||
|
||||
// Turning the watchdog off ends the cycle: the current instance
|
||||
// exits on its own and nothing relaunches it.
|
||||
mLauncher.RealAutoRestart = false;
|
||||
deadline = DateTime.UtcNow + TimeSpan.FromSeconds(15);
|
||||
while (DateTime.UtcNow < deadline && mClient.GetLaunchedApps().Length > 0)
|
||||
{
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
Assert.Empty(mClient.GetLaunchedApps());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealLaunch_Missing_Exe_Surfaces_The_Agents_Error()
|
||||
{
|
||||
mLauncher.RealLaunch = true;
|
||||
var app = SampleApp("Not Installed Yet"); // ExeFile doesn't exist on disk
|
||||
mClient.InstallApp(app);
|
||||
|
||||
var ex = Assert.ThrowsAny<Exception>(() => mClient.LaunchApp(app.LaunchPair.LaunchKey));
|
||||
Assert.Contains("executable not found", ex.Message);
|
||||
Assert.Empty(mClient.GetLaunchedApps());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dropped_Session_Still_Reports_Open_Until_Probed_Then_Reconnect_Works()
|
||||
{
|
||||
// The launcher (and vPOD) drops sessions idle >30s. Simulate the drop
|
||||
// server-side and pin the premise PodInfo.EnsureConnectionAlive relies
|
||||
// on: the client socket still REPORTS open until an I/O fails...
|
||||
mServer.Stop();
|
||||
mServer.Start(mKey);
|
||||
Thread.Sleep(100); // let the FIN arrive
|
||||
Assert.True(mClient.IsOpen, "a dropped-but-unused connection should still report open (the stale state)");
|
||||
|
||||
// ...the cheap Ping probe is what exposes the dead connection...
|
||||
Assert.ThrowsAny<Exception>(() => mClient.Ping(DateTime.UtcNow));
|
||||
|
||||
// ...and close + reopen (EnsureConnectionAlive's recovery) restores service.
|
||||
mClient.Close();
|
||||
mClient.Open(new IPEndPoint(IPAddress.Loopback, mPort), mKey);
|
||||
var now = new DateTime(2026, 7, 9, 12, 0, 0, DateTimeKind.Utc);
|
||||
Assert.Equal(now, mClient.Ping(now));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Installed_Apps_Persist_Across_Launcher_Restart()
|
||||
{
|
||||
var app = SampleApp("Persistent App");
|
||||
mClient.InstallApp(app);
|
||||
|
||||
var reloaded = new VirtualLauncher(mDataDir, Path.Combine(mDataDir, "Games"));
|
||||
var installed = reloaded.GetInstalledApps();
|
||||
Assert.Single(installed);
|
||||
Assert.Equal(app.LaunchPair.LaunchKey, installed[0].LaunchPair.LaunchKey);
|
||||
Assert.Equal("Persistent App", installed[0].LaunchPair.DisplayName);
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private static LaunchData SampleApp(string name)
|
||||
{
|
||||
return new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair { LaunchKey = Guid.NewGuid(), DisplayName = name },
|
||||
WorkingDirectory = @"C:\Games\Sample",
|
||||
ExeFile = @"C:\Games\Sample\game.exe",
|
||||
Arguments = "-net 1501 -res 1920 1080",
|
||||
AutoRestart = true
|
||||
};
|
||||
}
|
||||
|
||||
private string MakeTestZip(string folder, string fileName, string content)
|
||||
{
|
||||
string zipPath = Path.Combine(mDataDir, "product-" + Guid.NewGuid().ToString("N") + ".zip");
|
||||
using (var fs = File.Create(zipPath))
|
||||
using (var zip = new ZipArchive(fs, ZipArchiveMode.Create))
|
||||
{
|
||||
var entry = zip.CreateEntry(folder + "/" + fileName);
|
||||
using (var writer = new StreamWriter(entry.Open()))
|
||||
{
|
||||
writer.Write(content);
|
||||
}
|
||||
}
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
private OutOfBandProgress PollUntilCompleted(Guid callId)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(15);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
var progress = mClient.GetOutOfBandProgress(callId);
|
||||
if (progress.IsCompleted)
|
||||
{
|
||||
return progress;
|
||||
}
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
throw new TimeoutException("Install did not complete in time.");
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using Tesla;
|
||||
using VPod;
|
||||
using Xunit;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
{
|
||||
/// <summary>
|
||||
/// End-to-end loopback exercise of vPOD's pod-side SecureConfig provisioning
|
||||
/// against the REAL console-side implementation (Tesla.PodConfigurationServer,
|
||||
/// the exact code the console runs behind Manage Site's "Configure" button):
|
||||
/// RQST beacon reception, the AES "RPLY" config broadcast, and the OFB+RSA
|
||||
/// session-key exchange must all interoperate.
|
||||
///
|
||||
/// Uses the protocol's fixed ports (UDP 53291/53292, TCP 53292) and real UDP
|
||||
/// broadcasts on loopback — do not run while a console or another vPOD is
|
||||
/// provisioning on this machine.
|
||||
/// </summary>
|
||||
public class VPodProvisioningTests
|
||||
{
|
||||
[Fact]
|
||||
public void Console_Provisions_VPod_And_Both_Hold_The_Same_Session_Key()
|
||||
{
|
||||
byte[] mac = PodProvisioning.MacForHost(7);
|
||||
var provisioning = new PodProvisioning(mac);
|
||||
|
||||
byte[] beaconMac = null;
|
||||
string beaconRequestId = null;
|
||||
using (var beaconSeen = new ManualResetEventSlim())
|
||||
using (var podProvisioned = new ManualResetEventSlim())
|
||||
{
|
||||
byte[] podKey = null;
|
||||
provisioning.ConfigReceived += _ => { };
|
||||
provisioning.Provisioned += key =>
|
||||
{
|
||||
podKey = key;
|
||||
podProvisioned.Set();
|
||||
};
|
||||
|
||||
// The console side: listens on UDP 53291 for RQST beacons; the
|
||||
// delegate fires on vPOD's first beacon (sent at Start).
|
||||
PodConfigurationServer consoleServer;
|
||||
try
|
||||
{
|
||||
consoleServer = new PodConfigurationServer(53291, 53292, (m, id) =>
|
||||
{
|
||||
beaconMac = m;
|
||||
beaconRequestId = id;
|
||||
beaconSeen.Set();
|
||||
});
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException)
|
||||
{
|
||||
// UDP 53291 already bound — a real TeslaConsole is running on
|
||||
// this machine. The protocol ports are fixed, so the test can't
|
||||
// run; treat as inconclusive rather than failing the suite.
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
provisioning.Start();
|
||||
Assert.True(beaconSeen.Wait(TimeSpan.FromSeconds(15)),
|
||||
"console never received vPOD's RQST beacon");
|
||||
Assert.Equal(mac, beaconMac);
|
||||
Assert.Equal(provisioning.RequestId, beaconRequestId);
|
||||
|
||||
// The console side of Configure: broadcast the AES-encrypted
|
||||
// network config and run the RSA key exchange against the pod.
|
||||
byte[] consoleKey = consoleServer.SendEncryptionKey(
|
||||
provisioning.Passphrase,
|
||||
IPAddress.Loopback,
|
||||
IPAddress.Parse("255.255.255.0"),
|
||||
IPAddress.Any,
|
||||
IPAddress.Any,
|
||||
"vpod-test",
|
||||
TimeSpan.FromSeconds(30));
|
||||
|
||||
Assert.True(podProvisioned.Wait(TimeSpan.FromSeconds(15)),
|
||||
"vPOD never completed provisioning");
|
||||
Assert.NotNull(consoleKey);
|
||||
Assert.Equal(32, consoleKey.Length);
|
||||
Assert.Equal(consoleKey, podKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
provisioning.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||