Files
BT411/game/reconstructed/btl4galm.cpp
T
arcattackandClaude Fable 5 6f5a264835 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>
2026-07-25 16:04:34 -05:00

362 lines
15 KiB
C++

//===========================================================================//
// File: btl4galm.cpp //
// Project: BattleTech Brick: Gauge Renderer Manager //
// Contents: BTL4GaugeAlarmManager -- the BattleTech override of the MUNGA //
// GaugeAlarmManager. It supplies the two per-item stream virtuals //
// that the base class declares pure ("not overridden!"): it knows how //
// to compile a named cockpit-gauge alarm definition into a memory //
// stream (CreateGaugeAlarmStreamItem) and how to instantiate a live //
// GaugeAlarm for a given subsystem/condition from that stream //
// (ReadGaugeAlarmStreamItem). //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// 02/22/96 CPB Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1996, Virtual World Entertainment, Inc. All rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED -- IMPORTANT PROVENANCE NOTE
// ------------------------------------------
// ⚠ 2026-07-25 (#47): THE CENTRAL CLAIM BELOW IS WRONG. The BTL4 override
// bodies DO exist in the shipped image, at @004cc148..@004cc2fc -- immediately
// BELOW the @004cc40c boundary this note drew:
// @004cc2fc ReadGaugeAlarmStreamItem: stream-reads {trigger level, lamp
// code}; when the subsystem's alarm level matches, maps the lamp
// (<0x80 fixed via @004cc148; >=0x80 SUBSYSTEM-RELATIVE eng-page
// button via @004cc1a0; Condenser @004cc264 / power-family
// @004cc27c specials), FindLamp @00444c80 on the renderer's lamp
// map (+0x1c0c8), Lamp::SetAlertState(@00444e64, the FLASH
// 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:
// 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
// BTL4_OBJS order). In the shipped optimised image (BTL4OPT.EXE) there is **no
// distinct, separately-emitted BTL4GaugeAlarmManager override body**:
// * the only GaugeAlarmManager code in the binary is the MUNGA base class in
// gaugalrm.cpp (recovered/all/part_006.c, @00448928..@00448f48), including
// the two "not overridden!" trap virtuals
// CreateGaugeAlarmStreamItem @00448ab8 (asserts GAUGALRM.CPP:0x71)
// ReadGaugeAlarmStreamItem @00448ad4 (asserts GAUGALRM.CPP:0x7e)
// and the base driver helpers that CALL those virtuals
// GaugeAlarmManager::CreateGaugeAlarmStream @00448af0
// GaugeAlarmManager::BuildAlarmsForModel @00448d00
// * no subclass vtable (one whose Create/Read slots point into BattleTech
// code) appears in vtables.tsv, and no caller constructs a subclass
// (the base ctors @00448ebd/@00448edc/@00448efb have no external callers).
// The code region the linker reserved for btl4galm.obj (@004cc40c..@004cdac0)
// actually holds a 3-D HUD-model builder (PNAME1..8.bgf), i.e. the compiler
// folded BTL4GaugeAlarmManager's tiny overrides away / it was never instantiated
// in this build configuration.
//
// This file is therefore reconstructed to MATCH THE SURVIVING BTL4GALM.HPP and
// the base driver's calling contract (the argument order, the GaugeAlarm object
// it expects, and the "undefined alarm type ... resource not built" diagnostic
// the base prints when the virtual returns False). Bodies below are BEST-EFFORT
// (low confidence): they describe what the override must do for the base driver
// to function, not bytes lifted from this image.
//
// Base contract (recovered, high confidence):
// GaugeAlarmManager::CreateGaugeAlarmStream @00448af0
// - opens the "gaugeAlarm" sub-resource of a model record (FUN_00404720,
// "gaugeAlarm" @004eecc9), counts its entries, makes a MemoryStream
// (0x18), and for each entry calls the virtual
// this->CreateGaugeAlarmStreamItem(stream, entry_name, entry_data)
// (entry_name @entry+8, entry_data @entry[1]); if it returns 0 it prints
// "Model file '<model>' has undefined alarm type '<name>' -
// resource not built." (strings @004eecd4/@004eece1/@004eecfe)
// and aborts the build. On success it bakes the stream (FUN_00406f3c).
// GaugeAlarmManager::BuildAlarmsForModel @00448d00
// - for each baked alarm record it makes a GaugeAlarm (FUN_004489e8,
// 0x28 bytes; vtable PTR_FUN_004eed7c; ctor stores entity@+0xc,
// subsystem@+0x10, condition@+0x14), adds it to the manager's alarm list
// (this+4, list-insert vtbl+4), then calls the virtual
// this->ReadGaugeAlarmStreamItem(alarm, entity, subsystem, condition,
// stream)
// once per record to configure it.
//
// Engine-internal helper map (from the base + the BattleTech alarm bricks):
// FUN_004489e8 GaugeAlarm ctor FUN_00448a28 GaugeAlarm dtor
// FUN_00448ebd GaugeAlarmManager base ctor (chains a ChainOf<GaugeAlarm*>)
// FUN_0040385c Verify(msg,file,line)
// FUN_004dbb24 DebugStream << (char*) FUN_004db78c DebugStream << (int)
// DAT_00524e20 DebugStream (warning channel)
// MemoryStream write/read = (*stream + 0x20)(stream,&v,4) / (*stream+0x1c)(...)
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(BTL4GALM_HPP)
# include <btl4galm.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
//
// 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.
//
// 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[] =
{
{ "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]
//
// @004cc148 -- the fixed (<0x80) map: coolingLoop1..6 -> the heat-display bank.
//
static int BTFixedLampOf(int code)
{
static const int kFixed[6] = { 0x2F, 0x2E, 0x2D, 0x2C, 0x2B, 0x2A };
return (code >= 0 && code < 6) ? kFixed[code] : -1;
}
//
// @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(
MemoryStream *mem_stream,
const char *alarm_name,
const char *alarm_data
)
{
int condition, code;
if (!MechSubsystem::LookupStatusType(alarm_name, &condition)) // FUN_004ac194
{
return False; // -> "has undefined alarm type ... resource not built"
}
if (!BTLampCodeOf(alarm_data, &code)) // FUN_004cc108
{
return False;
}
mem_stream->WriteBytes(&condition, sizeof(condition));
mem_stream->WriteBytes(&code, sizeof(code));
return True;
}
//
// BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- @004cc2fc [T1]
//
// 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).
//
// (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*/,
Subsystem *the_subsystem,
Enumeration the_condition,
MemoryStream *mem_stream
)
{
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));
if ((int)the_condition != item_condition)
{
return;
}
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
}
}