23 Commits
Author SHA1 Message Date
CydandClaude Fable 5 91640dcbf2 XP11: whole suite on net40 — Console + vPOD run on XP SP3 through Win11
The Launcher's XP11 port (8730b9b) now extends to everything: one net40
flavor across Console, vPOD, Contract, and SecureConfig (Newtonsoft.Json
everywhere; the net48/System.Text.Json legs and their #if splits are gone
since nothing consumed them).

Console (net40, single TFM like the Launcher):
- The ~31 BinaryFormatter bitmap blobs in the .resx files became raw
  embedded files under assets/icons/ (extracted byte-faithfully via a
  serialization surrogate — the animated square_throbber.gif survives),
  loaded by Properties.Resources.EmbeddedBitmap/EmbeddedIcon. Reason:
  System.Resources.Extensions' DeserializingResourceReader is net461+
  and cannot load on net40. Strings stay in the .resx.
- IReadOnlyList -> IList in AppRegistry (net45+ interface).

vPOD (net40, single TFM):
- Zip extraction now shares the Launcher's MiniZip.cs (linked source), so
  the diff-test install round-trip exercises it against ZipArchive zips.
- RPC args as JTokens; LaunchApps.json persistence via Newtonsoft;
  Thread.VolatileRead instead of Volatile.Read.

Contract/SecureConfig: net40-only; Client/** (PodManagerConnection) now
ships in the one build. The Launcher package gains
TeslaSecureConfiguration.dll as a dependency of the client half.

Tests: the net48 xunit host loads the net40 assemblies (both CLR4), so
the suite exercises exactly what ships — 106/106 green. Also verified
live: net40 console provisioned, managed, and ran a full RP mission
against net40 vPOD (beacon/passphrase/RSA, 53290 RPC, egg load,
Run/Stop Mission).

Version: 4.11.4.3 across Launcher, Console, and vPOD (vPOD joins the
suite version line; was 1.0.0). Ship the dotNetFx40 redistributable in
Launcher/assets for XP-era pods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 21:01:34 -05:00
CydandClaude Fable 5 eefb8054e0 Catalog: launch-key convention, TeslaRel410 product, drop vPOD entry
- Adopt the launch-key convention (documented in the Apps.xml header):
  fresh Guid = product id, first Launch key reuses it, each extra entry
  increments the last hex digit (wrapping F->0). Never a -1 suffix:
  keys parse as System.Guid and silently collapse to Guid.Empty.
- Rewrite BT411 LC/MR + RIOJoy keys to the convention. RP4.11 LC/MR stay
  pinned to the original console's hardcoded Guids (SiteManagement
  constants + diff tests).
- Remove vPOD from the shipped catalog: dev tool, never a console-deployed
  product (README documents the ad-hoc Add Product path instead).
- Add TeslaRel410 (DOSBox-X preservation pods): six entries, BT/RP 4.10 x
  GameClient/LC/MR, all C:\Games\TeslaPod410\pod-launch.exe with mode
  bt/rp. LC/MR boot identically (role assigned via egg hostType) and no
  {res} token (output size fixed per rig at postinstall).
- CatalogTests: now 5 products / 14 entries; full diff suite 106/106.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:21:00 -05:00
CydandClaude Fable 5 8730b9b966 XP11: single net40 binary runs on XP SP3 through Windows 11
Merge TeslaLauncherService (Session 0) + TeslaLauncherAgent (tray) into one
userland TeslaLauncher.exe. The split existed only to work around Vista+
Session 0 isolation; running everything in the auto-logged-in admin session
needs no service, no named pipe, and no flat<->wire conversion layer. The
original Elsewhen software was likewise a single binary.

net40 is the newest framework XP SP3 can install, and net40 assemblies load
in-place on the 4.8 runtime in Win10/11 -- one exe covers both.

- Contract: multi-target net48;net40. The net40 leg of PodRpcProtocol uses
  Newtonsoft.Json (STJ has no net40 target); JSON is shape-identical on the
  wire, and the request reader keeps date strings raw so Ping echoes
  byte-identically. Console keeps the untouched net48/STJ leg.
- MiniZip.cs: central-directory ZIP extractor (stored/deflate/ZIP64) since
  net40 has no ZipFile; zip-slip guarded.
- SecureConfig: RSACryptoServiceProvider instead of RSA.Create (net46+),
  no leaveOpen BinaryWriter/Reader, struct instead of ValueTuple, and no
  SetCompatibleTextRenderingDefault on the passcode thread (the merged app
  already has a form). netsh "interface ip" syntax was already XP-correct.
- Volume: nircmd -> CoreAudio (Vista+) -> winmm waveOutSetVolume (XP).
- Paths: CommonApplicationData resolved per-OS (XP has no C:\ProgramData);
  launcher log moved next to the key/config in the data dir.
- install.bat: dual-OS (cacls/icacls, netsh firewall/advfirewall, dism and
  UAC/notification steps skipped on XP, .NET 4.0 redist check, XP DHCP reset
  via netsh); no service registration -- HKLM Run key + auto-login on both;
  RegisterApplicationRestart supplies crash-restart on Vista+.
- Bench switches: /skipconfig, /port:NNNN.

Verified: Console + diff suite (103 tests) still green on the net48 leg;
E2E smoke test drove the net40 launcher over real TCP with the Console's own
PodManagerConnection (STJ client vs Newtonsoft server) -- Ping echo, volume,
app registry, launch/kill/watch, InstallProduct zip transfer + extract, and
uninstall orphan cleanup: 17/17 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:59:06 -05:00
CydandClaude Fable 5 1d95a800fd Add verify skill: drive the console against vPOD
A project verify skill (.claude/skills/verify) capturing the recipe used
this session: build the console + vPOD, launch both, drive the console's
WinForms UI via UI Automation, and observe real behavior -- pod connection
endpoint via netstat, egg contents in vPOD, the full Load/Run/Stop mission
lifecycle -- rather than relying on tests alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:30:28 -05:00
CydandClaude Fable 5 f07df88acb 410console: BattleTech egress-hold addendum -- issue closed (~3s hold)
Decoded/disassembled finding from the TeslaRel410 side, verified against
a live DOSBox BTL4OPT pod driven by this console: the post-mission egress
window is a compiled ~3 s (stage-1 timer), not ~30 s, and byte-identical
across all four BTL4OPT builds. Marked won't-fix -- the floor egress lamps
no longer exist on any surviving cockpit. Console takeaway unchanged: send
StopMission promptly at timer end; the pod handles its own (short) close,
no egress delay to implement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:30:28 -05:00
CydandClaude Fable 5 13f8e0456b vPOD: real-launch auto-restart watchdog + optional postinstall.bat
Two additions to the virtual launcher's real-process mode:

- Auto-restart watchdog. Replaces the poll-on-query PruneExitedProcesses
  with a per-process watcher thread (StartWatcher): when a real-launched
  app exits on its own -- not via a Kill*/Uninstall, which untrack it
  first -- it is dropped from the running list and, if its LaunchData has
  AutoRestart and the "Auto-restart after the app exits (watchdog)"
  toggle is on, relaunched after the Agent's 2 s delay. A watchdog
  generation counter cancels pending restarts when the pod goes dark
  (power off / reboot / reprovision / WipeApps); the console's KillAllApps
  leaves them pending, matching the real Agent's race.

- postinstall.bat toggle. A "Run postinstall.bat after install" checkbox
  (above "Actually launch apps", off by default) makes an install execute
  a packaged postinstall.bat via cmd /c (waited up to 5 min) before
  deleting it, like the real service. Off, it is logged and removed unrun
  as before -- it runs package script code on the host.

