Field report (night 3): "two ammo bay fires and no death" (Cyd + RajelAran; one
purged, one left burning). Root cause = THREE independent kill-switches stacked
on the same path, all in ammobin.cpp:
1. `GameClock::Now() { return 0; }` -- `cookOffTime < Now()` was `0 < 0`, so
an ARMED bay fire never detonated.
2. `InjectHeat(void*) {}` -- the detonation body was a no-op.
3. The bin's Damage record was never stamped -- 0 damage of type 0 (which the
mech TakeDamage handler drops) even if 1+2 had fired.
THE FUSE (raw disasm, scratchpad/disammo.py): the old "RandomDelay" was a Ghidra
carve artifact -- FUN_004dcd94 is __ftol and the export DROPPED the caller's x87
expression. The real bytes @004bd450: fld 10.0 / fmul [ticksPerSecond] /
fadd 0.5 / __ftol -- a FIXED 10.0-SECOND fuse in clock ticks. New gotcha #19
(reconstruction-gotchas.md) documents the __ftol export blind spot.
THE DAMAGE RECORD: bin+0x1F0..0x21C is a real engine `Damage` (FUN_0041db7c IS
Damage::Damage(), byte-matched to T0 DAMAGE.cpp). The linked ProjectileWeapon's
ctor @004bc3fc stamps it from weapon->damageData @0x3A8 via
owner->roster[0x128][res+0x1C0] -- projweap.cpp's old comment called this "the
bin's HUD display block ... wired in the AmmoBin family"; both halves were wrong
and it was wired nowhere. Now stamped (before MissileLauncher's ctor divides by
missileCount -- a missile bin authentically holds the per-SALVO amount).
THE DETONATION (@004ac274 = MechSubsystem::DistributeCriticalHit -- the old
"HeatableSubsystem::InjectHeat" label was wrong, and the old reconstruction
iterated a stand-in CriticalChain whose First()/Next() returned 0):
statusAlarm pulse Exploding(2)->Destroyed(1) (slot 13 = the printSimulationState
state PRINT @004ac8c0, not an "explosion notify"), own private zone pinned
destroyed, then collect the mech DamageZones whose crit entries plug the bin
(the binary filters plug classID 0x4E = DamageZoneClassID -- VDATA.h idx 78,
cross-checked via idx 28 = AudioStateTrigger), split the amount evenly, and send
the OWNER one full Entity::TakeDamageMessage per zone: inflictingEntity = SELF,
damageZone = the zone index, inflictingSubsystemID = the bin (the message-
manager explosion-bundling key, ENTITY3.h's own NOTE), printing the binary's
exact "ammo explosion damaging <zoneName>" @0050df61.
Port shape: Mech::AmmoExplosionFanOut (mechdmg.cpp) behind a databinding bridge;
guarded deviation: zoneCount==0 warns instead of the binary's unguarded divide.
CriticalChain/CriticalEntry stand-ins DELETED from mechrecon.hpp.
VERIFIED LIVE (BT_BAYTEST hook = message 1, the crit-induced arm channel):
scratchpad/baytest.py : arm -> 10s -> "20 rounds x 35 = 700 (type 2)" ->
"ammo explosion damaging dz_ltorso" -> zone cascade -> mech DESTROYED
(authentic death list).
scratchpad/baypurge.py: arm -> eject-hold purge -> "bay fire EXTINGUISHED
(bin empty)", no detonation.
scratchpad/sim3.py : the HEAT route arms organically in combat (overheated
AFC100), detonates "11 x 25 = 275 (type 1)" split across dz_larm + dz_lgun.
BAYBOOM matchlog record added for MP field forensics.
FIX-OF-THE-FIX (caught by the sim3 regression, would have shipped a crash):
MechSubsystem's ReconDamageZone proxy puts structureLevel at OFFSET 0 -- which
ALIASES THE REAL DamageZone's VTABLE POINTER (the private zone is `new
DamageZone`, mechsub.cpp:154; mechsub.hpp:260 documents the alias). My first
DistributeCriticalHit kept the old body's `damageZone->structureLevel = 1.0f`
and OVERWROTE THE ZONE'S VPTR with 0x3F800000; the respawn sweep's virtual
SetGraphicState (vtable+0xC) then called through it -> AV at 0x3f80000c in
RespawnRepair, one frame after a bay-fire death. ALL EIGHT proxy-view sites in
mechsub.cpp swept to the engine view (((DamageZone*)damageZone)->damageLevel
@0x158) -- including two silently-wrong LIVE readers: GetStatusFlags (vptr as
float -> always "intact") and ApplyDamageAndMeasure (the crit cascade's
measure). Ruled out first by evidence: the weapon->bin stamps were all clean
(six stamps, all classID 0xbcb, logged).
#47 (half 1 -- the FIRE ICON): BallisticWeaponCluster::Execute @004c9a38 reads
bin+0x18C = cookOffArmed into the btefire.pcc TwoState, and while armed computes
(Now - cookOffTime)/ticksPerSecond -- the COOK-OFF COUNTDOWN -- into the numeric
beside it. The old reconstruction misread 0x18C as "the reload state" and
bridged the icon to BTAmmoBinFeeding, so it blinked on every feed and never lit
on a bay fire (RajelAran: "it doesn't"). Now driven by the
BTAmmoBinCookOffArmed/CookOffTime complete-type bridges. [T2 -- the data path
is rig-verified; the pixels await the next live session.]
#47 (half 2 -- the ENG-BUTTON FLASH): fully mapped, deliberately NOT built this
session. The authored data SHIPS (BTL4.RES carries exactly one type-31
GaugeAlarmStream); the chain is alarm SetLevel -> gauge-watcher socket ->
Renderer msg 7 -> GaugeAlarmManager::Activate @00448d00 (T0) -> the BTL4
override @004cc148..@004cc2fc (btl4galm.cpp's provenance note claiming "no
override body exists" is WRONG -- corrected in-file) -> LampManager::FindLamp
@00444c80 -> Lamp::SetAlertState @00444e64 (flash counter) -> the L4 flush
@00474e94 emitting flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239).
Missing: the override bodies, the gauge-watcher sender, the aux-button lamps.
3-piece plan in context/open-questions.md.
Also logged: HandleMessage is vtable slot 8/9 in the binary but NON-virtual
across 10 reconstruction classes (bit the BT_BAYTEST hook; typed call used, gap
documented in open-questions).
KB: combat-damage.md (the full cook-off section), decomp-reference.md (the
cluster addresses + the GaugeAlarm/lamp map + BT_BAYTEST env), gauges-hud.md
(the fire-icon correction), reconstruction-gotchas.md #19 (__ftol),
open-questions.md (2 entries), btl4galm.cpp provenance correction.
checkctx CLEAN. 40 LNK2019 unchanged (the two pre-existing families).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
227 lines
10 KiB
C++
227 lines
10 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). The
|
|
// bodies below remain best-effort scaffolding UNTIL re-transcribed from those
|
|
// addresses -- see context/open-questions.md for the 3-piece plan. 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
|
|
|
|
|
|
//
|
|
// 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.
|
|
//
|
|
enum GaugeAlarmType
|
|
{
|
|
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
|
|
};
|
|
|
|
|
|
//
|
|
// BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem
|
|
//
|
|
// 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).
|
|
//
|
|
// BEST-EFFORT reconstruction (no distinct override body survived in BTL4OPT.EXE).
|
|
//
|
|
Logical
|
|
BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem(
|
|
MemoryStream *mem_stream,
|
|
const char *alarm_name,
|
|
const char *alarm_data
|
|
)
|
|
{
|
|
GaugeAlarmType type = gaugeAlarmUndefined;
|
|
|
|
//
|
|
// 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)
|
|
{
|
|
//
|
|
// 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 ...
|
|
|
|
return True;
|
|
}
|
|
|
|
|
|
//
|
|
// BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem
|
|
//
|
|
// 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.
|
|
//
|
|
// BEST-EFFORT reconstruction (no distinct override body survived in BTL4OPT.EXE).
|
|
//
|
|
void
|
|
BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem(
|
|
GaugeAlarm *alarm,
|
|
Entity *the_entity,
|
|
Subsystem *the_subsystem,
|
|
Enumeration the_condition,
|
|
MemoryStream *mem_stream
|
|
)
|
|
{
|
|
GaugeAlarmType type;
|
|
mem_stream->ReadBytes(&type, sizeof(type)); // (*mem_stream+0x1c)(...,4)
|
|
|
|
//
|
|
// 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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
(void)the_entity;
|
|
(void)the_subsystem;
|
|
(void)the_condition;
|
|
}
|