diff --git a/context/decomp-reference.md b/context/decomp-reference.md index d348087..27cddf6 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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. diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 6715beb..fa01f91 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -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, diff --git a/context/open-questions.md b/context/open-questions.md index 285632c..0cc60cf 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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 diff --git a/engine/MUNGA_L4/L4LAMP.cpp b/engine/MUNGA_L4/L4LAMP.cpp index 711a3a0..723289d 100644 --- a/engine/MUNGA_L4/L4LAMP.cpp +++ b/engine/MUNGA_L4/L4LAMP.cpp @@ -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 //-------------------------------------- diff --git a/game/reconstructed/btl4galm.cpp b/game/reconstructed/btl4galm.cpp index 61f2af7..162f7bb 100644 --- a/game/reconstructed/btl4galm.cpp +++ b/game/reconstructed/btl4galm.cpp @@ -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 , parse its parameters out of , -// and serialise (type, parameters) into . Returning False makes -// the base print "Model file '...' has undefined alarm type '' - -// 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 and configure -// 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 + } } diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index 0d3ff86..e4c3679 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -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, diff --git a/game/reconstructed/btl4grnd.cpp b/game/reconstructed/btl4grnd.cpp index eba2878..7370835 100644 --- a/game/reconstructed/btl4grnd.cpp +++ b/game/reconstructed/btl4grnd.cpp @@ -74,6 +74,7 @@ # include #endif #include // GaugeAlarmManager (TearDownForViewpointRestream, Gitea #12) +#include // BTL4GaugeAlarmManager (the #47 override -- constructed in the ctor) #if !defined(L4WARE_HPP) # include #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]. diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 895fe04..301953d 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -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 +} diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 9a60536..8fe3567 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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) --- diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index dadb28a..c9146a7 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -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) diff --git a/game/reconstructed/mechtech.cpp b/game/reconstructed/mechtech.cpp index 3e26336..2ac2f69 100644 --- a/game/reconstructed/mechtech.cpp +++ b/game/reconstructed/mechtech.cpp @@ -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 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); } //===========================================================================// diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index ffa1b97..c70ac73 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -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; +} diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 0b4a4e5..7b54f67 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -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; } // diff --git a/scratchpad/lampflash.py b/scratchpad/lampflash.py new file mode 100644 index 0000000..be76167 --- /dev/null +++ b/scratchpad/lampflash.py @@ -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= 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 (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)