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>
This commit is contained in:
arcattack
2026-07-07 20:22:01 -05:00
co-authored by Claude Opus 4.8
parent 16f5f545ce
commit b91057aadc
4 changed files with 92 additions and 2 deletions
+15 -1
View File
@@ -242,6 +242,13 @@ Derivation*
return &classDerivations;
}
// ~~~ Condenser ctor/dtor: the REAL bodies live in heatfamily_reslice.cpp
// (@4ae568, which sets valveState=1 / refrigerationFactor / condenserNumber).
// These STUBs were ODR-duplicates that WON under /FORCE (heat.obj links before
// heatfamily_reslice.obj) -> valveState was left 0xCDCDCDCD (garbage valve gauge).
// #if 0'd so the real ctor is the sole definition. DefaultData / GetClassDerivations
// / ResetToInitialState below are NOT duplicated in the reslice TU -- kept here.
#if 0
Condenser::Condenser(
Mech *owner,
int subsystem_ID,
@@ -260,6 +267,7 @@ Condenser::~Condenser()
Check(this);
Check_Fpu();
}
#endif
//###########################################################################
// ResetToInitialState -- Condenser (HEAT.TCP)
@@ -271,8 +279,13 @@ void
}
//###########################################################################
// TestClass -- Condenser (HEAT.TCP)
// TestClass / TestInstance / CreateStreamedSubsystem -- Condenser
//
// The REAL bodies live in heatfamily_reslice.cpp (@4ae63c / @4ae658, the latter
// parsing "RefrigerationFactor"). These STUBs were ODR-duplicates -- #if 0'd so
// the reslice definitions are the sole ones (see the ctor note above).
//
#if 0
Logical
Condenser::TestClass(Mech &)
{
@@ -310,6 +323,7 @@ int
Check_Fpu();
return True;
}
#endif
//###########################################################################
+50
View File
@@ -811,3 +811,53 @@ Subsystem *CreateHeatSinkBankSubsystem(Mech *owner, int id, void *seg)
AggregateHeatSink(owner, id,
(AggregateHeatSink::SubsystemResource *)seg, AggregateHeatSink::DefaultData);
}
//===========================================================================//
// @0049f788 -- RecomputeCondenserValves. Distribute coolant flow across the
// mech's condensers so each one's ValveSetting gauge (coolantFlowScale@0x15C)
// reads its share of the total valve opening: flow_i = valveState_i / sum(valveState).
//
// The binary iterates the condenser chain @mech+0x7cc (GUID 0x50e4fc == Condenser);
// we walk the populated subsystem roster and filter for Condensers via IsDerivedFrom
// -- behaviorally identical (the chain holds exactly the condensers) and independent
// of whether the @0x7cc chain is wired. Called at the end of the Mech ctor (binary
// @9457, the post-init pass) so the valve gauge shows the authentic 1/N per condenser
// instead of 0 (the ctor leaves coolantFlowScale=0 by design; this is its first writer).
//
// The per-condenser condenserAlarm@0x1DC toggle (2 if flow<=old else 1, then 0) is the
// binary's change-notification pulse; reproduced exactly (FUN_0041bbd8 == SetLevel).
//===========================================================================//
void BTRecomputeCondenserValves(Entity *owner)
{
Check(owner);
Derivation &condClass = *Condenser::GetClassDerivations();
int count = owner->GetSubsystemCount();
// pass 1: total valve opening across all condensers
int total = 0;
for (int i = 0; i < count; ++i)
{
Subsystem *s = owner->GetSubsystem(i);
if (s != 0 && s->IsDerivedFrom(condClass))
total += ((Condenser *)s)->valveState; // iVar3 += *(int*)(iVar1+0x1d0)
}
// pass 2: each condenser's flow = its share of the total
for (int i = 0; i < count; ++i)
{
Subsystem *s = owner->GetSubsystem(i);
if (s == 0 || !s->IsDerivedFrom(condClass))
continue;
Condenser *c = (Condenser *)s;
Scalar flow = 0.0f; // _DAT_0049f850 (no-condenser fallback)
if (total > 0)
flow = (Scalar)c->valveState / (Scalar)total;
c->condenserAlarm.SetLevel(flow <= c->coolantFlowScale ? 2 : 1); // @0x1dc change pulse
c->coolantFlowScale = flow; // *(float*)(iVar1+0x15c) = local_8
c->condenserAlarm.SetLevel(0);
if (getenv("BT_VALVE_LOG"))
DEBUG_STREAM << "[valve] condenser#" << c->condenserNumber
<< " valveState=" << c->valveState << " flow=" << c->coolantFlowScale
<< " (total=" << total << ")\n" << std::flush;
}
}
+6 -1
View File
@@ -190,6 +190,7 @@ struct MechControlsMapper : ReconSubsystemStub { template<class...A> MechContro
extern Subsystem *CreateCondenserSubsystem(Mech *, int, void *); // 0xBBD
extern Subsystem *CreateHeatSinkBankSubsystem(Mech *, int, void *); // 0xBBE
extern Subsystem *CreateReservoirSubsystem(Mech *, int, void *); // 0xBC0
extern void BTRecomputeCondenserValves(Entity *); // @0049f788 (post-init valve distribution)
extern Subsystem *CreateHUDSubsystem(Mech *, int, void *); // 0xBD6
extern Subsystem *CreateMechTechSubsystem(Mech *, int, void *); // 0xBDC
extern Subsystem *CreateGeneratorSubsystem(Mech *, int, void *); // 0xBC1 (WAVE 3a)
@@ -1237,7 +1238,11 @@ Mech::Mech(
}
dir->Add(this); // (**(dir[4]+4))(dir+4,this)
FinishConstruction(); // FUN_0049f788 (post-init pass)
// @0049f788 -- distribute coolant flow across the condensers (post-init pass).
// The real RecomputeCondenserValves; sets each condenser's coolantFlowScale to
// valveState/sum(valveState) so the ValveSetting gauge reads the authentic 1/N
// (was a no-op stub -> the valve gauge showed 0).
BTRecomputeCondenserValves(this);
Check_Fpu();
}
+21
View File
@@ -0,0 +1,21 @@
import struct,sys
va=int(sys.argv[1],16)
f=open('content/BTL4OPT.EXE','rb').read()
# PE header
pe=struct.unpack_from('<I',f,0x3c)[0]
assert f[pe:pe+4]==b'PE\0\0'
numsec=struct.unpack_from('<H',f,pe+6)[0]
optsz=struct.unpack_from('<H',f,pe+20)[0]
imgbase=struct.unpack_from('<I',f,pe+0x34)[0]
sectab=pe+24+optsz
for i in range(numsec):
off=sectab+i*40
name=f[off:off+8].rstrip(b'\0').decode(errors='replace')
vsz,vaddr,rsz,raddr=struct.unpack_from('<IIII',f,off+8)
if imgbase+vaddr <= va < imgbase+vaddr+max(vsz,rsz):
fo=raddr+(va-imgbase-vaddr)
raw=f[fo:fo+4]
print(f"VA {va:#x} sec={name} bytes={raw.hex()} float={struct.unpack('<f',raw)[0]} int={struct.unpack('<i',raw)[0]}")
break
else:
print("not found; imgbase",hex(imgbase))