Gitea #47 COMPLETE: the ENG-button attention FLASH is live (jam / bay fire) -- MechTech status scan + BTL4GaugeAlarmManager + lamp chain, all from the binary

The pod behaviour Cyd described -- "on a jam or bay fire the display eng button
for the system flashes" -- is authored data + a five-stage chain, now running:

  MechTech::TechnicalAssistance (@004ad33c, per frame)
    edge-scans every monitored subsystem's GetStatusFlags() 7-bit condition
    mask (TechStatusType: Destroyed 0, Damaged 1, CoolantLeaking 2,
    Overheating 3, AmmoBurning 4, Jammed 5, BadPower 6)
  -> Start/StopEntityAlarmMessage (@00436688 id 7 size 0x20 / @004366b8 id 8
     size 0x1C; broadcast @004364e4; port shape: direct
     Start/StopEntityAlarmImplementation calls on the gauge renderer)
  -> GaugeAlarmManager::Activate(alarmModel) -- alarmModel = MechTech+0x100 =
     the 'mechalrm' ModelList (id 83) -> SearchList(83, type 31) -> the baked
     GaugeAlarmStream (id 331, 11 items {condition, lampCode}), decoded:
         Destroyed      -> gotoEngineering + engCooling + engBusMode
         CoolantLeaking -> gotoEngineering + engCooling
         AmmoBurning    -> gotoEngineering + engEject
         Jammed         -> gotoEngineering + engEject
         BadPower       -> gotoEngineering + engBusMode
  -> BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- THE REAL BODY, from
     @004cc2fc + helpers @004cc108/148/1a0/264/27c + tables @0051cf1c..0x51d084
     (gotoEngineering 0x80 = the subsystem's QUAD-SELECT bezel button via
     lamp[aux]/mode[aux] tables; 0x81+ descend the eng-page bank; Condenser /
     Generator specials on CoolantLeaking; <0x80 = the heat-bank fixed map).
     btl4galm.cpp's old bodies were admitted fabrications and its provenance
     note ("no override body exists in the image") was wrong -- corrected.
  -> LampManager::FindLamp (@00444c80) -> Lamp::SetAlertState (@00444e64, a
     COUNTER so stacked alarms hold the flash) -> L4Lamp::NotifyOfStateChange
     emits RIO flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239) -> the
     pod's physical lamps AND the glass panels (PadRIO IS rioPointer there).

FOUR load-bearing defects found and fixed en route -- each independently fatal
to the feature:

  1. HeatableSubsystem's Derivation chained Subsystem directly, SKIPPING
     MechSubsystem (written before the WAVE-1 re-basing) -- so EVERY subsystem
     on both family branches failed IsDerivedFrom(MechSubsystem), which is
     exactly MechTech's monitor filter: the status scan watched NOTHING.
  2. MechSubsystem::GetStatusFlags was non-virtual (binary: vtable slot 12) --
     the scan's MechSubsystem* call bound statically to the base tier and the
     weapon bits could never surface.
  3. ProjectileWeapon::GetStatusFlags sat in a Ghidra export gap -- raw-disasm
     @004bbf88: base | 0x20 (weaponAlarm==5 Jammed) | 0x10 (linked bin
     cookOffArmed@0x18C AmmoBurning).  The port had "defer to base".
  4. Mech::Reset's respawn sweep blanket-cast every roster entry to
     MechSubsystem and called RespawnRepair -- wrong for the Subsystem-level
     entries (MechTech 0xBDC, SubsystemMessageManager 0xBD3).  On MechTech the
     recon damageZone slot lands on its subsystemMonitors chain head: NULL
     while the scan was broken (fix #1's bug MASKED this one), but the moment
     the monitors populated, RespawnRepair virtual-called through a
     SubsystemMonitor as if it were a DamageZone -> load-time crash in
     StateIndicator::SetState (caught with cdb; call [edx+14h] on code bytes).
     The sweep now filters IsDerivedFrom(MechSubsystem) before the cast.

Also: GUID identities pinned -- 0x50f4bc = PoweredSubsystem::ClassDerivations
(via @004b1208 = its TestInstance; btl4gau2's two "Generator" comments were
wrong, swept), 0x50fb60 = Generator's.  A shadow-field instance documented for
the deferred de-shadow: MechSubsystem::damageZone re-declares the PUBLIC engine
Subsystem::damageZone (SUBSYSTM.h:159) -- mechtech's naive `sub->damageZone`
read the never-written engine member (0 monitors again); now reads the recon
member via GetDamageZoneProxy().  Logged in open-questions.

Wiring: BTL4GaugeRenderer now constructs + assigns the BTL4GaugeAlarmManager
(the base ctor NULLs it and Activate Check()s it); MechTech's Report*/
StatusMessageSink stubs are real; alarmModel confirmed baked (= 83) at runtime.

VERIFIED LIVE (scratchpad/lampflash.py, BT_LAMP_LOG chain trace):
    [techstat] MechTech id 32 monitors 29 subsystems, alarmModel 83
    [techstat] LRM15_1 condition 4 SET (alarmModel 83)      <- bay fire armed
    [galarm] condition 4 code 0x80 -> lamp 0xd mode 0x1 FLASH
    [lamp] 0xd <- 0x37  (FLASHING)                          <- the select button
    [galarm] condition 4 code 0x85 -> lamp 0xb mode 0x4 FLASH   <- engEject
    [techstat] LRM15_1 condition 4 CLEARED                  <- detonation clears
    [techstat] AmmoBinLRM15_1 condition 0 SET               <- bin Destroyed
Regressions: baytest PASS (bay fire kills), baypurge PASS (purge extinguishes),
sim3 3-pod brawl PASS with ZERO crashes (6 kills, organic heat-route bay fires,
respawns clean through the new sweep filter).

Env: BT_LAMP_LOG ([techstat]/[galarm]/[lamp]).  KB: gauges-hud (the flash
section), decomp-reference (the GaugeAlarm closure + tables + GUIDs),
open-questions (built-note + the de-shadow deferred item), btl4gau2 comment
sweep.  checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-25 16:04:34 -05:00
co-authored by Claude Fable 5
parent 5ae4410914
commit 6f5a264835
14 changed files with 556 additions and 122 deletions
+17 -4
View File
@@ -352,10 +352,23 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
COUNTER at lamp+0x1C) → the L4 lamp flush `@00474e94` emits `0x37`/`0x13` = `flashFast` RIO
states (== T0 L4LAMP.cpp:234-239) → `RIO::SetLamp` (binary `FUN_00476568`; cached wrapper
`@00474d54`). T0-compiled already: LAMP.cpp / L4LAMP.cpp / GAUGALRM.cpp / RENDERER messages.
**Missing in the port (3 pieces)**: the BTL4 override bodies (btl4galm.cpp is best-effort and
its provenance note claiming "no override exists in the binary" is WRONG — @004cc148-2fc IS
it), the gauge-watcher sender registration, and the lamp objects for the aux buttons. Plan in
[[open-questions]].
**✅ ALL THREE PIECES BUILT same day (see [[open-questions]] for the full closure note).**
Corrections pinned while building: the SENDER is **`MechTech::TechnicalAssistance` @004ad33c**
(not a StateIndicator gauge watcher) — the 7-condition `GetStatusFlags` edge-scan; message ctors
`@00436688` (Start, id 7 size 0x20) / `@004366b8` (Stop, id 8 size 0x1C) / broadcast `@004364e4`
(app+0x38 manager, renderer chain +0x10); `resourceID` = MechTech`+0x100` `alarmModel` = the
**'mechalrm' ModelList id 83** → `SearchList(83, type 31)` → stream **331** (11 items, decoded:
Destroyed→goto+cooling+busMode; CoolantLeaking→goto+cooling; AmmoBurning→goto+eject;
Jammed→goto+eject; BadPower→goto+busMode). `@004bbf88` = ProjectileWeapon::GetStatusFlags
(raw-disasm'd, export gap): base | 0x20 on `weaponAlarm==5` | 0x10 on linked-bin
`cookOffArmed@0x18C`. GUID identities: **0x50f4bc = PoweredSubsystem** (via @004b1208 =
its TestInstance; the old "Generator" comments were wrong — swept), **0x50fb60 = Generator**,
0x50e4fc = Condenser. Lamp tables: names @0051cf1c (coolingLoop1-6, gotoEngineering 0x80,
engGeneratorA-D 0x81-84, engEject 0x85, engBusMode 0x86, engCooling 0x87, engBackToQuad 0x88);
quad lamps @0051cffc {0xF,0xD,0xB,0x9, 0x27,0x25,0x23,0x21, 0x7,0x5,0x3,0x1}[aux] with modes
@0051cf9c; eng banks @0051d02c descend from {0xF/0x27/0x7} with modes @0051cfcc; condenser
@0051d058[condenserNumber]; placement @0051d070[auxScreenPlacement] = {0x29,0x1A-0x1D}.
Env: `BT_LAMP_LOG` = `[techstat]`/`[galarm]`/`[lamp]` — the full flash chain.
- **MechRIOMapper** @0x51DD30: RE-REGISTERS the base ids 3..0x13 (Aux1Quad..ZoomOut) with the
RIO override handlers @004d22fc..@004d24f8, + {0x19, Keypress→@004d2514} (RIO's OWN keypress,
unreconstructed — we use the shared L4 0x17 @004d1bf0) + {0x1a, Hotbox→@004d2574} @0x51DE98.
+19 -2
View File
@@ -360,8 +360,25 @@ reconstruction misread `bin+0x18C` as "the reload state" and bridged the icon to
`BTAmmoBinFeeding` — so it blinked with every feed cycle and never lit on a bay fire
(RajelAran: "it doesn't"). Now driven by `BTAmmoBinCookOffArmed/CookOffTime` complete-type
bridges (ammobin.cpp). The member names `firing/reloading/reloadSeconds` in btl4gau2 are
historic; semantically bayFireArmed / latch / secondsToDetonation. The ENG-BUTTON FLASH half
of #47 (the GaugeAlarm/lamp machinery) is mapped-but-unbuilt — see [[open-questions]].
historic; semantically bayFireArmed / latch / secondsToDetonation.
## The ENG-button ATTENTION FLASH (jam / bay fire) — BUILT (2026-07-25, #47) [T1 chain, T2 live]
The pod's authored annunciator: on a subsystem condition (TechStatusType — Destroyed 0 / Damaged 1
/ CoolantLeaking 2 / Overheating 3 / AmmoBurning 4 / Jammed 5 / BadPower 6) the affected
subsystem's OWN bezel buttons flash. Chain: `MechTech::TechnicalAssistance` (@004ad33c, per-frame)
edge-scans every monitored subsystem's `GetStatusFlags()` bitmask (now VIRTUAL — binary slot 12) →
`Start/StopEntityAlarm` to the gauge renderer (port: direct implementation calls) →
`GaugeAlarmManager::Activate(alarmModel=83 'mechalrm')` → the real `BTL4GaugeAlarmManager::
ReadGaugeAlarmStreamItem` (btl4galm.cpp, from @004cc2fc) maps `{condition, lampCode}` items via
the @0051cf1c vocabulary to bezel lamps (gotoEngineering = the subsystem's quad-select button;
engEject/engBusMode/engCooling = the eng-page bank; per-condenser/per-generator specials) →
`Lamp::SetAlertState` (a COUNTER — stacked alarms) → `L4Lamp` flushes `flashFast` RIO states
(0x37/0x13) → the pod's physical lamps AND the glass panels (PadRIO IS the rioPointer in glass
builds). The shipped 'mechalrm' table: Jammed/AmmoBurning → gotoEngineering + engEject (purge/
unjam); Destroyed → +engCooling+engBusMode; CoolantLeaking → +engCooling; BadPower → +engBusMode.
Verified live: bay fire → lamp 0xD (the LRM's select button) flashes 0x37 + engEject 0xB; clears
on detonation/purge. Diagnostics: `BT_LAMP_LOG` → `[techstat]`/`[galarm]`/`[lamp]`. Details +
the four load-bearing fixes en route: [[open-questions]] + [[decomp-reference]] §GaugeAlarm.
## ConfigMapGauge (the weapon panel's trigger-config joystick) — LIVE via LinkToEntity (2026-07-21)
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
+27 -1
View File
@@ -209,7 +209,33 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
reconstruction that dispatches subsystem messages through a base pointer (the binary's crit
path may) must first make the family virtual in one sweep (signatures must agree; some
declare `Logical`, some `void`+int variants — audit before flipping).
- **#47 remainder: the ENG-button attention FLASH (jam / bay fire) — machinery fully mapped,
- **#47 ENG-button attention FLASH BUILT same day (2026-07-25, evening).** All three pieces
landed: the REAL `BTL4GaugeAlarmManager` bodies (btl4galm.cpp, from @004cc294/@004cc2fc + the
lamp tables @0051cf1c..0x51d084), the sender (= `MechTech::TechnicalAssistance` @004ad33c
the "gauge watcher" guess was wrong; MechTech's 7-condition edge-scan builds
`Start/StopEntityAlarmMessage` via @00436688/@004366b8 and broadcasts @004364e4; port shape =
direct `Start/StopEntityAlarmImplementation` calls on the gauge renderer), and the lamps (the
L4LampManager + streamed-mapping lamps already existed; the manager is now constructed +
assigned in btl4grnd). Verified live end-to-end (`scratchpad/lampflash.py`): bay fire →
`[techstat] LRM15_1 condition 4 SET (alarmModel 83)` → gotoEngineering lamp 0xD flashes 0x37 +
engEject 0xB → detonation clears → the bin reports Destroyed. FOUR load-bearing fixes en
route: `HeatableSubsystem`'s Derivation chained Subsystem directly (skipping MechSubsystem —
the whole tree failed the monitor filter); `MechSubsystem::GetStatusFlags` made virtual (binary
slot 12; static binding hid the weapon bits); `ProjectileWeapon::GetStatusFlags` raw-disasm'd
from the @004bbf88 export gap (jam→0x20, bin cookOffArmed→0x10); and the Mech::Reset sweep's
blanket `(MechSubsystem*)` cast filtered (it RespawnRepair'd MechTech through the wrong layout —
benign only while the monitor chain head was NULL; #47 populating it exposed a load crash).
Remaining risks: the wooHoo latch fields are still untested; FindLamp needs exact modeMask
equality so unflashed lamps on rare pages would betray a mapping-stream mask mismatch.
- **DEFERRED de-shadow: `MechSubsystem::damageZone` re-declares the PUBLIC engine
`Subsystem::damageZone` (SUBSYSTM.h:159) — gotcha #1 (2026-07-25, found via #47).** All recon
code coherently uses the derived member; the ENGINE base member stays NULL forever, and engine
base paths that use it (`Subsystem::TakeDamage` derefs it unguarded!) would AV if ever reached.
mechtech's monitor filter hit this (naive `sub->damageZone` resolved to the engine one → 0
monitors). De-shadowing = make the recon write the ENGINE member and delete the re-declaration
— a layout-neutral but call-site-wide sweep; do it deliberately, with the ReconDamageZone-proxy
offset-0-vptr alias (#46's crash) retired in the same pass.
- **(superseded) the original #47 remainder note follows for the record:** machinery fully mapped,
3 pieces unbuilt (2026-07-25).** The authored data SHIPS: `BTL4.RES` carries one
`GaugeAlarmStream` (type 31). The full chain + addresses: [[decomp-reference]] §GaugeAlarm.
To make it live: (1) reconstruct the REAL `BTL4GaugeAlarmManager` override bodies from
+13
View File
@@ -121,6 +121,19 @@ void
if (previousLampState[lamp_number] != lamp_value)
{
previousLampState[lamp_number] = lamp_value;
// (Gitea #47 diag) BT_LAMP_LOG traces every lamp-state change ABOVE the
// RIO seam -- on a desktop build without a RIO/PadRIO, SetLamp below
// no-ops (rioPointer NULL, L4CTRL.cpp:1174), so this line is the only
// observable. Flash states carry low bits 1-3 (flashSlow/Med/Fast).
{
static int s_log = -1;
if (s_log < 0) s_log = (getenv("BT_LAMP_LOG") != 0) ? 1 : 0;
if (s_log)
DEBUG_STREAM << "[lamp] 0x" << std::hex << lamp_number
<< " <- 0x" << lamp_value << std::dec
<< (((lamp_value & 3) != 0) ? " (FLASHING)" : "")
<< "\n" << std::flush;
}
//--------------------------------------
// Send RIO command to update L4 lamp
//--------------------------------------
+220 -85
View File
@@ -31,9 +31,9 @@
// counter), and records the lamp on the GaugeAlarm's lampList.
// @004cc294 the Create-side sibling.
// This is the #47 ENG-BUTTON ATTENTION FLASH (jam / bay fire); the authored
// data survives as the type-31 GaugeAlarmStream in BTL4.RES (exactly 1). The
// bodies below remain best-effort scaffolding UNTIL re-transcribed from those
// addresses -- see context/open-questions.md for the 3-piece plan. The
// data survives as the type-31 GaugeAlarmStream in BTL4.RES (exactly 1:
// ModelList 'mechalrm' 83 -> stream 331). The bodies below ARE now the real
// re-transcriptions from those addresses (2026-07-25, same day). The
// original (wrong) note is preserved for the record:
//
// btl4galm.obj links after btl4grnd.obj and before btl4vid.obj (BTL4.MAK
@@ -102,36 +102,115 @@
//
// Recognised cockpit gauge-alarm type names. (BattleTech's l4gauge.cfg
// "gaugeAlarm" sections name one of these; the threshold / flash parameters
// follow in alarm_data.) The exact spelling list could not be recovered from
// this image -- it lived in the folded-away override -- so the set below is the
// minimal set implied by the GaugeAlarm fields the cockpit gauges flash on
// (heat / power / armour / ammo). BEST-EFFORT.
// THE REAL OVERRIDE BODIES (Gitea #47, 2026-07-25) -- re-transcribed from the
// binary at @004cc294 (Create) / @004cc2fc (Read) + the mapping helpers
// @004cc108/@004cc148/@004cc1a0/@004cc264/@004cc27c and their data tables
// @0051cf1c..@0051d084. The previous bodies here were admitted fabrications
// ("threshold/range/state/flash" types that exist nowhere in the image); the
// provenance note at the top of this file records why.
//
enum GaugeAlarmType
// The STREAM FORMAT (verified against the shipped resource -- BTL4.RES carries
// ModelList 'mechalrm' id=83 -> GaugeAlarmStream id=331, 11 items):
// { int32 condition; int32 lampCode; } per item
// where condition = the MechSubsystem TechStatusType (the @0x50de74 name table:
// Destroyed 0, Damaged 1, CoolantLeaking 2, Overheating 3, AmmoBurning 4,
// Jammed 5, BadPower 6) and lampCode = the @0051cf1c vocabulary below.
//
// The shipped 'mechalrm' authoring (decoded 2026-07-25):
// Destroyed -> gotoEngineering + engCooling + engBusMode
// CoolantLeaking -> gotoEngineering + engCooling
// AmmoBurning -> gotoEngineering + engEject
// Jammed -> gotoEngineering + engEject
// BadPower -> gotoEngineering + engBusMode
// ("on a jam or bay fire the display eng button for the system flashes" --
// Cyd's pod testimony, verbatim in the data.)
//
//
// @0051cf1c -- the lamp-code name vocabulary ({char*, code} pairs, 0-terminated).
//
struct BTLampNameEntry { const char *name; int code; };
static const BTLampNameEntry kBTLampNames[] =
{
gaugeAlarmThreshold, // value crosses a fixed threshold
gaugeAlarmRange, // value leaves a [low,high] band
gaugeAlarmState, // subsystem operational-state change
gaugeAlarmFlash, // timed flash while condition holds
gaugeAlarmTypeCount,
gaugeAlarmUndefined = -1
{ "coolingLoop1", 0x00 },
{ "coolingLoop2", 0x01 },
{ "coolingLoop3", 0x02 },
{ "coolingLoop4", 0x03 },
{ "coolingLoop5", 0x04 },
{ "coolingLoop6", 0x05 },
{ "gotoEngineering", 0x80 },
{ "engGeneratorA", 0x81 },
{ "engGeneratorB", 0x82 },
{ "engGeneratorC", 0x83 },
{ "engGeneratorD", 0x84 },
{ "engEject", 0x85 },
{ "engBusMode", 0x86 },
{ "engCooling", 0x87 },
{ "engBackToQuad", 0x88 },
{ 0, 0 }
};
//
// The lamp-resolution tables, byte-verified from the image:
// @0051cf9c quad-page mode masks [12] (aux screens 1..12)
// @0051cfcc eng-page mode masks [12]
// @0051cffc quad-select button lamp ids [12] (code 0x80 gotoEngineering)
// @0051d02c eng-page bank-top lamp ids [12] (codes 0x81.. descend from it)
// @0051d058 per-condenser lamps (index = condenserNumber @0x1D4, 1-based)
// @0051d070 per-placement lamps (index = auxScreenPlacement @0x1E0)
//
static const int kBTQuadModeMask[12] = { 0x1,0x1,0x1,0x1, 0x20,0x20,0x20,0x20, 0x400,0x400,0x400,0x400 };
static const int kBTEngModeMask[12] = { 0x2,0x4,0x8,0x10, 0x40,0x80,0x100,0x200, 0x800,0x1000,0x2000,0x4000 };
static const int kBTQuadLamp[12] = { 0xF,0xD,0xB,0x9, 0x27,0x25,0x23,0x21, 0x7,0x5,0x3,0x1 };
static const int kBTEngBankTop[12] = { 0xF,0xF,0xF,0xF, 0x27,0x27,0x27,0x27, 0x7,0x7,0x7,0x7 };
static const int kBTCondenserLamp[6] = { 0x7, 0x2F,0x2E,0x2D,0x2B,0x2A }; // [condenserNumber]
static const int kBTPlacementLamp[5] = { 0x29, 0x1A,0x1B,0x1C,0x1D }; // [auxScreenPlacement]
//
// BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem
// @004cc148 -- the fixed (<0x80) map: coolingLoop1..6 -> the heat-display bank.
//
// Called by GaugeAlarmManager::CreateGaugeAlarmStream (@00448af0) once per
// "gaugeAlarm" entry while a gauge model is being compiled. Recognise the
// alarm type spelled in <alarm_name>, parse its parameters out of <alarm_data>,
// and serialise (type, parameters) into <mem_stream>. Returning False makes
// the base print "Model file '...' has undefined alarm type '<alarm_name>' -
// resource not built." and abandon the model -- so an unknown name MUST yield
// False (that diagnostic is the observable contract).
static int BTFixedLampOf(int code)
{
static const int kFixed[6] = { 0x2F, 0x2E, 0x2D, 0x2C, 0x2B, 0x2A };
return (code >= 0 && code < 6) ? kFixed[code] : -1;
}
//
// BEST-EFFORT reconstruction (no distinct override body survived in BTL4OPT.EXE).
// @004cc108 -- map an authored lamp NAME to its code.
//
static Logical BTLampCodeOf(const char *data, int *out_code)
{
for (const BTLampNameEntry *e = kBTLampNames; e->name != 0; ++e)
{
if (strcmp(data, e->name) == 0) // FUN_004d4b58
{
*out_code = e->code;
return True;
}
}
return False;
}
static Logical BTLampLog()
{
static int s_on = -1;
if (s_on < 0) s_on = (getenv("BT_LAMP_LOG") != 0) ? 1 : 0;
return s_on ? True : False;
}
//
// BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem -- @004cc294 [T1]
//
// iVar1 = FUN_004ac194(name, &condition); // MechSubsystem::LookupStatusType
// if (!iVar1) return 0;
// iVar1 = FUN_004cc108(data, &code); // the @0051cf1c vocabulary
// if (!iVar1) return 0;
// stream->WriteBytes(&condition, 4); // (*stream+0x20)
// stream->WriteBytes(&code, 4);
// return 1;
//
// (Bake-side only -- the shipped BTL4.RES already carries the baked stream; this
// matters when re-baking resources from notation.)
//
Logical
BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem(
@@ -140,87 +219,143 @@ Logical
const char *alarm_data
)
{
GaugeAlarmType type = gaugeAlarmUndefined;
int condition, code;
//
// Map the type name -> enum. (Names best-effort; see note above.)
//
if (stricmp(alarm_name, "threshold") == 0) type = gaugeAlarmThreshold;
else if (stricmp(alarm_name, "range") == 0) type = gaugeAlarmRange;
else if (stricmp(alarm_name, "state") == 0) type = gaugeAlarmState;
else if (stricmp(alarm_name, "flash") == 0) type = gaugeAlarmFlash;
if (type == gaugeAlarmUndefined)
if (!MechSubsystem::LookupStatusType(alarm_name, &condition)) // FUN_004ac194
{
return False; // -> "has undefined alarm type ... resource not built"
}
if (!BTLampCodeOf(alarm_data, &code)) // FUN_004cc108
{
//
// Unknown alarm type: tell the caller so it emits the
// "undefined alarm type ... resource not built" warning.
//
return False;
}
//
// Serialise the type tag followed by the type-specific parameters parsed
// from the alarm_data text (thresholds, flash period, target colour, ...).
//
mem_stream->WriteBytes(&type, sizeof(type)); // (*mem_stream+0x20)(...,4)
// ... write parsed numeric parameters from alarm_data ...
mem_stream->WriteBytes(&condition, sizeof(condition));
mem_stream->WriteBytes(&code, sizeof(code));
return True;
}
//
// BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem
// BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- @004cc2fc [T1]
//
// Called by GaugeAlarmManager::BuildAlarmsForModel (@00448d00) once per baked
// alarm record, with a freshly-constructed GaugeAlarm (already carrying
// the_entity/the_subsystem/the_condition from the GaugeAlarm ctor @004489e8).
// Read the (type, parameters) back out of <mem_stream> and configure <alarm>
// so that, every frame, it samples the named subsystem attribute and raises /
// clears its flashing state when the recorded condition is met.
// Read one {condition, lampCode} item; when the item's condition equals the
// condition being ACTIVATED, resolve the lamp:
// * Condenser-derived (0x50e4fc) + condition CoolantLeaking(2):
// lamp = @0051d058[condenserNumber@0x1D4] (its own loop button)
// * Generator-derived (0x50fb60) + condition CoolantLeaking(2):
// lamp = @0051d070[auxScreenPlacement@0x1E0] (its radar-rail button)
// * else code < 0x80: the fixed heat-bank map (@004cc148)
// * else code >= 0x80: PoweredSubsystem-derived only (0x50f4bc -- the same
// gate BTGetSubsystemAuxScreen applies) + auxScreen@0x1DC > 0:
// 0x80 gotoEngineering: lamp = @0051cffc[aux], mode = @0051cf9c[aux]
// 0x81..: lamp = @0051d02c[aux] - (code - 0x81), mode = @0051cfcc[aux]
// then FindLamp(lamp, mode) on the renderer lamp manager (@00444c80), start it
// flashing (Lamp::SetAlertState(True) @00444e64 -- a COUNTER, alarms stack), and
// record it on the GaugeAlarm lampList so ~GaugeAlarm un-flashes on Deactivate.
// A lamp code that resolves to no lamp is silently skipped (iVar2 = -1 path).
//
// BEST-EFFORT reconstruction (no distinct override body survived in BTL4OPT.EXE).
// (0x50f4bc = PoweredSubsystem::ClassDerivations, NOT Generator -- pinned via
// @004b1208 = PoweredSubsystem::TestInstance in the powersub method cluster;
// 0x50fb60 = the Generator one, adjacent to its message table @0x50fb90.)
//
void
BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem(
GaugeAlarm *alarm,
Entity *the_entity,
Entity * /*the_entity*/,
Subsystem *the_subsystem,
Enumeration the_condition,
MemoryStream *mem_stream
)
{
GaugeAlarmType type;
mem_stream->ReadBytes(&type, sizeof(type)); // (*mem_stream+0x1c)(...,4)
int item_condition = -1, lamp_code = -1;
mem_stream->ReadBytes(&item_condition, sizeof(item_condition)); // (*stream+0x1c)
mem_stream->ReadBytes(&lamp_code, sizeof(lamp_code));
//
// Bind the alarm to its data source and trip parameters. The base
// GaugeAlarm already stores entity/subsystem/condition; here we attach the
// type-specific trip (threshold / band / state / flash period) read from
// the stream so GaugeAlarm::Execute can flash the owning gauge.
//
switch (type)
if ((int)the_condition != item_condition)
{
case gaugeAlarmThreshold:
// alarm->SetThreshold( read Scalar ); alarm->SetCondition(the_condition);
break;
case gaugeAlarmRange:
// alarm->SetBand( read low, read high );
break;
case gaugeAlarmState:
// alarm->WatchState(the_subsystem);
break;
case gaugeAlarmFlash:
// alarm->SetFlashPeriod( read Scalar );
break;
default:
Verify(False, "Bad gauge alarm type",
"d:\\tesla\\bt\\bt_l4\\BTL4GALM.CPP", 0);
break;
return;
}
(void)the_entity;
(void)the_subsystem;
(void)the_condition;
extern int BTSubsystemIsCondenser(::Subsystem *sub); // heat.cpp (0x50e4fc)
extern int BTCondenserNumber(::Subsystem *sub); // heat.cpp (+0x1D4)
extern int BTSubsystemIsGenerator(::Subsystem *sub); // powersub.cpp (0x50fb60)
extern bool BTGetSubsystemAuxScreen(::Subsystem *sub, int *screen, int *placement, char *label64); // powersub.cpp (0x50f4bc gate + 0x1DC/0x1E0)
int lamp_id = -1;
int mode = -1; // local_10: stays -1 for the fixed/family paths
if (BTSubsystemIsCondenser(the_subsystem))
{
if (the_condition == 2) // CoolantLeaking
{
int n = BTCondenserNumber(the_subsystem);
if (n >= 0 && n < 6)
lamp_id = kBTCondenserLamp[n];
}
}
else if (BTSubsystemIsGenerator(the_subsystem))
{
if (the_condition == 2) // CoolantLeaking
{
int screen = 0, placement = -1;
BTGetSubsystemAuxScreen(the_subsystem, &screen, &placement, 0);
if (placement >= 0 && placement < 5)
lamp_id = kBTPlacementLamp[placement];
}
}
else if (lamp_code < 0x80)
{
lamp_id = BTFixedLampOf(lamp_code);
}
else
{
int screen = 0, placement = -1;
if (BTGetSubsystemAuxScreen(the_subsystem, &screen, &placement, 0) // PoweredSubsystem gate
&& screen > 0)
{
int aux = screen - 1; // +0x1DC - 1
if (aux < 12)
{
if (lamp_code == 0x80) // gotoEngineering
{
lamp_id = kBTQuadLamp[aux];
mode = kBTQuadModeMask[aux];
}
else // engGeneratorA..engBackToQuad
{
lamp_id = kBTEngBankTop[aux] - (lamp_code - 0x81);
mode = kBTEngModeMask[aux];
}
}
}
}
if (lamp_id < 0)
{
return;
}
//
// @004cc2fc tail: FindLamp on the gauge renderer lamp manager, flash it,
// and record it on the alarm. (DAT_004efc94+0x4c = the gauge renderer.)
//
GaugeRenderer *renderer = (GaugeRenderer *)application->GetGaugeRenderer();
if (renderer == 0)
{
return;
}
Lamp *lamp = (renderer->GetLampManager() != 0)
? renderer->GetLampManager()->FindLamp((LampID)lamp_id, (ModeMask)mode)
: 0;
if (BTLampLog())
DEBUG_STREAM << "[galarm] condition " << (int)the_condition
<< " code 0x" << std::hex << lamp_code
<< " -> lamp 0x" << lamp_id << " mode 0x" << mode << std::dec
<< (lamp ? " FLASH" : " (no lamp registered -- skipped)") << "\n" << std::flush;
if (lamp != 0)
{
lamp->SetAlertState(True); // FUN_00444e64 -- the flash counter
alarm->lampList.Add(lamp); // un-flashed by ~GaugeAlarm on Deactivate
}
}
+3 -2
View File
@@ -31,7 +31,7 @@
// FUN_0041f98c Entity::FindSubObject(name)
// FUN_00447f1c GaugeRenderer::InstallPort / FUN_00447f84 GetGraphicsPort
// FUN_00417ab4 SharedData::Resolve / operator Entity*
// FUN_0041a1a4 IsDerivedFrom(classDerivations) (0x50f4bc = Generator)
// FUN_0041a1a4 IsDerivedFrom(classDerivations) (0x50f4bc = PoweredSubsystem -- pinned #47 via @004b1208 = PoweredSubsystem::TestInstance; the old 'Generator' reading was wrong)
// FUN_0042076c Subsystem::FindDamageZone(record) -> index
// FUN_004dcd94 Round(Scalar) -> int
// FUN_004700bc NumericDisplay ctor FUN_0047018c ::dtor
@@ -1485,7 +1485,8 @@ SubsystemCluster::SubsystemCluster(
WipeGaugeScalar::wipeUp, 0.0f, GeneratorMaxVoltage, subsystem_in, // pass the SUBSYSTEM
"GeneratorVoltage(slotOf)"); // -> BTGeneratorVoltage reads voltage by name (attr-shift fix)
// stateLamp only when the subsystem is a Generator (IsDerivedFrom 0x50f4bc).
// stateLamp gate: IsDerivedFrom 0x50f4bc = POWEREDSUBSYSTEM (#47 correction --
// the old 'Generator' label was wrong; every powered subsystem passes).
if (IsGeneratorDerived(subsystem_in))
stateLamp = new OneOfSeveralStates(ChildRate(), eng_mode, renderer_in, // @004c5470
engPort, 0xbe, 0, "btemode.pcc", 1, 3,
+12
View File
@@ -74,6 +74,7 @@
# include <l4lamp.hpp>
#endif
#include <GAUGALRM.hpp> // GaugeAlarmManager (TearDownForViewpointRestream, Gitea #12)
#include <btl4galm.hpp> // BTL4GaugeAlarmManager (the #47 override -- constructed in the ctor)
#if !defined(L4WARE_HPP)
# include <l4ware.hpp>
#endif
@@ -205,6 +206,17 @@ BTL4GaugeRenderer::BTL4GaugeRenderer()
lampManager = new L4LampManager(controls_manager); // this[0x7033]
Register_Object(lampManager);
//----------------------------------------
// Gauge-alarm manager (Gitea #47): the BTL4 override whose
// Read/CreateGaugeAlarmStreamItem bodies live at @004cc2fc/@004cc294.
// The base GaugeRenderer ctor NULLs gaugeAlarmManager (GAUGREND.cpp:2841)
// and Activate/Deactivate Check() it -- without this, MechTech's
// Start/StopEntityAlarm delivery (the jam / bay-fire ENG-button flash)
// had no receiver.
//----------------------------------------
gaugeAlarmManager = new BTL4GaugeAlarmManager();
Register_Object(gaugeAlarmManager);
//----------------------------------------
// Secondary / controls helper bound to the application
// alloc 0x44; FUN_00474c4c(application[0x3c]). this[0x7032].
+24 -1
View File
@@ -131,7 +131,16 @@ HeatableSubsystem::SharedData
Derivation*
HeatableSubsystem::GetClassDerivations()
{
static Derivation classDerivations(Subsystem::GetClassDerivations(), "HeatableSubsystem");
// #47 CORRECTION: was chained to Subsystem:: directly, SKIPPING
// MechSubsystem -- written before the WAVE-1 re-basing established
// `HeatableSubsystem : MechSubsystem`. That made EVERY subsystem on
// both family branches fail IsDerivedFrom(MechSubsystem::ClassDerivations),
// which is exactly the filter MechTech's monitor walk applies
// (FUN_0041a1a4(**sub[3], 0x50de2c)) -- so the status scan monitored
// NOTHING and no jam/bay-fire annunciation could ever fire. The
// derivation chain now mirrors the class hierarchy, as every other
// family file already does (Searchlight -> PowerWatcher -> HeatWatcher...).
static Derivation classDerivations(&MechSubsystem::ClassDerivations, "HeatableSubsystem");
return &classDerivations;
}
@@ -1249,3 +1258,17 @@ int BTCoolingLoopFrame(void *subsystem_v)
<< " (condenser '" << master->GetName() << "')\n" << std::flush;
return frame;
}
//
// Gitea #47 bridges (BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem):
// the Condenser family tests + fields the alarm lamp mapping needs
// (@004cc2fc gates on 0x50e4fc; @004cc264 reads condenserNumber @0x1D4).
//
int BTSubsystemIsCondenser(::Subsystem *sub)
{
return (sub != 0 && sub->IsDerivedFrom(*Condenser::GetClassDerivations())) ? 1 : 0;
}
int BTCondenserNumber(::Subsystem *sub)
{
return (sub != 0) ? ((Condenser *)sub)->condenserNumber : -1; // +0x1D4
}
+12 -2
View File
@@ -1787,8 +1787,18 @@ void
Subsystem *s = GetSubsystem(i);
if (s == 0)
continue;
s->DeathReset(mode); // -> per-class RTIS chain
((MechSubsystem *)s)->RespawnRepair(); // fresh-mech: zone + status
s->DeathReset(mode); // -> per-class RTIS chain (engine virtual -- safe for all)
// #47 regression fix: the blanket (MechSubsystem*) cast is WRONG for the
// Subsystem-level roster entries (MechTech 0xBDC, SubsystemMessageManager
// 0xBD3) -- RespawnRepair read the recon damageZone slot THROUGH them,
// which on MechTech lands on its subsystemMonitors chain head. While the
// monitor walk was broken (0 monitors) that head was NULL and the guard
// skipped; the moment #47 populated it, RespawnRepair virtual-called
// through a SubsystemMonitor as if it were a DamageZone (the
// StateIndicator::SetState load crash). Filter with the same
// IsDerivedFrom the binary's own sweeps use.
if (s->IsDerivedFrom(MechSubsystem::ClassDerivations))
((MechSubsystem *)s)->RespawnRepair(); // fresh-mech: zone + status
}
// --- locomotion pre-run + interest gates (a reset master must tick) ---
+12 -2
View File
@@ -232,7 +232,13 @@ class Damage;
// Subsystem virtual overrides (slots on vtable @0x50e210)
//
public:
LWord
// VIRTUAL (#47): the binary dispatches slot 12 through the vtable --
// MechTech's TechnicalAssistance scan calls (*sub_vtbl+0x30) on every
// monitored subsystem. Non-virtual here made a MechSubsystem* call bind
// statically to this base tier, so ProjectileWeapon's Jammed/AmmoBurning
// bits (@004bbf88) never reached the scan. (HeatableSubsystem already
// declared its own virtual; this unifies the root.)
virtual LWord
GetStatusFlags(); // slot 12, @0x4ac144
void
ResetToInitialState(Logical powered); // slot 8, @0x4ac1d4
@@ -294,9 +300,13 @@ class Damage;
void DistributeCriticalHit(Damage &damage);
// @0x4ac194 -- map a status-state name string -> TechStatusType value
// using the name table @0x50de74 (ConfigureActivePress support).
// using the name table @0x50de74. PUBLIC (was protected): the binary
// calls it cross-TU from BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem
// (@004cc294) to parse "gaugeAlarm Jammed = engEject"-style entries (#47).
public:
static Logical
LookupStatusType(const char *name, int *out_value);
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data (byte offsets into the shipped object)
+64 -16
View File
@@ -207,10 +207,17 @@ MechTech::MechTech(
for (int seg = entity->GetSubsystemCount() - 1; seg >= 0; --seg)
{
Subsystem *sub = entity->GetSubsystem(seg);
// #47 SHADOW-FIELD TRAP (gotcha #1): the naive `sub->damageZone`
// resolves to the PUBLIC engine Subsystem::damageZone
// (SUBSYSTM.h:159) -- which the reconstruction NEVER WRITES; the
// recon zone lives on the re-declared MechSubsystem member @0xE0.
// Testing the engine one rejected every subsystem (0 monitors).
// Read the recon member through its accessor. (The re-declaration
// itself is logged in open-questions as a deferred de-shadow.)
if (
sub != 0
&& sub->IsDerivedFrom(MechSubsystem::ClassDerivations) // FUN_0041a1a4(**sub[3], 0x50de2c)
&& sub->damageZone != 0 // damageable segment (segment bit 0x8)
&& ((MechSubsystem *)sub)->GetDamageZoneProxy() != 0 // damageable segment (segment bit 0x8)
)
{
SubsystemMonitor *monitor =
@@ -218,6 +225,21 @@ MechTech::MechTech(
subsystemMonitors.Add(monitor); // (*this[0x39].vt[1])(...)
}
}
// #47 diag: how many subsystems the status scan actually watches, and
// which alarm table it will Activate with. A monitor count of ~2 here
// betrays the roster-order hazard (the ctor walk sees only the slots
// built BEFORE MechTech in the stream).
if (getenv("BT_LAMP_LOG"))
{
int n = 0;
ChainIteratorOf<SubsystemMonitor*> it(&subsystemMonitors);
for (it.First(); it.GetCurrent() != 0; it.Next()) ++n;
DEBUG_STREAM << "[techstat] MechTech id " << subsystem_id
<< " monitors " << n << " subsystems (roster count "
<< entity->GetSubsystemCount() << "), alarmModel "
<< alarmModel << "\n" << std::flush;
}
}
else
{
@@ -462,12 +484,21 @@ Logical
//
//
// @0050e248-era global: the status / MFD message sink == *(DAT_004efc94 + 0x38).
// @0050e248-era global: the status / MFD message sink == *(DAT_004efc94 + 0x38)
// = the RENDERER MANAGER whose renderer chain (+0x10) FUN_004364e4 broadcasts
// Start/StopEntityAlarm messages to (ids 7/8, sizes 0x20/0x1C).
//
// PORT SHAPE (Gitea #47): the port has no renderer-manager broadcast -- the only
// alarm consumer is the gauge renderer (Renderer::Start/StopEntityAlarmMessage-
// Handler just forwards to Start/StopEntityAlarmImplementation, RENDERER.cpp:574/
// 601, both T0) -- so the sink is the gauge renderer and Report* below call the
// implementations directly. Faithful semantics, direct delivery (the same shape
// the port uses for StartEntityEffect).
//
void*
MechTech::StatusMessageSink()
{
return 0; // TODO(cross-family): *(theApplication + 0x38) -- HUD status sink
return (application != 0) ? (void *)application->GetGaugeRenderer() : 0;
}
//
@@ -480,32 +511,49 @@ Time
}
//
// FUN_004366b8 (build "status cleared" message) + FUN_004364e4 (dispatch).
// FUN_004366b8 = Renderer__StopEntityAlarmMessage ctor (id 8, size 0x1C) +
// FUN_004364e4 = the renderer broadcast. Port shape: direct implementation
// call on the gauge renderer (see StatusMessageSink). [Gitea #47]
//
void
MechTech::ReportStatusCleared(
void * /*sink*/,
Mech * /*owner*/,
MechSubsystem * /*sub*/,
int /*type*/
void *sink,
Mech *owner,
MechSubsystem *sub,
int type
)
{
// TODO(cross-family): build + dispatch the cockpit "status cleared" message.
if (getenv("BT_LAMP_LOG"))
DEBUG_STREAM << "[techstat] " << (sub ? sub->GetName() : "?")
<< " condition " << type << " CLEARED\n" << std::flush;
if (sink != 0)
((GaugeRenderer *)sink)->StopEntityAlarmImplementation(
owner, sub, (Enumeration)type);
}
//
// FUN_00436688 (build "status set" message, carries alarmModel) + FUN_004364e4.
// FUN_00436688 = Renderer__StartEntityAlarmMessage ctor (id 7, size 0x20:
// {msg 0xC | EntityID 8 | subsystem | condition | resourceID}) + FUN_004364e4.
// resourceID = MechTech's alarmModel (this+0x100) -- the 'mechalrm' ModelList
// (id 83) whose list resolves to the type-31 GaugeAlarmStream (id 331) inside
// GaugeAlarmManager::Activate's SearchList. [Gitea #47]
//
void
MechTech::ReportStatusSet(
void * /*sink*/,
Mech * /*owner*/,
MechSubsystem * /*sub*/,
int /*type*/,
ResourceDescription::ResourceID /*alarm_model*/
void *sink,
Mech *owner,
MechSubsystem *sub,
int type,
ResourceDescription::ResourceID alarm_model
)
{
// TODO(cross-family): build + dispatch the cockpit "status set" message.
if (getenv("BT_LAMP_LOG"))
DEBUG_STREAM << "[techstat] " << (sub ? sub->GetName() : "?")
<< " condition " << type << " SET (alarmModel " << alarm_model
<< ")\n" << std::flush;
if (sink != 0)
((GaugeRenderer *)sink)->StartEntityAlarmImplementation(
owner, sub, (Enumeration)type, alarm_model);
}
//===========================================================================//
+10
View File
@@ -1713,3 +1713,13 @@ int BTSubsystemDestroyed(Subsystem *sub)
return 0;
return (((PoweredSubsystem *)sub)->GetSimulationState() == 1) ? 1 : 0; // +0x40 == 1
}
//
// Gitea #47 bridge (BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem):
// the Generator family test the alarm lamp mapping needs (@004cc2fc gates its
// placement-lamp special on 0x50fb60 = Generator::ClassDerivations).
//
int BTSubsystemIsGenerator(::Subsystem *sub)
{
return (sub != 0 && sub->IsDerivedFrom(*Generator::GetClassDerivations())) ? 1 : 0;
}
+26 -7
View File
@@ -451,17 +451,36 @@ void
}
//
// @004bbf88 -- slot 12 (GetStatusFlags). Body NOT recovered by the decompiler
// (function prologue present at 0x4bbf88). Overrides MechWeapon's
// PoweredSubsystem::GetStatusFlags (@004b0f48); best-effort: ORs the weapon's
// ammo/jam status bits onto the base flags.
// @004bbf88 -- slot 12 (GetStatusFlags). RAW-DISASSEMBLED 2026-07-25 (#47; the
// body sat in a Ghidra export gap):
//
// call 0x4b0f48 ; flags = PoweredSubsystem::GetStatusFlags
// mov eax,[ebx+0x364] ; weaponAlarm level
// cmp eax,5 / jne / or esi,0x20 ; Jammed -> bit 5 (TechStatus Jammed)
// lea ebx,[ebx+0x43c] / call Resolve ; the linked AmmoBin
// mov eax,[eax+0x18c] / or esi,0x10 ; cookOffArmed -> bit 4 (AmmoBurning)
//
// These are the two bits MechTech's TechnicalAssistance edge-scan turns into
// the Start/StopEntityAlarm reports that flash the ENG-page buttons (the
// shipped 'mechalrm' table: Jammed/AmmoBurning -> gotoEngineering + engEject).
//
LWord
ProjectileWeapon::GetStatusFlags()
{
// BEST-EFFORT -- body unrecovered. Defer to base until a human disassembles
// 0x4bbf88.
return MechWeapon::GetStatusFlags();
LWord flags = PoweredSubsystem::GetStatusFlags(); // call 0x4b0f48 (direct in the raw)
if (weaponAlarm.GetLevel() == JammedState) // +0x364 == 5
{
flags |= 0x20; // Jammed
}
extern int BTAmmoBinCookOffArmed(void *bin); // ammobin.cpp bridge (+0x18C)
void *bin = ammoBinLink.Resolve(); // +0x43C, FUN_00417ab4
if (bin != 0 && BTAmmoBinCookOffArmed(bin) != 0)
{
flags |= 0x10; // AmmoBurning
}
return flags;
}
//
+97
View File
@@ -0,0 +1,97 @@
"""Gitea #46 -- verify the ammo bay fire detonates and applies real damage.
Before this fix, three stacked defects made an armed bay fire permanently inert:
the clock stub (`0 < 0` never fired), the InjectHeat no-op (nothing applied),
and the never-stamped Damage record (0 damage of type 0 even if it had).
Rig: BT_BAYTEST=<frame> sends message 1 (the crit-induced "begin cook-off
countdown", @004bdb94) to the first AmmoBin. The authentic fuse is a FIXED
10.0 seconds (raw disasm @004bd450: 10.0 x ticksPerSecond + 0.5, __ftol --
the old "RandomDelay" was a Ghidra artifact).
PASS =
[ammo] ... BAY FIRE (message): cook-off armed, N rounds, detonation in 10s
...~10 wall seconds later...
[ammo] ... BAY FIRE DETONATION: N rounds x D = total (type T)
ammo explosion damaging <zoneName> (the binary's exact @0050df61 print)
...and the mech takes real zone damage (BT_MP_NET handler line / DAMAGE state).
Kills only the PID it spawns.
"""
import os
import re
import subprocess
import sys
import time
REPO = r"C:\git\bt411"
LOG = os.path.join(REPO, "scratchpad_lampflash.log")
if os.path.exists(LOG):
os.remove(LOG)
env = dict(os.environ)
env.update({
"BT_START_INSIDE": "1",
"BT_DEV_GAUGES": "1",
"BT_BAYTEST": "400", # arm at sim frame 400
"BT_AMMO_LOG": "1",
"BT_MP_NET": "1",
"BT_LAMP_LOG": "1", # [techstat]/[galarm]/[lamp] -- the #47 flash chain # [mp-hdlr] TakeDamageHandler lines (zone + amount)
"BT_DEATH_LOG": "1", # crit cascade / zone destruction
"BT_LOG": LOG,
})
proc = subprocess.Popen(
[os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"],
cwd=os.path.join(REPO, "content"), env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("pid", proc.pid)
try:
deadline = time.time() + 240
armed = False
while time.time() < deadline:
if os.path.exists(LOG):
t = open(LOG, errors="replace").read()
if "BAY FIRE (message)" in t:
armed = True
break
time.sleep(2)
if not armed:
print("FAIL: never armed")
# wait through the 10s fuse + margin for the fan-out and damage
time.sleep(25)
finally:
proc.terminate()
time.sleep(1)
t = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
arm = re.findall(r"^\[ammo\].*BAY FIRE.*armed.*$", t, re.M)
boom = re.findall(r"^\[ammo\].*DETONATION.*$", t, re.M)
zones = re.findall(r"^ammo explosion damaging.*$", t, re.M)
hdlr = re.findall(r"^\[mp-hdlr\] TakeDamageHandler.*$", t, re.M)
dfx = re.findall(r"^\[deathfx\].*$", t, re.M)
ext = re.findall(r"^\[ammo\].*EXTINGUISHED.*$", t, re.M)
print("\n=============== RESULT ===============")
print("armed:", len(arm))
for l in arm[:2]:
print(" ", l[:120])
print("\ndetonation:", len(boom))
for l in boom[:2]:
print(" ", l[:130])
print("\n'ammo explosion damaging' zone prints:", len(zones))
for l in zones[:6]:
print(" ", l[:100])
print("\nTakeDamage handler runs:", len(hdlr))
for l in hdlr[:4]:
print(" ", l[:150])
print("\n[deathfx] zone/crit lines:", len(dfx))
for l in dfx[:8]:
print(" ", l[:120])
if ext:
print("\nextinguished (unexpected in this rig):", ext[:2])
ok = arm and boom and zones and hdlr
print("\nVERDICT:", "PASS -- bay fire armed, detonated on the 10s fuse, and applied real zone damage"
if ok else "FAIL -- see which stage is missing above")
sys.exit(0 if ok else 2)