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>
98 lines
3.4 KiB
Python
98 lines
3.4 KiB
Python
"""Gitea #46 -- verify the ammo bay fire detonates and applies real damage.
|
|
|
|
Before this fix, three stacked defects made an armed bay fire permanently inert:
|
|
the clock stub (`0 < 0` never fired), the InjectHeat no-op (nothing applied),
|
|
and the never-stamped Damage record (0 damage of type 0 even if it had).
|
|
|
|
Rig: BT_BAYTEST=<frame> sends message 1 (the crit-induced "begin cook-off
|
|
countdown", @004bdb94) to the first AmmoBin. The authentic fuse is a FIXED
|
|
10.0 seconds (raw disasm @004bd450: 10.0 x ticksPerSecond + 0.5, __ftol --
|
|
the old "RandomDelay" was a Ghidra artifact).
|
|
|
|
PASS =
|
|
[ammo] ... BAY FIRE (message): cook-off armed, N rounds, detonation in 10s
|
|
...~10 wall seconds later...
|
|
[ammo] ... BAY FIRE DETONATION: N rounds x D = total (type T)
|
|
ammo explosion damaging <zoneName> (the binary's exact @0050df61 print)
|
|
...and the mech takes real zone damage (BT_MP_NET handler line / DAMAGE state).
|
|
|
|
Kills only the PID it spawns.
|
|
"""
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
REPO = r"C:\git\bt411"
|
|
LOG = os.path.join(REPO, "scratchpad_lampflash.log")
|
|
if os.path.exists(LOG):
|
|
os.remove(LOG)
|
|
|
|
env = dict(os.environ)
|
|
env.update({
|
|
"BT_START_INSIDE": "1",
|
|
"BT_DEV_GAUGES": "1",
|
|
"BT_BAYTEST": "400", # arm at sim frame 400
|
|
"BT_AMMO_LOG": "1",
|
|
"BT_MP_NET": "1",
|
|
"BT_LAMP_LOG": "1", # [techstat]/[galarm]/[lamp] -- the #47 flash chain # [mp-hdlr] TakeDamageHandler lines (zone + amount)
|
|
"BT_DEATH_LOG": "1", # crit cascade / zone destruction
|
|
"BT_LOG": LOG,
|
|
})
|
|
proc = subprocess.Popen(
|
|
[os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"],
|
|
cwd=os.path.join(REPO, "content"), env=env,
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
print("pid", proc.pid)
|
|
|
|
try:
|
|
deadline = time.time() + 240
|
|
armed = False
|
|
while time.time() < deadline:
|
|
if os.path.exists(LOG):
|
|
t = open(LOG, errors="replace").read()
|
|
if "BAY FIRE (message)" in t:
|
|
armed = True
|
|
break
|
|
time.sleep(2)
|
|
if not armed:
|
|
print("FAIL: never armed")
|
|
# wait through the 10s fuse + margin for the fan-out and damage
|
|
time.sleep(25)
|
|
finally:
|
|
proc.terminate()
|
|
time.sleep(1)
|
|
|
|
t = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
|
|
arm = re.findall(r"^\[ammo\].*BAY FIRE.*armed.*$", t, re.M)
|
|
boom = re.findall(r"^\[ammo\].*DETONATION.*$", t, re.M)
|
|
zones = re.findall(r"^ammo explosion damaging.*$", t, re.M)
|
|
hdlr = re.findall(r"^\[mp-hdlr\] TakeDamageHandler.*$", t, re.M)
|
|
dfx = re.findall(r"^\[deathfx\].*$", t, re.M)
|
|
ext = re.findall(r"^\[ammo\].*EXTINGUISHED.*$", t, re.M)
|
|
|
|
print("\n=============== RESULT ===============")
|
|
print("armed:", len(arm))
|
|
for l in arm[:2]:
|
|
print(" ", l[:120])
|
|
print("\ndetonation:", len(boom))
|
|
for l in boom[:2]:
|
|
print(" ", l[:130])
|
|
print("\n'ammo explosion damaging' zone prints:", len(zones))
|
|
for l in zones[:6]:
|
|
print(" ", l[:100])
|
|
print("\nTakeDamage handler runs:", len(hdlr))
|
|
for l in hdlr[:4]:
|
|
print(" ", l[:150])
|
|
print("\n[deathfx] zone/crit lines:", len(dfx))
|
|
for l in dfx[:8]:
|
|
print(" ", l[:120])
|
|
if ext:
|
|
print("\nextinguished (unexpected in this rig):", ext[:2])
|
|
|
|
ok = arm and boom and zones and hdlr
|
|
print("\nVERDICT:", "PASS -- bay fire armed, detonated on the 10s fuse, and applied real zone damage"
|
|
if ok else "FAIL -- see which stage is missing above")
|
|
sys.exit(0 if ok else 2)
|