Files
BT411/game/reconstructed/btl4galm.cpp
T
Joe DiPrimaandClaude Opus 5 0254d9ef34 #98: leaking condensers now flash the RIGHT button -- half of them flashed the wrong one, one flashed nothing
Field reports were all "intermittent": "the display buttons aren't always
flashing or lighting up on leaking components" (Oracle), "I'm getting leaks but
no indicators" (Lynx), "I am getting indicators sometimes" (Sauron).  It is not
intermittent -- it is per-condenser, and three of the six were wrong.

condenserNumber is 1-BASED (verified live: 'Condenser6' reports 6), which the
lamp table's own comment stated.  The guard was `n >= 0 && n < 6` -- a 0-based
bound -- and the table it indexed was missing 0x2C:

    condenser 1..3  -> 0x2F 0x2E 0x2D    correct
    condenser 4     -> 0x2B              WRONG (loop 5's button)
    condenser 5     -> 0x2A              WRONG (loop 6's button)
    condenser 6     -> rejected          NOTHING flashes

So a leak in loop 6 annunciated nowhere, loops 4-5 lit a neighbour's button, and
loops 1-3 were fine -- which from the cockpit reads exactly as "sometimes".

Condenser N drives cooling-loop N, whose six lamps are the same ones the
coolingLoop1..6 codes already resolve through, so the fix routes condensers
through that verified map (BTFixedLampOf(N-1)) and retires the duplicate,
partly-wrong table rather than patching it.

⚠ PROVENANCE: the retired table cited @0051d058 as byte-verified.  That address
holds gauge-type NAME STRINGS, not lamp ids, and a byte search for the six loop
ids as consecutive int32 finds nothing -- so neither the old table nor the new
mapping is byte-verified.  The correction rests on three checkable things: the
1-based numbering (live), the guard contradicting its own documented indexing,
and six condensers mapping onto the six cooling-loop lamps of the verified fixed
map.  [T2 -- behaviour verified, not byte-grounded.]

Also adds the missing diagnostic on the silent path: an alarm item that matched
its condition but resolved no lamp now names the subsystem and why, instead of
returning quietly.  That is what found this, and it immediately surfaced a
SECOND gap for someone to pick up: a destroyed HeatSink (condition 0) resolves
no lamp either, because it is neither Condenser nor Generator nor a
PoweredSubsystem with an aux screen.

Verified (scratchpad/night8/leaklamp.sh, BT_LAMP_LOG):
  before  [galarm] condition 2 ... sub 'Condenser6' -> NO LAMP RESOLVED
  after   [galarm] condition 2 ... -> lamp 0x2a FLASH

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:12:46 -05:00

394 lines
17 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 };
// (the per-condenser lamp table that used to live here is retired: condenser N
// resolves through BTFixedLampOf(N-1), the same six cooling-loop lamps. #98)
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
{
//
// #98 -- "I'm getting leaks but no indicators" / "the display
// buttons aren't ALWAYS flashing on leaking components".
//
// condenserNumber is 1-BASED (verified live: 'Condenser6' reports 6),
// which the old table's own comment said -- but the guard was
// `n >= 0 && n < 6`, a 0-based bound. Condenser 6 was therefore
// REJECTED outright and could never annunciate, and the table it
// indexed was missing 0x2C, so condensers 4 and 5 flashed the NEXT
// loop's button instead of their own. Three of six correct, one
// silent, two lying -- exactly the "sometimes" players reported.
//
// Condenser N drives cooling-loop N, whose six lamps are the same
// ones the coolingLoop1..6 codes resolve to, so use that verified
// map rather than a second, partly-wrong copy of it.
//
int n = BTCondenserNumber(the_subsystem);
if (n >= 1 && n <= 6)
lamp_id = BTFixedLampOf(n - 1); // loop N -> its own button
}
}
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)
{
// DIAGNOSTIC (#98): this is the SILENT path -- the item matched the
// condition but no lamp could be resolved for this subsystem, so nothing
// flashes and nothing was logged. Players see "leaking but no
// indicator". Name the subsystem and why it fell through, so the
// mapping gap is visible in a field log instead of being invisible.
if (BTLampLog())
DEBUG_STREAM << "[galarm] condition " << (int)the_condition
<< " code 0x" << std::hex << lamp_code << std::dec
<< " sub '" << (the_subsystem && the_subsystem->GetName()
? the_subsystem->GetName() : "?")
<< "' -> NO LAMP RESOLVED (condenser=" << BTSubsystemIsCondenser(the_subsystem)
<< " condenserNumber=" << (BTSubsystemIsCondenser(the_subsystem)
? BTCondenserNumber(the_subsystem) : -1)
<< " generator=" << BTSubsystemIsGenerator(the_subsystem)
<< ")" << std::endl << std::flush;
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
}
}