Both are opt-in from the vPOD window. Verified against the real
LauncherRpcServer over a loopback socket: the watchdog test relaunches an
exited ping.exe with a new PID and stops once toggled off; a crafted
package's postinstall.bat runs (and is removed) only when enabled. Full
differential suite 103/103.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:30:28 -05:00
CydandClaude Fable 5 80ee1d26ea Console: move DOSBox IP shift from game pages into game defaults
The per-page "DOSBox IPs (+100)" checkbox becomes a single machine-level
setting per game, edited in Settings > Change <game> Defaults (a "DOSBox
Build" row below Autotranslocate Delay) and persisted in the game's
defaults file (RPDefaults.rpd / BTDefaults.btd) as a top-level
DosBoxAddressShift element.

The game pages no longer carry the checkbox; each RPGame/BTGame reads the
stored default once at construction into a readonly field and applies it
when enabling a pod (MungaGame.DosBoxAddressShift) and when building the
mission egg (MissionAddress). Since it is read at construction, an
already-open game page keeps its shift until reopened -- consistent with
how every other game default behaves.

Verified against vPOD: enabling the DOSBox default drives a freshly
opened game page's pod connection to 127.0.0.101 and the egg to
pilot=127.0.0.101 with no per-page control; unchecking round-trips to
False in the defaults file. All 103 diff tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:33:44 -05:00
CydandClaude Fable 5 a5f25c8bb8 Console: move the DOSBox IPs checkbox beside Print Last Mission
Same row instead of a fourth row below it: the mission-properties table
gains a ninth column holding the checkbox at (7,2) and the issues label
moves to column 8, so the group box returns to its original height.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:14:39 -05:00
CydandClaude Fable 5 6a77fd0991 Console: DOSBox IPs (+100) checkbox on all four game pages
The DOSBox-X preservation pods run the original DOS builds of BattleTech
4.10 / Red Planet 4.10 inside an emulator whose bridged NIC is enumerated
100 above the host pod's last octet. A new checkbox on each game page
(RP Death Race / Martian Football, BT Free For All / No Return) shifts
every siteconfig address the page uses by +100 so the same pages control
either version of the games:

- Munga game connections (port 1501) target host+100; toggling the box
  drops and reconnects any requested pods at the new address.
- Mission egg player and camera entries carry the shifted address.
- The checkbox locks while a mission is loaded/running, like the other
  mission properties.

Launcher / site-management traffic is untouched -- those services still
run on the pod host itself. Pages share a pod's MungaGame, so the most
recent page to toggle or enable wins if two pages fight over one pod.

Verified end-to-end against vPOD: netstat shows the game connection move
127.0.0.1 -> 127.0.0.101 -> 127.0.0.1 as the box is toggled, and the egg
received by vPOD carries pilot=127.0.0.101. All 103 diff tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:59:58 -05:00
CydandClaude Fable 5 d30ce8bdbe vPOD: real C:\Games installs, optional real app launching, power-group swap
- InstallProduct now extracts into the real C:\Games (the launcher's
  GAMES_DIR) so deployed products land where their catalog launch entries
  point; uninstall removes the real product folder. Tests pass an isolated
  games root through the VirtualLauncher ctor.
- New "Actually launch apps (real processes)" toggle (off by default =
  simulated PIDs): LaunchApp starts the entry's exe exactly like the Agent
  (same start info, same registered-but-not-installed error), Kill*/
  Uninstall/Wipe terminate the real processes (kill before folder delete),
  and self-exited apps are pruned from GetLaunchedApps/FullUpdate. Real
  processes die with the machine: power off, reboot, or closing vPOD. The
  Agent's autoRestart watchdog is deliberately not emulated.
- Pod Power and Mimicking Game groups swapped (game left, power right).
- Provisioning round-trip test now skips as inconclusive when a running
  TeslaConsole holds UDP 53291 instead of failing the suite; two new tests
  cover real launch/kill (ping.exe) and the missing-exe error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:46:55 -05:00
CydandClaude Fable 5 cb7c655530 Promote vPOD to a top-level project (Console/vPOD -> vPOD/)
vPOD has outgrown its home inside the console's folder: it now emulates both
halves of a pod (Munga game client + TeslaLauncher service / provisioning),
so it lives at the repo root beside Console/, Launcher/, Contract/ and
SecureConfig/, like the peer it has become.

Accompanying changes: project references rebased (Contract, SecureConfig,
the console's vendored Munga Net.dll), solution + DiffTests reference paths,
the console csproj's now-obsolete vPOD source exclusion removed, and the
root README / Apps.xml / vPOD README path mentions updated. pack.ps1 is
self-relative and now emits vPOD/dist/vPOD.zip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:52:59 -05:00
CydandClaude Fable 5 60893172c3 vPOD: Reprovision also wipes the installed-apps store
The UI's Reprovision button only dropped the session key; the console-driven
ClearStore was the only path that cleared LaunchApps.json. Both now share
VirtualLauncher.WipeApps, so Reprovision = fresh pod (key + app registry
gone, beacon mode; extracted Games\ files stay, as on a real pod).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:47:48 -05:00
CydandClaude Fable 5 e87a8a22f1 vPOD: pod power vs game split; install bar finishes; command column
- Pod Power (new group) is the whole machine: Power On boots the launcher /
  site-management side and auto-starts the game (a real pod's boot-time
  autoRestart launch); Power Off darkens everything. Start Game / Stop Game
  control just the emulated game exe, so "machine up, game not running" is
  now representable — Manage Site keeps seeing a healthy pod while the
  console's game connection is down. Watchdog stays game-level and no-ops if
  the pod is powered off during its window; console Shutdown/Restart stays a
  pod-level power cycle.
- The install progress bar snaps to 100% when the transfer completes (the
  wire still reports 99 — the console retries on anything else).
- Installed-apps list shows the full command line (exe + arguments).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:40:27 -05:00
CydandClaude Fable 5 e0a3c72370 Console: survive a launcher-dropped connection during product installs
The launcher drops RPC sessions idle >30s (easily hit while the operator sits
in the install dialogs), but a dropped socket still reports Connected until an
I/O fails. InstallProductWorker then skipped its reopen, streamed the archive
on the fresh out-of-band connection, and died on the first progress poll —
and the install-completed handler registered launch entries without checking
e.Error, so AddApp's disconnected-guard exception crashed the whole console.

- PodInfo.EnsureConnectionAlive: probe an "open" connection with a Ping and
  reconnect when it is dead; used by the install and uninstall workers.
- SiteManagement.PodInfo_InstallProductCompleted: register launch entries only
  on success, and surface registration failures as the row's Install Failed
  state instead of an unhandled exception.
- Regression test pins the premise + recovery against vPOD's launcher server
  (stale socket reports open, Ping exposes it, reconnect restores service).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:40:14 -05:00
CydandClaude Fable 5 1daaf4af78 vPOD: virtual launcher — test Site Management / product deploys without a cockpit
vPOD now also impersonates the pod's TeslaLauncher service, so the console's
Manage Site works against it unmodified:

- LauncherRpcServer: ILauncherService over OFB + framed JSON on TCP 53290,
  mirroring TeslaLauncherService (concurrent sessions, out-of-band install
  zip on a second connection, the 99%-not-100 completion convention).
  Packages extract to %LocalAppData%\vPOD\Games; postinstall.bat is logged
  but never executed.
- PodProvisioning: pod side of SecureConfig (RQST beacon, RPLY decrypt, RSA
  session-key exchange), display-only — never touches the NIC/registry. The
  console's Configure flow mints the key exactly as for a real pod; console
  Reconfigure (ClearStore) drops the key and re-enters beacon mode.
- VirtualLauncher: installed-app registry (persisted), simulated launch PIDs,
  volume, install progress; console Shutdown/Restart power-cycles the pod.
- Form gets a Launcher/Site Management column (passcode display, RPC status,
  install progress, app list, Reprovision); Power Off darkens the launcher
  side too; new -nomanage flag disables it.

vPOD references the shared Tesla.Contract/Tesla.SecureConfig projects (server
side of the existing contract only, no new RPCs). Loopback tests drive the
real PodManagerConnection and PodConfigurationServer against the new code
(VPodLauncherServerTests, VPodProvisioningTests) — suite now 99 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:45:33 -05:00
CydandClaude Fable 5 fd0c8c5196 vPOD: pod power controls, end-mission restart cycle, egg viewer polish
Adds pod-lifecycle controls to the vPOD window:

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:04:03 -05:00
CydandClaude Fable 5 deaade4f72 Track the 410console hand-off docs; ignore the raw Mac archive
The BattleTech port spec, the Console 4.10 decompilation notes, the golden
reference eggs, and the PEF/rsrc analysis tools come into the repo. The .sit
archive and its extraction (Mac binaries, __MACOSX/.finf metadata) stay
untracked for now.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:57:18 -05:00
132 changed files with 14157 additions and 2870 deletions
+58
View File
@@ -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).
+7
View File
@@ -22,3 +22,10 @@
Thumbs.db
desktop.ini
.DS_Store
__pycache__/
# 410console hand-off: the docs/spec/reference/tools are tracked; the raw Mac
# archive and its extraction are not (may be committed later for completeness)
/410console/410consoleArchive.sit
/410console/4_10-console-extracted/
/410console/.finf/
+554
View File
@@ -0,0 +1,554 @@
# Console 4.10 — Decompilation & Documentation
Reverse-engineering notes for the classic Mac OS **operator console** that
commanded the networked Virtual World Entertainment (VWE) BattleTech / Red
Planet pods. Part of the VWE Tesla restoration project.
- **Target dir:** `c:\VWE\TeslaRel410\410console\4_10-console-extracted\`
- **Binary:** `Console 4.10` (728,064 bytes) — classic Mac OS **fat application**
(68K `CODE` resources **+** PowerPC **PEF/CFM** fragment in the data fork).
- **Internal identity:** type `APPL`, creator **`vwe4`**, version resource
**`2.4.0b3` / "Console 2.4.0b3", © 19951997 FASA Interactive, Inc.**
(The "4.10" in the folder name is the site/config release, not the app's own
`vers`.)
- **Analysis method:** custom Python PEF/PPC + resource-fork tooling (no Ghidra
on this machine); capstone in PPC big-endian mode. See `410console/tools/`.
- **Status:** read-only analysis. Nothing in the target was modified.
---
## 0. TL;DR — what the console does
The console is a **document-based Mac app** (Metrowerks **PowerPlant** framework,
CodeWarrior C++). Each open document is a **Mission**. The operator picks an
*Adventure* (Solo/Team × BattleTech/Red Planet), a *Location* (arena) and a
*Scenario*, assigns *pilots/vehicles*, then drives a fleet of **cockpit pods**
through a mission lifecycle. It talks to two networks:
1. **Pod command/control** — one **MacTCP TCP** stream per pod on **port 1501**
(console `200.0.0.1`, pods `200.0.0.11``200.0.0.120`). This is the protocol
that matters for the emulator. Commands (Load / Translocate / Stop / End /
Reset) go out as small binary packets; pods stream back a one-byte
**Cockpit State** (012) that the console records in `Console Log`.
2. **4th Dimension (4D) database** — a second client link to a 4D server for the
**pilot roster and mission scheduling** ("Get Next Departure", Add/Delete/
Lookup Pilot, scoring). Evidenced by the `_4D_Select4DServer` /
`_4D_Find4DServer` error strings; the wire protocol for 4D was **not**
reverse-engineered (it is ACI's proprietary 4D client library).
The rest of this document backs every claim above with offsets, string refs,
resource dumps and log excerpts.
---
## 1. File inventory & formats
| File | Size | Format / role |
|---|---|---|
| `Console 4.10` | 728 KB | The application. Fat binary: PEF/PPC in data fork, 68K `CODE` + all UI resources in the resource fork (see §5). |
| `Console Log` | 5.3 MB | Runtime log, CR-terminated records. Ground truth for the pod protocol (§7). |
| `Console.ini` | 92 KB | Live config: 586 `::`-namespaced sections — pods, adventures, locations, scenarios, vehicles (§8). |
| `Console.ini copy` | 91 KB | Prior copy of the above. |
| `Stationary.ini` | 98 KB | The **stationery/template** master config (Mac "stationery" = template document) used to seed a fresh `Console.ini`. |
| `ini Folder/DB<City>/` | — | Per-venue config overrides: Atlanta, Chicago, Cincinnati, Denver, Detroit, Houston, Miami, Ontario, Pittsburgh, San Jose, Toronto, LaZerPark. |
| `ConsoleSorter/` + `Console Log Sorter`, `Console Log.rpl` | 0 B here | A companion log-sorter tool (data forks empty in this extraction; only `Instructions` survived). |
| `FRC.rsrc`, `KR.rsrc`, `US.rsrc` | 225 B each | Tiny locale resource stubs (French / Korean / US). |
| `Fonts/` (`Killmark`, `Stencil`) | 0 B | Bitmap/suitcase fonts for kill-marker/stencil display (data forks empty here). |
| `PlainText16 Folder/` | 173 KB | `PlainText` support (text traits). |
| `.finf/` | 32 B each | Mac Finder-info sidecars from extraction (type/creator). |
| `__MACOSX/._*` | — | **AppleDouble sidecars — these carry the resource forks.** `._Console 4.10` is 3.38 MB and holds the app's entire resource fork (§5). |
**Resource-fork location:** the app's resource fork was preserved inside
`__MACOSX/._Console 4.10` (AppleDouble magic `0x00051607`, entry id 2 =
resource fork at file offset `0x52`, length `0x3391CA`). Finder info at offset
`0x32` reads `APPLvwe4`**type `APPL`, creator `vwe4`**.
---
## 2. PEF container structure
First 16 bytes: `4A6F7921 70656666 70777063 00000001` = `Joy! peff pwpc` v1
(classic Mac OS CFM PowerPC container, big-endian).
**Container header (40 bytes):**
| Field | Value |
|---|---|
| tag1 / tag2 / arch | `Joy!` / `peff` / `pwpc` |
| formatVersion | 1 |
| dateTimeStamp | `0xAF4A5504` |
| currentVersion | 0 |
| sectionCount | 3 |
| instSectionCount | 2 |
**Section table** (each header 28 bytes; bodies follow the name table):
| # | Kind | default | total | unpacked | packed | file off | notes |
|---|---|---|---|---|---|---|---|
| 0 | Code (0) | 0 | 625,536 | 625,536 | 625,536 | `0x003840` | PPC text (`0x98B80` bytes). |
| 1 | PatternInitData (2) | 0 | 127,204 | 122,866 | 88,128 | `0x09C3C0` | RLE-packed data/TOC/strings; unpacks to `0x1DFF2`. |
| 2 | Loader (4) | 0 | 0 | 0 | 14,272 | `0x000080` | Imports/exports/relocs/entry. |
**Entry points (from the Loader header):**
- **main** = data-section transition vector at offset `0x5154``[code 0x00082E5C, TOC 0x00008000]`.
So `main` executes at **code `0x82E5C`** and the runtime **TOC/`r2` = data offset `0x8000`**.
(This TOC value is the key that lets us resolve all `r2`-relative constant loads — see §6.)
- **init** = none (`-1`), **term** = none (`-1`).
- **exported symbols = 0** (nothing is exported; this is a leaf application, not a shared library).
- imported libraries = 3, imported symbols = 448, reloc sections = 1.
Tool: `py -3.13 tools/pefparse.py "Console 4.10" header|loader|strings|dump|dis`.
---
## 3. CFM imports (448 symbols across 3 libraries) — interpreted
Only three shared libraries are imported. **Notably absent: OpenTransport,
AppleTalk, and any TCP shared library** — so the console does *not* use Open
Transport. Its IP networking is done the older way, through the **MacTCP `.IPP`
driver via the Device Manager** (see the `PBControlAsync/Sync` + `PBOpenSync`
imports and the `CTCPDriver`/`.IPP` strings in §6).
### MathLib (6)
`num2dec, dec2num, str2dec, dec2str` (SANE-style decimal ↔ string) and
`fetestexcept, feclearexcept` (FP exception flags). Used for numeric formatting
of scores/stats.
### InterfaceLib (436) — the Mac Toolbox surface
Grouped by subsystem (representative, not exhaustive):
- **QuickDraw / GWorld / PICT:** `InitGraf, OpenCPort, NewGWorld*(via UGWorld),
LockPixels, CopyBits(implied), OpenCPicture, DrawPicture(implied), PlotCIcon,
PlotIconID, RGBBackColor, FrameRoundRect, InvertRgn, ClipRect …` — draws the
console windows, pod photos (PICT), mech/vehicle art, kill markers.
- **Window Manager:** `NewCWindow, GetNewCWindow, ZoomWindow, SelectWindow,
HiliteWindow, FindWindow, BeginUpdate/EndUpdate, DragWindow(implied) …`
- **Menu Manager:** `NewMenu, InsertMenu(implied), DeleteMenu, DrawMenuBar,
MenuKey, HiliteMenu, EnableItem, CountMItems, GetItemMark …`
- **Control Manager:** `NewControl, TrackControl, FindControl, Draw1Control,
SetControlValue/Min/Max, HiliteControl, SetControlAction …` (scrollers,
buttons, the toolbar).
- **List / Dialog / TextEdit:** `LGetSelect, LSetSelect, LSetCell, LGetCell`
(List Manager — player/network tables), `CautionAlert, ParamText`,
`TEInsert, TEActivate, TEGetHeight, TECut, TEUpdate` (mission notes / fields).
- **File Manager:** `FSpOpenDF, FSpCreate, FSpDelete, FSRead, FSWrite, FSClose,
PBOpenSync, PBReadSync, PBWriteSync, PBHCreateSync, PBGetEOFSync,
PBSetEOFSync, FSpCreateResFile, FSpOpenResFile, HOpenResFile …` — reads
`Console.ini`, writes `Console Log`, loads/saves mission **egg** files.
- **Device Manager (→ MacTCP):** **`PBOpenSync`, `PBControlAsync`,
`PBControlSync`** — open the `.IPP` driver and issue TCP control calls
(TCPCreate/ActiveOpen/Send/Rcv/Close) — the pod network layer.
- **Printing Manager:** `PrOpen, PrOpenDoc, PrOpenPage, PrCloseDoc, PrClose` —
prints score sheets / mission highlights.
- **Apple Event Manager:** `AEInstallEventHandler, AEProcessAppleEvent,
AECreateAppleEvent, AESend, AEGetParamDesc, AEGetNthDesc, AEPutParamDesc,
AECreateList, AEGetAttributePtr, AEResolve(+ObjectSupportLib) …` (see §9).
- **Misc:** `Random, BlockMove, GetScrap, SysEnvirons, ExitToShell,
NewRoutineDescriptor/NewFatRoutineDescriptor` (68K↔PPC mixed-mode glue).
### ObjectSupportLib (6) — Apple Event object model
`AEResolve, AESetObjectCallbacks, AEInstallObjectAccessor, CreateObjSpecifier,
AEObjectInit, CreateOffsetDescriptor` — the app is an AppleScript object-model
server over its window/document objects (standard PowerPlant `UAppleEventsMgr`).
---
## 4. Code organization (from the 27 named `CODE` segments)
Because this is a fat binary, the resource fork still carries the CodeWarrior
**68K segment names**, which are a clean module map of the whole program
(the PPC side is the same code, differently packaged):
```
1 Main 10 Utilities 19 RP Panes
2 Commanders 11 Libraries 20 Data Objects
3 Connection 12 Debug 21 BT Data Objects
4 Panes 13 TCP/IP 22 RP Data Objects
5 Table 14 MUNGA 23 Printing
6 Files 15 Console Menus 24 RP Printing
7 Apple Events 16 Console Windows 25 BT Printing
8 Array 17 Console Panes 26 Console Features
9 Features 18 BT Panes
```
- **Connection / TCP/IP** = the pod networking (`CTCPDriver`, `CTCPStream`,
`CTCPEndpoint`, `CTCPAsyncCall`, `CTCPResolverCall`, `NetworkEndpoint`).
- **MUNGA** = data marshalling (the mission/egg (de)serializer — "munge").
- **BT / RP** split = per-game code (BattleTech vs Red Planet) for panes, data
objects, and printing.
`.cp` source-module names recovered from the binary (the CodeWarrior debug
string table) include, among ~140 files:
`ConsoleApp, ConsoleDoc, ConsoleWindow, PrefsDoc, NetworkEndpoint,
NetworkEndpointList, NetworkTable, NetworkWindow, CTCPDriver, CTCPStream,
CTCPEndpoint, CTCPAsyncCall, CTCPResolverCall, MissionObject, MissionEvent,
MissionEventQueue, MissionView, MissionGraphView, MissionGraphWindow,
MissionClockView, Participant, TeamList, TeamPrtcpntList, PlayerTable,
BTScenario, BTVehicle, BTLocation, RPScenario, RPVehicle, RPLocation, Vehicle,
Weather, Experience, Adventure, ScoreSheetView, DamageMatrix, BTKillMarker` and
the PowerPlant framework classes (`LApplication, LDocument, LWindow, LView,
LPane, LTableView, LCommander, LListener/LBroadcaster(implied), UAppleEventsMgr,
UGWorld, UMemoryMgr, UReanimator, UModalDialogs`).
---
## 5. Resource fork (550 resources, 39 types)
Parsed from `__MACOSX/._Console 4.10` with `tools/rsrcparse.py`. Highlights:
- **`cfrg` (1)** — the Code Fragment resource that points the CFM loader at the
PEF in the data fork (confirms the fat-binary layout).
- **`vers` (2)** — `2.4.0b3`, "Console 2.4.0b3", © 19951997 **FASA Interactive, Inc.**
- **`SIZE` / `BNDL` / `FREF` / `ICN#`…** — standard app packaging; `icm#/ics#`
application icons.
- **`ºMWC` (32000)** — Metrowerks CodeWarrior project stamp (confirms toolchain).
### Operator commands & UI
- **Toolbar buttons** (`ICN#`/`icl4`/`icl8`, ids 10001011):
**Get Next Departure, Translocate, Load Mission, Add Pilot, Delete Pilot, Lookup.**
- **`MENU`/`Mcmd` (10)**: Apple, File (New / Close / Save / Save a Copy… / Page
Setup… / Print / Quit), Edit, **Configuration**, **Adventure**, **BattleTech**,
**Red Planet** + scenario menus (`BTScenario`, `RPScenario`).
- **`WIND`/`PPob` (windows)**: **`RPMR ConsoleWindow`, `RPMF ConsoleWindow`,
`BTFA ConsoleWindow`, `BTSB ConsoleWindow`** (the four game-mode console
layouts), **Mission Graph**, **Toolbar**, **Network Diagnostic**, **Lookup**.
- **`ALRT`/`DITL`**: About Box, Save-before-closing/quitting, Confirm Revert,
Low Memory Warning, **TCP Warning**, Landscape/Portrait mode.
### Data/game resources
- **`Mech`/`MWpn`/`MDgZ` (×24/×24/×22)** — BattleTech mechs + weapons + damage
zones: *BlackHawk, Loki, MadCat, Owens, Thor, Vulture, Avatar, Sunder,
TimberWolf, Satyr, Hellbringer, Summoner, …*
- **`PICT` (×98)** — pod photos (*Carpe Diem, Calamity Jane, Man O' War,…*) and
faction crests (*Davion, Kurita, Steiner, Liao, Marik, Comstar, VGL*). These
match `pictID=` in `Console.ini`.
- **`Txtr` (×57)** — named text traits (fonts/styles for each field, plasma
screen, mission clock).
### String lists (`STR#`) — the semantic vocabulary
61 `STR#` resources. The protocol-relevant ones:
- **`STR# 131 "Mission State"`** — the console-side (document) mission state
descriptions, index 0..6:
1. `Mission data is incomplete.`
2. `The mission is ready to load.`
3. `The mission is being loaded.`
4. `The mission is ready for translocation.`
5. `The mission is being translocated.`
6. `The mission is in progress.`
7. `The mission has been completed.`
- **`STR# 130 "Network Endpoints"`** — the 50-name roster: cockpits (*Frequent
Flyer, Privateer, Man O' War, Divine Wind, Carpe Diem, Gypsy, Puck, Icarus,
Calamity Jane, Valkyrie, Pegasus,…*), cameras (*Alpha/Beta/Gamma/Delta/Epsilon
Camera*), *Mission Review* stations, and PC/test targets (*BrownCartPC,
GaugeCartPC, ZabswareTarget*). Index 0 = `Bogon Alert!` (invalid),
`<Available>` = a free slot.
- **`STR# 1002/1003 "Network Errors"`** — explicitly a **4D database** client:
`_4D_Select4DServer`, `_4D_Find4DServer`, "connection to the database",
"asynchronous mode query already in progress", "invalid connection handle".
- **`STR# 1001 Database Errors`, `1004 Database Tasks`, `1005 General Errors`,
`128 General Strings`** (`"%s Mission %s"`, `"Mission Time & Date: %s, %s"`,
Show/Hide Toolbar, Show/Hide Network), **`300 Adventure Strings`**
(`BattleTech`, `Red Planet`), plus large game tables (scenarios, drop zones,
roles, kill/death/damage strings, textures, football positions, …).
- **`eTbl` (3)** — error tables named *Database Errors*, *Network Errors*,
*4D Open Errors* (again confirming the 4D backend).
---
## 6. Pod command/control protocol (MacTCP, port 1501)
This is the highest-value result. Evidence chain: binary strings →
disassembly (TOC-resolved) → `Console Log` → `Console.ini`.
### 6.1 Transport
- Custom C++ classes **`CTCPDriver` / `CTCPStream` / `CTCPEndpoint` /
`CTCPAsyncCall` / `CTCPResolverCall`** wrap **MacTCP**. The literal driver
name **`.IPP`** appears at data `0x1053F` right next to `CTCPDriver` — this is
the classic MacTCP IP driver opened with `PBOpenSync`, then driven with
`PBControlAsync/Sync`.
- One TCP connection per pod. From `Console.ini` each
`[NetworkEndpoint::Cockpit::*]` sets `classID=Ptcp`, `defaultPort=1501`,
`localHostPort=1501`, and the pod's `addressIP` (`200.0.0.11`…`200.0.0.117`,
etc.). Console host = `200.0.0.1`; pods `200.0.0.11``200.0.0.120` (from the
log). The many `Ptcp` sub-keys (`openTimeout, listenTimeout, delay,
throughput, reliability, precedence, fragment, timeToLive, security,
queryTimeout`) are **MacTCP TCP/IP ULP parameters** passed to TCPActiveOpen /
the IP header — further proof this is MacTCP, not OT.
### 6.2 Receive path — pod → console "Cockpit State"
Message dispatch lives at **code `0x63B3C`** (inside the participant's
message handler). The received message's low byte is the type:
```
0x63B3C clrlwi r0, r31, 0x18 ; r0 = msg & 0xFF (message type)
0x63B40 cmplwi r0, 0xC ; 0..12 valid
0x63B44 bgt 0x63BEC ; >12 → default → "Unknown message type!!!"
0x63B48 lwz r3, -0x6CA4(r2) ; TOC[data 0x135C] = jump table @ data 0x17B1C
0x63B4C slwi r0, r0, 2
0x63B50 lwzx r3, r3, r0 ; handler = jumptable[type]
0x63B54 mtctr r3 ; bctr
```
Every case calls **`SetCockpitState(participant+0x30C, type)` at `0x62720`**:
```
0x62720 SetCockpitState:
stb r4, 0x289(r6) ; participant->cockpitState = type (field at +0x289)
li r4, 0xBBC ; bl BroadcastMessage ; notify the UI panes/graph
```
(`0x62778` is the matching getter: `lbz r3, 0x289(r3)`.) The console then logs
`"<IP> Cockpit State: <type>"`. So **the pod's message-type byte is literally
the Cockpit State number, 012**; type > 12 logs `"Unknown message type!!!"`
(seen twice in the 5.3 MB log). The jump table at data `0x17B1C` (13 code
pointers, default `0x63BEC`) is the physical proof of the 012 range.
### 6.3 Send path — console → pod commands
Commands are transmitted by **`NetworkEndpoint::Send` at `0x629EC`**, called as
`Send(endpoint, &msgHeaderWord, payloadObj, payloadLen)`. It frames a packet =
**16-byte header + payload**:
```
buf = alloc(len + 0x10)
memcpy(buf+0x10, payloadObj, len) ; body
buf[0..3] = *msgHeaderWord ; message-class word (observed 0x04000000)
buf[4..7] = 0
buf[8..11] = value from 0x3CB38 ; sequence / timestamp
CTCPStream::Write(endpoint, buf, len+0x10) ; via 0x39748 → 0x3BD28
```
The **payload object** is a small serialized struct whose **byte[0] = its size**
and **byte[4] = the command code**. Two confirmed by disassembling the object
constructors:
| Command | Ctor | payload size | **command code (byte[4])** | Verb logged | Sender |
|---|---|---|---|---|---|
| Translocate mission | `0x3EC74` | `0x0C` | **5** | `Translocating Mission…` | `0x67438` |
| Stop mission | `0x3ECC8` | `0x10` | **6** | `Stopping Mission…` | `0x6755C` |
Other senders (same shape, codes not yet pinned): **Loading Mission**
(`0x64xxx`/`Received egg file` `0x648C0`), **Ending Mission** `0x66464` (uses a
variant send `0x5E474` that packs the destination IP from a 4-byte struct field
`+0x10..0x13`), **Reset Mission** `0x6769C`. Command codes for these are
recoverable with the same method (find the `li r0,N; stb r0,-0x10(r1)` that
seeds object byte[4]) — see Open Questions.
### 6.4 Internal PowerPlant command/broadcast IDs
The app's own command constants cluster in **`0xBBA``0xBDF`** (decimal
30023039). Confirmed meanings:
`0xBBC` = cockpit-state-changed broadcast, `0xBC5` = "received egg file",
`0xBCB` = ending mission, `0xBBA/0xBBB` = handled in the participant
`ListenToMessage` at `0x628D8`. (`0xBC1` is the most frequent; others 0xBCE
0xBDF map to menu/toolbar commands.)
### 6.5 Observed mission lifecycle (from the log)
A full run for one pod, with the two state machines interleaved
(**Cockpit State N** = pod→console; **verbs** = console actions):
```
Cockpit State 2 (present/connected)
Cockpit State 1 (ready)
Loading Mission… → console loads the egg
Cockpit State 6
Cockpit State 7
Received egg file → egg in hand; "Ready to translocate"
Cockpit State 8
Translocating Mission… → console sends egg/mission to pod (cmd code 5)
Cockpit State 9
Cockpit State 10 (in progress / running)
Stopping Mission… (cmd code 6)
Cockpit State 11
Ending Mission…
Cockpit State 12
Cockpit at <IP> initiated close!!! → pod closed its TCP stream
Cockpit State 3 (closed/disconnected)
Reset Mission… → console rearms for the next departure
Cockpit State 2 …
```
Cockpit-state frequencies in the sample log: `1:3997 2:13258 3:12809
6:3098 7:3093 8:3262 9:2852 10:2852 11:2990 12:2890` (states 0, 4, 5
never appear in this log though the dispatch table has slots for them — likely
transient/internal). Verb counts: `initiated close 24702, Loading 3129,
Received egg 3124, Stopping 2996, Ending 2905, Translocating 2882, Reset 953`.
---
## 7. Console Log format
- **Encoding:** Mac Roman. **Records are CR-terminated** (`0x0D`; classic Mac,
**no LF** — 98,910 CRs for ~91,607 records in the sample). `0xC9` = "…"
ellipsis inside the mission verbs (13,415 occurrences).
- **Record grammar:**
`M/D/YYYY` `HH:MM:SS`␠`AM|PM`␠`<message>` `CR`
where `<message>` is usually `<pod-IP> <text>` (state/verb lines) or the
IP-embedded form `Cockpit at <pod-IP> initiated close!!!`.
- The timestamps in this capture read year **1956** — the logging Mac's clock
was unset (default RTC), not a real date. Times of day are valid.
- **72 distinct message templates**; the load-bearing ones:
`"<IP> Cockpit State: <N>"`, `"Cockpit at <IP> initiated close!!!"`,
`"<IP> Loading|Translocating|Stopping|Ending|Reset Mission…"`,
`"<IP> Received egg file"`, `"<IP> Unknown message type!!!"`.
- **Log writer:** each line is assembled by a string-append helper (`0x85324`,
repeatedly) and flushed by `0x851C4`; the leading date/time comes from the
global at TOC `-0x77CC(r2)` (data `0x834`). Field separator string is at data
`0x17B8`.
---
## 8. Console.ini schema
INI-style but with **no newlines** — sections are `[Name]` headers and
`key=value` pairs run together; parse by splitting on `[`. Section names use a
`::` **hierarchical namespace**. 586 sections total in the live file.
**Section families (by count):** `RP` 342, `BT` 196, `rpl4` 34,
`NetworkEndpoint` 10, `Adventure` 3, `NetworkEndpointList` 1.
**Every object carries a 4-char `classID`** (an OSType tag naming its C++
class). Confirmed: `Ptcp` (TCP endpoint), `Advn` (adventure), `RPLn` (Red
Planet location); BT/RP scenario & vehicle classes follow the same pattern.
### 8.1 Network endpoints (the pods)
```
[NetworkEndpointList]
networkEndpoint = NetworkEndpoint::Cockpit::Frequent Flyer
networkEndpoint = NetworkEndpoint::Cockpit::Privateer
… (10 active endpoints)
[NetworkEndpoint::Cockpit::Frequent Flyer]
classID=Ptcp hostType=0 state=0 addressUse=0
addressIP=200.0.0.11
defaultPort=1501 localHostPort=1501 openInterval=40 openTimeout=10
listenTimeout=0
resourceID=130 resourceIndex=2 pictID=308 ; STR# 130 name + PICT photo
; MacTCP TCP/IP ULP parameters:
delay=1 throughput=1 reliability=1 precedence=0 fragment=0
timeToLive=60 security=0 queryTimeout=60
loadQueryInterval=1 inProgressQueryInterval=10
```
`resourceID=130` + `resourceIndex` selects the pod's display name from
`STR# 130`; `pictID` selects its photo PICT.
### 8.2 Adventure → Location → Scenario tree (populates the menus)
```
[Adventure::Solo::Red Planet] classID=Advn adventureType=2 resourceID=300 resourceIndex=2 list=RP::Solo::LocationList
[RP::Solo::LocationList] location=rpl4::Solo::Location::Yip's Yahoorama …
[rpl4::Solo::Location::Wiseguy's Wake] classID=RPLn resourceID=4000 resourceIndex=3 tag=wise pictID=601 list=RP::Wiseguy's Wake::Solo::ScenarioList
[BT::Cavern::ScenarioList] scenario=BT::Cavern::NoReturn scenario=BT::Cavern::FreeForAll
[BT::Solo::LocationList] location=BT::Solo::Ravines / Arena1 / Polar4 / Cavern …
```
Four top-level adventures exist in the binary:
`Adventure::{Solo,Team}::{BattleTech,Red Planet}` — matching the four console
windows `BTFA/BTSB/RPMF/RPMR ConsoleWindow`.
### 8.3 Key inventory
`classID, resourceID, resourceIndex, pictID, tag, list, scenario, location,
default, value, max, time` (structural) and the game-object keys
`vehicle/vehicel(sic)/vehicleClass, mechID, camo, color, texture, emblem,
killMarker, experience, role, position, dropZone, weather, temperature,
adventureType, language, scriptCode` plus the full `Ptcp` network key set.
`Stationary.ini` is the same schema used as a **template** to regenerate a
site's `Console.ini`; the per-city `ini Folder/DB<City>` files are venue
overrides.
---
## 9. Apple Events
The app installs standard AE handlers (`AEInstallEventHandler`) and is a full
AppleScript **object-model server** (`ObjectSupportLib` + `UAppleEventsMgr.cp`).
Its `aete` (id 0, "English") declares only the **standard suites** — Required
(`reqd`: run/open/print/quit), Standard/Core (`CoRe`: close/get/set/make/save/
data size), and Miscellaneous Standards (`misc`: revert/select) — over the
generic `application`/`window`/`document` classes. `aedt` (3) = *Required Suite,
Core Suite, Misc Standards*.
**Conclusion:** Apple Events are used for **OS/Finder integration**
(open-documents = open mission/ini files, print score sheets, quit), **not** for
commanding pods. Pod command is exclusively the MacTCP protocol in §6. (No
custom application scripting suite is defined.)
---
## 10. Tooling written (`410console/tools/`)
Reusable, dependency-light (Python 3.13 + capstone):
- **`pefparse.py`** — PEF container parser + PIDATA (RLE) unpacker + loader
(imports/exports/entry) + string extractor + capstone PPC disassembler.
`header | loader | strings [minlen] | dump <sec> <off> <len> | dis <sec> <off> <count>`.
- **`ppcxref.py`** — TOC-aware annotator (`r2` = data `0x8000`): resolves
`r2`-relative constant loads to data addresses/strings and builds a
string→code cross-reference. `annotate <start> <count> | xref <substr> | toc <off>`.
- **`rsrcparse.py`** — classic Mac resource-fork parser (reads the AppleDouble
`._` sidecar). `map | names <TYPE> | strn [id] | dump <TYPE> [id]`.
Example: `py -3.13 tools/rsrcparse.py "4_10-console-extracted/__MACOSX/._Console 4.10" strn 131`.
---
## 11. Confidence & open questions
**High confidence (multiple corroborating sources):**
- Fat PEF/PPC app, PowerPlant/CodeWarrior, FASA Interactive, creator `vwe4`,
vers 2.4.0b3. *(PEF header + resource fork + strings.)*
- MacTCP `.IPP`, port 1501, per-pod TCP, addressing scheme. *(imports +
`CTCPDriver`/`.IPP` string + ini + log.)*
- Cockpit State machine 012, pod→console, dispatch at `0x63B3C`, stored at
Participant `+0x289`, logged verbatim. *(disasm + jump table + log stats.)*
- Send framing = 16-byte header + payload; command payload byte[0]=size,
byte[4]=code; Translocate=5, Stop=6. *(disasm of `0x629EC` + two ctors.)*
- Mission lifecycle ordering. *(log, thousands of runs.)*
- ini schema and 4D-database backend. *(ini + `STR#`/`eTbl` + error strings.)*
**Open questions / next steps:**
1. **Finish the command-code table.** Pin byte[4] for Load, End, Reset,
Get-Next-Departure (method: disassemble each sender's object ctor near
`0x3EBxx0x3EFxx` for `li r0,N; stb r0,-0x10(r1)`). Also decode the
header word-2 (`0x3CB38`) — sequence counter vs. timestamp.
2. **Header semantics.** Confirm whether the pod reads packet byte[0] (class=4)
or a payload byte as its "message type", and map the console's **command**
codes to the resulting **Cockpit State** transitions (5→9, 6→11 observed).
3. **Egg transfer mechanism.** Determine whether the `.egg` mission bundle
(note the `Egg.ini` string at data `0x98E9`, and the **MUNGA** segment)
travels to the pod over the same 1501 stream during "Translocate", or via a
separate transfer. `Received egg file` is logged **console-side** — clarify
direction (console reads egg from disk vs. receives from DB/pod).
4. **States 0/4/5.** Never seen in this log; identify their handlers
(`jumptable[0]=0x63BEC` default, `[4]/[5]=0x63B78`) and meaning.
5. **4D protocol.** Out of scope here (proprietary ACI 4D client); the console↔
4D link governs scheduling ("Get Next Departure") and the pilot roster.
6. **Relocations.** `pefparse` does not yet apply the Loader relocation opcode
stream (`reloc_instr_offset`); constant addresses were resolved via the TOC
value instead, which was sufficient. A full reloc pass would make every data
pointer absolute for deeper xref.
---
### Appendix A — key addresses (code section, base 0)
| Addr | Role |
|---|---|
| `0x82E5C` | program `main` (via TVector @ data `0x5154`) |
| `0x63B3C` | received-message dispatch (msg&0xFF → jumptable @ data `0x17B1C`) |
| `0x62720` | `SetCockpitState(participant, state)` → Participant `+0x289`, broadcast `0xBBC` |
| `0x62778` | `GetCockpitState` (`lbz +0x289`) |
| `0x629EC` | `NetworkEndpoint::Send` (16-byte header + payload) |
| `0x39748`→`0x3BD28` | `CTCPStream::Write` |
| `0x3EC74` / `0x3ECC8` | Translocate (code 5) / Stop (code 6) command-object ctors |
| `0x66464` / `0x67438` / `0x6755C` / `0x6769C` / `0x648C0` | senders: Ending / Translocating / Stopping / Reset / Received-egg |
| `0x85324` / `0x851C4` | log-line append / flush |
| data `0x8000` | TOC base (`r2`) |
| data `0x17B1C` | 13-entry cockpit-message jump table |
### Appendix B — reproduce
```
cd c:\VWE\TeslaRel410\410console
py -3.13 tools\pefparse.py "4_10-console-extracted\Console 4.10" loader
py -3.13 tools\ppcxref.py xref "Mission"
py -3.13 tools\ppcxref.py annotate 0x629ec 0x60
py -3.13 tools\rsrcparse.py "4_10-console-extracted\__MACOSX\._Console 4.10" map
py -3.13 tools\rsrcparse.py "4_10-console-extracted\__MACOSX\._Console 4.10" strn 131
```
@@ -0,0 +1,271 @@
# BattleTech → TeslaConsole port spec
**Hand-off package for the `TeslaSuite` repo.** Everything learned in the
`TeslaRel410` repo about the Mac **4.10 operator console** and the path to add
**BattleTech** support to the modernized **TeslaConsole**. Implement this over in
`TeslaSuite` (that repo's Claude has the console's own memories and can build it).
This folder is self-contained — the `reference/` files travel with it.
> Sibling doc: `410console/CONSOLE-4.10-DECOMP.md` (full decompilation of the Mac
> `Console 4.10` PowerPC binary). This spec assumes it.
---
## 0. The situation in one paragraph
The Mac **`Console 4.10`** (decompiled — see the sibling doc) and the Windows
**`TeslaConsole.exe`** (4.11.x, which `TeslaSuite/Console/TeslaConsole` is a .NET
reconstruction of) are two generations of the *same* operator console. The Mac
one drove **both** games — BattleTech and Red Planet. **The modernized
TeslaConsole implements only Red Planet** (`TeslaConsole.RedPlanet`), and its
shell is even hardcoded to it (`TeslaConsoleForm`: `AppID != 0 → "Pod Not Running
RP"`, status strings `"RP Initalizing"`…). The task: **add a
`TeslaConsole.BattleTech` game that mirrors `TeslaConsole.RedPlanet`**, and
de-hardcode the shell to pick the game by `ApplicationID`. The transport layer is
game-agnostic and reused unchanged.
---
## 1. The shared spine (reused verbatim by BattleTech — do NOT re-implement)
Both games and both console generations use one command/control path:
- **Munga over TCP 1501**, one `MungaSocket` per pod (`Munga Net.dll`, vendored in
`Console/lib/`). Console `200.0.0.1`, pods `200.0.0.11…`.
- `MungaGame.cs` (in `TeslaConsole`, **game-agnostic**): connect pod:1501, poll
`StateQueryMessage``StateResponseMessage(ApplicationID, ApplicationState)`
every ~1 s, and on `ApplicationState.WaitingForEgg` stream the egg as 1000-byte
`EggFileMessage[]` chunks, await `AcknowledgeEggFileMessage`, then run.
- **`ApplicationState`** (the named "cockpit states"): `CreatingMission,
LaunchingMission, LoadingMission, ResumingMission, RunningMission,
SuspendingMission, WaitingForLaunch, WaitingForEgg, …`. In the Mac 4.10 console
these were the **numeric Cockpit States 012** (decomp §6.2).
- **`HostType`** (`TeslaConsole/HostType.cs`): `GameMachineHostType=0,
MissionReviewHostType=2` (camera/spectator), `ConsoleHostType=3`.
- Reused unchanged by BT: `MungaGame`, `Pod`, `PodManager`, `Site`, `SitePanel`,
`PlasmaBitmaps`, `PlasmaFontTool`, the ordinals blocks, and the whole
provisioning/launch path (that's the separate RPC channel on TCP 53290; game
control is Munga 1501).
**Only the egg *content* and the per-game UI/data differ between RP and BT.**
---
## 2. The BattleTech egg format (authoritative)
Reconstructed from the console-generated **`reference/cavern.egg`** (freeforall,
map cavern, night; pilot `cyd` in a `madcat`, veteran) and
**`reference/TESTARN.EGG`**. This is what `BTMission.ToEggString()` must produce.
### Structure (section by section)
```
[mission] adventure=BattleTech map=cavern scenario=freeforall time=night
weather=clear temperature=27 length=120
[ordinals] ← 1st4th place plasma bitmaps, 128×32, IDENTICAL to RP
bitmap=Ordinal::BitMap::1 … [Ordinal::BitMap::1] …32 rows… x=128 y=32 width=8
(…::2 ::3 ::4)
[pilots]
pilot=200.0.0.113 ← one per player + one per camera
[200.0.0.113] ← one participant block per pilot line
hostType=0 advancedDamage=1 loadzones=1 name=cyd bitmapindex=1
experience=veteran vehicle=madcat vehicleValue=0 badge=VGL
dropzone=one color=Grey patch=Red role=Role::Default
[largebitmap]
bitmap=BitMap::Large::cyd
[BitMap::Large::cyd] …32 rows… x=128 y=32 width=8 ← pilot-name plasma bitmap
[smallbitmap]
bitmap=BitMap::Small::cyd
[BitMap::Small::cyd] …16 rows… x=64 y=16 width=4
[Role::Default] model=dfltrole ← one block per referenced role
```
### BattleTech vs Red Planet — the field diff
`RPMission.ToEggString()` / `RPPlayer.AppendParticipantSpecificData()` is the
template. BT differs as follows:
| Field | Red Planet | BattleTech |
|---|---|---|
| `[mission] adventure=` | `Red Planet` | **`BattleTech`** |
| `[mission] temperature=` | `0` | **nonzero** (e.g. `27`) |
| `[mission] compression=` | present (`0`/`1`) | **absent** |
| participant `advancedDamage=` | — | **present** (`1`) advanced damage model |
| participant `experience=` | — | **present** — pilot skill (`novice`/`veteran`/`expert`) |
| participant `vehicleValue=` | — | **present** (`0`) |
| participant `patch=` | — | **present** (e.g. `Red`) |
| participant `role=` | — | **present** — `Role::<key>`, needs a `[Role::<key>]` block |
| participant `vehicle=` | RP racer (quark/bug…) | **a mech** (madcat/…) |
| `[Role::*]` blocks | — | **present** — `model=<rolemodel>` (`Default`→`dfltrole`; TESTARN.EGG also has `NoReturn`) |
| `ordinals`, `largebitmap`, `smallbitmap` | present | **identical** — reuse as-is |
Shared participant fields (same as RP): `hostType, loadzones, name, bitmapindex,
dropzone, color, badge`.
### Wire encoding (mirror RP exactly)
`RPMission` builds the string with `\n` between every `key=value`, then
`ToEggFileMessages()` does `Replace("\r\n","\0").Replace('\n','\0')`, ASCII-
encodes, and chunks into 1000-byte `EggFileMessage(index, totalLen, thisLen,
buf)`. So on the wire the egg is **NUL-delimited**. `cavern.egg` on disk is the
same NUL-delimited blob (renders as run-together fields). **BT reuses this
verbatim** — only `ToEggString()` content changes.
### Section-order caveat (don't chase a false byte-match)
`cavern.egg` was written by the *original* console; its section order (ordinals
early, bitmap defs inlined) differs from the modernized `RPMission.ToEggString`
order (bitmaps listed then defined, ordinals last). The pod parses the egg
**INI-style by section name**, so order is almost certainly non-critical. **Match
RP's builder structure** (it's known-good against real pods) rather than
byte-matching `cavern.egg`; use `cavern.egg` as the **field/value** reference and
verify a generated egg on a real BT pod. (If a byte-exact golden test is wanted,
generate from BT classes and pin *that*, like the existing DiffTests do.)
---
## 3. The RedPlanet template → BattleTech counterparts
Mirror `Console/TeslaConsole.RedPlanet/` into `Console/TeslaConsole.BattleTech/`.
The `.csproj` is SDK-style (globs `**/*.cs`), so new files under a new folder are
picked up automatically; add `Content Include` lines for the BT XML (see §5).
| RedPlanet file | BattleTech counterpart | Notes |
|---|---|---|
| `RPParticipant.cs` (abstract) | `BTParticipant` | `[podIP]` + `hostType`; abstract `AppendParticipantSpecificData` |
| `RPPlayer.cs` (abstract) | `BTPlayer` | adds the BT participant fields (§2 diff); `HostType.GameMachine` |
| `RPRacePlayer` / `RPFootballPlayer` | `BTPlayer` concretes per scenario | e.g. `BTFreeForAllPlayer`, `BTTeamPlayer` |
| `RPCamera.cs` | `BTCamera` | `HostType.MissionReview`, `vehicle=camera` |
| `RPMission.cs` (abstract) | `BTMission` | `ToEggString()` per §2; `ToEggFileMessages()` reusable as-is; emit `[Role::*]` blocks for referenced roles |
| `RPRaceMission` / `RPFootballMission` | `BTFreeForAllMission` / `BTTeamMission` | `AppendMissionSpecificData` for scenario extras |
| `Scenario.cs` + `FootballScenario`/`DeathRaceScenario` | `Scenario` + BT scenarios | loaded from `BTConfig.xml`; `<invalid>` filters valid maps/vehicles |
| `RPConfig.cs` + `RedPlanet/RPConfig.xml` | `BTConfig.cs` + `BattleTech/BTConfig.xml` | the data catalog (§5) |
| `RPDefaults.cs` / `RPDefaultsDialog.cs` | `BTDefaults` / `BTDefaultsDialog` | operator defaults UI |
| `RPMap.cs` / `RPVehicle.cs` | `BTMap` (arena) / `BTVehicle` (mech) | key/name/image; images under `images/battletech maps|vehicles/` |
| `RPTeam.cs` | `BTTeam` (if BT team scenario) | — |
| `MissionEvent` + `Scored/Killed/Damaged/Boost/ScoreUpdate` | BT `MissionEvent` + `Killed/Damaged/…` | BT has **DamageMatrix** + **KillMarker** (decomp §5 string lists); RP-only `Boost`/football events drop; confirm BT's set against the pod's Munga event messages |
| `RPMissionResults.cs` / `RPMissionRecorder.cs` / `RPPrintDocument.cs` | `BT*` | `.rpm`-style saved results + printout |
| `RPGame.cs` (the DockContent pane, ~66 KB) | `BTGame` | the big one: mission-config UI + the run/stop state loop driving each pod's `MungaGame` |
| `TeslaConsole/RPStrings.cs` + `RedPlanet/RPStrings.xml` | `BTStrings` | localized strings |
`RPGame`'s engine (worth copying closely): a per-pod `GameStateData
{EggMessageSent, RunMessageSent, AbortMessageSent, StopMessageSent,
PodAppearsReset}`, an `RPGameState` machine, `sStopToIdleStates` (the
`ApplicationState`s that mean "busy, must stop first"), `mEggFileMessages`,
`mMissionRecorder`, `mMissionPlayers` (index→player, to decode incoming events),
`mLastMissionResults`. BT reuses this shape unchanged.
---
## 4. Shell de-hardcoding (`TeslaConsole`)
The shell currently assumes RP. To support both games:
- **`TeslaConsoleForm.cs` ~line 139**: `if (pod.MungaGame.AppID.Value != 0) →
"Pod Not Running RP"`. `ApplicationID 0 = Red Planet`. Generalize: map the pod's
reported `ApplicationID` to a game (RP vs BT) and pick the right game module +
status strings ("RP Initalizing" → per-game). **OPEN: the `ApplicationID` value
for BattleTech** — see §6.
- Game selection where a game pane is created (Site/SitePanel/SiteManagement — how
`RPGame` is instantiated today): allow creating a `BTGame` for BT pods.
- Per-game status string table instead of the inline "RP …" literals.
---
## 5. The BattleTech catalog (`BTConfig.xml`) — seed + sources
Mirror `reference/RPConfig.xml.template`: a root element with per-scenario blocks
carrying scenario-specific option lists, plus top-level `<map>`/`<vehicle>`/
`<weather>`/`<time>` catalogs and `<invalid>` exclusions. For BT:
```xml
<BattleTech>
<freeforall name="Free For All">
<experience key="novice" name="Novice"/>
<experience key="veteran" name="Veteran"/>
<experience key="expert" name="Expert"/>
<color key="Grey" name="Grey"/> …
<badge key="VGL" name="VGL"/> …
<patch key="Red" name="Red"/> …
<role key="Default" model="dfltrole"/>
<role key="NoReturn" model="…"/>
</freeforall>
<!-- team/other scenarios from the Console.ini BT tree -->
<map key="cavern" name="Cavern" image="images/battletech maps/Cavern.bmp"/>
<map key="arena1" name="…"/>
<vehicle key="madcat" name="Mad Cat" image="images/battletech vehicles/Mad Cat.bmp"/>
<vehicle key="loki" name="Loki"/> …
<time key="day" name="Day"/> <time key="night" name="Night"/>
<weather key="clear" name="Clear"/> …
</BattleTech>
```
**Known values** (from this repo's eggs + reversing this session):
- **scenario**: `freeforall` (confirmed). More in the Console.ini BT tree.
- **map (arena)**: `cavern`, `arena1` (confirmed). Full list: Console.ini BT
LocationList + game maps.
- **vehicle (mech)**: `madcat` (Mad Cat). Others seen in this repo's mech data:
`loki`, `thor`, `vulture`, `bhk1` (Black Hawk); OWN/PGN skeletons exist too.
(Torso-twist limits found this session: MadCat ±130°, Loki/Thor 110°, Vulture
90°; some skeletons — BLH/OWN/PGN — have no torso joint.)
- **experience**: `veteran` (confirmed); `novice`/`expert` are the other BTL4
pilot levels.
- **color**: `Grey` (confirmed). **badge**: `VGL`. **patch**: `Red`.
- **role**: `Default` (`model=dfltrole`), `NoReturn` (from TESTARN.EGG).
- **time**: `day`, `night`. **weather**: `clear`. **temperature**: numeric (27).
**length**: seconds (120).
**Where to get the exhaustive catalog (in this repo):**
- `410console/4_10-console-extracted/Console.ini` — the Mac console's full
`[Adventure::…::BattleTech]` → LocationList → scenario/vehicle tree (minified;
parse by `::`-namespaced sections; decomp §8.2 shows the RP tree shape).
- `ALPHA_1/REL410/BT/` — the game itself: `GAUGE/L4GAUGE.CFG`, `*.RES` resources,
`*.SKL` skeletons (mech list), and the `.egg` files.
- `CODE/BT/` — BattleTech game source (mech/weapon/damage classes; the
DamageMatrix/KillMarker semantics for the event model).
---
## 6. Open questions — resolve in the TeslaSuite repo
1. **`ApplicationID` for BattleTech.** RP is `0`. A BT pod (running `BTL4OPT`)
reports its own value in `StateResponseMessage.Application`. Get it by
decompiling `Console/lib/Munga Net.dll`'s `ApplicationID` enum, or by reading a
live BT pod's state response. Needed for §4 shell game-selection.
2. **Full BT scenario list** + per-scenario `<invalid>` maps/vehicles — from the
Console.ini BT tree.
3. **BT in-match event messages.** RP receives `Scored/Killed/Damaged/Boost/
ScoreUpdate` `MungaMessage`s during a game. Confirm BT's set (BT models
DamageMatrix + KillMarker) against `Munga Net.dll` / a live BT match.
4. **Image assets** for BT mechs/arenas (RP ships `images/red planet …`; BT needs
`images/battletech …`). Sourceable from the game `.RES`/gauge art or recreated.
5. **Whether BT reuses RP's exact plasma bitmaps/ordinals** — `cavern.egg` shows
YES (identical `[ordinals]` + `[BitMap::Large|Small::*]` mechanism, same
128×32 / 64×16). Reuse `PlasmaBitmaps`/`PlasmaFontTool` unchanged.
---
## 7. Verification
- **Golden-egg check:** construct the `cavern.egg` scenario in BT classes
(freeforall/cavern/night/clear/27/120, pilot `cyd`/`madcat`/`veteran`/`VGL`/
`Grey`/`Red`/`Role::Default`) and diff `ToEggString()` field-by-field against
`reference/cavern.egg`. Values must match; section order may not (see §2 caveat).
- **Live check:** feed a generated egg to a real BT pod over Munga 1501 and drive
the state machine `WaitingForEgg → … → RunningMission`.
- Add BT cases to `Console/tests/TeslaConsole.DiffTests` alongside the RP ones.
---
## 8. `reference/` contents (travels with this spec)
- `cavern.egg` — console-generated BattleTech egg (the golden field reference).
- `TESTARN.EGG` — 2nd BT egg (shows multiple `[Role::*]` blocks).
- `RPConfig.xml.template` — the RP catalog XML to mirror as `BTConfig.xml`.
- `L4PLASMA.HPP` — the pod plasma-display class (128×32) the egg bitmaps target.
## 9. The plasma tie-in (context)
The console **authors** the pod's plasma-scoreboard graphics: the egg's
`[BitMap::Large::<pilot>]` (128×32) / `[BitMap::Small::<pilot>]` (64×16) and the
`[ordinals]` 1st4th place bitmaps are rendered on the pod's **plasma display**
(`L4PLASMA.HPP`: `plasmaWidth=128, plasmaHeight=32`, serial out on DOS COM2). So
console → egg → pod plasma panel. `PlasmaBitmaps`/`PlasmaFontTool` in TeslaConsole
generate them; BT reuses both. (In `TeslaRel410` the pod side of this is emulated
— see that repo's plasma-display notes.)
@@ -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.
+18
View File
@@ -0,0 +1,18 @@
# battletech-port — hand-off package
Everything needed to add **BattleTech** support to the modernized **TeslaConsole**
(in the `TeslaSuite` repo), mirroring its existing **Red Planet** implementation.
Assembled in the `TeslaRel410` repo from the Mac 4.10 console decompilation + the
game's own data. **Transplant this whole folder into the TeslaSuite repo** and
implement from the spec there (that repo can build the .NET solution).
- **`BATTLETECH-PORT-SPEC.md`** — start here. The full port spec: shared Munga/1501
spine, the BattleTech egg format + the BT-vs-RP field diff, the RedPlanet→BattleTech
class map, the shell de-hardcoding, the `BTConfig.xml` catalog + data sources,
open questions, and verification.
- **`reference/cavern.egg`** — console-generated BattleTech egg (golden field reference).
- **`reference/TESTARN.EGG`** — 2nd BT egg (shows multiple `[Role::*]` blocks).
- **`reference/RPConfig.xml.template`** — the RP catalog XML to mirror as `BTConfig.xml`.
- **`reference/L4PLASMA.HPP`** — the pod plasma-display class the egg bitmaps target.
Context: `../CONSOLE-4.10-DECOMP.md` (the Mac console decompilation this builds on).
@@ -0,0 +1,70 @@
//===========================================================================//
// File: L4plasma.hpp //
// Project: MUNGA Brick: Applications-specific controls module //
// Contents: Interface specification for plasma display //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- -----------------------------------------------------------//
// 04/20/95 CPB Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All rights reserved //
// PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#if !defined(L4PLASMA_HPP)
# define L4PLASMA_HPP
# if !defined(L4VB8_HPP)
# include <l4vb8.hpp>
# endif
# if !defined(L4SERIAL_HPP)
# include <l4serial.hpp>
# endif
//########################################################################
//################################ Plasma ################################
//########################################################################
class PlasmaDisplay :
public Video8BitBuffered
{
public:
enum
{
plasmaWidth=128,
plasmaHeight=32
};
PlasmaDisplay(Word port, Word int_num);
~PlasmaDisplay();
Logical
TestInstance() const;
void
ShowInstance(char *indent);
Logical
Update(Logical forceall);
void
WaitForUpdate();
protected:
void
Scan(Logical forceall);
Logical
forceAll,
updateInProgress;
PCSerial
*pc_serial;
int
currentTransferLine;
char
updateFlag[32];
};
#endif
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8" ?>
<RedPlanet>
<race name="Martian Death Race">
<invalid type="map" key="headmf" />
<color key="Red" name="Red" />
<color key="Blue" name="Blue" />
<color key="Green" name="Green" />
<color key="White" name="White" />
<color key="Pink" name="Pink" />
<color key="Aqua" name="Aqua" />
<color key="Yellow" name="Yellow" />
<color key="Purple" name="Purple" />
<color key="Black" name="Black" />
<badge key="None" name="None" />
<badge key="Celtic" name="Celtic" />
<badge key="Desert" name="Desert" />
<badge key="Flames" name="Flames" />
<badge key="Giraffe" name="Giraffe" />
<badge key="Hawaii" name="Hawaii" />
<badge key="Korean" name="Korean Camo" />
<badge key="Lightning" name="Lightning" />
<badge key="Razzle Dazzle" name="Razzle Dazzle" />
<badge key="Rising Sun" name="Rising Sun" />
<badge key="Tiger Stripe" name="Tiger Stripes" />
</race>
<football name="Martian Football">
<invalid type="map" key="pain" />
<invalid type="map" key="headoff" />
<team key="Red/Pink" name="Red/Pink" runnerColor="Pink" teamColor="Red" />
<team key="Blue/Aqua" name="Blue/Aqua" runnerColor="Aqua" teamColor="Blue" />
<team key="Green/Yellow" name="Green/Yellow" runnerColor="Yellow" teamColor="Green" />
<team key="Black/Purple" name="Black/Purple" runnerColor="Purple" teamColor="Black" />
<position key="crusher" name="Crusher" />
<position key="blocker" name="Blocker" />
<position key="runner" name="Runner" />
</football>
<map key="wise" name="Wiseguy's Wake" image="images/red planet maps/Wiseguy's Wake.bmp" />
<map key="lyzlane" name="Lyz's Lane" image="images/red planet maps/Lyz's Lane.bmp" />
<map key="yip" name="Yip's Yahoorama" image="images/red planet maps/Yip's Yahoorama.bmp" />
<map key="blade" name="Blade's Edge" image="images/red planet maps/Blade's Edge.bmp" />
<map key="otto" name="Berserker's Bahnzai" image="images/red planet maps/Berserker's Bahnzai.bmp" />
<map key="frstrm" name="Firestorm's Fury" image="images/red planet maps/Firestorm's Fury.bmp" />
<map key="burnt" name="Burnt Cookies" image="images/red planet maps/Burnt Cookies.bmp" />
<map key="brewers" name="Brewer's Bane" image="images/red planet maps/Brewer's Bane.bmp" />
<map key="pain" name="Paingod's Passage" image="images/red planet maps/Pain God's Passage.bmp" />
<map key="headoff" name="Freezemoon's Freeway" />
<map key="headmf" name="Freezemoon's Freeway" />
<vehicle key="quark" name="Quark" image="images/red planet vehicles/Quark.bmp" />
<vehicle key="lepton" name="Lepton" image="images/red planet vehicles/Quark.bmp" />
<vehicle key="proton" name="Proton" image="images/red planet vehicles/Quark.bmp" />
<vehicle key="bug" name="Bug" image="images/red planet vehicles/Bug.bmp" />
<vehicle key="speck" name="Speck" image="images/red planet vehicles/Bug.bmp" />
<vehicle key="blkspk" name="Black Speck" image="images/red planet vehicles/Bug.bmp" />
<vehicle key="chigger" name="Chigger" image="images/red planet vehicles/Skeeter.bmp" />
<vehicle key="flea" name="Flea" image="images/red planet vehicles/Skeeter.bmp" />
<vehicle key="roach" name="Roach" image="images/red planet vehicles/Skeeter.bmp" />
<vehicle key="skeeter" name="Skeeter" image="images/red planet vehicles/Skeeter.bmp" />
<vehicle key="wasp" name="Wasp" image="images/red planet vehicles/Skeeter.bmp" />
<vehicle key="barge" name="Barge" image="images/red planet vehicles/Mule.bmp" />
<vehicle key="broccoli" name="Screaming Broccoli" image="images/red planet vehicles/Mule.bmp" />
<vehicle key="burro" name="Burro" image="images/red planet vehicles/Mule.bmp" />
<vehicle key="mule" name="Mule" image="images/red planet vehicles/Mule.bmp" />
<vehicle key="bttlbrg" name="Battle Barge" image="images/red planet vehicles/Vole.bmp" />
<vehicle key="gator" name="Gator" image="images/red planet vehicles/Vole.bmp" />
<vehicle key="grunt" name="Grunt" image="images/red planet vehicles/Vole.bmp" />
<vehicle key="vole" name="Vole" image="images/red planet vehicles/Vole.bmp" />
<vehicle key="bull" name="Bull" image="images/red planet vehicles/Bull.bmp" />
<vehicle key="tarntula" name="Tarantula" image="images/red planet vehicles/Bull.bmp" />
<!--<vehicle key="blktran" name="Black Tarantula" image="images/red planet vehicles/Bull.bmp" />-->
<vehicle key="mantis" name="Mantis" image="images/red planet vehicles/Roadblock.bmp" />
<vehicle key="ripper" name="Ripper" image="images/red planet vehicles/Roadblock.bmp" />
<vehicle key="roadblk" name="Road Block" image="images/red planet vehicles/Roadblock.bmp" />
<vehicle key="spitter" name="Spitter" image="images/red planet vehicles/Roadblock.bmp" />
<vehicle key="puck" name="Armadillo" image="images/red planet vehicles/Armadillo.bmp" />
<vehicle key="dragon" name="Dragon" image="images/red planet vehicles/Police.bmp" />
<time key="day" name="Day" />
<time key="night" name="Night" />
<weather key="clear" name="Clear" />
<weather key="fog" name="Light Fog" />
<weather key="soup" name="Heavy Fog" />
</RedPlanet>
@@ -0,0 +1,229 @@
[mission]
adventure=BattleTech
map=arena1
scenario=freeforall
time=day
weather=clear
temperature=27
length=600
[ordinals]
bitmap=Ordinal::BitMap::1
bitmap=Ordinal::BitMap::2
bitmap=Ordinal::BitMap::3
bitmap=Ordinal::BitMap::4
[Ordinal::BitMap::1]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000038000003C000001FF00000000F00
bitmap=0000F8000003C000003FF80000000F00
bitmap=0001F8000003C00000707C0000000F00
bitmap=0001F8000003C00000603C0000000F00
bitmap=00007801FC0FF00000003C3DF807FF00
bitmap=00007803FE0FF00000003C3FFC0FFF00
bitmap=00007803C703C00000003C3E3C1F0F00
bitmap=000078078303C0000000783C1E1E0F00
bitmap=000078078003C0000000783C1E1E0F00
bitmap=00007807C003C0000000F03C1E1E0F00
bitmap=00007807F003C0000001E03C1E1E0F00
bitmap=00007803FC03C0000003C03C1E1E0F00
bitmap=00007801FE03C0000007803C1E1E0F00
bitmap=000078007F03C000000F003C1E1E0F00
bitmap=000078001F03C000001E003C1E1E0F00
bitmap=000078000F03C000003C003C1E1E0F00
bitmap=000078060F03C0000078003C1E1E0F00
bitmap=000078071E03E0000078003C1E1F1F00
bitmap=00007803FE01F000007FFC3C1E0FFF00
bitmap=00007801FC00F000007FFC3C1E07EF00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::2]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF0000003C0000003C03C0780000
bitmap=001FFF0000003C0000007C03C0780000
bitmap=00000E0000003C000000FC03C0780000
bitmap=00003C0000003C000001BC03C0780000
bitmap=0000700F3E1FFC000003BC0FF07BF000
bitmap=0001E00F7E3FFC0000073C0FF07FF800
bitmap=0003800FFE7C3C0000063C03C07C7800
bitmap=0007F80FFE783C00000C3C03C0783C00
bitmap=0007FE0F80783C0000183C03C0783C00
bitmap=00001E0F00783C0000383C03C0783C00
bitmap=00000F0F00783C0000703C03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00180F0F00783C0000003C03C0783C00
bitmap=001C1F0F007C7C0000003C03E0783C00
bitmap=000FFE0F003FFC0000003C01F0783C00
bitmap=0007FC0F001FBC0000003C00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::3]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000000FC03C0780000
bitmap=001FFF03C07800000003FC03C0780000
bitmap=001E0003C07800000007C003C0780000
bitmap=001E0003C0780000000F0003C0780000
bitmap=001E000FF07BF000000F000FF07BF000
bitmap=001E000FF07FF800001E000FF07FF800
bitmap=001E0003C07C7800001E0003C07C7800
bitmap=001FFC03C0783C00001EFC03C0783C00
bitmap=001FFE03C0783C00001FFE03C0783C00
bitmap=00001F03C0783C00001F1F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00180F03C0783C00001E0F03C0783C00
bitmap=001C1F03E0783C00001F1F03E0783C00
bitmap=000FFE01F0783C00000FFE01F0783C00
bitmap=0007FC00F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::4]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000007FC03C0780000
bitmap=001FFF03C0780000000FFE03C0780000
bitmap=00000F03C0780000001F1F03C0780000
bitmap=00000F03C0780000001E0F03C0780000
bitmap=00000F0FF07BF000001E0F0FF07BF000
bitmap=00001F0FF07FF800001E0F0FF07FF800
bitmap=00001E03C07C7800001E0F03C07C7800
bitmap=00003E03C0783C00001E0F03C0783C00
bitmap=00003C03C0783C00000F1E03C0783C00
bitmap=00003C03C0783C000007FC03C0783C00
bitmap=00007803C0783C000007FC03C0783C00
bitmap=00007803C0783C00000F1E03C0783C00
bitmap=00007803C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003E0783C00000F1E03E0783C00
bitmap=0000F001F0783C00000FFE01F0783C00
bitmap=0000F000F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[pilots]
pilot=200.0.0.113
[200.0.0.113]
hostType=0
advancedDamage=1
loadzones=1
name=cyd
bitmapindex=1
experience=veteran
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=madcat
vehicleValue=0
color=White
[largebitmap]
bitmap=BitMap::Large::cyd
[BitMap::Large::cyd]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000FCF00E0FBC0000000000
bitmap=000000000003FE781C1FFC0000000000
bitmap=000000000007FE781C3C7C0000000000
bitmap=00000000000F82783C3C3C0000000000
bitmap=00000000001F003C38783C0000000000
bitmap=00000000001E003C78783C0000000000
bitmap=00000000001E003C70783C0000000000
bitmap=00000000001E003E70783C0000000000
bitmap=00000000001E001EE0783C0000000000
bitmap=00000000001E001EE0783C0000000000
bitmap=00000000001F001FE0783C0000000000
bitmap=00000000000F020FC07C7C0000000000
bitmap=000000000007FE0FC03FFC0000000000
bitmap=000000000003FE0F803FFC0000000000
bitmap=000000000000FC07800F3C0000000000
bitmap=00000000000000070000000000000000
bitmap=00000000000000070000000000000000
bitmap=000000000000000F0000000000000000
bitmap=000000000000000E0000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[smallbitmap]
bitmap=BitMap::Small::cyd
[BitMap::Small::cyd]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000600000
bitmap=00000000006000000000000000600000
bitmap=000001EC33E000000000030666600000
bitmap=000003066660000000000303C6600000
bitmap=00000303C66000000000030186600000
bitmap=000001E183E000000000000300000000
bitmap=00000003000000000000000000000000
x=64
y=16
width=4
[Role::Default]
model=dfltrole
[Role::NoReturn]
model=noretun
Binary file not shown.
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""
pefparse.py -- Minimal parser/disassembler for classic Mac OS CFM
Preferred Executable Format (PEF) PowerPC containers ('Joy!peff pwpc').
Written for the VWE Tesla restoration project to decompile "Console 4.10".
Big-endian PowerPC. Pure-python; only external dep is capstone (optional,
for the `dis` command).
PEF reference: Apple "Mac OS Runtime Architectures", ch. Preferred
Executable Format. Container header (40 bytes), then N section headers
(28 bytes each), then the section name table, then section bodies.
Usage:
py -3.13 pefparse.py <file> header # container + section headers
py -3.13 pefparse.py <file> loader # loader section: imports/exports/entry
py -3.13 pefparse.py <file> strings # printable strings in code+data
py -3.13 pefparse.py <file> dis [secidx] [start] [count] # disassemble
py -3.13 pefparse.py <file> dump <secidx> <off> <len> # hex dump section body
"""
import sys, struct, string
def u32(b, o): return struct.unpack_from('>I', b, o)[0]
def u16(b, o): return struct.unpack_from('>H', b, o)[0]
def s32(b, o): return struct.unpack_from('>i', b, o)[0]
def u8(b, o): return b[o]
SECTION_KINDS = {
0: 'Code', 1: 'UnpackedData', 2: 'PatternInitData', 3: 'Constant',
4: 'Loader', 5: 'Debug', 6: 'ExecutableData', 7: 'Exception', 8: 'Traceback',
}
class Section:
def __init__(self, idx, hdr, container):
(self.name_off, self.default_addr, self.total_size, self.unpacked_size,
self.packed_size, self.container_off, self.kind, self.share,
self.align, self.rsvd) = hdr
self.idx = idx
self.name = None
self._container = container
def raw(self):
"""Raw (possibly packed) bytes of the section body as stored."""
return self._container[self.container_off:self.container_off + self.packed_size]
def data(self):
"""Unpacked section body. PatternInitData(2) is RLE-decompressed."""
raw = self.raw()
if self.kind == 2:
return unpack_pidata(raw, self.unpacked_size)
return raw
class PEF:
def __init__(self, blob):
self.blob = blob
assert blob[0:4] == b'Joy!', "not a PEF (missing Joy!)"
assert blob[4:8] == b'peff', "not a PEF (missing peff)"
self.arch = blob[8:12]
self.format_version = u32(blob, 12)
self.timestamp = u32(blob, 16)
self.old_def_ver = u32(blob, 20)
self.old_imp_ver = u32(blob, 24)
self.current_ver = u32(blob, 28)
self.section_count = u16(blob, 32)
self.inst_section_count = u16(blob, 34)
self.sections = []
o = 40
for i in range(self.section_count):
hdr = struct.unpack_from('>iIIIIIBBBB', blob, o)
self.sections.append(Section(i, hdr, blob))
o += 28
# section name table follows the section headers
name_tbl_off = o
for s in self.sections:
if s.name_off >= 0:
s.name = read_cstr(blob, name_tbl_off + s.name_off)
def loader(self):
for s in self.sections:
if s.kind == 4:
return s
return None
def read_cstr(b, o):
e = o
while e < len(b) and b[e] != 0:
e += 1
return b[o:e].decode('mac_roman', 'replace')
def unpack_pidata(raw, unpacked_size):
"""Decompress PEF pattern-initialized data (RLE opcode stream)."""
out = bytearray()
i = 0
n = len(raw)
def argcount():
nonlocal i
val = 0
while True:
b = raw[i]; i += 1
val = (val << 7) | (b & 0x7f)
if not (b & 0x80):
break
return val
while i < n:
op = raw[i]; i += 1
opcode = op >> 5
count5 = op & 0x1f
cnt = count5 if count5 else argcount()
if opcode == 0: # Zero
out.extend(b'\x00' * cnt)
elif opcode == 1: # blockCopy
out.extend(raw[i:i+cnt]); i += cnt
elif opcode == 2: # repeatedBlock
rpt = argcount()
block = raw[i:i+cnt]; i += cnt
for _ in range(rpt + 1):
out.extend(block)
elif opcode == 3: # interleaveRepeatBlockWithBlockCopy
cs = argcount() # custom size
rc = argcount() # repeat count
common = raw[i:i+cnt]; i += cnt
for _ in range(rc):
out.extend(common)
out.extend(raw[i:i+cs]); i += cs
out.extend(common)
elif opcode == 4: # interleaveRepeatBlockWithZero
cs = argcount()
rc = argcount()
for _ in range(rc):
out.extend(b'\x00' * cnt)
out.extend(raw[i:i+cs]); i += cs
out.extend(b'\x00' * cnt)
else:
raise ValueError("bad PIDATA opcode %d at %d" % (opcode, i))
return bytes(out[:unpacked_size])
# ---- Loader section parsing -------------------------------------------------
def parse_loader(sec):
b = sec.data()
(main_section, main_offset, init_section, init_offset,
term_section, term_offset, imported_lib_count, total_imported_sym_count,
reloc_section_count, reloc_instr_offset, loader_strings_offset,
export_hash_offset, export_hash_table_power, exported_symbol_count) = \
struct.unpack_from('>iIiIiIIIIIIIII', b, 0)
info = dict(main_section=main_section, main_offset=main_offset,
init_section=init_section, init_offset=init_offset,
term_section=term_section, term_offset=term_offset,
imported_lib_count=imported_lib_count,
total_imported_sym_count=total_imported_sym_count,
reloc_section_count=reloc_section_count,
reloc_instr_offset=reloc_instr_offset,
loader_strings_offset=loader_strings_offset,
export_hash_offset=export_hash_offset,
export_hash_table_power=export_hash_table_power,
exported_symbol_count=exported_symbol_count)
# Imported library table: starts at offset 56
libs = []
o = 56
for _ in range(imported_lib_count):
(name_off, old_impl_ver, curr_ver, imp_sym_count,
first_imp_sym, options, rsvdA, rsvdB) = struct.unpack_from('>IIIIIBBH', b, o)
libs.append(dict(name_off=name_off, imp_sym_count=imp_sym_count,
first_imp_sym=first_imp_sym, options=options))
o += 24
# Imported symbol table follows lib table
imp_syms = []
for _ in range(total_imported_sym_count):
val = u32(b, o); o += 4
sym_class = val >> 24
name_off = val & 0x00FFFFFF
imp_syms.append((sym_class, name_off))
strtab = info['loader_strings_offset']
for lib in libs:
lib['name'] = read_cstr(b, strtab + lib['name_off'])
syms = []
for k in range(lib['first_imp_sym'], lib['first_imp_sym'] + lib['imp_sym_count']):
cls, noff = imp_syms[k]
syms.append((cls, read_cstr(b, strtab + noff)))
lib['symbols'] = syms
# Exported symbols (loader export hash). Layout:
# export hash table (2^power u32 slots) at export_hash_offset
# then key table (u32 per exported symbol)
# then exported symbol table (10 bytes each: class/name u32, value u32, sect u16)
exports = []
hash_slots = 1 << export_hash_table_power
key_tab_off = export_hash_offset + hash_slots * 4
sym_tab_off = key_tab_off + exported_symbol_count * 4
for k in range(exported_symbol_count):
eo = sym_tab_off + k * 10
clsname = u32(b, eo)
value = u32(b, eo + 4)
sect = u16(b, eo + 8)
sym_class = clsname >> 24
name_off = clsname & 0x00FFFFFF
name = read_cstr(b, strtab + name_off)
exports.append(dict(name=name, sym_class=sym_class, value=value, section=sect))
return info, libs, exports
SYM_CLASS = {0: 'code(TVect)', 1: 'data', 2: 'TVector', 3: 'TOC/Glue',
4: 'linker/glue', 0x40: 'weak?'}
# ---- commands ---------------------------------------------------------------
def cmd_header(pef):
print("PEF container: arch=%s formatVersion=%d timestamp=0x%08x currentVer=0x%08x"
% (pef.arch.decode('ascii','replace'), pef.format_version, pef.timestamp, pef.current_ver))
print("sectionCount=%d instantiatedSectionCount=%d" % (pef.section_count, pef.inst_section_count))
print()
print("idx name kind default_addr total unpacked packed fileoff align")
for s in pef.sections:
print("%3d %-14s %-19s 0x%08x %-9d %-9d %-9d 0x%06x %d" % (
s.idx, (s.name or '-'), "%d:%s" % (s.kind, SECTION_KINDS.get(s.kind, '?')),
s.default_addr, s.total_size, s.unpacked_size, s.packed_size,
s.container_off, s.align))
def cmd_loader(pef):
lsec = pef.loader()
if not lsec:
print("no loader section"); return
info, libs, exports = parse_loader(lsec)
print("=== Loader header ===")
for k, v in info.items():
if isinstance(v, int) and v > 9 and k.endswith(('offset','section','count')) is False:
print(" %-28s 0x%x (%d)" % (k, v, v))
else:
print(" %-28s %s" % (k, v))
ms = info['main_section']
print(" ENTRY main: section %d offset 0x%x" % (info['main_section'], info['main_offset']) if ms>=0 else " (no main)")
print(" ENTRY init: section %d offset 0x%x" % (info['init_section'], info['init_offset']))
print(" ENTRY term: section %d offset 0x%x" % (info['term_section'], info['term_offset']))
print()
print("=== Imported libraries (%d) ===" % len(libs))
for lib in libs:
print("\n-- %s (%d symbols) --" % (lib['name'], lib['imp_sym_count']))
for j,(cls,name) in enumerate(lib['symbols']):
print(" [%4d] cls=%s %s" % (lib['first_imp_sym']+j, SYM_CLASS.get(cls, hex(cls)), name))
print()
print("=== Exported symbols (%d) ===" % len(exports))
for e in exports:
print(" %-40s cls=%s value=0x%x sect=%d" % (e['name'], SYM_CLASS.get(e['sym_class'], hex(e['sym_class'])), e['value'], e['section']))
def iter_strings(data, minlen=4):
cur = bytearray()
start = 0
printable = set(bytes(string.printable, 'ascii')) - set(b'\t\n\r\x0b\x0c')
printable |= set(b' ')
for i, ch in enumerate(data):
if ch in printable:
if not cur:
start = i
cur.append(ch)
else:
if len(cur) >= minlen:
yield start, cur.decode('ascii', 'replace')
cur = bytearray()
if len(cur) >= minlen:
yield start, cur.decode('ascii', 'replace')
def cmd_strings(pef, minlen=4):
for s in pef.sections:
if s.kind in (0,1,2,3):
data = s.data()
print("### section %d (%s) base=0x%x ###" % (s.idx, SECTION_KINDS.get(s.kind), s.default_addr))
for off, st in iter_strings(data, minlen):
print("0x%08x %s" % (s.default_addr + off, st))
def cmd_dump(pef, secidx, off, length):
data = pef.sections[secidx].data()
chunk = data[off:off+length]
for i in range(0, len(chunk), 16):
row = chunk[i:i+16]
hexs = ' '.join('%02x' % c for c in row)
asci = ''.join(chr(c) if 32 <= c < 127 else '.' for c in row)
print('%08x %-48s %s' % (off+i, hexs, asci))
def cmd_dis(pef, secidx=0, start=0, count=200):
import capstone
sec = pef.sections[secidx]
data = sec.data()
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
md.detail = False
base = sec.default_addr
for insn in md.disasm(data[start:start+count*4], base+start):
print("0x%08x %-8s %s" % (insn.address, insn.mnemonic, insn.op_str))
def main():
path = sys.argv[1]
cmd = sys.argv[2] if len(sys.argv) > 2 else 'header'
with open(path, 'rb') as f:
blob = f.read()
pef = PEF(blob)
if cmd == 'header': cmd_header(pef)
elif cmd == 'loader': cmd_loader(pef)
elif cmd == 'strings':
minlen = int(sys.argv[3]) if len(sys.argv) > 3 else 4
cmd_strings(pef, minlen)
elif cmd == 'dump':
cmd_dump(pef, int(sys.argv[3]), int(sys.argv[4],0), int(sys.argv[5],0))
elif cmd == 'dis':
secidx = int(sys.argv[3]) if len(sys.argv) > 3 else 0
start = int(sys.argv[4],0) if len(sys.argv) > 4 else 0
count = int(sys.argv[5],0) if len(sys.argv) > 5 else 200
cmd_dis(pef, secidx, start, count)
else:
print("unknown cmd", cmd)
if __name__ == '__main__':
main()
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
ppcxref.py -- TOC-aware PPC annotator for the Console 4.10 PEF.
Resolves r2(TOC)-relative loads to data addresses and, when those point at
C strings, annotates the disassembly. Also builds a string cross-reference:
which code addresses reference which strings.
r2 (TOC base) = data section offset 0x8000 (from the main transition vector).
Usage:
py -3.13 ppcxref.py annotate <start> <count> # annotated disasm
py -3.13 ppcxref.py xref <substring> # find code refs to strings matching substring
py -3.13 ppcxref.py toc <hexoff> # what does TOC[-off(r2)] point to
"""
import sys, struct, capstone
sys.path.insert(0, __import__('os').path.dirname(__file__))
from pefparse import PEF
BIN = r"C:\VWE\TeslaRel410\410console\4_10-console-extracted\Console 4.10"
TOC_BASE = 0x8000 # r2 value = data section offset
pef = PEF(open(BIN,'rb').read())
CODE = pef.sections[0].data()
DATA = pef.sections[1].data()
def d32(off):
if off < 0 or off+4 > len(DATA): return None
return struct.unpack_from('>I', DATA, off)[0]
def cstr_at(off, maxlen=80):
if off < 0 or off >= len(DATA): return None
e = off
while e < len(DATA) and DATA[e] != 0 and (e-off) < maxlen:
c = DATA[e]
if c < 0x09 or (c > 0x0d and c < 0x20) or c > 0x7e:
# allow only printable; stop otherwise
if c != 0xc9: # ellipsis
break
e += 1
s = DATA[off:e]
try:
txt = s.decode('mac_roman')
except Exception:
return None
# require mostly printable and length>=3
printable = sum(1 for c in s if 0x20 <= c <= 0x7e)
if len(s) >= 3 and printable >= max(3, int(len(s)*0.8)):
return txt
return None
def toc_target(disp):
"""disp is signed offset from r2. Returns (data_off, pointed_value, string_or_None)."""
off = TOC_BASE + disp
val = d32(off)
s = None
if val is not None:
s = cstr_at(val)
return off, val, s
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
md.detail = True
def annotate(start, count):
base = 0
for insn in md.disasm(CODE[start:start+count*4], base+start):
note = ''
# detect r2-relative: op_str contains "r2)" with a displacement
ops = insn.op_str
# capstone PPC prints e.g. "r4, -0x6c9c(r2)" or "r3, r2, -0x6848"
disp = None
if '(r2)' in ops:
# form: rX, disp(r2)
try:
d = ops.split(',')[-1].strip()
d = d[:d.index('(r2)')]
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
except Exception:
disp = None
if disp is not None:
o, val, s = toc_target(disp)
if val is not None:
note = "; TOC[0x%x]=*0x%x=0x%x" % (o, o, val)
if s: note += ' "%s"' % s
elif 'r2,' in ops and insn.mnemonic in ('addi','addic','subi'):
# form: rX, r2, disp -> address = TOC_BASE+disp (pointer to data directly)
try:
d = ops.split(',')[-1].strip()
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
except Exception:
disp = None
if disp is not None:
addr = TOC_BASE + disp
s = cstr_at(addr)
note = "; &data[0x%x]" % addr
if s: note += ' "%s"' % s
elif insn.mnemonic == 'lis':
# high-half load, often followed by addi to form an absolute data ptr
note = ''
print("0x%08x %-8s %-28s %s" % (insn.address, insn.mnemonic, insn.op_str, note))
def build_string_table():
"""All C strings in data with >=4 printable chars -> {addr: text}."""
tbl = {}
i = 0
n = len(DATA)
while i < n:
if 0x20 <= DATA[i] <= 0x7e:
j = i
while j < n and (0x20 <= DATA[j] <= 0x7e):
j += 1
if j - i >= 4:
tbl[i] = DATA[i:j].decode('ascii','replace')
i = j
else:
i += 1
return tbl
def xref(substr):
# find data offsets of strings containing substr
strtbl = build_string_table()
targets = {a:t for a,t in strtbl.items() if substr.lower() in t.lower()}
if not targets:
print("no strings match", substr); return
print("=== target strings ===")
for a,t in sorted(targets.items()):
print(" 0x%06x %s" % (a, t))
# Build TOC reverse map: which TOC slots hold pointers into these strings
# (pointer may target start OR interior). Map value->slot.
tocslots = {}
for slot in range(0, len(DATA), 4):
v = d32(slot)
if v in targets:
tocslots.setdefault(v, []).append(slot)
# Also direct addi r2 references use TOC_BASE+disp == string addr directly
print("\n=== code references ===")
# Disassemble whole code, look for r2-relative producing an address in targets
for insn in md.disasm(CODE, 0):
ops = insn.op_str
hit = None
if '(r2)' in ops:
try:
d = ops.split(',')[-1].strip(); d = d[:d.index('(r2)')]
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
o = TOC_BASE+disp; v = d32(o)
if v in targets: hit = (v, targets[v], 'TOC')
except Exception: pass
elif 'r2,' in ops and insn.mnemonic in ('addi','subi'):
try:
d = ops.split(',')[-1].strip()
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
addr = TOC_BASE+disp
if addr in targets: hit = (addr, targets[addr], 'direct')
except Exception: pass
if hit:
print(" 0x%08x %-8s %-24s -> %s \"%s\"" % (insn.address, insn.mnemonic, ops, hit[2], hit[1]))
if __name__ == '__main__':
cmd = sys.argv[1]
if cmd == 'annotate':
annotate(int(sys.argv[2],0), int(sys.argv[3],0))
elif cmd == 'xref':
xref(sys.argv[2])
elif cmd == 'toc':
o,v,s = toc_target(int(sys.argv[2],0))
print("TOC off=0x%x val=0x%x str=%s" % (o, v if v else 0, s))
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
rsrcparse.py -- classic Mac OS resource-fork parser.
Reads a resource fork (raw, or embedded in an AppleDouble ._ sidecar) and
lists resource types/IDs/names. Optionally dumps a given type.
Usage:
py -3.13 rsrcparse.py <._file> [--base 0x52] map
py -3.13 rsrcparse.py <._file> [--base 0x52] dump <TYPE> [id]
"""
import sys, struct
def u32(b,o): return struct.unpack_from('>I',b,o)[0]
def u16(b,o): return struct.unpack_from('>H',b,o)[0]
def appledouble_rfork_base(blob):
# AppleDouble: magic 0x00051607; entries describe forks. Entry id 2 = resource fork.
if u32(blob,0) != 0x00051607:
return 0, len(blob)
n = u16(blob, 24)
o = 26
for _ in range(n):
eid, off, length = struct.unpack_from('>III', blob, o); o += 12
if eid == 2:
return off, length
return 0, len(blob)
def parse(blob, base):
rf = blob[base:]
data_off, map_off, data_len, map_len = struct.unpack_from('>IIII', rf, 0)
m = map_off
type_list_off = u16(rf, m+24) # from map start
name_list_off = u16(rf, m+26)
tlo = m + type_list_off
ntypes = u16(rf, tlo) + 1
types = []
for i in range(ntypes):
eo = tlo + 2 + i*8
typ = rf[eo:eo+4].decode('mac_roman','replace')
count = u16(rf, eo+4) + 1
ref_off = u16(rf, eo+6)
refs = []
rlo = tlo + ref_off
for j in range(count):
ro = rlo + j*12
rid = struct.unpack_from('>h', rf, ro)[0]
noff = struct.unpack_from('>h', rf, ro+2)[0]
attr_dataoff = u32(rf, ro+4)
attrs = attr_dataoff >> 24
dataoff = attr_dataoff & 0x00FFFFFF
name = None
if noff != -1:
nlo = m + name_list_off + noff
ln = rf[nlo]
name = rf[nlo+1:nlo+1+ln].decode('mac_roman','replace')
# resource data: at data_off + dataoff, first 4 bytes = length
dstart = data_off + dataoff
dlen = u32(rf, dstart)
refs.append(dict(id=rid, name=name, attrs=attrs, data_start=base+dstart+4, dlen=dlen))
types.append((typ, refs))
return types
def cmd_map(blob, base):
types = parse(blob, base)
total = sum(len(r) for _,r in types)
print("resource types: %d, total resources: %d" % (len(types), total))
for typ, refs in sorted(types, key=lambda x:x[0]):
ids = ', '.join('%d%s' % (r['id'], ('("%s")'%r['name']) if r['name'] else '') for r in refs[:12])
more = '' if len(refs)<=12 else ' ...(%d total)'%len(refs)
print(" '%s' x%-4d : %s%s" % (typ, len(refs), ids, more))
def cmd_dump(blob, base, typ, wantid=None):
types = dict(parse(blob, base))
if typ not in types:
print("no type", typ); return
for r in types[typ]:
if wantid is not None and r['id'] != wantid: continue
data = blob[r['data_start']:r['data_start']+r['dlen']]
print("=== '%s' id=%d name=%s len=%d ===" % (typ, r['id'], r['name'], r['dlen']))
# hex + ascii
for i in range(0, min(len(data), 4096), 16):
row = data[i:i+16]
hexs=' '.join('%02x'%c for c in row)
asci=''.join(chr(c) if 32<=c<127 else '.' for c in row)
print(' %04x %-48s %s' % (i, hexs, asci))
def cmd_names(blob, base, typ):
types = dict(parse(blob, base))
if typ not in types:
print("no type", typ); return
for r in types[typ]:
print(" id=%-6d %s" % (r['id'], r['name'] or ''))
def cmd_strn(blob, base, wantid=None):
"""Decode STR# (list of Pascal strings)."""
types = dict(parse(blob, base))
for r in types.get('STR#', []):
if wantid is not None and r['id'] != wantid: continue
d = blob[r['data_start']:r['data_start']+r['dlen']]
n = u16(d,0); o=2; items=[]
for _ in range(n):
ln=d[o]; o+=1; items.append(d[o:o+ln].decode('mac_roman','replace')); o+=ln
print("STR# %d (%s): %s" % (r['id'], r['name'], ' | '.join(items)))
if __name__ == '__main__':
args = sys.argv[1:]
path = args[0]; args = args[1:]
base = None
if args and args[0] == '--base':
base = int(args[1],0); args = args[2:]
blob = open(path,'rb').read()
if base is None:
base, _ = appledouble_rfork_base(blob)
cmd = args[0] if args else 'map'
if cmd == 'map': cmd_map(blob, base)
elif cmd == 'dump': cmd_dump(blob, base, args[1], int(args[2]) if len(args)>2 else None)
elif cmd == 'names': cmd_names(blob, base, args[1])
elif cmd == 'strn': cmd_strn(blob, base, int(args[1]) if len(args)>1 else None)
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
BattleTech data catalog for TeslaConsole (mirror of RedPlanet\RPConfig.xml).
Reconstructed from the Mac Console 4.10 `Console.ini` BattleTech tree
(Adventure::Solo::BattleTech -> Location -> Scenario -> Time/Weather/
Experience/Vehicle/Emblem/DropZone/Camo/Patch) and the two console-generated
golden eggs (410console/battletech-port/reference/cavern.egg, TESTARN.EGG).
Wire (egg) mapping of the console fields:
scenario -> [mission] scenario= (BOTH Free For All and No Return send "freeforall")
map -> [mission] map=
time -> [mission] time=
weather -> [mission] weather= (temperature is a property of the weather entry)
experience-> participant experience=
vehicle -> participant vehicle= (the mech)
color -> participant color= (this is the CAMO list)
patch -> participant patch= (a color-name list, distinct from camo)
badge -> participant badge= (the faction EMBLEM)
role -> participant role=Role::<key>, plus a [Role::<key>] model=<model> block
Free For All and No Return differ ONLY by the pilot role (Default vs NoReturn);
both emit scenario=freeforall on the wire. Image assets are added in a later
phase (the image= attribute is optional; a missing one just shows the name).
Map-specific restrictions from the Mac ini that this flat model does NOT yet
enforce (candidates for a later refinement): Cavern is night-only; Ravines has
no "clear" weather (fog/soup only); Polar Missile Base uses 4 drop zones.
-->
<BattleTech>
<freeforall name="Free For All">
<!-- Pilot skill. key = egg value; name = operator-facing label (Mac STR# 307). -->
<experience key="novice" name="Standard" />
<experience key="standard" name="Veteran" />
<experience key="veteran" name="Expert" />
<!-- color= : mech camo (Mac BT::Standard::Camo::* tags). -->
<color key="Grey" name="Grey" />
<color key="White" name="White" />
<color key="Black" name="Black" />
<color key="Brown" name="Brown" />
<color key="Crimson" name="Crimson" />
<color key="Green" name="Green" />
<color key="Tan" name="Tan" />
<!-- patch= : team/badge color (Mac BT::Standard::Badge* tags). -->
<patch key="Red" name="Red" />
<patch key="Yellow" name="Yellow" />
<patch key="Blue" name="Blue" />
<patch key="Green" name="Green" />
<patch key="Grey" name="Grey" />
<patch key="Violet" name="Violet" />
<patch key="White" name="White" />
<patch key="Black" name="Black" />
<!-- badge= : faction emblem (Mac BT::5DropZone::* emblem tags / STR# 3006). -->
<badge key="VGL" name="VGL" />
<badge key="Davion" name="Davion" />
<badge key="Kurita" name="Kurita" />
<badge key="Liao" name="Liao" />
<badge key="Marik" name="Marik" />
<badge key="Steiner" name="Steiner" />
<badge key="None" name="None" />
<!-- role= : Free For All uses Default, No Return uses NoReturn. model= is the
egg value. The Mac ini tag is "noreturn" and the game's BTL4.RES role
resources are named dfltrole/NoReturn; the 4.10 console emitted a broken
"noretun" (a dropped character) in every egg on the production box, which
cannot resolve against the RES, so the correct spelling is used here. -->
<role key="Default" model="dfltrole" name="Default" />
<role key="NoReturn" model="noreturn" name="No Return" />
</freeforall>
<!-- Arenas. key = egg map value (Mac tag=); name = Mac STR# 3000 label. -->
<map key="arena1" name="Abandoned Arena" />
<map key="arena2" name="Destroyed Arena" />
<map key="polar3" name="Polar Missile Base" />
<map key="polar4" name="Helicopter Base" />
<map key="rav" name="Ravines" />
<map key="grass" name="Savannah" />
<map key="dbase" name="Badlands" />
<map key="cavern" name="Cavern" />
<!-- Mechs. key = egg vehicle value (Mac tag=); name = Mac STR# 3002 label. -->
<vehicle key="blkhawk" name="Blackhawk" />
<vehicle key="loki" name="Loki" />
<vehicle key="bhk1" name="Nova" />
<vehicle key="madcat" name="Mad Cat" />
<vehicle key="owens" name="Owens" />
<vehicle key="thor" name="Thor" />
<vehicle key="vulture" name="Vulture" />
<vehicle key="avatar" name="Avatar" />
<vehicle key="sunder" name="Sunder" />
<vehicle key="mad1" name="Timber Wolf" />
<vehicle key="ava1" name="Satyr" />
<vehicle key="lok1" name="Hellbringer" />
<vehicle key="thr1" name="Summoner" />
<vehicle key="vul1" name="Maddog" />
<vehicle key="own1" name="Kotori" />
<vehicle key="snd1" name="Denkou" />
<vehicle key="lok2" name="Mischief" />
<vehicle key="mad2" name="Zanin Neko" />
<!-- Time of day (Mac STR# 301 / BT TimeList). -->
<time key="morning" name="Morning" />
<time key="day" name="Day" />
<time key="evening" name="Evening" />
<time key="night" name="Night" />
<!-- Weather. temperature is carried on the weather entry (both known BT weathers
report 27 in the Mac ini); it becomes the egg [mission] temperature=. -->
<weather key="clear" name="Clear" temperature="27" />
<weather key="fog" name="Light Fog" temperature="27" />
<weather key="soup" name="Heavy Fog" temperature="27" />
</BattleTech>
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
BattleTech mission-highlight narrative strings (mirror of RedPlanet\RPStrings.xml).
Placeholders: {time} = mm:ss game time, {player} = pilot name, {vehicle} = mech
display name, {damager} = the other pilot involved.
-->
<BTStrings>
<LightDamage>{time} - Armor plating flakes off as {damager} lightly damages {player}'s mech.</LightDamage>
<LightDamage>{time} - {damager} lands a glancing hit on {player}.</LightDamage>
<LightDamage>{player}'s armor shrugs off a light volley from {damager}.</LightDamage>
<LightDamage>{player} laughs over the comm as {damager} can only scratch {player}'s paint.</LightDamage>
<ModerateDamage>{time} - Armor buckles as {damager} moderately damages {player}'s mech.</ModerateDamage>
<ModerateDamage>{player} is still in the fight as {damager} delivers a solid blow to {player}'s mech.</ModerateDamage>
<ModerateDamage>{damager} carves a respectable chunk out of {player}'s armor.</ModerateDamage>
<ModerateDamage>{time} - {damager} rocks {player}'s cockpit with a moderate impact.</ModerateDamage>
<HeavyDamage>{time} - Molten armor sprays as {damager} heavily damages {player}'s mech.</HeavyDamage>
<HeavyDamage>{player} fights the controls as {damager} nearly cores {player}'s mech.</HeavyDamage>
<HeavyDamage>{damager} unloads a devastating alpha strike into {player}'s mech.</HeavyDamage>
<HeavyDamage>{time} - Warning klaxons scream in {player}'s cockpit after a heavy impact from {damager}.</HeavyDamage>
<Kill>{time} - {damager} delivers a righteous kill to {player}.</Kill>
<Kill>{damager} stands victorious over the smoking wreckage of {player}'s {vehicle}.</Kill>
<Kill>{time} - {damager} cores {player}'s reactor. Glory to the victor.</Kill>
<Kill>The shattered remains of {player}'s {vehicle} litter the arena floor as {damager} salutes.</Kill>
<DeathWithoutHonor>{time} - {player} dies without honor as the {vehicle} destroys itself.</DeathWithoutHonor>
<DeathWithoutHonor>Shame falls upon {player}, whose {vehicle} is lost to no enemy's hand.</DeathWithoutHonor>
<DeathWithoutHonor>{time} - {player}'s {vehicle} overheats catastrophically. A death without honor.</DeathWithoutHonor>
<DeathWithoutHonor>{player} learns a hard lesson in mech management as the {vehicle} goes down on its own.</DeathWithoutHonor>
<Recap>...and at the end of the mission it's: {winners} followed by {looser}.</Recap>
</BTStrings>
+2 -2
View File
@@ -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")]
+19 -14
View File
@@ -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
+87 -1
View File
@@ -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");
}
}
+117
View File
@@ -0,0 +1,117 @@
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Loads and exposes the BattleTech data catalog (BattleTech\BTConfig.xml),
/// mirroring <c>TeslaConsole.RedPlanet.RPConfig</c>. The single BT scenario is
/// "Free For All"; No Return reuses the same catalog and differs only by role.
/// </summary>
public static class BTConfig
{
private static readonly BTScenario mFreeForAll;
private static readonly Dictionary<string, BTMap> mMaps;
private static readonly Dictionary<string, BTVehicle> mVehicles;
private static readonly Dictionary<string, string> mTimesOfDay;
private static readonly Dictionary<string, BTWeather> mWeather;
public static BTScenario FreeForAll => mFreeForAll;
public static Dictionary<string, BTMap> Maps => mMaps;
public static Dictionary<string, BTVehicle> Vehicles => mVehicles;
public static Dictionary<string, string> TimesOfDay => mTimesOfDay;
public static Dictionary<string, BTWeather> Weather => mWeather;
static BTConfig()
{
mMaps = new Dictionary<string, BTMap>();
mVehicles = new Dictionary<string, BTVehicle>();
mTimesOfDay = new Dictionary<string, string>();
mWeather = new Dictionary<string, BTWeather>();
XmlNode freeForAllNode = null;
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "BattleTech\\BTConfig.xml"));
foreach (XmlNode childNode in xmlDocument.DocumentElement.ChildNodes)
{
switch (childNode.Name)
{
case "freeforall":
freeForAllNode = childNode;
break;
case "map":
{
BTMap bTMap = new BTMap(childNode);
mMaps.Add(bTMap.Key, bTMap);
break;
}
case "vehicle":
{
BTVehicle bTVehicle = new BTVehicle(childNode);
mVehicles.Add(bTVehicle.Key, bTVehicle);
break;
}
case "time":
AddKeyNameNode(childNode, mTimesOfDay);
break;
case "weather":
{
BTWeather bTWeather = new BTWeather(childNode);
mWeather.Add(bTWeather.Key, bTWeather);
break;
}
}
}
mFreeForAll = new BTScenario(freeForAllNode);
}
internal static void AddKeyNameNode(XmlNode node, Dictionary<string, string> list)
{
list.Add(node.Attributes["key"].InnerText, node.Attributes["name"].InnerText);
}
public static string MapNameOrKey(string mapKey)
{
if (!mMaps.ContainsKey(mapKey))
{
return mapKey;
}
return mMaps[mapKey].Name;
}
public static string VehicleNameOrKey(string vehicleKey)
{
if (!mVehicles.ContainsKey(vehicleKey))
{
return vehicleKey;
}
return mVehicles[vehicleKey].Name;
}
public static string TimeOfDayNameOrKey(string timeOfDayKey)
{
if (!mTimesOfDay.ContainsKey(timeOfDayKey))
{
return timeOfDayKey;
}
return mTimesOfDay[timeOfDayKey];
}
public static string WeatherNameOrKey(string weatherKey)
{
if (!mWeather.ContainsKey(weatherKey))
{
return weatherKey;
}
return mWeather[weatherKey].Name;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A mech took damage (<c>MechDamagedMessage</c>). Carries the Red Planet
/// damage fields plus BT's damage-matrix extras: which zone was hit, whether it
/// was destroyed, and the weapon that did it.
/// </summary>
[Serializable]
internal class BTDamagedEvent : BTMissionEvent
{
public readonly BTPlayer Damager;
public readonly int DamageLoss;
public readonly int PointsTransfered;
public readonly int DamageZoneIndex;
public readonly bool DamageZoneDestroyed;
public readonly int WeaponIndex;
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[2] { ReportingPilot, Damager };
public BTDamagedEvent(BTPlayer reportingPilot, TimeSpan missionTime, BTPlayer damager, int damageLoss, int pointsTransfered, int damageZoneIndex, bool damageZoneDestroyed, int weaponIndex)
: base(reportingPilot, missionTime)
{
Damager = damager;
DamageLoss = damageLoss;
PointsTransfered = pointsTransfered;
DamageZoneIndex = damageZoneIndex;
DamageZoneDestroyed = damageZoneDestroyed;
WeaponIndex = weaponIndex;
}
private BTDamagedEvent()
{
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A mech died without a killer to credit — overheating, terrain, or
/// self-destruction (<c>MechDeathWithoutHonorMessage</c>).
/// </summary>
[Serializable]
internal class BTDeathWithoutHonorEvent : BTMissionEvent
{
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[1] { ReportingPilot };
public BTDeathWithoutHonorEvent(BTPlayer reportingPilot, TimeSpan missionTime)
: base(reportingPilot, missionTime)
{
}
private BTDeathWithoutHonorEvent()
{
}
}
@@ -0,0 +1,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)
{
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>A mech was destroyed (<c>MechKilledMessage</c>).</summary>
[Serializable]
internal class BTKilledEvent : BTMissionEvent
{
public readonly BTPlayer Killer;
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[2] { ReportingPilot, Killer };
public BTKilledEvent(BTPlayer reportingPilot, TimeSpan missionTime, BTPlayer killer)
: base(reportingPilot, missionTime)
{
Killer = killer;
}
private BTKilledEvent()
{
}
}
+44
View File
@@ -0,0 +1,44 @@
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
public class BTMap
{
private readonly string mKey;
private readonly string mName;
private readonly string mImagePath;
public string Key => mKey;
public string Name => mName;
public Image Image
{
get
{
if (mImagePath == null)
{
return null;
}
return ImageCache.GetImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), mImagePath));
}
}
internal BTMap(XmlNode mapNode)
{
XmlAttribute xmlAttribute = mapNode.Attributes["image"];
mKey = mapNode.Attributes["key"].InnerText;
mName = mapNode.Attributes["name"].InnerText;
mImagePath = xmlAttribute?.InnerText;
}
public override string ToString()
{
return mName;
}
}
@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using System.Text;
using Munga.Net;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech mission and its egg serializer. Mirrors
/// <c>TeslaConsole.RedPlanet.RPMission</c>: the wire framing
/// (<see cref="ToEggFileMessages" />) and the plasma bitmap / ordinal sections
/// are identical to Red Planet. BattleTech differs only in the egg body — see
/// <see cref="ToEggString" /> (adventure name, non-zero temperature, no score
/// compression, BT participant fields and the <c>[Role::*]</c> blocks).
/// </summary>
[Serializable]
internal abstract class BTMission
{
private readonly string mScenarioKey;
private readonly string mMapKey;
private readonly string mTimeOfDayKey;
private readonly string mWeatherKey;
private readonly int mTemperature;
private readonly TimeSpan mGameLength;
private readonly List<BTCamera> mCameras = new List<BTCamera>();
public string ScenarioKey => mScenarioKey;
public string MapKey => mMapKey;
public string TimeOfDayKey => mTimeOfDayKey;
public string WeatherKey => mWeatherKey;
public int Temperature => mTemperature;
public TimeSpan GameLength => mGameLength;
public abstract IEnumerable<BTPlayer> Players { get; }
public abstract int PlayerCount { get; }
public List<BTCamera> Cameras => mCameras;
protected BTMission(string scenarioKey, string mapKey, string timeOfDayKey, string weatherKey, int temperature, TimeSpan gameLength)
{
if (scenarioKey == null)
{
throw new ArgumentNullException("scenarioKey");
}
if (mapKey == null)
{
throw new ArgumentNullException("mapKey");
}
if (timeOfDayKey == null)
{
throw new ArgumentNullException("timeOfDayKey");
}
if (weatherKey == null)
{
throw new ArgumentNullException("weatherKey");
}
if (gameLength <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException("gameLength");
}
mScenarioKey = scenarioKey;
mMapKey = mapKey;
mTimeOfDayKey = timeOfDayKey;
mWeatherKey = weatherKey;
mTemperature = temperature;
mGameLength = gameLength;
}
public EggFileMessage[] ToEggFileMessages()
{
string text = ToEggString();
text = text.Replace("\r\n", "\0");
text = text.Replace('\n', '\0');
byte[] bytes = Encoding.ASCII.GetBytes(text);
EggFileMessage[] array = new EggFileMessage[(bytes.Length + 999) / 1000];
byte[] array2 = new byte[1000];
for (int i = 0; i < array.Length; i++)
{
int num = i * 1000;
int num2 = bytes.Length - num;
if (num2 > 1000)
{
num2 = 1000;
}
Buffer.BlockCopy(bytes, num, array2, 0, num2);
array[i] = new EggFileMessage(i, bytes.Length, num2, array2);
}
return array;
}
public string ToEggString()
{
StringBuilder stringBuilder = new StringBuilder();
List<string> list = new List<string>();
stringBuilder.Append("[mission]\n");
stringBuilder.Append("adventure=BattleTech\n");
stringBuilder.AppendFormat("map={0}\n", mMapKey);
stringBuilder.AppendFormat("scenario={0}\n", mScenarioKey);
stringBuilder.AppendFormat("time={0}\n", mTimeOfDayKey);
stringBuilder.AppendFormat("weather={0}\n", mWeatherKey);
stringBuilder.AppendFormat("temperature={0}\n", mTemperature);
stringBuilder.AppendFormat("length={0}\n", mGameLength.TotalSeconds);
stringBuilder.Append("[pilots]\n");
foreach (BTPlayer player in Players)
{
stringBuilder.AppendFormat("pilot={0}\n", player.PodHostAddress);
}
foreach (BTCamera camera in Cameras)
{
stringBuilder.AppendFormat("pilot={0}\n", camera.PodHostAddress);
}
int num = 1;
List<BTRole> roles = new List<BTRole>();
foreach (BTPlayer player2 in Players)
{
player2.AppendParticipant(num++, stringBuilder);
list.Add(player2.Name);
if (!roles.Exists((BTRole r) => r.Key == player2.Role.Key))
{
roles.Add(player2.Role);
}
}
foreach (BTCamera camera2 in Cameras)
{
camera2.AppendParticipant(num++, stringBuilder);
list.Add("Camera");
}
AppendMissionSpecificData(stringBuilder);
stringBuilder.Append("[largebitmap]\n");
foreach (string item in list)
{
stringBuilder.AppendFormat("bitmap=BitMap::Large::{0}\n", item);
}
stringBuilder.Append("[smallbitmap]\n");
foreach (string item2 in list)
{
stringBuilder.AppendFormat("bitmap=BitMap::Small::{0}\n", item2);
}
foreach (string item3 in list)
{
stringBuilder.AppendFormat("[BitMap::Large::{0}]\n", item3);
stringBuilder.Append(PlasmaBitmaps.GenerateString(128, 32, item3));
stringBuilder.Append("width=8\n");
stringBuilder.AppendFormat("[BitMap::Small::{0}]\n", item3);
stringBuilder.Append(PlasmaBitmaps.GenerateString(64, 16, item3));
stringBuilder.Append("width=4\n");
}
foreach (BTRole role in roles)
{
stringBuilder.AppendFormat("[Role::{0}]\n", role.Key);
stringBuilder.AppendFormat("model={0}\n", role.Model);
}
AppendOrdinals(stringBuilder);
return stringBuilder.ToString();
}
protected abstract void AppendMissionSpecificData(StringBuilder egg);
private static void AppendOrdinals(StringBuilder sb)
{
sb.Append("[ordinals]\n");
sb.Append("bitmap=Ordinal::BitMap::1\n");
sb.Append("bitmap=Ordinal::BitMap::2\n");
sb.Append("bitmap=Ordinal::BitMap::3\n");
sb.Append("bitmap=Ordinal::BitMap::4\n");
sb.Append("[Ordinal::BitMap::1]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=000038000003C000001FF00000000F00\n");
sb.Append("bitmap=0000F8000003C000003FF80000000F00\n");
sb.Append("bitmap=0001F8000003C00000707C0000000F00\n");
sb.Append("bitmap=0001F8000003C00000603C0000000F00\n");
sb.Append("bitmap=00007801FC0FF00000003C3DF807FF00\n");
sb.Append("bitmap=00007803FE0FF00000003C3FFC0FFF00\n");
sb.Append("bitmap=00007803C703C00000003C3E3C1F0F00\n");
sb.Append("bitmap=000078078303C0000000783C1E1E0F00\n");
sb.Append("bitmap=000078078003C0000000783C1E1E0F00\n");
sb.Append("bitmap=00007807C003C0000000F03C1E1E0F00\n");
sb.Append("bitmap=00007807F003C0000001E03C1E1E0F00\n");
sb.Append("bitmap=00007803FC03C0000003C03C1E1E0F00\n");
sb.Append("bitmap=00007801FE03C0000007803C1E1E0F00\n");
sb.Append("bitmap=000078007F03C000000F003C1E1E0F00\n");
sb.Append("bitmap=000078001F03C000001E003C1E1E0F00\n");
sb.Append("bitmap=000078000F03C000003C003C1E1E0F00\n");
sb.Append("bitmap=000078060F03C0000078003C1E1E0F00\n");
sb.Append("bitmap=000078071E03E0000078003C1E1F1F00\n");
sb.Append("bitmap=00007803FE01F000007FFC3C1E0FFF00\n");
sb.Append("bitmap=00007801FC00F000007FFC3C1E07EF00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
sb.Append("[Ordinal::BitMap::2]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=001FFF0000003C0000003C03C0780000\n");
sb.Append("bitmap=001FFF0000003C0000007C03C0780000\n");
sb.Append("bitmap=00000E0000003C000000FC03C0780000\n");
sb.Append("bitmap=00003C0000003C000001BC03C0780000\n");
sb.Append("bitmap=0000700F3E1FFC000003BC0FF07BF000\n");
sb.Append("bitmap=0001E00F7E3FFC0000073C0FF07FF800\n");
sb.Append("bitmap=0003800FFE7C3C0000063C03C07C7800\n");
sb.Append("bitmap=0007F80FFE783C00000C3C03C0783C00\n");
sb.Append("bitmap=0007FE0F80783C0000183C03C0783C00\n");
sb.Append("bitmap=00001E0F00783C0000383C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000703C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C00007FFF03C0783C00\n");
sb.Append("bitmap=00000F0F00783C00007FFF03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=00180F0F00783C0000003C03C0783C00\n");
sb.Append("bitmap=001C1F0F007C7C0000003C03E0783C00\n");
sb.Append("bitmap=000FFE0F003FFC0000003C01F0783C00\n");
sb.Append("bitmap=0007FC0F001FBC0000003C00F0783C00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
sb.Append("[Ordinal::BitMap::3]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=001FFF03C07800000000FC03C0780000\n");
sb.Append("bitmap=001FFF03C07800000003FC03C0780000\n");
sb.Append("bitmap=001E0003C07800000007C003C0780000\n");
sb.Append("bitmap=001E0003C0780000000F0003C0780000\n");
sb.Append("bitmap=001E000FF07BF000000F000FF07BF000\n");
sb.Append("bitmap=001E000FF07FF800001E000FF07FF800\n");
sb.Append("bitmap=001E0003C07C7800001E0003C07C7800\n");
sb.Append("bitmap=001FFC03C0783C00001EFC03C0783C00\n");
sb.Append("bitmap=001FFE03C0783C00001FFE03C0783C00\n");
sb.Append("bitmap=00001F03C0783C00001F1F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00180F03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=001C1F03E0783C00001F1F03E0783C00\n");
sb.Append("bitmap=000FFE01F0783C00000FFE01F0783C00\n");
sb.Append("bitmap=0007FC00F0783C000007FC00F0783C00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
sb.Append("[Ordinal::BitMap::4]\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=001FFF03C07800000007FC03C0780000\n");
sb.Append("bitmap=001FFF03C0780000000FFE03C0780000\n");
sb.Append("bitmap=00000F03C0780000001F1F03C0780000\n");
sb.Append("bitmap=00000F03C0780000001E0F03C0780000\n");
sb.Append("bitmap=00000F0FF07BF000001E0F0FF07BF000\n");
sb.Append("bitmap=00001F0FF07FF800001E0F0FF07FF800\n");
sb.Append("bitmap=00001E03C07C7800001E0F03C07C7800\n");
sb.Append("bitmap=00003E03C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=00003C03C0783C00000F1E03C0783C00\n");
sb.Append("bitmap=00003C03C0783C000007FC03C0783C00\n");
sb.Append("bitmap=00007803C0783C000007FC03C0783C00\n");
sb.Append("bitmap=00007803C0783C00000F1E03C0783C00\n");
sb.Append("bitmap=00007803C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
sb.Append("bitmap=0000F003E0783C00000F1E03E0783C00\n");
sb.Append("bitmap=0000F001F0783C00000FFE01F0783C00\n");
sb.Append("bitmap=0000F000F0783C000007FC00F0783C00\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("bitmap=00000000000000000000000000000000\n");
sb.Append("x=128\n");
sb.Append("y=32\n");
sb.Append("width=8\n");
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Base class for a recorded in-match BattleTech event. Mirrors the Red Planet
/// <c>MissionEvent</c>, typed over <see cref="BTPlayer" />.
/// </summary>
[Serializable]
internal abstract class BTMissionEvent
{
public readonly TimeSpan GameTime;
public readonly BTPlayer ReportingPilot;
public abstract IEnumerable<BTPlayer> InvolvedPilots { get; }
public BTMissionEvent(BTPlayer reportingPilot, TimeSpan missionTime)
{
ReportingPilot = reportingPilot;
GameTime = missionTime;
}
protected BTMissionEvent()
{
}
}
@@ -0,0 +1,99 @@
using System;
namespace TeslaConsole.BattleTech;
/// <summary>
/// Records the in-match Munga event messages of one BattleTech mission into a
/// <see cref="BTMissionResults" />. Mirrors <c>RPMissionRecorder</c>; the event
/// set follows the BT messages in Munga Net.dll (Mech* / EndMission).
/// </summary>
internal class BTMissionRecorder
{
private BTMissionResults mMissionResults;
private bool mStartSet;
public BTMissionRecorder(BTMission mission)
{
mMissionResults = new BTMissionResults(mission);
}
public void SetMissionStart()
{
if (mStartSet)
{
throw new InvalidOperationException();
}
mMissionResults.mMissionStart = DateTime.Now;
mStartSet = true;
}
private void ValidatePlayer(BTPlayer player, string param)
{
if (player == null)
{
throw new ArgumentNullException(param);
}
foreach (BTPlayer player2 in mMissionResults.Mission.Players)
{
if (player2 == player)
{
return;
}
}
throw new ArgumentException(param);
}
private TimeSpan GetMissionTime()
{
if (!mStartSet)
{
return TimeSpan.Zero;
}
return DateTime.Now - mMissionResults.mMissionStart;
}
public void PlayerKilled(BTPlayer reportingPlayer, BTPlayer killer)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
ValidatePlayer(killer, "killer");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTKilledEvent(reportingPlayer, missionTime, killer));
}
public void PlayerDeathWithoutHonor(BTPlayer reportingPlayer)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTDeathWithoutHonorEvent(reportingPlayer, missionTime));
}
public void PlayerScoreUpdate(BTPlayer reportingPlayer, int score)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTScoreUpdateEvent(reportingPlayer, missionTime, score));
}
public void PlayerDamaged(BTPlayer reportingPlayer, BTPlayer damager, int damageLoss, int pointsTransfered, int damageZoneIndex, bool damageZoneDestroyed, int weaponIndex)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
ValidatePlayer(damager, "damager");
TimeSpan missionTime = GetMissionTime();
mMissionResults.mEvents.Add(new BTDamagedEvent(reportingPlayer, missionTime, damager, damageLoss, pointsTransfered, damageZoneIndex, damageZoneDestroyed, weaponIndex));
}
public void PlayerFinalScore(BTPlayer reportingPlayer, int finalScore)
{
ValidatePlayer(reportingPlayer, "reportingPlayer");
mMissionResults.mFinalScores.Add(reportingPlayer, finalScore);
}
public BTMissionResults EndMission(TimeSpan missionLength)
{
mMissionResults.mActuallMissionTime = missionLength;
BTMissionResults result = mMissionResults;
mMissionResults = null;
return result;
}
}
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.IO;
using System.IO.Compression;
using System.Runtime.Serialization.Formatters.Binary;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The recorded outcome of one BattleTech mission, saved as a gzip'd
/// BinaryFormatter blob (.btm) exactly like the RP .rpm mechanism. Mirrors
/// <c>RPMissionResults</c> minus the football branches (BT battle modes have
/// no teams, so every ranking entry is a single pilot).
/// </summary>
[Serializable]
internal class BTMissionResults
{
private BTMission mMission;
internal DateTime mMissionStart;
internal TimeSpan mActuallMissionTime;
internal readonly List<BTMissionEvent> mEvents = new List<BTMissionEvent>();
internal readonly Dictionary<BTPlayer, int> mFinalScores = new Dictionary<BTPlayer, int>();
public BTMission Mission => mMission;
public DateTime MissionStart => mMissionStart;
public TimeSpan ActuallMissionTime => mActuallMissionTime;
public IEnumerable<BTMissionEvent> Events => mEvents;
internal BTMissionResults(BTMission mission)
{
mMission = mission;
}
public static string GetBTMissionsDirectory()
{
string text = Path.Combine(Program.GetCommonAppDataDirectory(), "BT Missions");
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
return text;
}
public static BTMissionResults Load(string file)
{
using GZipStream gZipStream = new GZipStream(File.OpenRead(file), CompressionMode.Decompress);
BinaryFormatter binaryFormatter = new BinaryFormatter();
BTMissionResults result = (BTMissionResults)binaryFormatter.Deserialize(gZipStream);
gZipStream.Close();
return result;
}
public void Save()
{
string path = Path.Combine(GetBTMissionsDirectory(), mMissionStart.ToString("yyyy-MM-dd HH-mm-ss.ffff") + ".btm");
using GZipStream gZipStream = new GZipStream(File.OpenWrite(path), CompressionMode.Compress);
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(gZipStream, this);
gZipStream.Close();
}
public int GetScore(BTPlayer player)
{
return GetScore(player, TimeSpan.MaxValue);
}
public int GetScore(BTPlayer player, TimeSpan gameTime)
{
if (gameTime == TimeSpan.MaxValue && mFinalScores.ContainsKey(player))
{
return mFinalScores[player];
}
int result = 1000;
TimeSpan timeSpan = TimeSpan.MinValue;
foreach (BTMissionEvent mEvent in mEvents)
{
if (mEvent is BTScoreUpdateEvent && mEvent.ReportingPilot == player && mEvent.GameTime > timeSpan && mEvent.GameTime <= gameTime)
{
result = ((BTScoreUpdateEvent)mEvent).Score;
timeSpan = mEvent.GameTime;
}
}
return result;
}
public int GetFinalPlace(BTPlayer player)
{
int num = 1;
int score = GetScore(player, TimeSpan.MaxValue);
foreach (BTPlayer player2 in mMission.Players)
{
if (player2 != player && GetScore(player2, TimeSpan.MaxValue) > score)
{
num++;
}
}
return num;
}
public BTPlayer[][] GetChartOrder()
{
return GetChartOrder(TimeSpan.MaxValue);
}
public BTPlayer[][] GetChartOrder(TimeSpan gameTime)
{
List<Tuple<BTPlayer[], int>> list = new List<Tuple<BTPlayer[], int>>();
foreach (BTPlayer player in Mission.Players)
{
list.Add(new Tuple<BTPlayer[], int>(new BTPlayer[1] { player }, GetScore(player, gameTime)));
}
for (int i = 0; i < list.Count - 1; i++)
{
for (int j = i + 1; j < list.Count; j++)
{
if (list[i].B < list[j].B || (list[i].B == list[j].B && string.CompareOrdinal(list[i].A[0].Name, list[j].A[0].Name) > 0))
{
Tuple<BTPlayer[], int> value = list[i];
list[i] = list[j];
list[j] = value;
}
}
}
List<BTPlayer[]> list2 = new List<BTPlayer[]>();
foreach (Tuple<BTPlayer[], int> item in list)
{
list2.Add(item.A);
}
return list2.ToArray();
}
public PrintDocument GetPrintDocument()
{
return new BTPrintDocument(this);
}
}
@@ -0,0 +1,31 @@
using System;
using System.Text;
namespace TeslaConsole.BattleTech;
[Serializable]
internal abstract class BTParticipant
{
private readonly string mPodHostAddress;
private readonly HostType mHostType;
public string PodHostAddress => mPodHostAddress;
public HostType HostType => mHostType;
protected BTParticipant(string podHostAddress, HostType hostType)
{
mPodHostAddress = podHostAddress;
mHostType = hostType;
}
public void AppendParticipant(int index, StringBuilder sb)
{
sb.AppendFormat("[{0}]\n", mPodHostAddress);
sb.AppendFormat("hostType={0}\n", (int)mHostType);
AppendParticipantSpecificData(index, sb);
}
protected abstract void AppendParticipantSpecificData(int index, StringBuilder sb);
}
+122
View File
@@ -0,0 +1,122 @@
using System;
using System.Text;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech combatant (mech pilot). Mirrors <c>RPPlayer</c> but carries the
/// BT-only participant fields (advanced damage model, pilot experience, mech
/// value, faction emblem, camo, patch colour and role). Free For All and No
/// Return use the same player type and differ only by the assigned
/// <see cref="Role" />.
/// </summary>
[Serializable]
internal class BTPlayer : BTParticipant
{
private readonly string mName;
private readonly string mVehicleKey;
private readonly string mCamoKey;
private readonly string mPatchKey;
private readonly string mEmblemKey;
private readonly string mExperienceKey;
private readonly BTRole mRole;
private readonly bool mAdvancedDamage;
private readonly string mDropZoneKey;
private readonly int mVehicleValue;
public string Name => mName;
public string VehicleKey => mVehicleKey;
public string CamoKey => mCamoKey;
public string PatchKey => mPatchKey;
public string EmblemKey => mEmblemKey;
public string ExperienceKey => mExperienceKey;
public BTRole Role => mRole;
public bool AdvancedDamage => mAdvancedDamage;
public string DropZoneKey => mDropZoneKey;
public int VehicleValue => mVehicleValue;
public BTPlayer(string podHostAddress, string name, string vehicleKey, string camoKey, string patchKey, string emblemKey, string experienceKey, BTRole role, bool advancedDamage = true, string dropZoneKey = "one", int vehicleValue = 0)
: base(podHostAddress, HostType.GameMachineHostType)
{
if (podHostAddress == null)
{
throw new ArgumentNullException("podHostAddress");
}
if (name == null)
{
throw new ArgumentNullException("name");
}
if (vehicleKey == null)
{
throw new ArgumentNullException("vehicleKey");
}
if (camoKey == null)
{
throw new ArgumentNullException("camoKey");
}
if (patchKey == null)
{
throw new ArgumentNullException("patchKey");
}
if (emblemKey == null)
{
throw new ArgumentNullException("emblemKey");
}
if (experienceKey == null)
{
throw new ArgumentNullException("experienceKey");
}
if (role == null)
{
throw new ArgumentNullException("role");
}
if (dropZoneKey == null)
{
throw new ArgumentNullException("dropZoneKey");
}
mName = name;
mVehicleKey = vehicleKey;
mCamoKey = camoKey;
mPatchKey = patchKey;
mEmblemKey = emblemKey;
mExperienceKey = experienceKey;
mRole = role;
mAdvancedDamage = advancedDamage;
mDropZoneKey = dropZoneKey;
mVehicleValue = vehicleValue;
}
protected sealed override void AppendParticipantSpecificData(int index, StringBuilder sb)
{
sb.AppendFormat("advancedDamage={0}\n", mAdvancedDamage ? "1" : "0");
sb.Append("loadzones=1\n");
sb.AppendFormat("name={0}\n", mName);
sb.AppendFormat("bitmapindex={0}\n", index);
sb.AppendFormat("experience={0}\n", mExperienceKey);
sb.AppendFormat("vehicle={0}\n", mVehicleKey);
sb.AppendFormat("vehicleValue={0}\n", mVehicleValue);
sb.AppendFormat("badge={0}\n", mEmblemKey);
sb.AppendFormat("dropzone={0}\n", mDropZoneKey);
sb.AppendFormat("color={0}\n", mCamoKey);
sb.AppendFormat("patch={0}\n", mPatchKey);
sb.AppendFormat("role=Role::{0}\n", mRole.Key);
}
}
@@ -0,0 +1,748 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Printing;
using System.Text;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The BattleTech score-sheet print document: one landscape page per pilot with
/// the mech portrait, placing, kill/death/damage stats, a randomized
/// mission-highlights narrative (<see cref="BTStrings" />), the pilot-vs-pilot
/// damage matrix, and the place-over-time chart. A close mirror of
/// <c>RPPrintDocument</c> minus the football branches and the RP-only
/// lap/boost/score-zone concepts; the chart marks kills and deaths only.
/// </summary>
public class BTPrintDocument : PrintDocument
{
private const int sThinBorderSize = 3;
private const int sThickBorderSize = 14;
private const int sChampherSize = 14;
private const double sSmallRectWidthDropFraction = 0.5721493440968718;
private const double sSmallRectHeightDropFraction = 245.0 / 372.0;
private const string sSansSerifFontName = "Helvetica LT Std";
private const string sSerifFontName = "Courier New";
private readonly BTMissionResults mMissionResults;
private readonly BTPlayer[][] mFinalRanking;
private readonly BTPlayer[] mPrintOrder;
private int mCurrentPlayer;
private Dictionary<BTPlayer, List<Point>> mPlayerLines;
private int[] mChartRows;
private Dictionary<TimeSpan, int> mChartCols;
internal BTPrintDocument(BTMissionResults missionResults)
{
mMissionResults = missionResults;
bool noReturn = false;
foreach (BTPlayer player in mMissionResults.Mission.Players)
{
if (player.Role.Key == "NoReturn")
{
noReturn = true;
break;
}
}
base.DocumentName = string.Format("BattleTech {0} mission at {1} on {2}", noReturn ? "No Return" : "Free For All", mMissionResults.MissionStart.ToLongTimeString(), mMissionResults.MissionStart.ToLongDateString());
mFinalRanking = mMissionResults.GetChartOrder();
List<BTPlayer> list = new List<BTPlayer>();
for (int i = 0; i < mFinalRanking.Length; i++)
{
list.Add(mFinalRanking[i][0]);
}
for (int j = 0; j < mFinalRanking.Length; j++)
{
for (int k = 1; k < mFinalRanking[j].Length; k++)
{
list.Add(mFinalRanking[j][k]);
}
}
mPrintOrder = list.ToArray();
}
protected override void OnQueryPageSettings(QueryPageSettingsEventArgs e)
{
e.PageSettings.Margins = new Margins(50, 50, 50, 50);
e.PageSettings.Landscape = true;
base.OnQueryPageSettings(e);
}
protected override void OnBeginPrint(PrintEventArgs e)
{
mCurrentPlayer = 0;
base.OnBeginPrint(e);
}
protected override void OnPrintPage(PrintPageEventArgs e)
{
BTPlayer bTPlayer = mPrintOrder[mCurrentPlayer];
List<BTMissionEvent> list = new List<BTMissionEvent>();
foreach (BTMissionEvent @event in mMissionResults.Events)
{
if (Contains(@event.InvolvedPilots, bTPlayer))
{
list.Add(@event);
}
}
for (int i = 0; i < list.Count - 1; i++)
{
for (int j = i + 1; j < list.Count; j++)
{
if (list[i].GameTime > list[j].GameTime)
{
BTMissionEvent value = list[i];
list[i] = list[j];
list[j] = value;
}
}
}
int score = mMissionResults.GetScore(bTPlayer);
int finalPlace = mMissionResults.GetFinalPlace(bTPlayer);
Size size = new Size(e.PageBounds.Width - 100, e.PageBounds.Height - 100);
if (size.Width > e.MarginBounds.Width)
{
size.Width = e.MarginBounds.Width;
}
if (size.Height > e.MarginBounds.Height)
{
size.Height = e.MarginBounds.Height;
}
Rectangle rectangle = new Rectangle((e.MarginBounds.Width - size.Width) / 2 + e.MarginBounds.Left, (e.MarginBounds.Height - size.Height) / 2 + e.MarginBounds.Top, size.Width, size.Height);
Rectangle inputRect = rectangle;
inputRect.Inflate(-14, -14);
Rectangle rectangle2 = rectangle;
rectangle2.Inflate(-3, -3);
Rectangle inputRect2 = rectangle2;
inputRect2.Inflate(-14, -14);
Rectangle rectangle3 = rectangle2;
rectangle3.Inflate(-7, -7);
Rectangle inputRect3 = rectangle3;
inputRect3.Inflate(-14, -14);
Rectangle rectangle4 = rectangle2;
rectangle4.Inflate(-14, -14);
Rectangle inputRect4 = rectangle4;
inputRect4.Inflate(-14, -14);
Rectangle rectangle5 = rectangle4;
rectangle5.Inflate(-3, -3);
Rectangle inputRect5 = rectangle5;
inputRect5.Inflate(-14, -14);
int widthDrop = (int)(0.5721493440968718 * (double)rectangle3.Width + 0.5);
int heightDrop = (int)(245.0 / 372.0 * (double)rectangle3.Height + 0.5);
Rectangle rectangle6 = RectDrop(rectangle, widthDrop, heightDrop);
Rectangle rectangle7 = RectDrop(inputRect, widthDrop, heightDrop);
Rectangle rectangle8 = RectDrop(rectangle2, widthDrop, heightDrop);
Rectangle rectangle9 = RectDrop(inputRect2, widthDrop, heightDrop);
Rectangle rectangle10 = RectDrop(rectangle3, widthDrop, heightDrop);
Rectangle rectangle11 = RectDrop(inputRect3, widthDrop, heightDrop);
Rectangle rectangle12 = RectDrop(rectangle4, widthDrop, heightDrop);
Rectangle rectangle13 = RectDrop(inputRect4, widthDrop, heightDrop);
Rectangle rectangle14 = RectDrop(rectangle5, widthDrop, heightDrop);
Rectangle rectangle15 = RectDrop(inputRect5, widthDrop, heightDrop);
using (GraphicsPath graphicsPath = new GraphicsPath())
{
graphicsPath.AddLine(rectangle.Left, inputRect.Bottom, rectangle.Left, inputRect.Top);
graphicsPath.AddLine(inputRect.Left, rectangle.Top, inputRect.Right, rectangle.Top);
graphicsPath.AddLine(rectangle3.Right, rectangle3.Top, inputRect3.Left, rectangle3.Top);
graphicsPath.AddLine(rectangle3.Left, inputRect3.Top, rectangle3.Left, rectangle3.Bottom);
graphicsPath.StartFigure();
graphicsPath.AddLine(rectangle3.Left, inputRect5.Bottom, rectangle5.Left, inputRect5.Bottom);
graphicsPath.AddLine(inputRect5.Left, rectangle5.Bottom, inputRect5.Right, rectangle5.Bottom);
graphicsPath.AddLine(rectangle5.Right, inputRect5.Bottom, rectangle5.Right, rectangle3.Top);
graphicsPath.AddLine(rectangle3.Right, rectangle3.Top, rectangle3.Right, inputRect3.Bottom);
graphicsPath.AddLine(inputRect3.Right, rectangle3.Bottom, rectangle3.Left, rectangle3.Bottom);
graphicsPath.StartFigure();
graphicsPath.AddLine(rectangle10.Left, rectangle15.Bottom, rectangle14.Left, rectangle15.Bottom);
graphicsPath.AddLine(rectangle15.Left, rectangle14.Bottom, rectangle15.Right, rectangle14.Bottom);
graphicsPath.AddLine(rectangle14.Right, rectangle15.Bottom, rectangle14.Right, rectangle10.Top);
graphicsPath.AddLine(rectangle10.Right, rectangle10.Top, rectangle10.Right, rectangle11.Bottom);
graphicsPath.AddLine(rectangle11.Right, rectangle10.Bottom, rectangle10.Left, rectangle10.Bottom);
e.Graphics.FillPath(Brushes.LightGray, graphicsPath);
}
using (GraphicsPath graphicsPath2 = new GraphicsPath())
{
graphicsPath2.AddLine(rectangle2.Left, inputRect2.Bottom, rectangle.Left, inputRect.Bottom);
graphicsPath2.AddLine(inputRect.Left, rectangle.Bottom, inputRect.Right, rectangle.Bottom);
graphicsPath2.AddLine(rectangle.Right, inputRect.Bottom, rectangle.Right, inputRect.Top);
graphicsPath2.AddLine(inputRect.Right, rectangle.Top, inputRect2.Right, rectangle2.Top);
graphicsPath2.AddLine(inputRect2.Right, rectangle3.Top, rectangle3.Right, inputRect3.Top);
graphicsPath2.AddLine(rectangle3.Right, inputRect3.Bottom, inputRect3.Right, rectangle3.Bottom);
graphicsPath2.AddLine(inputRect2.Left, rectangle3.Bottom, rectangle3.Left, inputRect2.Bottom);
graphicsPath2.StartFigure();
graphicsPath2.AddLine(rectangle12.Left, rectangle13.Bottom, rectangle14.Left, rectangle15.Bottom);
graphicsPath2.AddLine(rectangle14.Left, rectangle15.Top, rectangle15.Left, rectangle14.Top);
graphicsPath2.AddLine(rectangle15.Right, rectangle14.Top, rectangle14.Right, rectangle15.Top);
graphicsPath2.AddLine(rectangle12.Right, rectangle13.Top, rectangle10.Right, rectangle13.Top);
graphicsPath2.AddLine(rectangle10.Right, rectangle10.Top, rectangle11.Left, rectangle10.Top);
graphicsPath2.AddLine(rectangle10.Left, rectangle11.Top, rectangle10.Left, rectangle13.Bottom);
graphicsPath2.StartFigure();
graphicsPath2.AddLine(rectangle4.Left, inputRect4.Bottom, rectangle5.Left, inputRect5.Bottom);
graphicsPath2.AddLine(rectangle5.Left, rectangle6.Bottom + 14, inputRect5.Left, rectangle6.Bottom);
graphicsPath2.AddLine(rectangle7.Right, rectangle6.Bottom, rectangle6.Right, rectangle7.Bottom);
graphicsPath2.AddLine(rectangle6.Right, inputRect5.Top, rectangle6.Right + 14, rectangle5.Top);
graphicsPath2.AddLine(inputRect5.Right, rectangle5.Top, rectangle5.Right, inputRect5.Top);
graphicsPath2.AddLine(rectangle4.Right, inputRect4.Top, rectangle3.Right, inputRect4.Top);
graphicsPath2.AddLine(rectangle3.Right, inputRect3.Top, inputRect3.Right, rectangle3.Top);
graphicsPath2.AddLine(rectangle10.Right, rectangle3.Top, rectangle10.Right, rectangle11.Bottom);
graphicsPath2.AddLine(rectangle11.Right, rectangle10.Bottom, inputRect3.Left, rectangle10.Bottom);
graphicsPath2.AddLine(rectangle3.Left, rectangle10.Bottom, rectangle3.Left, inputRect4.Bottom);
e.Graphics.FillPath(Brushes.DarkGray, graphicsPath2);
}
using (GraphicsPath graphicsPath3 = new GraphicsPath())
{
graphicsPath3.AddLine(rectangle2.Left, inputRect2.Bottom, rectangle2.Left, inputRect2.Top);
graphicsPath3.AddLine(inputRect2.Left, rectangle2.Top, inputRect2.Right, rectangle2.Top);
graphicsPath3.AddLine(rectangle2.Right, inputRect2.Top, rectangle2.Right, inputRect2.Bottom);
graphicsPath3.AddLine(inputRect2.Right, rectangle2.Bottom, inputRect2.Left, rectangle2.Bottom);
graphicsPath3.StartFigure();
graphicsPath3.AddLine(rectangle12.Left, rectangle13.Bottom, rectangle12.Left, rectangle13.Top);
graphicsPath3.AddLine(rectangle13.Left, rectangle12.Top, rectangle13.Right, rectangle12.Top);
graphicsPath3.AddLine(rectangle12.Right, rectangle13.Top, rectangle12.Right, rectangle13.Bottom);
graphicsPath3.AddLine(rectangle13.Right, rectangle12.Bottom, rectangle13.Left, rectangle12.Bottom);
graphicsPath3.StartFigure();
graphicsPath3.AddLine(rectangle4.Left, inputRect4.Bottom, rectangle4.Left, rectangle8.Bottom + 14);
graphicsPath3.AddLine(rectangle13.Left, rectangle8.Bottom, rectangle9.Right, rectangle8.Bottom);
graphicsPath3.AddLine(rectangle8.Right, rectangle9.Bottom, rectangle8.Right, inputRect4.Top);
graphicsPath3.AddLine(rectangle8.Right + 14, rectangle4.Top, inputRect4.Right, rectangle4.Top);
graphicsPath3.AddLine(rectangle4.Right, inputRect4.Top, rectangle4.Right, inputRect4.Bottom);
graphicsPath3.AddLine(inputRect4.Right, rectangle4.Bottom, inputRect4.Left, rectangle4.Bottom);
e.Graphics.FillPath(Brushes.Gray, graphicsPath3);
}
using Font font3 = new Font("Helvetica LT Std", 12f);
using Font font = new Font("Helvetica LT Std", 8f);
Rectangle rectangle16 = rectangle14;
rectangle16.X += 175;
rectangle16.Width -= 178;
rectangle16.Y += 58;
rectangle16.Height -= 86;
BTVehicle bTVehicle = (BTConfig.Vehicles.ContainsKey(bTPlayer.VehicleKey) ? BTConfig.Vehicles[bTPlayer.VehicleKey] : null);
Image image = bTVehicle?.Image;
if (image != null)
{
e.Graphics.DrawImage(image, GetCenteredScaledRect(rectangle16, image.Size));
}
else
{
StringFormat stringFormat = new StringFormat();
stringFormat.LineAlignment = StringAlignment.Center;
stringFormat.Alignment = StringAlignment.Center;
using (StringFormat format = stringFormat)
{
Rectangle rectangle17 = rectangle16;
rectangle17.Inflate(-20, -20);
e.Graphics.DrawString("The tech crew's holo-imager is down for repairs. Recon imagery could not be obtained for this mech.", font, Brushes.Black, rectangle17, format);
}
e.Graphics.DrawRectangle(Pens.Black, rectangle16);
}
using (Font font2 = new Font("Helvetica LT Std", 18f, FontStyle.Bold))
{
using StringFormat stringFormat2 = new StringFormat();
stringFormat2.Alignment = StringAlignment.Center;
stringFormat2.LineAlignment = StringAlignment.Far;
e.Graphics.DrawString(bTPlayer.Name, font2, Brushes.Black, rectangle14.Left + rectangle14.Width / 2, rectangle16.Top - 10, stringFormat2);
}
using (StringFormat stringFormat3 = new StringFormat())
{
stringFormat3.Alignment = StringAlignment.Center;
e.Graphics.DrawString((bTVehicle != null) ? bTVehicle.Name : bTPlayer.VehicleKey, font3, Brushes.Black, rectangle16.Left + rectangle16.Width / 2, rectangle16.Bottom + 5, stringFormat3);
}
int num = rectangle15.Left - 7;
Rectangle rectangle18 = new Rectangle(num, rectangle16.Top, rectangle16.Left - num, rectangle15.Bottom - rectangle16.Top);
using (Font font4 = new Font("Helvetica LT Std", 16f, FontStyle.Bold))
{
using StringFormat stringFormat4 = new StringFormat();
stringFormat4.Alignment = StringAlignment.Center;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine(string.Format("{0}{1} Place", finalPlace, finalPlace switch
{
3 => "rd",
2 => "nd",
1 => "st",
_ => "th",
}));
e.Graphics.DrawString(stringBuilder.ToString(), font4, Brushes.Black, rectangle18, stringFormat4);
}
using (StringFormat stringFormat5 = new StringFormat())
{
stringFormat5.LineAlignment = StringAlignment.Far;
stringFormat5.SetTabStops(0f, new float[1] { 80f });
int deaths = 0;
int kills = 0;
GetPlayerStats(out deaths, out kills, bTPlayer, list);
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.AppendLine($"Score:\t{score}");
stringBuilder2.AppendLine();
stringBuilder2.AppendLine($"Deaths:\t{deaths}");
stringBuilder2.AppendLine();
stringBuilder2.AppendLine($"Kills:\t{kills}");
e.Graphics.DrawString(stringBuilder2.ToString(), font, Brushes.Black, rectangle18, stringFormat5);
}
int num2 = rectangle5.Top + 40;
Rectangle rectangle19 = new Rectangle(rectangle6.Right + 5, num2, rectangle5.Right - rectangle6.Right - 10, rectangle6.Bottom - 5 - num2);
using (StringFormat stringFormat6 = new StringFormat())
{
stringFormat6.Alignment = StringAlignment.Center;
e.Graphics.DrawString("Mission Highlights", font3, Brushes.Black, (rectangle19.Right + rectangle19.Left) / 2, rectangle5.Top + 15, stringFormat6);
}
using (Font font5 = new Font("Courier New", 10f))
{
string text = mMissionResults.MissionStart.ToString() + " ";
string s = text;
List<string> list2 = new List<string>();
BTPlayer[][] array = mFinalRanking;
foreach (BTPlayer[] array2 in array)
{
list2.Add(array2[0].Name);
}
string text2 = BTStrings.Recap(list2.ToArray());
Random random = new Random();
List<BTMissionEvent> list3 = new List<BTMissionEvent>(list);
SortedList<TimeSpan, string> sortedList = new SortedList<TimeSpan, string>();
string text3 = text + text2;
while (e.Graphics.MeasureString(text3, font5, rectangle19.Width).Height < (float)rectangle19.Height)
{
s = text3;
if (list3.Count < 1)
{
break;
}
int index = random.Next(list3.Count);
BTMissionEvent bTMissionEvent = list3[index];
list3.RemoveAt(index);
string text4 = null;
if (bTMissionEvent is BTDamagedEvent)
{
BTDamagedEvent bTDamagedEvent = (BTDamagedEvent)bTMissionEvent;
if (bTDamagedEvent.DamageLoss > 0)
{
text4 = ((bTDamagedEvent.DamageLoss < 10) ? BTStrings.LightDamage(bTDamagedEvent.GameTime, bTDamagedEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTDamagedEvent.ReportingPilot.VehicleKey), bTDamagedEvent.Damager.Name) : ((bTDamagedEvent.DamageLoss >= 100) ? BTStrings.HeavyDamage(bTDamagedEvent.GameTime, bTDamagedEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTDamagedEvent.ReportingPilot.VehicleKey), bTDamagedEvent.Damager.Name) : BTStrings.ModerateDamage(bTDamagedEvent.GameTime, bTDamagedEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTDamagedEvent.ReportingPilot.VehicleKey), bTDamagedEvent.Damager.Name)));
}
}
else if (bTMissionEvent is BTKilledEvent)
{
BTKilledEvent bTKilledEvent = (BTKilledEvent)bTMissionEvent;
text4 = ((bTKilledEvent.ReportingPilot != bTKilledEvent.Killer) ? BTStrings.Kill(bTKilledEvent.GameTime, bTKilledEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTKilledEvent.ReportingPilot.VehicleKey), bTKilledEvent.Killer.Name) : BTStrings.DeathWithoutHonor(bTKilledEvent.GameTime, bTKilledEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTKilledEvent.ReportingPilot.VehicleKey)));
}
else if (bTMissionEvent is BTDeathWithoutHonorEvent)
{
text4 = BTStrings.DeathWithoutHonor(bTMissionEvent.GameTime, bTMissionEvent.ReportingPilot.Name, BTConfig.VehicleNameOrKey(bTMissionEvent.ReportingPilot.VehicleKey));
}
if (text4 != null)
{
TimeSpan gameTime;
for (gameTime = bTMissionEvent.GameTime; sortedList.ContainsKey(gameTime); gameTime += TimeSpan.FromTicks(1L))
{
}
sortedList.Add(gameTime, text4);
}
StringBuilder stringBuilder3 = new StringBuilder(text);
foreach (KeyValuePair<TimeSpan, string> item in sortedList)
{
if (stringBuilder3.Length > 0)
{
stringBuilder3.Append(" ");
}
stringBuilder3.Append(item.Value);
}
if (stringBuilder3.Length > 0)
{
stringBuilder3.Append(" ");
}
stringBuilder3.Append(text2);
text3 = stringBuilder3.ToString();
}
e.Graphics.DrawString(s, font5, Brushes.Black, rectangle19);
}
int num3 = rectangle6.Bottom + 5;
Rectangle rectangle20 = new Rectangle(inputRect5.Left, num3, inputRect5.Width, rectangle5.Bottom - num3);
Rectangle rectangle21 = new Rectangle(rectangle20.Right - 250, rectangle20.Top, 250, rectangle20.Height - 20);
Image image2 = (BTConfig.Maps.ContainsKey(mMissionResults.Mission.MapKey) ? BTConfig.Maps[mMissionResults.Mission.MapKey] : null)?.Image;
if (image2 != null)
{
e.Graphics.DrawImage(image2, GetCenteredScaledRect(rectangle21, image2.Size));
}
else
{
StringFormat stringFormat7 = new StringFormat();
stringFormat7.LineAlignment = StringAlignment.Center;
stringFormat7.Alignment = StringAlignment.Center;
using (StringFormat format2 = stringFormat7)
{
Rectangle rectangle22 = rectangle21;
rectangle22.Inflate(-20, -20);
e.Graphics.DrawString("The recon satellite is currently on a transitional orbit. Satellite imagery could not be obtained for this arena.", font, Brushes.Black, rectangle22, format2);
}
e.Graphics.DrawRectangle(Pens.Black, rectangle21);
}
Rectangle rect = new Rectangle(rectangle20.Left, rectangle20.Top, rectangle21.Left - 5 - rectangle20.Left, 92);
Dictionary<BTPlayer, int[]> dictionary = new Dictionary<BTPlayer, int[]>();
foreach (BTPlayer player in mMissionResults.Mission.Players)
{
if (player != bTPlayer)
{
dictionary.Add(player, new int[4]);
}
}
foreach (BTMissionEvent event2 in mMissionResults.Events)
{
if (event2 is BTKilledEvent)
{
BTKilledEvent bTKilledEvent2 = (BTKilledEvent)event2;
if (bTKilledEvent2.ReportingPilot != bTKilledEvent2.Killer)
{
if (bTKilledEvent2.Killer == bTPlayer)
{
dictionary[bTKilledEvent2.ReportingPilot][3]++;
}
else if (bTKilledEvent2.ReportingPilot == bTPlayer)
{
dictionary[bTKilledEvent2.Killer][2]++;
}
}
}
else
{
if (!(event2 is BTDamagedEvent))
{
continue;
}
BTDamagedEvent bTDamagedEvent2 = (BTDamagedEvent)event2;
if (bTDamagedEvent2.ReportingPilot != bTDamagedEvent2.Damager)
{
if (bTDamagedEvent2.Damager == bTPlayer)
{
dictionary[bTDamagedEvent2.ReportingPilot][1] += bTDamagedEvent2.DamageLoss;
}
else if (bTDamagedEvent2.ReportingPilot == bTPlayer)
{
dictionary[bTDamagedEvent2.Damager][0] += bTDamagedEvent2.DamageLoss;
}
}
}
}
using (Pen pen = new Pen(Color.Gray, 2f))
{
using Font font6 = new Font("Helvetica LT Std", 6f, FontStyle.Regular);
using StringFormat stringFormat8 = new StringFormat();
e.Graphics.DrawRectangle(Pens.Gray, rect);
int num4 = 120;
int num5 = 14;
int num6 = rect.Left + 1;
int num7 = rect.Left + num4;
int num8 = (rect.Width - num4) / Math.Max(1, dictionary.Count);
if (dictionary.Count == 0)
{
e.Graphics.DrawLine(pen, num7, rect.Top, num7, rect.Bottom);
}
int num9 = rect.Bottom;
for (int l = 0; l < 5; l++)
{
num9 -= num5;
e.Graphics.DrawLine(Pens.Gray, rect.Left, num9, rect.Right, num9);
}
int num10 = rect.Top + 1;
stringFormat8.LineAlignment = StringAlignment.Near;
e.Graphics.DrawString("Pilot", font6, Brushes.Black, num6, num10, stringFormat8);
int num11 = rect.Bottom - num5 / 2;
int num12 = num11 - num5;
int num13 = num12 - num5;
int num14 = num13 - num5;
int num15 = num14 - num5;
stringFormat8.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString("Score", font6, Brushes.Black, num6, num15, stringFormat8);
e.Graphics.DrawString("Damage You Received From", font6, Brushes.Black, num6, num14, stringFormat8);
e.Graphics.DrawString("Damage You Inflicted On", font6, Brushes.Black, num6, num13, stringFormat8);
e.Graphics.DrawString("Times You Were Killed By", font6, Brushes.Black, num6, num12, stringFormat8);
e.Graphics.DrawString("Times You Killed", font6, Brushes.Black, num6, num11, stringFormat8);
int num16 = num7;
foreach (KeyValuePair<BTPlayer, int[]> item2 in dictionary)
{
int num17 = num16 + 1;
e.Graphics.DrawLine(pen, num16, rect.Top, num16, rect.Bottom);
stringFormat8.LineAlignment = StringAlignment.Near;
e.Graphics.DrawString(item2.Key.Name, font6, Brushes.Black, num17, num10, stringFormat8);
stringFormat8.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString(mMissionResults.GetScore(item2.Key).ToString(), font6, Brushes.Black, num17, num15, stringFormat8);
e.Graphics.DrawString(item2.Value[0].ToString(), font6, Brushes.Black, num17, num14, stringFormat8);
e.Graphics.DrawString(item2.Value[1].ToString(), font6, Brushes.Black, num17, num13, stringFormat8);
e.Graphics.DrawString(item2.Value[2].ToString(), font6, Brushes.Black, num17, num12, stringFormat8);
e.Graphics.DrawString(item2.Value[3].ToString(), font6, Brushes.Black, num17, num11, stringFormat8);
num16 += num8;
}
}
int num18 = rectangle20.Left + 60;
int num19 = rect.Bottom + 120;
Rectangle rectangle23 = new Rectangle(num18, num19, rect.Right - 75 - num18, rectangle20.Bottom - 85 - num19);
if (mPlayerLines == null)
{
mPlayerLines = new Dictionary<BTPlayer, List<Point>>();
BTPlayer[][] array = mFinalRanking;
foreach (BTPlayer[] array3 in array)
{
BTPlayer[] array4 = array3;
foreach (BTPlayer key in array4)
{
mPlayerLines.Add(key, new List<Point>());
}
}
mChartRows = new int[mPlayerLines.Count];
float num20 = (float)rectangle23.Height / (float)mPlayerLines.Count;
for (int n = 0; n < mChartRows.Length; n++)
{
mChartRows[n] = (int)(num20 * (float)n + 0.5f) + rectangle23.Top;
}
mChartCols = new Dictionary<TimeSpan, int>();
TimeSpan timeSpan = mMissionResults.ActuallMissionTime.Subtract(TimeSpan.FromSeconds(10.0));
TimeSpan timeSpan2 = TimeSpan.FromSeconds(15.0);
for (TimeSpan timeSpan3 = timeSpan2; timeSpan3 < timeSpan; timeSpan3 += timeSpan2)
{
mChartCols.Add(timeSpan3, (int)((float)timeSpan3.Ticks / (float)mMissionResults.ActuallMissionTime.Ticks * (float)rectangle23.Width + 0.5f) + rectangle23.Left);
}
int num21 = 0;
array = mFinalRanking;
foreach (BTPlayer[] array5 in array)
{
BTPlayer[] array4 = array5;
foreach (BTPlayer key2 in array4)
{
mPlayerLines[key2].Add(new Point(rectangle23.Left, mChartRows[num21++]));
}
}
foreach (KeyValuePair<TimeSpan, int> mChartCol in mChartCols)
{
num21 = 0;
array = mMissionResults.GetChartOrder(mChartCol.Key);
foreach (BTPlayer[] array6 in array)
{
BTPlayer[] array4 = array6;
foreach (BTPlayer key3 in array4)
{
mPlayerLines[key3].Add(new Point(mChartCol.Value, mChartRows[num21++]));
}
}
}
num21 = 0;
array = mFinalRanking;
foreach (BTPlayer[] array7 in array)
{
BTPlayer[] array4 = array7;
foreach (BTPlayer key4 in array4)
{
mPlayerLines[key4].Add(new Point(rectangle23.Right, mChartRows[num21++]));
}
}
}
using (Font font7 = new Font("Helvetica LT Std", 8f))
{
using (StringFormat stringFormat9 = new StringFormat())
{
stringFormat9.LineAlignment = StringAlignment.Center;
foreach (KeyValuePair<BTPlayer, List<Point>> mPlayerLine in mPlayerLines)
{
if (mPlayerLine.Key != bTPlayer)
{
DrawPlayerLine(e.Graphics, mPlayerLine.Key, mPlayerLine.Value.ToArray(), rectangle23.Right + 5, Pens.LightGray, Brushes.LightGray, font7, stringFormat9);
}
}
DrawPlayerLine(e.Graphics, bTPlayer, mPlayerLines[bTPlayer].ToArray(), rectangle23.Right + 5, Pens.Black, Brushes.Black, font7, stringFormat9);
}
using Pen pen2 = new Pen(Color.Black, 1.5f);
using (Font font8 = new Font("Courier New", 8f))
{
int num22 = 5;
int num23 = 3;
e.Graphics.DrawLines(pen2, new Point[3]
{
new Point(rectangle23.Left, rectangle23.Top),
new Point(rectangle23.Left, rectangle23.Bottom),
new Point(rectangle23.Right, rectangle23.Bottom)
});
using StringFormat stringFormat10 = new StringFormat();
stringFormat10.Alignment = StringAlignment.Far;
stringFormat10.LineAlignment = StringAlignment.Center;
int num24 = 0;
BTPlayer[][] array = mFinalRanking;
foreach (BTPlayer[] array8 in array)
{
int num25 = mChartRows[num24];
e.Graphics.DrawLine(pen2, rectangle23.Left - num22, num25, rectangle23.Left + num22, num25);
Graphics graphics = e.Graphics;
int num26 = ++num24;
graphics.DrawString(num26.ToString(), font8, Brushes.Black, rectangle23.Left - 10, num25, stringFormat10);
_ = array8;
}
int num27 = rectangle23.Bottom + 23;
stringFormat10.Alignment = StringAlignment.Center;
stringFormat10.LineAlignment = StringAlignment.Near;
e.Graphics.DrawString("0", font8, Brushes.Black, rectangle23.Left, num27, stringFormat10);
foreach (KeyValuePair<TimeSpan, int> mChartCol2 in mChartCols)
{
if (mChartCol2.Key.Seconds == 0)
{
e.Graphics.DrawLine(pen2, mChartCol2.Value, rectangle23.Bottom - num22, mChartCol2.Value, rectangle23.Bottom + num22);
e.Graphics.DrawString(mChartCol2.Key.Minutes.ToString(), font8, Brushes.Black, mChartCol2.Value, num27, stringFormat10);
}
else
{
e.Graphics.DrawLine(pen2, mChartCol2.Value, rectangle23.Bottom - num23, mChartCol2.Value, rectangle23.Bottom);
}
}
e.Graphics.DrawLine(pen2, rectangle23.Right, rectangle23.Bottom - num22, rectangle23.Right, rectangle23.Bottom + num22);
e.Graphics.DrawString(((int)(mMissionResults.ActuallMissionTime.TotalMinutes + 0.5)).ToString(), font8, Brushes.Black, rectangle23.Right, num27, stringFormat10);
e.Graphics.DrawString("Time", font, Brushes.Black, (rectangle23.Left + rectangle23.Right) / 2, rectangle23.Bottom + 68, stringFormat10);
stringFormat10.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString("P\r\nl\r\na\r\nc\r\ne", font, Brushes.Black, rectangle23.Left - 54, (rectangle23.Bottom + rectangle23.Top) / 2, stringFormat10);
int num28 = rectangle23.Top - 70;
int num29 = rectangle23.Left + 10;
int num30 = (rectangle23.Right + rectangle23.Left) / 2 - 30;
stringFormat10.Alignment = StringAlignment.Near;
DrawKillMarker(e.Graphics, pen2, num29, num28, 15f);
e.Graphics.DrawString("Kill", font7, Brushes.Black, num29 + 15, num28, stringFormat10);
DrawDeathMarker(e.Graphics, pen2, num30, num28, 15f);
e.Graphics.DrawString("Death", font7, Brushes.Black, num30 + 15, num28, stringFormat10);
}
foreach (BTMissionEvent event3 in mMissionResults.Events)
{
if (event3 is BTKilledEvent)
{
BTKilledEvent bTKilledEvent3 = (BTKilledEvent)event3;
if (bTKilledEvent3.Killer != bTKilledEvent3.ReportingPilot)
{
DrawKillMarker(e.Graphics, (bTKilledEvent3.ReportingPilot == bTPlayer || bTKilledEvent3.Killer == bTPlayer) ? Pens.Black : Pens.LightGray, GetMarkerPosition(bTKilledEvent3.GameTime, mPlayerLines[bTKilledEvent3.Killer], rectangle23), 10f);
}
DrawDeathMarker(e.Graphics, (bTKilledEvent3.ReportingPilot == bTPlayer || bTKilledEvent3.Killer == bTPlayer) ? Pens.Black : Pens.LightGray, GetMarkerPosition(bTKilledEvent3.GameTime, mPlayerLines[bTKilledEvent3.ReportingPilot], rectangle23), 10f);
}
else if (event3 is BTDeathWithoutHonorEvent)
{
DrawDeathMarker(e.Graphics, (event3.ReportingPilot == bTPlayer) ? Pens.Black : Pens.LightGray, GetMarkerPosition(event3.GameTime, mPlayerLines[event3.ReportingPilot], rectangle23), 10f);
}
}
}
e.HasMorePages = ++mCurrentPlayer < mPrintOrder.Length;
base.OnPrintPage(e);
}
private PointF GetMarkerPosition(TimeSpan time, List<Point> chartLine, RectangleF chartArea)
{
TimeSpan timeSpan = TimeSpan.Zero;
TimeSpan timeSpan2 = mMissionResults.ActuallMissionTime;
int num = 0;
foreach (KeyValuePair<TimeSpan, int> mChartCol in mChartCols)
{
timeSpan2 = mChartCol.Key;
if (mChartCol.Key < time)
{
num++;
timeSpan = timeSpan2;
continue;
}
break;
}
if (timeSpan == timeSpan2)
{
timeSpan2 = mMissionResults.ActuallMissionTime;
}
float num2 = ((float)time.Ticks - (float)timeSpan.Ticks) / ((float)timeSpan2.Ticks - (float)timeSpan.Ticks);
PointF pointF = chartLine[num];
PointF pointF2 = chartLine[num + 1];
return new PointF((pointF2.X - pointF.X) * num2 + pointF.X, (pointF2.Y - pointF.Y) * num2 + pointF.Y);
}
private static void DrawKillMarker(Graphics graphics, Pen pen, PointF point, float size)
{
DrawKillMarker(graphics, pen, point.X, point.Y, size);
}
private static void DrawKillMarker(Graphics graphics, Pen pen, float x, float y, float size)
{
float y2 = y + size / 2f;
graphics.DrawPolygon(pen, new PointF[3]
{
new PointF(x - size / 2f, y2),
new PointF(x + size / 2f, y2),
new PointF(x, y - size / 2f)
});
}
private static void DrawDeathMarker(Graphics graphics, Pen pen, PointF point, float size)
{
DrawDeathMarker(graphics, pen, point.X, point.Y, size);
}
private static void DrawDeathMarker(Graphics graphics, Pen pen, float x, float y, float size)
{
graphics.DrawRectangle(pen, x - size / 2f, y - size / 2f, size, size);
}
private static void DrawPlayerLine(Graphics graphics, BTPlayer player, Point[] points, int playerNameX, Pen pen, Brush brush, Font font, StringFormat format)
{
graphics.DrawLines(pen, points);
graphics.DrawString(player.Name, font, brush, playerNameX, points[points.Length - 1].Y, format);
}
private static void GetPlayerStats(out int deaths, out int kills, BTPlayer player, IEnumerable<BTMissionEvent> relaventEventsSorted)
{
deaths = 0;
kills = 0;
foreach (BTMissionEvent item in relaventEventsSorted)
{
if (item.ReportingPilot == player)
{
if (item is BTKilledEvent || item is BTDeathWithoutHonorEvent)
{
deaths++;
}
}
else if (item is BTKilledEvent && ((BTKilledEvent)item).Killer == player)
{
kills++;
}
}
}
private static Rectangle RectDrop(Rectangle inputRect, int widthDrop, int heightDrop)
{
return new Rectangle(inputRect.X, inputRect.Y, inputRect.Width - widthDrop, inputRect.Height - heightDrop);
}
private static Rectangle GetCenteredScaledRect(Rectangle bounds, Size originalSize)
{
float num = (float)bounds.Width / (float)bounds.Height;
float num2 = (float)originalSize.Width / (float)originalSize.Height;
Size size = bounds.Size;
if (num > num2)
{
size.Width = (int)((float)size.Height * num2 + 0.5f);
}
else
{
size.Height = (int)((float)size.Width / num2 + 0.5f);
}
return new Rectangle(bounds.X + (bounds.Width - size.Width) / 2, bounds.Y + (bounds.Height - size.Height) / 2, size.Width, size.Height);
}
private static bool Contains(IEnumerable<BTPlayer> players, BTPlayer player)
{
foreach (BTPlayer player2 in players)
{
if (player2 == player)
{
return true;
}
}
return false;
}
}
+48
View File
@@ -0,0 +1,48 @@
using System;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech pilot role. Each participant references one role
/// (<c>role=Role::&lt;Key&gt;</c>); the egg then carries a matching
/// <c>[Role::&lt;Key&gt;] model=&lt;Model&gt;</c> block. Free For All uses the
/// Default role, No Return uses the NoReturn role. Serializable because it is
/// reachable from a saved <see cref="BTMissionResults" /> via the mission's
/// players (mirrors the .rpm save mechanism).
/// </summary>
[Serializable]
public class BTRole
{
private readonly string mKey;
private readonly string mModel;
private readonly string mName;
public string Key => mKey;
public string Model => mModel;
public string Name => mName;
internal BTRole(XmlNode roleNode)
{
mKey = roleNode.Attributes["key"].InnerText;
mModel = roleNode.Attributes["model"].InnerText;
XmlAttribute xmlAttribute = roleNode.Attributes["name"];
mName = ((xmlAttribute != null) ? xmlAttribute.InnerText : mKey);
}
public BTRole(string key, string model, string name)
{
mKey = key;
mModel = model;
mName = name;
}
public override string ToString()
{
return mName;
}
}
@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech scenario (game mode). Mirrors the Red Planet
/// <c>Scenario</c>/<c>DeathRaceScenario</c> pattern: it inherits the shared
/// map/vehicle/time/weather catalogs from <see cref="BTConfig" /> (minus any
/// <c>&lt;invalid&gt;</c> exclusions) and adds the BT-specific option lists
/// (experience, camo, patch, emblem, role).
/// </summary>
public class BTScenario
{
private readonly string mKey;
private readonly string mName;
private readonly Dictionary<string, BTMap> mMaps = new Dictionary<string, BTMap>();
private readonly Dictionary<string, BTVehicle> mVehicles = new Dictionary<string, BTVehicle>();
private readonly Dictionary<string, string> mTimesOfDay = new Dictionary<string, string>();
private readonly Dictionary<string, BTWeather> mWeather = new Dictionary<string, BTWeather>();
private readonly Dictionary<string, string> mExperiences = new Dictionary<string, string>();
private readonly Dictionary<string, string> mColors = new Dictionary<string, string>();
private readonly Dictionary<string, string> mPatches = new Dictionary<string, string>();
private readonly Dictionary<string, string> mBadges = new Dictionary<string, string>();
private readonly Dictionary<string, BTRole> mRoles = new Dictionary<string, BTRole>();
public string Key => mKey;
public string Name => mName;
public Dictionary<string, BTMap> Maps => mMaps;
public Dictionary<string, BTVehicle> Vehicles => mVehicles;
public Dictionary<string, string> TimesOfDay => mTimesOfDay;
public Dictionary<string, BTWeather> Weather => mWeather;
public Dictionary<string, string> Experiences => mExperiences;
public Dictionary<string, string> Colors => mColors;
public Dictionary<string, string> Patches => mPatches;
public Dictionary<string, string> Badges => mBadges;
public Dictionary<string, BTRole> Roles => mRoles;
internal BTScenario(XmlNode scenarioNode)
{
mKey = scenarioNode.Name;
mName = scenarioNode.Attributes["name"].InnerText;
List<KeyValuePair<string, string>> invalid = new List<KeyValuePair<string, string>>();
foreach (XmlNode childNode in scenarioNode.ChildNodes)
{
switch (childNode.Name)
{
case "invalid":
invalid.Add(new KeyValuePair<string, string>(childNode.Attributes["type"].InnerText, childNode.Attributes["key"].InnerText));
break;
case "experience":
BTConfig.AddKeyNameNode(childNode, mExperiences);
break;
case "color":
BTConfig.AddKeyNameNode(childNode, mColors);
break;
case "patch":
BTConfig.AddKeyNameNode(childNode, mPatches);
break;
case "badge":
BTConfig.AddKeyNameNode(childNode, mBadges);
break;
case "role":
{
BTRole role = new BTRole(childNode);
mRoles.Add(role.Key, role);
break;
}
}
}
foreach (BTMap value in BTConfig.Maps.Values)
{
if (!IsInvalid("map", value.Key, invalid))
{
mMaps.Add(value.Key, value);
}
}
foreach (BTVehicle value2 in BTConfig.Vehicles.Values)
{
if (!IsInvalid("vehicle", value2.Key, invalid))
{
mVehicles.Add(value2.Key, value2);
}
}
foreach (KeyValuePair<string, string> item in BTConfig.TimesOfDay)
{
if (!IsInvalid("time", item.Key, invalid))
{
mTimesOfDay.Add(item.Key, item.Value);
}
}
foreach (BTWeather value3 in BTConfig.Weather.Values)
{
if (!IsInvalid("weather", value3.Key, invalid))
{
mWeather.Add(value3.Key, value3);
}
}
}
private static bool IsInvalid(string type, string value, List<KeyValuePair<string, string>> invalid)
{
foreach (KeyValuePair<string, string> item in invalid)
{
if (item.Key == type && item.Value == value)
{
return true;
}
}
return false;
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TeslaConsole.BattleTech;
/// <summary>A pilot's running score changed (<c>MechScoreUpdateMessage</c>).</summary>
[Serializable]
internal class BTScoreUpdateEvent : BTMissionEvent
{
public readonly int Score;
public override IEnumerable<BTPlayer> InvolvedPilots => new BTPlayer[1] { ReportingPilot };
public BTScoreUpdateEvent(BTPlayer reportingPilot, TimeSpan missionTime, int score)
: base(reportingPilot, missionTime)
{
Score = score;
}
private BTScoreUpdateEvent()
{
}
}
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// The BattleTech mission-highlight narrative strings, loaded from
/// BattleTech\BTStrings.xml. Mirrors <c>TeslaConsole.RPStrings</c>: each
/// category is a pool of templates and one is picked at random with the
/// {time}/{player}/{vehicle}/{damager} placeholders substituted. The RP-only
/// categories (score/boost/lap) have no BT counterpart; DeathWithoutHonor
/// replaces SelfKill.
/// </summary>
internal class BTStrings
{
private static bool sInitalized = false;
private static List<string> sLightDamage = new List<string>();
private static List<string> sModerateDamage = new List<string>();
private static List<string> sHeavyDamage = new List<string>();
private static List<string> sKill = new List<string>();
private static List<string> sDeathWithoutHonor = new List<string>();
private static List<string> sRecap = new List<string>();
private static Random sRandom = new Random();
private static void Initalize()
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "BattleTech\\BTStrings.xml"));
if (xmlDocument.DocumentElement.Name != "BTStrings")
{
throw new XmlException("Document element was not a BTStrings node.");
}
foreach (XmlNode childNode in xmlDocument.DocumentElement.ChildNodes)
{
switch (childNode.Name)
{
case "LightDamage":
if (!sLightDamage.Contains(childNode.InnerText))
{
sLightDamage.Add(childNode.InnerText);
}
break;
case "ModerateDamage":
if (!sModerateDamage.Contains(childNode.InnerText))
{
sModerateDamage.Add(childNode.InnerText);
}
break;
case "HeavyDamage":
if (!sHeavyDamage.Contains(childNode.InnerText))
{
sHeavyDamage.Add(childNode.InnerText);
}
break;
case "Kill":
if (!sKill.Contains(childNode.InnerText))
{
sKill.Add(childNode.InnerText);
}
break;
case "DeathWithoutHonor":
if (!sDeathWithoutHonor.Contains(childNode.InnerText))
{
sDeathWithoutHonor.Add(childNode.InnerText);
}
break;
case "Recap":
if (!sRecap.Contains(childNode.InnerText))
{
sRecap.Add(childNode.InnerText);
}
break;
}
}
sInitalized = true;
}
private static string GetRandomStringAndReplace(List<string> list, TimeSpan time, string player, string vehicle, string damager)
{
if (!sInitalized)
{
Initalize();
}
if (list.Count == 0)
{
return null;
}
return list[sRandom.Next(list.Count)].Replace("{time}", GetTimeString(time)).Replace("{player}", player).Replace("{vehicle}", vehicle)
.Replace("{damager}", damager);
}
public static string GetTimeString(TimeSpan time)
{
int num = (int)(time.TotalSeconds + 0.5);
int num2 = num / 60;
return $"{num2:D2}:{num % 60:D2}";
}
public static string LightDamage(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sLightDamage, time, player, vehicle, damager);
}
public static string ModerateDamage(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sModerateDamage, time, player, vehicle, damager);
}
public static string HeavyDamage(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sHeavyDamage, time, player, vehicle, damager);
}
public static string Kill(TimeSpan time, string player, string vehicle, string damager)
{
return GetRandomStringAndReplace(sKill, time, player, vehicle, damager);
}
public static string DeathWithoutHonor(TimeSpan time, string player, string vehicle)
{
return GetRandomStringAndReplace(sDeathWithoutHonor, time, player, vehicle, "");
}
public static string Recap(string[] playerRanking)
{
StringBuilder stringBuilder = new StringBuilder(playerRanking[0]);
for (int i = 1; i < playerRanking.Length - 1; i++)
{
stringBuilder.AppendFormat(", {0}", playerRanking[i]);
}
return GetRandomStringAndReplace(sRecap, TimeSpan.Zero, "", "", "").Replace("{winners}", stringBuilder.ToString()).Replace("{looser}", (playerRanking.Length > 1) ? playerRanking[playerRanking.Length - 1] : "nobody");
}
}
@@ -0,0 +1,44 @@
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;
namespace TeslaConsole.BattleTech;
public class BTVehicle
{
private readonly string mKey;
private readonly string mName;
private readonly string mImagePath;
public string Key => mKey;
public string Name => mName;
public Image Image
{
get
{
if (mImagePath == null)
{
return null;
}
return ImageCache.GetImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), mImagePath));
}
}
internal BTVehicle(XmlNode vehicleNode)
{
XmlAttribute xmlAttribute = vehicleNode.Attributes["image"];
mKey = vehicleNode.Attributes["key"].InnerText;
mName = vehicleNode.Attributes["name"].InnerText;
mImagePath = xmlAttribute?.InnerText;
}
public override string ToString()
{
return mName;
}
}
@@ -0,0 +1,36 @@
using System.Xml;
namespace TeslaConsole.BattleTech;
/// <summary>
/// A BattleTech weather option. Unlike Red Planet (where weather is a plain
/// key/name pair), a BT weather entry also carries the ambient temperature that
/// becomes the egg's <c>[mission] temperature=</c> field.
/// </summary>
public class BTWeather
{
private readonly string mKey;
private readonly string mName;
private readonly int mTemperature;
public string Key => mKey;
public string Name => mName;
public int Temperature => mTemperature;
internal BTWeather(XmlNode weatherNode)
{
mKey = weatherNode.Attributes["key"].InnerText;
mName = weatherNode.Attributes["name"].InnerText;
XmlAttribute xmlAttribute = weatherNode.Attributes["temperature"];
mTemperature = ((xmlAttribute != null) ? int.Parse(xmlAttribute.InnerText) : 0);
}
public override string ToString()
{
return mName;
}
}
File diff suppressed because one or more lines are too long
+65 -208
View File
@@ -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();
}
+11 -3
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20 -7
View File
@@ -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" />
+3 -1
View File
@@ -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");
+28
View File
@@ -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);
}
}
+34 -1
View File
@@ -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)
{
+35 -8
View File
@@ -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();
}
+18 -3
View File
@@ -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));
}
+2 -2
View File
@@ -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);
+193 -8
View File
@@ -5,6 +5,7 @@ using System.Drawing.Drawing2D;
using System.Drawing.Printing;
using System.Windows.Forms;
using Munga.Net;
using TeslaConsole.BattleTech;
using TeslaConsole.RedPlanet;
using WeifenLuo.WinFormsUI.Docking;
@@ -38,6 +39,10 @@ public class TeslaConsoleForm : Form
private ToolStripMenuItem mRpMartianFootball;
private ToolStripMenuItem mBtFreeForAll;
private ToolStripMenuItem mBtNoReturn;
private ToolStripMenuItem mPlasmaFontTool;
private ToolStripMenuItem mRpMissionPrintPreview;
@@ -46,6 +51,12 @@ public class TeslaConsoleForm : Form
private ToolStripMenuItem mPrintRpMission;
private ToolStripMenuItem mBtMissionPrintPreview;
private ToolStripMenuItem mPrintBtMission;
private OpenFileDialog mBTMissionOpenDialog;
private ToolStripSeparator mSeperator1;
private PrintDialog mRPPrintDialog;
@@ -68,10 +79,20 @@ public class TeslaConsoleForm : Form
private ToolStripMenuItem exportRedPlanetDefaultsToolStripMenuItem;
private ToolStripMenuItem battleTechDefaultsToolStripMenuItem;
private ToolStripMenuItem importBattleTechDefaultsToolStripMenuItem;
private ToolStripMenuItem exportBattleTechDefaultsToolStripMenuItem;
private OpenFileDialog mRPDefaultsOpenDialog;
private SaveFileDialog mRPDefaultsSaveDialog;
private OpenFileDialog mBTDefaultsOpenDialog;
private SaveFileDialog mBTDefaultsSaveDialog;
private ToolStripSeparator toolStripSeparator1;
private ToolStripMenuItem enableCustomBitmapsToolStripMenuItem;
@@ -86,10 +107,12 @@ public class TeslaConsoleForm : Form
{
InitializeComponent();
mRpMissionPrintPreview.Visible = false;
mBtMissionPrintPreview.Visible = false;
mSeperator1.Visible = false;
mExceptionsMenu.Visible = false;
enableCustomBitmapsToolStripMenuItem.Checked = PlasmaBitmaps.EnableCustomBitmaps;
mRPMissionOpenDialog.InitialDirectory = RPMissionResults.GetRPMissionsDirectory();
mBTMissionOpenDialog.InitialDirectory = BTMissionResults.GetBTMissionsDirectory();
TeslaConsole.Site.LoadFromStore();
PodManager.StartConfigurationServer();
}
@@ -136,15 +159,16 @@ public class TeslaConsoleForm : Form
pod.MungaGame.SetStatus(ImageCache.PodQuestion16, "Waiting For State Update...");
break;
}
if (pod.MungaGame.AppID.Value != 0)
string gameTag = GameTag(pod.MungaGame.AppID.Value);
if (gameTag == null)
{
pod.MungaGame.SetStatus(ImageCache.PodBad16, "Pod Not Running RP");
pod.MungaGame.SetStatus(ImageCache.PodBad16, "Unknown Pod Application");
break;
}
switch (pod.MungaGame.AppState.Value)
{
case ApplicationState.InitializingState:
pod.MungaGame.SetStatus(ImageCache.PodQuestion16, "RP Initalizing");
pod.MungaGame.SetStatus(ImageCache.PodQuestion16, gameTag + " Initalizing");
break;
case ApplicationState.WaitingForEgg:
pod.MungaGame.SetStatus(ImageCache.PodOnline16, "Waiting For A Game");
@@ -202,6 +226,26 @@ public class TeslaConsoleForm : Form
}
}
/// <summary>
/// Short display tag for a pod's reported application, or null when the
/// console has no game module for it. RPL4 is Red Planet, BTL4 is
/// BattleTech; NDL4 (the never-released air-combat game) has no console
/// support. The "Initalizing" spelling in the status strings is preserved
/// from the original console.
/// </summary>
private static string GameTag(ApplicationID appID)
{
switch (appID)
{
case ApplicationID.RPL4:
return "RP";
case ApplicationID.BTL4:
return "BT";
default:
return null;
}
}
private void redPlanetDeathRaceToolStripMenuItem_Click(object sender, EventArgs e)
{
CreateNewRPGame(footballMode: false);
@@ -219,6 +263,23 @@ public class TeslaConsoleForm : Form
rPGame.Show(mMainDockPanel, DockState.Document);
}
private void battleTechFreeForAllToolStripMenuItem_Click(object sender, EventArgs e)
{
CreateNewBTGame(noReturnMode: false);
}
private void battleTechNoReturnToolStripMenuItem_Click(object sender, EventArgs e)
{
CreateNewBTGame(noReturnMode: true);
}
private void CreateNewBTGame(bool noReturnMode)
{
BTGame bTGame = new BTGame(TeslaConsole.Site.Active, noReturnMode);
bTGame.MdiParent = this;
bTGame.Show(mMainDockPanel, DockState.Document);
}
private void plasmaEditorToolStripMenuItem_Click(object sender, EventArgs e)
{
new PlasmaEditor().Show();
@@ -280,6 +341,52 @@ public class TeslaConsoleForm : Form
}
}
private PrintDocument GetBTMissionPrintDocument()
{
if (mBTMissionOpenDialog.ShowDialog() == DialogResult.OK)
{
try
{
BTMissionResults bTMissionResults = BTMissionResults.Load(mBTMissionOpenDialog.FileName);
return bTMissionResults.GetPrintDocument();
}
catch (Exception ex)
{
MessageBox.Show("The selected document could not be loaded because it appears to be corrupt.", "Error Loading Mission Data", MessageBoxButtons.OK);
Program.LogException(ex, $"Error Loading BT Mission Results (\"{mBTMissionOpenDialog.FileName}\").");
}
}
return null;
}
private void btPrintPreviewToolStripMenuItem_Click(object sender, EventArgs e)
{
PrintDocument bTMissionPrintDocument = GetBTMissionPrintDocument();
if (bTMissionPrintDocument != null)
{
if (mRPPrintPreviewDialog.IsDisposed)
{
mRPPrintPreviewDialog = new PrintPreviewDialog();
}
mRPPrintPreviewDialog.Document = bTMissionPrintDocument;
mRPPrintPreviewDialog.Show();
mRPPrintPreviewDialog.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
}
}
private void printBtMissionToolStripMenuItem_Click(object sender, EventArgs e)
{
PrintDocument bTMissionPrintDocument = GetBTMissionPrintDocument();
if (bTMissionPrintDocument != null)
{
mRPPrintDialog.Document = bTMissionPrintDocument;
if (mRPPrintDialog.ShowDialog() == DialogResult.OK)
{
mRPPrintDialog.Document.Print();
}
}
}
private void TeslaConsoleForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (mSitePanel != null && !mSitePanel.IsDisposed)
@@ -331,6 +438,43 @@ public class TeslaConsoleForm : Form
}
}
private void changeBattleTechDefaultsToolStripMenuItem_Click(object sender, EventArgs e)
{
BTDefaultsDialog.ShowSingleton();
}
private void importBattleTechDefaultsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (mBTDefaultsOpenDialog.ShowDialog() == DialogResult.OK)
{
try
{
BTDefaults.Import(mBTDefaultsOpenDialog.FileName);
}
catch (Exception)
{
MessageBox.Show("This BattleTech defaults file appears to be corrupt.", "Error Loading BattleTech Defaults!", MessageBoxButtons.OK);
return;
}
BTDefaults.Save();
}
}
private void exportBattleTechDefaultsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (mBTDefaultsSaveDialog.ShowDialog() == DialogResult.OK)
{
try
{
BTDefaults.Export(mBTDefaultsSaveDialog.FileName);
}
catch (Exception)
{
MessageBox.Show("The BattleTech defaults file could not be exported.", "Error Exporting BattleTech Defaults!", MessageBoxButtons.OK);
}
}
}
private void enableCustomBitmapsToolStripMenuItem_Click(object sender, EventArgs e)
{
enableCustomBitmapsToolStripMenuItem.Checked = (PlasmaBitmaps.EnableCustomBitmaps = !PlasmaBitmaps.EnableCustomBitmaps);
@@ -394,6 +538,8 @@ public class TeslaConsoleForm : Form
this.mSitePanelSeparator = new System.Windows.Forms.ToolStripSeparator();
this.mRpMissionPrintPreview = new System.Windows.Forms.ToolStripMenuItem();
this.mPrintRpMission = new System.Windows.Forms.ToolStripMenuItem();
this.mBtMissionPrintPreview = new System.Windows.Forms.ToolStripMenuItem();
this.mPrintBtMission = new System.Windows.Forms.ToolStripMenuItem();
this.mSeperator1 = new System.Windows.Forms.ToolStripSeparator();
this.mPlasmaEditor = new System.Windows.Forms.ToolStripMenuItem();
this.mPlasmaFontTool = new System.Windows.Forms.ToolStripMenuItem();
@@ -404,11 +550,16 @@ public class TeslaConsoleForm : Form
this.redPlanetDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.importRedPlanetDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportRedPlanetDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.battleTechDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.importBattleTechDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportBattleTechDefaultsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.enableCustomBitmapsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.mGamesMenu = new System.Windows.Forms.ToolStripMenuItem();
this.mRpDeathRace = new System.Windows.Forms.ToolStripMenuItem();
this.mRpMartianFootball = new System.Windows.Forms.ToolStripMenuItem();
this.mBtFreeForAll = new System.Windows.Forms.ToolStripMenuItem();
this.mBtNoReturn = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
this.mExceptionsMenu = new System.Windows.Forms.ToolStripMenuItem();
@@ -417,8 +568,11 @@ public class TeslaConsoleForm : Form
this.mRPPrintPreviewDialog = new System.Windows.Forms.PrintPreviewDialog();
this.mRPPrintDialog = new System.Windows.Forms.PrintDialog();
this.mRPMissionOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mBTMissionOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mRPDefaultsOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mRPDefaultsSaveDialog = new System.Windows.Forms.SaveFileDialog();
this.mBTDefaultsOpenDialog = new System.Windows.Forms.OpenFileDialog();
this.mBTDefaultsSaveDialog = new System.Windows.Forms.SaveFileDialog();
this.mMenu.SuspendLayout();
base.SuspendLayout();
this.mMainDockPanel.ActiveAutoHideContent = null;
@@ -480,7 +634,7 @@ public class TeslaConsoleForm : Form
this.mMenu.Size = new System.Drawing.Size(1168, 24);
this.mMenu.TabIndex = 3;
this.mMenu.Text = "menuStrip1";
this.mFileMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[10] { this.mShowSitePanelMenuItem, this.mSitePanelSeparator, this.mRpMissionPrintPreview, this.mPrintRpMission, this.mSeperator1, this.mPlasmaEditor, this.mPlasmaFontTool, this.mPlasmaBitmapDecoder, this.mSeperator2, this.mExit });
this.mFileMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[12] { this.mShowSitePanelMenuItem, this.mSitePanelSeparator, this.mRpMissionPrintPreview, this.mPrintRpMission, this.mBtMissionPrintPreview, this.mPrintBtMission, this.mSeperator1, this.mPlasmaEditor, this.mPlasmaFontTool, this.mPlasmaBitmapDecoder, this.mSeperator2, this.mExit });
this.mFileMenu.Name = "mFileMenu";
this.mFileMenu.Size = new System.Drawing.Size(37, 20);
this.mFileMenu.Text = "File";
@@ -498,6 +652,14 @@ public class TeslaConsoleForm : Form
this.mPrintRpMission.Size = new System.Drawing.Size(204, 22);
this.mPrintRpMission.Text = "Print RP Mission";
this.mPrintRpMission.Click += new System.EventHandler(printToolStripMenuItem_Click);
this.mBtMissionPrintPreview.Name = "mBtMissionPrintPreview";
this.mBtMissionPrintPreview.Size = new System.Drawing.Size(204, 22);
this.mBtMissionPrintPreview.Text = "BT Mission Print Preview";
this.mBtMissionPrintPreview.Click += new System.EventHandler(btPrintPreviewToolStripMenuItem_Click);
this.mPrintBtMission.Name = "mPrintBtMission";
this.mPrintBtMission.Size = new System.Drawing.Size(204, 22);
this.mPrintBtMission.Text = "Print BT Mission";
this.mPrintBtMission.Click += new System.EventHandler(printBtMissionToolStripMenuItem_Click);
this.mSeperator1.Name = "mSeperator1";
this.mSeperator1.Size = new System.Drawing.Size(201, 6);
this.mPlasmaEditor.Name = "mPlasmaEditor";
@@ -518,7 +680,7 @@ public class TeslaConsoleForm : Form
this.mExit.Size = new System.Drawing.Size(204, 22);
this.mExit.Text = "Exit";
this.mExit.Click += new System.EventHandler(mExit_Click);
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[5] { this.redPlanetDefaultsToolStripMenuItem, this.importRedPlanetDefaultsToolStripMenuItem, this.exportRedPlanetDefaultsToolStripMenuItem, this.toolStripSeparator1, this.enableCustomBitmapsToolStripMenuItem });
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[8] { this.redPlanetDefaultsToolStripMenuItem, this.importRedPlanetDefaultsToolStripMenuItem, this.exportRedPlanetDefaultsToolStripMenuItem, this.battleTechDefaultsToolStripMenuItem, this.importBattleTechDefaultsToolStripMenuItem, this.exportBattleTechDefaultsToolStripMenuItem, this.toolStripSeparator1, this.enableCustomBitmapsToolStripMenuItem });
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "Settings";
@@ -534,13 +696,25 @@ public class TeslaConsoleForm : Form
this.exportRedPlanetDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.exportRedPlanetDefaultsToolStripMenuItem.Text = "&Export Red Planet Defaults";
this.exportRedPlanetDefaultsToolStripMenuItem.Click += new System.EventHandler(exportRedPlanetDefaultsToolStripMenuItem_Click);
this.battleTechDefaultsToolStripMenuItem.Name = "battleTechDefaultsToolStripMenuItem";
this.battleTechDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.battleTechDefaultsToolStripMenuItem.Text = "Change &BattleTech Defaults";
this.battleTechDefaultsToolStripMenuItem.Click += new System.EventHandler(changeBattleTechDefaultsToolStripMenuItem_Click);
this.importBattleTechDefaultsToolStripMenuItem.Name = "importBattleTechDefaultsToolStripMenuItem";
this.importBattleTechDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.importBattleTechDefaultsToolStripMenuItem.Text = "Import BattleTech Defaults";
this.importBattleTechDefaultsToolStripMenuItem.Click += new System.EventHandler(importBattleTechDefaultsToolStripMenuItem_Click);
this.exportBattleTechDefaultsToolStripMenuItem.Name = "exportBattleTechDefaultsToolStripMenuItem";
this.exportBattleTechDefaultsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.exportBattleTechDefaultsToolStripMenuItem.Text = "Export BattleTech Defaults";
this.exportBattleTechDefaultsToolStripMenuItem.Click += new System.EventHandler(exportBattleTechDefaultsToolStripMenuItem_Click);
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(217, 6);
this.enableCustomBitmapsToolStripMenuItem.Name = "enableCustomBitmapsToolStripMenuItem";
this.enableCustomBitmapsToolStripMenuItem.Size = new System.Drawing.Size(220, 22);
this.enableCustomBitmapsToolStripMenuItem.Text = "Enable Custom Bitmaps";
this.enableCustomBitmapsToolStripMenuItem.Click += new System.EventHandler(enableCustomBitmapsToolStripMenuItem_Click);
this.mGamesMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[2] { this.mRpDeathRace, this.mRpMartianFootball });
this.mGamesMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[4] { this.mRpDeathRace, this.mRpMartianFootball, this.mBtFreeForAll, this.mBtNoReturn });
this.mGamesMenu.Name = "mGamesMenu";
this.mGamesMenu.Size = new System.Drawing.Size(55, 20);
this.mGamesMenu.Text = "Games";
@@ -552,6 +726,14 @@ public class TeslaConsoleForm : Form
this.mRpMartianFootball.Size = new System.Drawing.Size(223, 22);
this.mRpMartianFootball.Text = "Red Planet: Martian Football";
this.mRpMartianFootball.Click += new System.EventHandler(redPlanetMartianFootballToolStripMenuItem_Click);
this.mBtFreeForAll.Name = "mBtFreeForAll";
this.mBtFreeForAll.Size = new System.Drawing.Size(223, 22);
this.mBtFreeForAll.Text = "BattleTech: Free For All";
this.mBtFreeForAll.Click += new System.EventHandler(battleTechFreeForAllToolStripMenuItem_Click);
this.mBtNoReturn.Name = "mBtNoReturn";
this.mBtNoReturn.Size = new System.Drawing.Size(223, 22);
this.mBtNoReturn.Text = "BattleTech: No Return";
this.mBtNoReturn.Click += new System.EventHandler(battleTechNoReturnToolStripMenuItem_Click);
this.aboutToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[1] { this.aboutToolStripMenuItem1 });
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(52, 20);
@@ -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";
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+5 -4
View File
@@ -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 1st4th place art is identical to the RP ordinals the BT
// builder reuses, so here the pixel rows are compared exactly and in order.
// (cavern.egg is excluded on purpose: its ordinal art is Mac-font-rendered
// and legitimately differs — the spec's known false byte-match.)
var gen = ParseEgg(Generate(TestarnArgs));
var gold = ParseEgg(ReadGolden("TESTARN.EGG"));
for (int n = 1; n <= 4; n++)
{
string name = "Ordinal::BitMap::" + n;
Assert.True(gen.ContainsKey(name), "Generated egg lacks [" + name + "]");
Assert.Equal(gold[name], gen[name]);
}
}
// ---- wire framing ----
[Fact]
public void Egg_Wire_Framing_Chunks_And_Reassembles()
{
string result = _fx.Recovered.Run("BTEggFraming", CavernArgs);
var m = Regex.Match(result, @"^messages=(\d+);total=(\d+);framing=True;reassembled=True$");
Assert.True(m.Success, "Unexpected framing result: " + result);
int messages = int.Parse(m.Groups[1].Value);
int total = int.Parse(m.Groups[2].Value);
Assert.Equal((total + 999) / 1000, messages);
Assert.True(total > 5000, "Suspiciously small egg (" + total + " bytes)");
}
// ---- behavior pinned by the resolved spec questions ----
[Fact]
public void Role_Blocks_Are_Deduplicated_Across_Pilots()
{
var args = CavernArgs.Concat(new[]
{
"200.0.0.114", "kai", "loki", "Black", "Blue", "Davion", "novice", "two", "Default", "dfltrole",
}).ToArray();
string egg = Generate(args);
int roleRefs = Regex.Matches(egg, Regex.Escape("role=Role::Default\n")).Count;
int roleBlocks = Regex.Matches(egg, Regex.Escape("[Role::Default]\n")).Count;
Assert.Equal(2, roleRefs);
Assert.Equal(1, roleBlocks);
}
[Fact]
public void NoReturn_Mode_Sends_Freeforall_Scenario_With_NoReturn_Role()
{
// No Return is not a separate scenario on the wire: both golden eggs say
// scenario=freeforall, and the modes differ only by the pilots' role.
// The model is "noreturn" (the Mac ini tag, resolvable against the
// game's BTL4.RES "NoReturn" resource) — NOT the golden eggs' "noretun",
// which was a 4.10 console bug that dropped a character.
var args = (string[])CavernArgs.Clone();
args[13] = "NoReturn";
args[14] = "noreturn";
var egg = ParseEgg(Generate(args));
Assert.Contains("scenario=freeforall", egg["mission"]);
Assert.Contains("role=Role::NoReturn", egg["200.0.0.113"]);
Assert.True(egg.ContainsKey("Role::NoReturn"), "Missing [Role::NoReturn] block");
Assert.Contains("model=noreturn", egg["Role::NoReturn"]);
Assert.False(egg.ContainsKey("Role::Default"), "Unreferenced [Role::Default] emitted");
}
// ---- the shipped catalog ----
[Fact]
public void BTConfig_Catalog_Ships_With_Build_And_Has_Expected_Counts()
{
// Verifies the csproj Content include actually lands BTConfig.xml next to
// the exe (BTConfig loads it from there at runtime) and that the catalog
// carries the full Mac Console.ini BT tree.
string path = Path.Combine(Path.GetDirectoryName(AssemblyPaths.RecoveredExe),
"BattleTech", "BTConfig.xml");
Assert.True(File.Exists(path), "BTConfig.xml not copied next to the build: " + path);
var doc = new XmlDocument();
doc.Load(path);
XmlElement root = doc.DocumentElement;
Assert.Equal("BattleTech", root.Name);
Assert.Equal(8, root.SelectNodes("map").Count);
Assert.Equal(18, root.SelectNodes("vehicle").Count);
Assert.Equal(4, root.SelectNodes("time").Count);
Assert.Equal(3, root.SelectNodes("weather").Count);
foreach (XmlNode weather in root.SelectNodes("weather"))
Assert.Equal("27", weather.Attributes["temperature"].InnerText);
XmlNode ffa = root.SelectSingleNode("freeforall");
Assert.NotNull(ffa);
Assert.Equal(3, ffa.SelectNodes("experience").Count);
Assert.Equal(7, ffa.SelectNodes("color").Count);
Assert.Equal(8, ffa.SelectNodes("patch").Count);
Assert.Equal(7, ffa.SelectNodes("badge").Count);
Assert.Equal(2, ffa.SelectNodes("role").Count);
Assert.Equal("dfltrole", ffa.SelectSingleNode("role[@key='Default']").Attributes["model"].InnerText);
Assert.Equal("noreturn", ffa.SelectSingleNode("role[@key='NoReturn']").Attributes["model"].InnerText);
}
}
}
@@ -0,0 +1,229 @@
[mission]
adventure=BattleTech
map=arena1
scenario=freeforall
time=day
weather=clear
temperature=27
length=600
[ordinals]
bitmap=Ordinal::BitMap::1
bitmap=Ordinal::BitMap::2
bitmap=Ordinal::BitMap::3
bitmap=Ordinal::BitMap::4
[Ordinal::BitMap::1]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000038000003C000001FF00000000F00
bitmap=0000F8000003C000003FF80000000F00
bitmap=0001F8000003C00000707C0000000F00
bitmap=0001F8000003C00000603C0000000F00
bitmap=00007801FC0FF00000003C3DF807FF00
bitmap=00007803FE0FF00000003C3FFC0FFF00
bitmap=00007803C703C00000003C3E3C1F0F00
bitmap=000078078303C0000000783C1E1E0F00
bitmap=000078078003C0000000783C1E1E0F00
bitmap=00007807C003C0000000F03C1E1E0F00
bitmap=00007807F003C0000001E03C1E1E0F00
bitmap=00007803FC03C0000003C03C1E1E0F00
bitmap=00007801FE03C0000007803C1E1E0F00
bitmap=000078007F03C000000F003C1E1E0F00
bitmap=000078001F03C000001E003C1E1E0F00
bitmap=000078000F03C000003C003C1E1E0F00
bitmap=000078060F03C0000078003C1E1E0F00
bitmap=000078071E03E0000078003C1E1F1F00
bitmap=00007803FE01F000007FFC3C1E0FFF00
bitmap=00007801FC00F000007FFC3C1E07EF00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::2]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF0000003C0000003C03C0780000
bitmap=001FFF0000003C0000007C03C0780000
bitmap=00000E0000003C000000FC03C0780000
bitmap=00003C0000003C000001BC03C0780000
bitmap=0000700F3E1FFC000003BC0FF07BF000
bitmap=0001E00F7E3FFC0000073C0FF07FF800
bitmap=0003800FFE7C3C0000063C03C07C7800
bitmap=0007F80FFE783C00000C3C03C0783C00
bitmap=0007FE0F80783C0000183C03C0783C00
bitmap=00001E0F00783C0000383C03C0783C00
bitmap=00000F0F00783C0000703C03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00180F0F00783C0000003C03C0783C00
bitmap=001C1F0F007C7C0000003C03E0783C00
bitmap=000FFE0F003FFC0000003C01F0783C00
bitmap=0007FC0F001FBC0000003C00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::3]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000000FC03C0780000
bitmap=001FFF03C07800000003FC03C0780000
bitmap=001E0003C07800000007C003C0780000
bitmap=001E0003C0780000000F0003C0780000
bitmap=001E000FF07BF000000F000FF07BF000
bitmap=001E000FF07FF800001E000FF07FF800
bitmap=001E0003C07C7800001E0003C07C7800
bitmap=001FFC03C0783C00001EFC03C0783C00
bitmap=001FFE03C0783C00001FFE03C0783C00
bitmap=00001F03C0783C00001F1F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00180F03C0783C00001E0F03C0783C00
bitmap=001C1F03E0783C00001F1F03E0783C00
bitmap=000FFE01F0783C00000FFE01F0783C00
bitmap=0007FC00F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::4]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000007FC03C0780000
bitmap=001FFF03C0780000000FFE03C0780000
bitmap=00000F03C0780000001F1F03C0780000
bitmap=00000F03C0780000001E0F03C0780000
bitmap=00000F0FF07BF000001E0F0FF07BF000
bitmap=00001F0FF07FF800001E0F0FF07FF800
bitmap=00001E03C07C7800001E0F03C07C7800
bitmap=00003E03C0783C00001E0F03C0783C00
bitmap=00003C03C0783C00000F1E03C0783C00
bitmap=00003C03C0783C000007FC03C0783C00
bitmap=00007803C0783C000007FC03C0783C00
bitmap=00007803C0783C00000F1E03C0783C00
bitmap=00007803C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003E0783C00000F1E03E0783C00
bitmap=0000F001F0783C00000FFE01F0783C00
bitmap=0000F000F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[pilots]
pilot=200.0.0.113
[200.0.0.113]
hostType=0
advancedDamage=1
loadzones=1
name=cyd
bitmapindex=1
experience=veteran
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=madcat
vehicleValue=0
color=White
[largebitmap]
bitmap=BitMap::Large::cyd
[BitMap::Large::cyd]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000FCF00E0FBC0000000000
bitmap=000000000003FE781C1FFC0000000000
bitmap=000000000007FE781C3C7C0000000000
bitmap=00000000000F82783C3C3C0000000000
bitmap=00000000001F003C38783C0000000000
bitmap=00000000001E003C78783C0000000000
bitmap=00000000001E003C70783C0000000000
bitmap=00000000001E003E70783C0000000000
bitmap=00000000001E001EE0783C0000000000
bitmap=00000000001E001EE0783C0000000000
bitmap=00000000001F001FE0783C0000000000
bitmap=00000000000F020FC07C7C0000000000
bitmap=000000000007FE0FC03FFC0000000000
bitmap=000000000003FE0F803FFC0000000000
bitmap=000000000000FC07800F3C0000000000
bitmap=00000000000000070000000000000000
bitmap=00000000000000070000000000000000
bitmap=000000000000000F0000000000000000
bitmap=000000000000000E0000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[smallbitmap]
bitmap=BitMap::Small::cyd
[BitMap::Small::cyd]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000600000
bitmap=00000000006000000000000000600000
bitmap=000001EC33E000000000030666600000
bitmap=000003066660000000000303C6600000
bitmap=00000303C66000000000030186600000
bitmap=000001E183E000000000000300000000
bitmap=00000003000000000000000000000000
x=64
y=16
width=4
[Role::Default]
model=dfltrole
[Role::NoReturn]
model=noretun
@@ -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);
}
}
}
+22 -2
View File
@@ -27,7 +27,25 @@ proprietary dependencies — still resolves its references for metadata inspecti
and property/event accessor methods are excluded — the README at the repo root
notes those legitimately differ between a decompilation and the lost sources.
2. **Behavioral output** (`BehavioralEquivalenceTests`)
2. **Recovered-only characterization** (`CatalogTests`, `BTGoldenEggTests`)
Features that were *added* in the reconstruction have no counterpart in the
original exe, so these run against the recovered build only:
- `CatalogTests` — the data-driven product catalog reproduces the exact
`LaunchData` the old hardcoded code emitted.
- `BTGoldenEggTests` — the new `TeslaConsole.BattleTech` mission builder is
diffed field-by-field against two golden eggs captured from the original
consoles ([`BattleTech/cavern.egg`](BattleTech/cavern.egg),
[`BattleTech/TESTARN.EGG`](BattleTech/TESTARN.EGG)). The comparison is
per-section and order-independent (the pod parses eggs INI-style; the two
golden eggs themselves disagree on field order). Font-rendered name-bitmap
pixel rows are excluded, but TESTARN's ordinal art — identical to the
RP-inherited rows — is compared byte-exactly. Also covers the
`EggFileMessage` wire framing (NUL-delimited ASCII, 1000-byte chunks,
byte-exact reassembly), role-block de-duplication, the No Return mode
(same `scenario=freeforall`, different role), and the shipped
`BattleTech\BTConfig.xml` catalog contents.
3. **Behavioral output** (`BehavioralEquivalenceTests`)
The same deterministic, dependency-free methods are invoked in *both* assemblies
over a battery of inputs and the results must match byte-for-byte:
- `RPStrings.GetTimeString` (mm:ss formatting + 0.5 s rounding)
@@ -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();
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More