4537cb42e5b2f68ce358859195e4c42d92ef2180
35
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
5ae4410914 |
Gitea #46: ammo bay fire now DETONATES and KILLS (+ #47 fire icon) -- three stacked kill-switches removed, the fuse decoded, and a vptr-alias corruption caught by regression
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>
|
||
|
|
d39227ef39 |
Gitea #51 ROOT CAUSE: LoopAtWill was mapped to "loop forever", arming 224 of 603 samples as endless OpenAL sources
SAURON's "coolant flush sound glitched and perma" is one instance of a systemic
defect. SetupPatch (L4AUDLVL.cpp) decided looping from the SAMPLE flag alone:
AL_LOOPING = (info.loop != ForceStatic)
That armed every non-ForceStatic sample -- 224 of the 603 authored zones,
including unmistakable one-shots -- as an OpenAL source that never ends on its
own. Any such sound whose note-off never arrives plays forever. Measured 1946
LoopAtWill x Transient arming events in one short session.
The sample flag expresses PERMISSION; the SOURCE decides. Three facts [T1]:
* the enum's own comments (L4AUDLVL.h:14-19) -- LoopAtWill = "will play once
OR LOOP AS DESIRED", ForceStatic = "plays only once EVEN IF LOOPED" (a
veto), LoopAlways = "ramp up and then down";
* LoopAtWill is enum value 0, i.e. the DEFAULT an unauthored sample receives
(WTPresets.cpp:37) -- it cannot mean "loop forever";
* LoopAlways, the real always-loop, is used by ZERO shipped samples.
"As desired" is the source's authored AudioRenderType (Transient=one-shot vs
Sustained=held), streamed from AUDIO*.RES (AUDLVL.cpp:28) and already consulted
by the renderer (L4AUDRND.cpp:567, AUDREND.cpp:184). SetupPatch now takes it,
threaded from all three L4AudioSource call sites (Direct/Dynamic3D/Static3D).
New rule: LoopAlways->1, ForceStatic->0, LoopAtWill->the source's render type.
MEASURED before changing behaviour (BT_LOOP_AUDIT=1, scratchpad/loopaudit.py):
LoopAtWill x Transient -> loop=0 (was 1) 1946 events
LoopAtWill x Sustained -> loop=1 unchanged 65 events
ForceStatic x Transient -> loop=0 unchanged 292 events
The engine loops (EngineAccel07_z0..z2, EngineMotor01, EnginePower01) are all
LoopAtWill/Sustained and PRESERVED -- no regression there. All 24 reclassified
samples are genuine one-shots: laser charge/fire/explosion/loaded, missile
loading, engine shift, coolant pressure inc/dec.
A/B PROOF (scratchpad/loopab.py, same session both arms, only BT_LOOP_LEGACY
differs), asking the engine's own BT_AUDIO_DUMP what is still playing with
loop=1 long after every release:
[legacy] EnginePower01, CoolantPresInc03_z2, CoolantPresDcr03_z2
[fixed] EnginePower01
The two stuck coolant sources are the reported symptom. They were eventually
reclaimed when a later trigger reused the source, which is why the bug was
intermittent -- the "perma" case is when nothing else retriggers it. The fix
removes the mechanism.
BT_LOOP_LEGACY=1 restores the old rule for a field A/B without a rebuild. The
layers to listen to are the beam sustains: LaserA/CSustain* are authored
LoopAtWill on a TRANSIENT source, so they now end with the sample instead of
looping until note-off. The render type is T1 authored data; the interpretation
that LoopAtWill defers to it is a strong reading of the enum + defaults rather
than a disassembled statement, and is flagged as such in the KB.
Plausibly bears on #32 (audio cutting in/out late in a match): a stuck looping
source holds its pool slot for the rest of the round.
Rigs: scratchpad/flushsnd.py (flush start/stop pairing), flushsnd2.py (stuck-
source hunt), loopaudit.py (the classification matrix), loopab.py (the A/B).
KB: context/wintesla-port.md audio section, context/decomp-reference.md env
table (BT_LOOP_AUDIT / BT_LOOP_LEGACY / BT_AUDIO_DUMP / BT_AUD_TAIL),
docs/AUDIO_FIDELITY.md F38. checkctx.py CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f3bdb3b85a |
Searchlight + ThermalSight ToggleLamp WIRED (#61) -- and a swapped table attribution ROOT-CAUSED, retracting a false "1995 latent bug"
Both classes' Receiver::MessageHandlerSet were default-constructed blackholes (input-path audit systemic cause #1): entryCount 0, no parent chain, Find() returns NullHandler for every id, Receive drops silently. The new unhandled- message trace printed it on the first press: [btntest] PRESS 0x14 at poll 400 [msg] UNHANDLED: Searchlight has no handler for message id 3 Each class now has a GetMessageHandlers() function-local static chained to PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is void(const Message*) while the decomp shape is Logical(Message&), so a cast would be UB that merely happens to work on x86 __thiscall. DefaultData re-pointed off the dead empty sets. MessageArg now reads the NAMED ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the binary's message+0xC (was a raw offset read -- databinding rule). ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's** ToggleLamp (table @0x51120C), NOT Searchlight's. The two TUs emit parallel shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84 Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT pooled across them -- two copies exist, each emitted immediately before its own class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475). [T1: reference/decomp/section_dump.txt:69661-69711] Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60). RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the EjectAmmo technique) -- and it corrected two things I had inferred wrong: * NO ControlsAllowLights/+0x25C novice gate. Searchlight tests the press alone; that lock is ThermalSight-only. A NOVICE pilot CAN work the lamp. * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY. Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light" claim is RETRACTED. It compared Searchlight's Performance (@004b841c, reads requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different classes -- and so invented a missing 0x1DC->0x1E0 bridge. Every sibling toggles the field its own Performance reads. The searchlight DID light in the arcade, the searchlight->fog swap was NOT inert there, and building PullFogRenderable is FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is" decision is void; the sim needs no repair). Verified live, real click seam (BT_BTNTEST): 0x14 -> [light] requested ON -> reported lightState 0 -> 1 (lamp lights) 0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1 UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing CreateStreamedSubsystem + Entity__SharedData::DefaultData families only). Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp, hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C consumer list, context/{decomp-reference,subsystems,rendering,open-questions, pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md. checkctx.py CLEAN. Still open (documented, not fixed): ThermalSight has no visible IR effect (ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has "LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no identified role and measured 21 live, hinting our SubsystemResource +0x28 differs from the binary's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
33ca99eb69 |
Input-audit cause #1: revive the Avionics power bank + add the unhandled-message trace
THE CLASS OF BUG. A default-constructed Receiver::MessageHandlerSet is a total blackhole: entryCount 0 and NO parent chain, so Find() returns NullHandler for every id and Receiver::Receive dropped the message with no log, no counter, nothing. docs/INPUT_PATH_AUDIT.md ranked this systemic cause #1; running its own lint finds SEVEN such sets (Sensor, Searchlight, ThermalSight, Gyroscope, MechTech, SubsystemMessageManager, Torso). It is also why generators cannot be switched off (#53) -- same shape. FIX 1 -- Sensor / Avionics (the big one). The streamed mappings aim 108 records across the 18 mechs at it: the MFD2 ENG1 power bank, 6 clickable buttons plus keys F5-F9 (route Avionics power to generators A-D, gen mode, coolant cut). All silently swallowed. Sensor needed NO handlers of its own -- ids 4-8 already live on PoweredSubsystem, which chains HeatSink for id 3, covering the whole 3-8 range those buttons send. It only ever needed to CHAIN, which is exactly why the byte-identical bank on ENG2 works (Myomers chains, myomers.cpp:88-100; Sensor did not). Added Sensor::GetMessageHandlers() with the same function-local-static idiom and pointed DefaultData at it instead of the empty set. VERIFIED LIVE (scratchpad/avionics.py -- pages MFD2 and presses the bank): [gensel] Avionics -> GeneratorA (tapped) ... B, C, D [gensel] Avionics mode -> 2 i.e. Avionics now behaves exactly like Myomers. 24 handler hits, 0 unhandled. FIX 2 -- the standing guard. Receiver::Receive now logs once per (class, message id) pair when no handler is found: '[msg] UNHANDLED: <class> has no handler for message id N -- silently dropped (is its MessageHandlerSet chained?)'. Once-per-pair on purpose, since some ids broadcast every frame. This one line would have printed Avionics, Searchlight, ThermalSight, crouch and all four generator buttons on the first boot instead of costing us months of player reports. Uses SharedData::derivedClasses -> Derivation::className. DEFERRED, with reasons (not guessed at): Searchlight's ToggleLamp body exists but is unreachable -- it needs a correctly-typed forwarder (Receiver::Handler is void(const Message*); ToggleLamp is Logical(Message&), so a cast would be UB that happens to work on x86) AND an id check, because its id 3 collides with HeatSink's ToggleCooling and I have not traced whether PowerWatcher's chain reaches HeatSink. #53 needs more than chaining: ToggleGeneratorOnOff @004b1ed0 has no body at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
08977ff128 |
Gitea #55: respawn now restores COOLANT / heat / generator state (+ a Generator reset re-transcribed from the binary)
THE GAP: Mech::Reset sweeps every subsystem with the virtual DeathReset (mech4.cpp:1789), but only 7 weapon/ammo classes overrode it -- the entire heat/coolant/power family fell through to the empty Subsystem::DeathReset base (SUBSYSTM.h:161), so the respawn reset was a SILENT NO-OP for them. All the ResetToInitialState bodies already existed; nothing ever called them. Hence the field report: 'respawned with drained coolant'. Added DeathReset forwarders (ResetToInitialState is not virtual, so each class with its own body needs one): HeatableSubsystem, HeatSink, Condenser, PoweredSubsystem, Generator. HeatSink's also serves Reservoir -- the coolant tank -- which has no body of its own; that is the one that refills coolant. TWO MIS-TRANSCRIPTIONS FIXED, both verified against the binary with capstone: 1. HeatSink::ResetToInitialState chained HeatableSubsystem::ResetToInitialState with the comment 'FUN_004ac22c'. But @004ac22c is Subsystem's terminus -- disassembled: it reads the damage zone at this+0xe0, clears zone+0x158, and Set_Alarm_Levels zone+0x10 and this+0x2c to 0. And the binary's HeatSink @004ad760 calls only @004ad884 (ClearHeatFilter), @004ad7f0 (UpdateHeatLoad) and @004ac22c -- never HeatableSubsystem. Worse, HeatableSubsystem's body sets currentTemperature = 300.0f, CLOBBERING the startingTemperature that HeatSink assigned four lines earlier. Dropped the call; the terminus work is already done for every subsystem by MechSubsystem::RespawnRepair (mech4.cpp:1790). 2. Generator::ResetToInitialState chained HeatableSubsystem and set outputVoltage = 0 -- which matches GNRATOR.TCP, but BT's BINARY DIVERGES from the TCP source, and the binary is what shipped. Disassembled @004b215c instruction by instruction: it chains HeatSink (so the coolant refill DOES run for a generator), then generatorOn=1, startTimer=0, stateAlarm 0 then 2, **outputVoltage = ratedVoltage**, coolantAvailable=1, coolantFlowScale=1.0, startTimer=startTime. Every offset matches our declared members exactly. The old version brought generators back from a respawn at ZERO output voltage, so energy weapons could never charge -- no recharge ring, no ready dot, nothing damaged. That is a strong candidate for #54 (David's three dead lasers). LIVE VERIFICATION (sim3.py, 3 mechs + force damage): 4 death/respawn cycles, and every heat sink logged coolant == capacity on every respawn, with temp restored to startingTemperature (77) rather than the clobbered 300. Left as a silent regression guard that only speaks if a respawn leaves coolant short. KNOWN REMAINING [T3, noted in code]: Condenser's body chains HeatableSubsystem rather than HeatSink, so a coolant loop's VALVE DETENT may still persist across a respawn -- the binary's Condenser reset is not yet decompiled. Do not claim valves are fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
2518e43719 |
Gitea #59: the first death permanently disabled ExecuteWatchers -- simulationFlags |= 0x1 should be ForceUpdate()
Verified the claim in the binary myself with capstone before changing behaviour: 0x4c0155: or word ptr [ebx + 0x18], 1 +0x18 is Simulation::updateModel, a **Word** -- and the 16-bit 'or word ptr' settles it, since simulationFlags is an LWord at +0x28 and would assemble as 'or dword ptr'. So the authentic op is updateModel |= DefaultUpdateModelFlag, which is exactly Simulation::ForceUpdate() (SIMULATE.h:146-147). The old transcription wrote simulationFlags |= 0x1 instead. Bit 0 there is DelayWatchersFlag (SIMULATE.h:170); Simulation::Simulate then skips ExecuteWatchers() forever (SIMULATE.cpp:461), and NOTHING clears it -- the only ClearWatcherDelay() in the tree is in the encore path (UPDATE.cpp:215), which a Player never takes. So every pilot's first death permanently disabled watcher execution on their simulation, and the replication mark the binary intended was never set at all. context/reconstruction-gotchas.md had flagged this exact line as the un-audited sibling of the #12 dirty-bit class, asking for the disasm first; that is now on the record. LIVE VERIFICATION (scratchpad/sim3.py, 3 mechs + BT_MP_FORCE_DMG, 180s): 4 consecutive death/respawn cycles on pod3, every one completing -- death cycle START (death #N) -> RESET at drop zone [COMPLETE] with 'player watchersDelayed=0' after every death, and no crash. Also re-confirms the #57 latch fix under repeated deaths (the interleaved SWALLOWED lines are the authentic dedup of a second notification, each followed by a completed RESET). Leaves a silent regression guard: the death path now logs only if it ever finds the watcher-delay flag set again. Rigs: sim3.py (3-mech combat + force damage), destest.py (2-node designation proof: 18 designations, deathPending clean on all of them). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
2dd4ee3910 |
ROOT CAUSE: target designation wrote a Mech* into deathPending, disabling respawn (#57/#48)
MEASURED our compiled BTPlayer layout (new one-shot [layout] ctor diagnostic): sizeof=652 (0x28c) killCount@0x270 pad_0x280@0x274 objectiveMech@0x278 deathPending@0x284 The three target-designation sites wrote RAW at pilotArray[0]+0x284 intending the BINARY's objectiveMech. On our object 0x284 is deathPending -- the death-cycle latch. So EVERY target designation (the Comm bank 0x30-0x37, or the typed t/y/u/i/o keys) stored a Mech pointer into deathPending, and a non-zero deathPending makes VehicleDeadMessageHandler take its dedup early-return, which skips the warp, ++deathCount, the PLAYER_DEAD record AND the drop-zone hunt. => designating a target silently disabled your respawn for the rest of the process. That is #57's missing first cause: it explains David's very first death being swallowed with no prior death, and the 3-of-3 swallowed deaths in the final round. It also explains the operator's persistent observation that the trouble began when the comms panel went live -- before the pilot list populated, there was nothing to designate. And the designation never worked either: the value never reached objectiveMech (input-audit finding #1). FIX: all three sites now use named-member bridges in the complete-BTPlayer TU -- BTPilotSetObjectiveMech / BTPilotObjectiveMech / BTPilotVehicle / BTPilotPosition / BTVehicleDestroyed. Also retires the raw +0x1fc (playerVehicle) reads and the raw +0x100 position reads in ChooseNearestPilot, and routes its destroyed-check through the real Mech predicate instead of the Is_Destroyed stub. LAYOUT LOCKS: static_asserts on objectiveMech@0x278, deathPending@0x284, killCount@0x270 and sizeof==0x28c, in the friend fn BTPlayerLayoutSelfCheck, so a member shift fails the BUILD instead of silently clobbering the latch again. Rigs: scratchpad/commstest.py (drives designation + captures panels), valvetest.py, lampgallery.py (428-bitmap gallery for #48 visual search). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
81fbfc7eaa |
Add scratchpad/cursoraudit.py: static audit for the #42 stale-cursor draw family
Scans the gauge TUs for cursor-relative draws with no MoveToAbsolute in scope. Found 6 of 29, three of them STATE-CHANGE-ONLY draws -- notably L4GraphicLamp::NotifyOfStateChange (L4LAMP.cpp:376/380), which blits a lamp using whatever cursor the view happens to hold. That is the '#48 misplaced lamp' class: lamp-shaped, on the panels that have lamps, only on damage/jam/heat state changes, and persistent. Explains why the 5-pilot rig capture was clean (no combat = no lamp state change). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
b517c4cb30 |
Input-path audit: 206 paths traced, 22 dead -- plan doc only, no code changes
13-agent read-only audit (6 tracers -> adversarial verifiers -> triage), validated against 2,294 streamed control-mapping records decoded from all 18 mechs in BTL4.RES plus the live 121-record install dump. Verdict: 22 dead dispatch routes, 11 player-visible, 8 documentation defects, 2 entirely dormant input layers (CONTROLS.MAP and the whole DirectInput grammar). RETRACTS MY OWN DIAGNOSIS from the reverse-thrust fix earlier today: streamed BUTTON mappings ARE consumed (L4CTRL.cpp:2254-2262 / :2327-2336, and ctrlmap.log:32 shows reverse's record resolving to +0xec). The AddOrErase Fail bodies are the config-session base traps, overridden by MechRIOMapper. So 'reconstruct AddOrErase' would clear 0 of 2,294 records. The false claim is in pod-hardware.md:121-135 AND in mechmppr.cpp:894-919 -- both owed a sweep, and reverseThrust now has two writers (unknown #2 decides which to keep). The 'how did it break' question is therefore reopened; leading candidate is the unhandled WM_SYSKEYDOWN/SC_KEYMENU menu-modal loop on a held ALT. Biggest new finding: a default-constructed Receiver::MessageHandlerSet is a silent blackhole -- Sensor/Avionics alone kills 108 records (6 cockpit buttons + F5-F9). One accessor in sensor.cpp is the best findings-cleared/risk ratio. Worse-than-dead: target designation writes raw +0x284 on a modern-layout object, possibly out of bounds, reachable by pressing 't' -- flagged as a playtest safety note. Also lands scratchpad/keysweep.py: the empirical counterpart (presses every bound key, records what actually moves). Not yet run -- needs the machine idle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
23229e573e |
Reverse thrust was dead on the desktop (user report): wire the 0x3F throttle-handle button
LALT -> button 0x3F was bound in both binding layers and PadRIO pushed the RIO ButtonPressed event correctly, but nothing ever set the mapper's ReverseThrust: - the authentic route is BTL4.RES 'L4' streamed record [2] (Button Throttle1 0x3F -> attr 6 ReverseThrust@0x124), but our streamed BUTTON mappings are not consumed at all -- MechControlsMapper::AddOrErase is still the unreconstructed Fail stub, so the event never reaches the attribute; - the ONLY writer of reverseThrust was the keyboard bridge's 'key_throttle < 0' test, which is bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the desktop) AND is unreachable regardless, because L4PADRIO clamps the Throttle channel to [0,1]. Net: Alt did nothing and the flag was re-zeroed every frame. FIX: publish the 0x3F hold state from PadRIO::EmitButton -- the one chokepoint every desktop source funnels through (keyboard, gamepad, DirectInput joystick, glass-panel clicks via SetScreenButton) -- and apply it in InterpretControls gated on BTPadRIOActive() so real pod hardware is untouched (there the streamed mapping owns the flag). Marked [T3]; delete once streamed button mappings land. Reverse is a HOLD (manual: hold the red throttle button, release = forward). Verified with scratchpad/revtest.py (keybd_event injection, BT_KEY_NOFOCUS): forward = rev=0 dmd +61.501; LALT held = rev=1 dmd -61.501; release edge logged. KB: pod-hardware.md now records the streamed-button-mapping gap + this seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
5df5c954ae |
Operator: manual launch gate + restart-session flow
The operator now holds the lobby: with 'Manual launch' checked (default), the relay stops at 'all pods have the egg; WAITING FOR OPERATOR LAUNCH' instead of auto-firing on a 20s timer -- the app shows 'ALL PODS READY -- press LAUNCH MISSION' with the roster lights, and the big green button fires the mission when the operator is ready (everyone seated, voice chat confirmed). A 'Restart session' button cycles the same mission/roster for the next round (players just re-run their join script). - btconsole.py --relay --manual-launch: launch armed by the line 'launch' on stdin (a daemon reader thread; the app's Launch button writes it via QProcess). The settle window is still honoured relative to egg delivery (max(now, eggs_done + 20s)) -- firing RunMission before the pods reach WaitingForLaunch Fail()s them, the same hazard the auto timer guards. Auto mode (no flag) unchanged -- CLI/test recipes unaffected. - btoperator.py: Manual-launch checkbox (Network box, default on), LAUNCH MISSION button (enabled by the relay's READY line, disabled after fire), Restart session button, status headline shows the ready state. - operator_e2e.py: now exercises the gate -- waits for READY, asserts the button enabled, presses it, verifies the operator-gated LAUNCH. Verified: scripted e2e PASS -- registered -> READY (held) -> operator launch -> settle honoured -> both pods LAUNCHED (operator-gated=True). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43fa53d542 |
Operator console app: PySide6 station + validated egg model (the lost 1995 console, recreated)
The operator station the archive never preserved: build missions with authoritative dropdowns, run the console/relay, watch pods arrive, launch -- LAN mesh or internet relay -- plus one-click local instances for testing. tools/eggmodel.py (headless, importable, self-tested 14/14): - Order-preserving egg parse/emit: the editor manages [mission]/[pilots]/ per-pilot pages; bitmap rasters, ordinals and role pages survive untouched. - EVERY value set read LIVE from content/BTL4.RES (the resscan parser, importable): maps = MakeMessageStream(14) INTERSECT ExistanceBoxStream(26) (the game aborts unless both exist); colors/badges/patches from the VehicleTable resource (type 25, NUL-line NotationFile text with ;-comments); vehicles = ModelList names, known-good mechs (bhk1, madcat) surfaced first. - validate(): the full required-key ruleset from MISSION.cpp/btl4mssn.cpp (missing map/time/weather/scenario, per-pilot hostType/vehicle/dropzone/ color/patch/badge/experience/role, role page model=, illegal values, duplicate addresses) -- kills the hand-edited-egg crash class, incl. the color=Red-vs-Crimson gotcha that bit the dev eggs. tools/btoperator.py (PySide6, dark Fusion theme): - Mission form + pilot roster table (all dropdowns from eggmodel), relay-tag auto-numbering, add/remove pilots, egg New/Open/Save/Validate. - Mode switch: Relay (internet; runs btconsole --relay) / Mesh (LAN legacy; dial-out console at address-port minus 1 per the +1 rule). - Session panel: QProcess drives btconsole.py; stdout parsed live into per-pilot state lights (waiting/egg/registered/LAUNCHED) + relay stats. - Local launcher: spawns btl4.exe per roster row with BT_RELAY/BT_SELF (or mesh -net port), BT_DEV_GAUGES/BT_START_INSIDE toggles. - Export player scripts: per-pilot join_as_playerN.bat with the public relay hostname (a remote player's whole setup = run one .bat). - Wire/protocol code stays in btconsole.py/eggmodel.py -- the GUI is only UI + process management; CLI + test harnesses share the same tested core. Verified: eggmodel self-test 14/14; scripted end-to-end (scratchpad/operator_e2e.py) drives the real window through its own methods -- start relay session -> launch 2 local instances -> monitor observes the mission LAUNCH (user-confirmed live on screen: both pods in-mission). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bf8c2ffaae |
D1 phase 1: relay TCP core in btconsole.py (--relay mode)
The console emulator gains a relay/dispatcher mode for internet play (D1):
pods dial OUT to this process (NAT-friendly) instead of the console dialing
into each pod. Legacy dial-out mode is untouched (verified: an unmodified
2-node mesh session still forms end-to-end through it).
--relay <consolePort> <egg> [--bind ADDR] [--udp-drop PCT], single
selectors-based event loop:
- console listener (consolePort): streams the NUL-line egg to each pod on
connect; GLOBAL launch timer -- RunMission x2 to ALL pods at +20s/+4s after
the LAST pod has its egg (fixes the per-pod skew of the legacy tool).
- game TCP listener (consolePort+1): envelope router {route i32, length u32}.
First frame must be HELLO ('BTR1' + hostID validated against the egg
[pilots] roster); PEER_UP/PEER_DOWN exchange; route>=2 unicast (route
rewritten to sender), -1 broadcast-except-sender (the client sends each
broadcast ONCE -- kills the N-1x upload duplication). Frame cap 1600
(NETWORKMANAGER_BUFFER_SIZE); protocol violations drop the connection.
- game UDP socket (consolePort+1/udp): endpoint learned per-datagram
(NAT-rebind tolerant), HELLO->HELLO-ACK, verbatim forward by route,
TCP-wrap fallback when the target's UDP endpoint is unknown; --udp-drop
test hook for the robustness phase.
Verified: scratchpad/relay_stub_test.py (self-contained; spawns the relay +
stub clients) -- 24/24 checks pass: egg delivery, registration, PEER events,
unicast route-rewrite, broadcast fan-out (sender excluded), UDP ack/forward/
fallback, and all reject paths (bad magic, dup id, out-of-roster, oversize).
Plan: ~/.claude/plans/partitioned-snuggling-piglet.md (D1 relay + UDP).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bc4d6e5597 |
MP: console-death freeze NOT REPRODUCIBLE + fix the latent game-listener-close bug (task #50)
Investigated the "console death froze peer replication" open question (2026-07-14)
with a direct 2-node repro (BT_REPL_LOG on a circling peer, affinity-pinned):
- Clean relay kill -> replicant kept circling smoothly through the kill, NO
disconnect logged, node survived. Replication did NOT freeze.
- Deliberately STUCK console (scratchpad/btconsole_stuck.py: connect, start
mission, then stop recv() while holding the socket OPEN = the exact
"receive pad full -> close never seen" mode hypothesized) -> replicant kept
moving the whole run. Replication did NOT freeze.
Conclusion: peer replication is INDEPENDENT of the console (pods replicate
peer-to-peer over the GAME socket; the console is a separate egg/mission/status
channel). The original freeze was a transient -- plausibly the same single-box
packet-jitter/CPU-contention artifact root-caused in
|
||
|
|
fcaea1862a |
Diag: BT_AF_PERIOD=<sec> autofire cadence throttle + afc_dump.py (raw RES verify)
Closes the coolant/jam investigation. BT_AF_PERIOD pulses the trigger every <sec>s so a slower-than-max fire rate can be tested; afc_dump.py reads the AFC100 record raw from BTL4.RES. Both confirm the QA report is FAITHFULLY reproduced (see KB). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
065c114590 |
Impact-FX FORENSICS wave: the i860 specialfx engine, per-round detonations, the ram economy closed
The "we're off the rails" reinvestigation -- a 5-thread evidence workflow
(logs / PFX data / RES model lists / decomp / port audit) + the i860 firmware
decode, then surgical fixes. Every claim tiered; the wrong turns are on the
record in the KB alongside the corrections.
THE OLD-STYLE SPECIALFX ENGINE [T1, firmware-decoded from VREND.MNG]:
rebuilt scratchpad/i860dis.py (binutils opcode table); mapped the dispatch
(data+0xdd0c; sfx trigger 0xf040cda0, install 0xf040cdc0, step ~0xf0413698,
instance init 0xf04128d8); decoded the heat model EXACTLY: per 30Hz board
frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch - 0.25) + 0.25 (K=0.25 =
the ember floor @VA 0xF080; kill at h <= 1e-4 @0xF0A0); alpha fades cool_b
x RAW dt (PER-SECOND -- the x30 scaling made laser hits invisible); 7s cap;
y_off = the kill plane; "variance" is DEAD DATA (binary reads "variance",
INI authors "varience"). The 13 descriptors (PPCHit/LaserHit/MissileHit/
Chunks/Sparks/Fireball...) parse from BTDPL.INI and render via the BTPfx
layer -- heat bursts draw the FIERY sheet (brightness over fire), .PFX keeps
GRAYSCALE (authored colours: DNBOOM orange, DDAM gray).
HIT-PACKAGE CENSUS [T1, RES byte-verified]: ppchit=[8] lzrhit=[9] mghit=[7,11]
canhit=[11] mslhit=[10,12,1023] explode=[6]; all 8 mech death lists identical.
Effect routing corrected in BOTH consumers: <100 = specialfx, >=1000 = psfx.
"SILVER MIST x5" ROOT CAUSE: SHKWAVE.PFX (mslhit's 1023) authors maxIssue=5
relPeriod=0.2 rate=1 -- the ONE file where rate contradicts the window; the
emitter trusted rate -> 5 shells at exactly 1Hz. Emission rate now always
maxIssue/releasePeriod.
PER-ROUND DETONATIONS [T1 @004bef78]: every missile round spawns its own
ExplosionModelFile at ITS impact (hull + terrain) -- a volley ripples 12
fireballs like the demos; rack-tube launch spread [T3] (GUIDED rounds only --
the slot-0 deflection sent every AFC100 shell 3.4deg left/2.5deg down: the
phantom "4th gold beam"); replicant salvos detonate too (launcher index rides
the visual push). Missile damage bundles through the shooter's messmgr with
the launcher's subsystemID (mslhit fires at the consolidated point; the
binary's dedup CONFIRMED [T1 @0049b784] -- a workflow agent's per-record
claim REFUTED by direct decomp read).
DAMAGE-BAND SEMANTICS [T1]: MechDeathHandler fires the CURRENT band effect on
any damage rise (the binary's changed-flag coalescing) -- a mauled mech under
fire smokes/burns per hit; bands 3/4 are authored fire plumes.
THE RAM ECONOMY -- CLOSED (3 layers, measured live):
1. armor = POINTS (every zone: damageScale = 1/armorPoints, armor 50-140;
the [zone-armor] BT_DMG_LOG dump);
2. StaticBounce prices rams with authored moverMass ~1.3e6 -> ~59,000 pts
@10m/s; the binary dispatches it RAW but pod MP dropped it on the local
replicant (MECH.CPP:986 warns) -- ram damage was NETWORK-INERT; our
task-#47 replicant forwarding surfaced it as a one-shot. Port
normalizes x1e-3 to the point economy [T3];
3. contact EDGE (ramLastVictim/ramContactLinger): the binary's bounce made
separation implicit; our gait-derived velocity re-priced full rams at
60Hz (the respawn explode-loop). One bump = one hit; pressed = BLOCK.
PLUS: sfx size x0.5 + 1.25m visibility floor + hot-phase occlusion [T3];
particle pool 2048->8192 (missile traffic starved laser bursts -- the
"intermittent effects"); replicant beam aging (a lost beam-END record pinned
a stale beam on forever); UV-variant noise stamps (mask-safe mirror/swap);
no scroll on particle stamps (material-path only); BT_FX_TEST/fxshot.py
self-verification harness ([beam-draw]/[zone-armor]/[collide-tx] telemetry).
KB: rendering.md (specialfx engine + census + closeout), combat-damage.md
(ram economy + band semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
48c9c8444f |
Fire VISUALS wave: the authored firesmoke sheet, vertex-alpha effect cards, the case-4 wreck dressing
The "fireballs like the demo vids" arc, decomp/content-grounded end to end,
plus the live-play UX batch verified over the same sessions:
FIRESMOKE SHEET (the PFX fireball fix): every firesmokeN_scr_tex in BTFX.VMF
maps the SAME 64x64 tileable noise image bintA (variants differ only in SCROLL
rate) and firesmoke1_mtl colours it through the "fiery" ramp (0.3,0.1,0.1)->
(0.9,0.7,0.3). The particle layer now bakes ramp(lum(bintA)) as its sprite
colour (noise detail in alpha) and SCROLLS it at firesmoke1's authored rate
via a texture-transform; the port's radial soft-edge mask moved to a second
CLAMPed stage so the WRAPPED scroll rolls flame through the sprite without
scrolling the edge away. Old grit x radial bake kept as the no-BINTA fallback.
Impact hits, damage-band smoke and death booms all ride this layer.
AUTHORED TEXTURE SCROLL in the model path: the BMF TEXTURE records carry
SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037); the draw path always supported
per-op scrolling (SetTextureScrolling) but the BGF loader never parsed it, so
every scrolling material rendered frozen. Wired TexRef -> MatInfo -> batch ->
L4TEXOP.doScroll: the flame cards (flamebig/fire5) now roll fire noise.
VERTEX-ALPHA EFFECT CARDS (the "twisted drill bit of fire" fix): FLAMEBIG's
verts carry authored float RGBA -- white-hot base (1.0,0.99,0.97) -> dark-red
tip fading to alpha -0.2 (the DPL clamp convention). The loader kept a flat
batch colour and drew it OPAQUE = a solid orange spike. Corpus sweep: exactly
14 shipped BGFs carry vertex alpha, ALL effect cards (flames, MUZFLASH,
EXDISK_A/B/C, TMST_A/B/C, beam models, DECLOUDS). Such batches now keep the
authored per-vertex gradient and route to the alpha-blend pass, unlit,
colour = texture x gradient, alpha = the vertex fade; sky objects excluded
(drawAsSky + alphaTest passes NEITHER pass filter -- DECLOUDS stays in the
sky pass). MUZFLASH/EXDISK render correctly for free when the muzzle-model
work lands.
WRECK DRESSING (the 1996 ExplosionScripts case-4 transcription): pieces spawn
HIDDEN and reveal 0.25s after the boom (the InstanceSwitch delay, behind the
dnboom flash); flamebig hangs over the pile, Y-BILLBOARDED at the camera
(SetOffsetYaw + a camera-pos getter -- the dpl_SetDCSReorientAxes analog);
the MakeDCSFall settle arms at the reveal with the two authored rates (hulk/
debris -0.025 t^2, fires -0.01 t^2 -- the flames ride above the sinking pile
and die with it at burial). EMPTY-PLACEHOLDER hulk guard: THRDBR.BGF is a
153-byte zero-geometry stub that "loads fine" -- vertex-count check now routes
it to the gendbr fallback (a Thor wreck was invisible). Hulk content census
recorded: AVADBR==MADDBR==VULDBR geometry (palette-only prefix diffs),
RAPDBR==SNDDBR==STIDBR byte-identical -- wreck variety is materials + the
dressing, not unique piles.
LIVE-PLAY BATCH: muzzle resolve uses the named segmentIndex (raw +0xdc read
was layout garbage); forward launch frame (authored MuzzleVelocity +Z vs the
mech's -Z facing); dock-bottom single window (gauge strip appended below the
world viewport, 1100x600 default, BT_DEV_GAUGES_WINDOW=1 restores the separate
window); portrait sec surface unrotated CW; ammo counters live via typed
bridges (BTAmmoBinCountPtr/BTAmmoBinFeeding/BTWeaponAmmoBin -- raw bin+0x180
and a hand-rolled link walk were garbage); fourth fire key ('4' = Pinky);
panel/arc probes de-aliased (%61 prime).
KB: rendering.md (vertex-alpha card family + scroll), combat-damage.md (hulk
census + THRDBR stub), gauges-hud.md (ammo bridges).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4ed2bbc293 |
Heat: authentic 1e7-unit heat + the ambient radiator land (task #9)
- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2 (PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat adds (raw -- resources author pre-scaled 1e7 units). - The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) + the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the inversion blocked condenser->bank links and closed the system). - Constant corrections (all byte-verified float80s): coolant epsilons 0.0025/0.003/1e-4 (was a single 1e-4 serving three sites); CoolantCapacityScale 0.05 (was 1.738). - Verified: heat flows + exits; max-rate autofire overheats weapons into the authentic heatLoad range-cutout (thermal spam unsustainable by design). - KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1; authored map says PPC->C4/C6) -- the heat-stream offset audit, filed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd055281a8 |
Combat: THE AUTHENTIC DAMAGE ECONOMY -- authored per-weapon amounts through the real fire chain (task #8)
Three root causes, all fixed: 1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2 (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35). The observed "0.25" was the degenerate EC=1 fallback, never authored data. 2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the release value (-65) is a negative NaN. The binary's x87 unordered compare read it as "released"; IEEE-correct float compares latched the edge detector shut after the first release -- the reason the emitter discharge chain NEVER fired in-game. Fixed with bit-pattern sign compares. 3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData (amount + damageForce=target-muzzle [the gyro directional-bounce feed] + impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) -> messmgr consolidation with per-weapon records + explosion bundling. The mech4 bring-up damage block + flat kShotDamage retired to diag hooks. Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread). Zone model verified byte-exact (no change). Solo end-to-end: 5-record volleys (2 PPC + 3 ERML with authored amounts), per-weapon zone granularity, explosions, kill. Heat stays bring-up scale pending the heat-calibration audit [T3]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8ed6184d65 |
Combat: AUTHENTIC weapon groups -- streamed per-mech button bindings + the real fire chain (task #5)
The recovered system: fire channels = LBE4ControlsManager buttonGroups (0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port already called -- it needed only the TriggerState attribute (id 0x13 PINNED to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an input feed. Keyboard/harness now push press/release edges into the button groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger = 4 weapons). Myomers @4b9550/@4b95b8 misattribution corrected (they are MechWeapon ConfigureMappables/ChooseButton). Verified 2-node: kill through the authentic chain (12 hits vs ~36 pre-groups). Config-mode session (regrouping UI) = the remaining stage, KB-scoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a652ddcdbc |
MP: authentic update-record deadbands streamed from the model record (task #3)
Binary ctor @0x4a26a7-0x4a26ef: mech+0x768/76c/770 <- raw record words 0x26/0x27/0x28 (UpdatePositionDiffrence / UpdateTurnVelocityDiffrence / UpdateTurnDegreeDiffrence x pi/180 [const @0x4a2d44]). Read at RAW record offsets -- the Mech__ModelResource struct is layout-skewed (field98 gave -1.5 where the record holds 0.3; audit filed as task #4, also explains the old forwardCycleRate "floor 25" hack). Sender triggers now use the authored constants: pos-deadband type 0, quat-Y delta + yaw-rate delta + stopped-turn edge type 4 (binary @0x4aad35/@0x4aac2b/@0x4aac6c). Verified 2-node: madcat streams 0.3/0.045/3deg, type-4 spam gone, walking replicant clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1ace2e3cb4 |
Gyro: BT_GYRO_TRACE per-frame integrator trace -- oscillation verified vs authored constants (task #56)
25-hit MP autofire trace: kick -> damped oscillation -> settle, peaks well inside clamps, no NaN/drift; measured Y:X frequency ratio 7.75x matches sqrt(springK.y/springK.x) exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48b17750e5 |
HUD reticle + weapon pips LIVE: dpl2d 2D display-list port (task #35)
- dpl2d API fully recovered from the binary recorders (@487f34-488630): opcode model (points/lines/polyline/circle/color/width/matrix/push-pop), CallList = INLINE include (state persists to caller), centered coordinate frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework. - BTReticleRenderable ctor @004cc40c transcribed with the authentic calibration (originX .35, originY .25, scaleY .5, 0..1200m right range ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings, turn arrows); range caret slides from the live target range fed by the mech4 targeting step (BTSetHudTargetRange). - Weapon pips: the binary gate is IsDerivedFrom(0x511830 = MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL 7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A (lit, authored PipColor) on TargetWithinRange, else dark ring B. - AddWeapon @004cdac0 store map corrected to the verified order (part_014.c:4827-4837); both state attrs are literally named "SimulationState" (strings @51d526/51d577) -> weapon simulationState. - Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster (0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag table added to context/decomp-reference.md. - Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked in context/open-questions.md with the blx_cop canopy + PNAME pip meshes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cba4c3098f |
scratchpad: bgfbounds pointed at the beam models (the natural-width evidence)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5236d4263a |
Wreck sink (quadratic burial) + ldbr debris field -- the 1996 script completed
Answers "does the wreck fade away?": YES, by sinking. FUN_00456410 (the 1996 sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is -0.025 -> the ~7-unit hulk is fully underground ~17s after the kill. The script also pairs the standing hulk with the LDBR strewn-debris field (12x13u flat scatter), parented together and sinking together. Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble sculpt. The "just a leg standing there" report is the authentic art. (THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices; more evidence the hardcode was an unfinished dev shortcut.) Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the quadratic sink per frame (driven from the dead mech's UpdateDeathState), hides both pieces at burial and reports it so the wreck-smoke re-arm stops with the wreck. DPLStaticChildRenderable::SetOffsetTranslation added (Execute re-reads OrientationMatrix per frame -- same in-place idiom as SetDrawObj). Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm @10s -> wreck buried @~17s -> smoke stops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c0fa6bf21a |
Death = the burning WRECK-HULK swap (effect 104) -- the authentic chain, reconstructed
The user was right: a dead mech turns into a pile of wreckage. The authentic
1995 chain, recovered end-to-end:
kill -> the victim's per-mech DEATH ModelList ('blhdead'/'lokdead'/'owndead'/
'thrdead', .RES ids 22-25) -> its authored effects: 104 (the WRECK script) +
1007 (dnboom big explosion) + 1001 (ddthsmk rubble smoke plume) + a damage-
smoke burst (3/4/5/15).
Effect 104 = ExplosionScripts case 4 (part_008.c:2663, LIVE in the 1996 binary;
the "disabled" warning is case 6): loads the destroyed hulk + flamesml/flamebig
flame meshes with sweep flicker. Every mech ships its hulk (BLHDBR/MADDBR/
LOKDBR/... + GENDBR generic); the 1996 script hardcoded thrdbr.bgf (dev
shortcut) -- we use the victim's own.
Reconstructed as BTL4VideoRenderer::SwapToWreck: hide every segment mesh, hang
"<prefix>dbr.bgf" on the tree root (pending-swap if death precedes tree build);
routed from the engine's ExplosionClassID dispatch (effect 104 ->
BTSwapMechToWreck(explosion->GetEntityHit())). The kill now fires the
authentic 'blhdead' resource (manual 7+1 pfx calls removed -- the list carries
1007/1001 itself). Verified live:
[death] firing authentic death list 'blhdead' id=22
** effect_number = 104
[BTrender] wreck swap: victim -> 'blhdbr.bgf'
Follow-ups noted: mesh flames + hulk settle (cosmetic), DeathSplash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d9254736ab |
Death is a FREEZE, not a collapse -- fall-latch vestige proven; band-effect impact frames
Task #32 (death collapse animation) resolved by decomp evidence -- BT 4.11 has NO collapse animation; the movementMode 5-8 fall latch (clips 0x1c-0x1f) is an engine-lineage vestige: (1) the clip-table loader FUN_004a80d4 fills slots 0x00-0x1b + 0x20 (bmp knockdown) and returns -- slots 0x1c-0x1f are never written; (2) mech+0x63c..0x648 appear in NO exported function; (3) no fall clip exists in the shipped 27-clip set; (4) firing the latch would bind resource id 0 -- a StaticAudioStream -- as keyframes. Authentic death modes are the FREEZE modes (IsDestroyed == mode 2||9), so UpdateDeathState now settles straight to 9 (was a [T3] mode-5 guess that would trip the garbage latch in the binary). The death READ = freeze + dnboom + ddthsmk smoke plume + destroyed skins + shutdown; the wreck stands. Also: damage-band effects orient toward the ATTACKER (impact frame) via lastInflictingID -- which was declared but never written (recon gap); now maintained by Mech::TakeDamageMessageHandler, unblocking the DamageZone LOD same-attacker redirect too. KB corrected + proofs recorded (combat-damage "Death SEQUENCE", open-questions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a3d67cc639 |
Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed faithfully: - STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111) -- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit silently no-op'd. Promoted to the named member Mech::damageLookupTable (binary this[0x111], was mislabeled ammoExpended). New gotcha class recorded (reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged. - Fire path migrated off the stale vital-zone aim onto the completed STEP-6 unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the exterior zone). No more invisible 1-shot kills; death via the authentic cascade (~14 center-mass hits). Wreck stays TARGETED on kill (beams stop on it); scoring latches off. - SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43 unbound plugs/mech logged as an open question -- the binding itself is a gap). - RemakeEntity (render damage swap): the 1996 render state machine's missing Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each segment's damage-zone graphic state (tree dtor doesn't cascade -> never rebuild). Destroyed arms/guns visibly wreck (the only variants the RES registers). - BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer, unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all stubs). Parses the authentic VIDEO/*.PFX definitions via the [pfx_day] psfxN mapping; premultiplied blending renders BOTH families from the same data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK ramps negative: impossible additively); depth-sorted billboards with a radial-masked grit sprite; impact-frame orientation (.PFX offsets are authored mech-local, -Z = out of the struck armor toward the shooter) for weapon hits AND damage bands (via lastInflictingID, now maintained -- was declared but never written). Both effect-number encodings route (raw dpl <100 + WinTesla 1000+slot carried by the band resources). Death fires the authentic dnboom (7) + ddthsmk smoke plume (1). - Effects anchor at the impact point / damaged zone's segment, not the mech origin (no more fire at the feet). - Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain verification; BT_PFX_ADD=1 flips the particle blend for A/B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f914fc040a |
context-system: complete migration -> CLAUDE.md is now a 160-line router (zero context lost)
Full migration of the 2236-line monolithic CLAUDE.md into the progressive-context knowledge graph (per spark-lesson / expert-seed.md), so the deep RE knowledge loads on-demand instead of every session. ZERO CONTEXT LOST: - docs/PROGRESS_LOG.md = the complete old CLAUDE.md, VERBATIM (byte-identical) -- the lossless safety net + the "full detail" quick-lookup fallback. - 18 context/*.md topic files (1343 lines) digest every section (§1-3 -> project-overview, §4 -> content-archives, §5 -> asset-formats/bgf-format, §5a -> source-completeness, §5b/§8 -> wintesla-port, §7/§10 -> locomotion, §10a -> build-and-run, §10b -> reconstruction-method, §10c -> combat-damage + reconstruction-gotchas, §10d -> subsystems, render notes -> rendering, gauges -> gauges-hud, MP -> multiplayer, §9 -> open-questions). - reference/glossary.yaml (53 terms). decomp-reference.md = the offsets/ClassIDs/addresses hub. CLAUDE.md (160 lines) = router: identity, answer/reason protocols, quick-lookup table, evidence tiers (T0 engine-truth / T1 decompiled+verified / T2 reconstructed+runtime / T3 guarded / T4 hypothesis), conventions + DO-NOT (the systemic bug classes), structure. Retains the load-bearing work directives (build recipe pointer, "keep current" mandate). Knowledge graph validates CLEAN (scratchpad/checkctx.py -- all [[links]] + quick-lookup + docs refs resolve; [[name]] -> topic file or glossary term). docs/*.md ledgers stay as the detailed logs; context/*.md are the curated digests that route into them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7fc4acb89f |
gauge-complete P4g: MessageBoard reconstructed + registered -> LAST parse-skip cleared (0 unregistered gauges)
The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker, L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This was the last unregistered gauge primitive. Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive @4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed (decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at +0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED (BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c). Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription = 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[]. DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62) is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real /FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer TU, dodging the databinding trap). The name-cell path is a marked structural simplification to restore when the feed lands. VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c0d2eb0aae |
gauge-complete P4f: PrepEngrScreen reconstructed + registered -> 12 engineering-screen label overlays build
The "prepEngr" cockpit primitive (the per-engineering-screen static label overlay,
12 CFG calls at L4GAUGE.CFG:4595-4759) was a MISLABELED stub (base Gauge, an Execute
override, mech/screenNumber SWAPPED) with no methodDescription -> parse-SKIPPED, none built.
Reconstructed byte-verified (ctor @4c7bf0, BecameActive @4c7e48, Make @4c7b30; vtable
0051a06c). THREE corrections vs the stub (decode workflow + vtables.tsv proof):
(1) base = GraphicGaugeBackground, not Gauge -- vtable-identical shape to the engine's
BackgroundBitmap (only dtor + BecameActive overridden); (2) the overridden slot is
BecameActive (a paint-on-activation), NOT Execute -- GraphicGaugeBackground has no Execute
virtual, so the Gauge::Execute->Fail->abort hazard doesn't apply; (3) screenNumber@0x6C /
mech@0x70 were swapped. sizeof 0x90.
BecameActive walks the mech roster (databinding-safe GetSubsystemCount/GetSubsystem +
BTGetSubsystemAuxScreen bridge, like VehicleSubSystems::Make), finds the subsystem whose
auxScreenNumber == this screen (1..12), and paints the screen-number numeric + subsystem
label + type-specific label cells dispatched on GetClassID (Sensor/Myomers/Emitter/PPC/
Projectile/Missile/GaussRifle). methodDescription = 9 params (mode, screen, 7 .pcc names).
Registered in BTL4MethodDescription[]. Two raw subsystem reads (heat-sink #, +0x224 label)
are guarded fail-soft best-efforts (marked; the aux-screen bridge label64 substitutes).
VERIFIED: the skip list drops from {prepEngr x12, messageBoard} to just {messageBoard} --
all 12 prepEngr gauges build; no screen-range/crash; boot+sim+combat run; gauge composite
renders identically (COOLANT 300/77, clusters, radar all un-regressed). The prepEngr labels
paint on the Eng (ModeMFD*Eng*) screens, not the default composite view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4a4ec6855c |
gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/ methodDescription/registration) -> the config line was parse-SKIPPED and never built. Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c, methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute reads the linked mech's world position and shows two 100-unit sector numerics: numericA = Round(-localOrigin.z * 0.01) + 500 numericB = Round( localOrigin.x * 0.01) + 500 (rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong "truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm). Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating), Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked (static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling; the config image name is copied (nameCopy) since Execute reads it per-frame. Registered in BTL4MethodDescription[]. VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1; Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504 -- authentic 100-unit sectors from live mech position). Gauge composite renders full, no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard). Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each unregistered gauge primitive the dev-parse skips -- the tool that pinned this down (earlier "not built" runs were killed before the lazy gauge-renderer init). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b91057aadc |
gauge-complete P4c: Condenser dedup + RecomputeCondenserValves -> ValveSetting gauge reads authentic 1/N
Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale @0x15C) reads its authentic value instead of garbage/zero. STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/ TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD. #if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1, coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated) stay in heat.cpp. STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264): distribute coolant flow across the mech's condensers, coolantFlowScale_i = valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse (2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read). Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6) for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED, un-regressed. Diagnostic BT_VALVE_LOG added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
16f5f545ce |
gauge-complete P4b: AggregateHeatSink 0xBBE -> HeatSink/AmbientTemperature bound (last config NULL cleared)
The 0xBBE heat-sink BANK was built as a plain HeatSink, so the numeric-R cockpit gauge binding HeatSink/AmbientTemperature (L4GAUGE.CFG:4552) had no publisher -> the LAST unresolved config attribute. Reconstruct AggregateHeatSink : HeatSink (ctor @4ae8d0, own GUID 0x50e590), byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature @0x1D4=300, helper@0x1D8 0xC link node, sizeof 0x1E4 == factory alloc @9993). Publish HeatSinkCount + AmbientTemperature via a dense-prefix attribute table chained to HeatSink::NextAttributeID (shared HeatSink table unchanged, so CoolantMass/CoolantCapacity keep resolving). Move CreateHeatSinkBankSubsystem into heatfamily_reslice.cpp (needs the class def) and build the real class at factory case 0xBBE; mech.cpp unchanged. DELIBERATE DEVIATION (documented in the class): keep the base HeatSinkSimulation the HeatSink ctor installs; do NOT reimplement the authentic Performance @4ae73c -- it derefs a raw self+0xE0 -> [+0x158] that does not map in our compiled layout (AV/NaN, and runs for EVERY mech), and ambientTemperature is a frozen constant so the gauge reads 300 either way. Authentic relaxation model deferred. Verified: [attr] HeatSink/AmbientTemperature OK; all 50 config attribute bindings resolve (0 NULL); no every-mech crash; combat TARGET DESTROYED, FIRED #41+, un-regressed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |