diff --git a/context/decomp-reference.md b/context/decomp-reference.md index a97bbd7..b058a7d 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -135,6 +135,19 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a - One BLH ER-M cycle = 0.2 + 2.0 = **2.2 s**; ~12 lasers staggered → aggregate ≈ 5/s. - `WeaponRange` 500 (ER laser), `DamageAmount` 3.5, `PipColor` 1.0 0.0 0.0 (RED). - Charge constants: `_DAT_004bb3c4`=1.0, `_DAT_004bb3b8`=1e-4, `_DAT_004bb3b4`=0.5; RatedVoltage (GENRATOR.SUB)=1e4; fire threshold `seekVoltage[rec]=0.8*RatedVoltage`. +- **The electrical chain (task #10, LANDED) [T1]:** ctor @004bb120 calibrates + `voltageScale@0x310 = (RechargeRate / -ln(1 - 1e-4*seekV[rec])) / EC`; + `ChargeTimeScale` @004b0d50 reads it back every tick x `(1 + thermalResistivityCoefficient + (res+0x100, emitters 0.0024) x max(0, srcT-srcT0))` -- hot generators charge slower; + `TrackSeekVoltage` @004ba838 puts the charging I^2R (`seekRate^2 x dtScale x dt`) into the + GENERATOR's `pendingHeat@0x1c8` (~3.5e8/full PPC charge). `ComputeOutputVoltage` @004ba738 + snaps rechargeLevel@0x320 to 1.0 within 0.01. **FailureHeat consumers:** `this+0x184` + (own heatAlarm level) == 2 -> emitter @004baa88 resets firing + holds charge 0; ballistic + @004bbd36 pins `recoil=rechargeRate` + alarm 7. @004bbd04 (ProjectileWeaponSimulation) + opens with `call 0x4b0bd0` (the powered/heat step), like @004baa88. Cockpit power-routing + handlers @004b099c/@004b09e4/@004b0a2c/@004b0a74 (bind generator by authored id 1-4 via + @004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30 + (generator client capacity @0x1e4/count @0x1e8). --- diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 13a9e17..7f7c314 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -69,11 +69,14 @@ intact). The `overlay` port (SectorDisplay lives there) shares the `sec` physica different bit-plane (0x00C0). [T2] ## Authentically-static (do NOT "fix") -`currentTemperature ~77` (huge thermalMass ~1.39e6 → a 3.5-heat shot rounds to zero); Degradation/Failure temps are fixed markers; AmbientTemperature 300; CoolantMassLeakRate 0 on a pristine mech (damage-gated); cmArmor/cmCrit all-green (undamaged solo player — BT is PvP-only); -RANK 1 solo; MessageBoard empty (no status messages exist in bring-up). The Heat MFD is -authentically NEAR-STATIC, not a live heat-accumulation display. [T2] +RANK 1 solo; MessageBoard empty (no status messages exist in bring-up). +**CORRECTION (task #10, 2026-07-11):** the old "Heat MFD is authentically NEAR-STATIC / +currentTemperature ~77 rounds to zero" claim was computed at the DEGENERATE bring-up heat scale. +At the authentic 1e7-unit economy (tasks #9-#10) the Heat MFD is fully dynamic: weapons run +77→2000 (the authored failure threshold), condensers/generators run 100-1400 under sustained +fire, and the bank plateaus ~600 — temperatures, heatLoad and the heat alarms all animate. [T2] ## Remaining = DATA FEEDS, not widgets (deferred systems) The **condenser valve CONTROL** gates on the owning **BTPlayer**'s game-mode flag (`mech+0x190`, diff --git a/context/open-questions.md b/context/open-questions.md index 7e6eaf1..c7139f3 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -176,15 +176,44 @@ authentic path scoped. CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows, exits (~2/3 of a 1e10 spam run drained), weapons overheat under max-rate autofire exactly as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; - heatLoad -> effectiveRange 0 = the authentic overheat cutout). **REMAINING DEFECT: the - linked-sink ROUTING is scrambled** -- both PPCs' heat pools in Condenser1 (observed 6800 K) - while their AUTHORED condensers (PPC_1->C4, PPC_2->C6) sit at 72 K; heat.hpp:186 already - notes the heat-stream parse reading neighbouring fields for thermalMass/heatSinkIndex -- - audit the type-0x11/heat-family stream offsets + the "+2-biased" index resolve. Also - [T4]: who reads FailureHeat to BLOCK firing; the HeatWatcher/AmmoBin cook-off watchedLink; - the mech-level heatAlarm@0x450 producer. KB sweep pending: the "Heat MFD authentically - NEAR-STATIC" claim (gauges-hud.md) was computed at the degenerate scale -- at authentic - scale the MFD is dynamic. + heatLoad -> effectiveRange 0 = the authentic overheat cutout). +- **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER + scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink + exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5, + condensers+reservoir->bank@slot2). The "pools in Condenser1" observation was the + DIAGNOSTIC-SAMPLER ALIASING trap (one shared static 1-Hz timer, pendingHeat-gated); the + census log is now per-instance. The REAL defect was the fire RATE: emitter.cpp's local + `FUN_00417ab4` stub returned NULL, so although the PoweredSubsystem ctor attaches the + authored generator to `voltageSource@0x1D0`, the Emitter TU could never see it -- the whole + electrical model was inert and the E7 force-charge recharged every emitter in ONE frame + (~0.3 s cycle, 1501 fires/90 s, ~1.7e9 heat/s input). Fixed authentically [T1, disasm + + byte-verified constants]: + * Emitter ctor @004bb120: seekVoltage[i] = authored fraction x generator ratedVoltage + (authored 10000); EC = energyTotal/(seekV[rec]^2 x 0.5); **voltageScale@0x310 = + (RechargeRate / -ln(1 - 0.0001 x seekV)) / EC** -- the exponential charge reaches + seekV[rec] in EXACTLY the authored RechargeRate on a cold generator (PPC 5 s, ERLLaser + 4 s, SRM6 3 s, ERMLaser 2 s, LRM15 6 s). The gate reads the OWNER's simulationFlags + (the usual ctor-gate gotcha; the old this-flags read never armed). + * `PoweredSubsystem::ChargeTimeScale()` (@004b0d50, was a =1.0 stub): voltageScale x + (1 + thermalResistivityCoefficient x max(0, srcT - srcT0)) -- a HOT generator charges + slower ("voltageScale is never read back" was wrong -- this reads it every tick). + * TrackSeekVoltage @004ba838: charging I^2R (`seekRate^2 x dtScale x dt`) lands in the + GENERATOR's pendingHeat@0x1c8 (~3.5e8 per full PPC charge -> generators self-heat -> + conduct to their condensers -> slow further charging). This closes the heat/firepower + feedback economy. + * **FailureHeat consumers found [T1, disasm]:** `this+0x184` (the weapon's own heatAlarm + status level) == 2 gates BOTH families -- @004baa88 (emitter: ResetFiringState + + currentLevel=0 until it cools) and @004bbd36 (ballistic: recoil pinned to rechargeRate + + alarm state 7). Emitter::GetFaultState() now returns heatAlarm.GetLevel() (was a 0 stub). + * ProjectileWeaponSimulation @004bbd04 opens with `call 0x4b0bd0` (disasm-verified) -- the + launchers now run the powered/heat step (their task-#9 firing heat previously accumulated + in pendingHeat forever; SRM6s now census at ~950 under sustained fire). + Verified live (120 s max-rate autofire): PPC ~470-500 (was 55,000), bank plateaus ~600 and + sheds, generators 1100-1400, ERMLaser self-regulates at the 2000 failure threshold + (shutdown -> cool -> resume). Remaining tails: the HeatWatcher/AmmoBin cook-off + watchedLink; the mech-level heatAlarm@0x450 producer; the cockpit power-routing message + handlers (@004b099c..@004b0abc: assign weapon to Generator 1-4 + auto/manual toggle via + modeAlarm@0x2CC -- wired to buttons, not yet reachable). ## Locomotion / combat polish (non-gating) - Authentic per-mech TURN-RATE constant (currently a bring-up constant rate). diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index 7427f2e..13d85b5 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -105,7 +105,6 @@ static void Subtract(Vector3D &o, const Point3D &a, const Point3D &b) { o.x=a.x- static void Negate(Point3D &o, const LinearMatrix &) { o = Point3D(0.0f,0.0f,0.0f); } // extract origin (best-effort) static void Normalize(EulerAngles &) {} static void MakeIdentity(LinearMatrix &m) { m.BuildIdentity(); } -static Scalar VoltageCurve(Scalar) { return 1.0f; } // _DAT_004bb3c4 - _DAT_004bb3b8*v static void CopyColor(void *, const void *) {} // engine-internal artifacts the decomp called by address (stand-ins) @@ -124,9 +123,14 @@ static int BTMechDestroyed(Entity *e) return 0; return ((Mech *)e)->IsDisabled() ? 1 : 0; } -static Scalar FUN_004b0d50(void *) { return 1.0f; }// PoweredSubsystem dt scale +// (task #10: the FUN_004b0d50 / FUN_00417ab4 NULL stubs are RETIRED -- they +// were what kept the whole electrical model inert: PoweredSubsystem attaches +// the authored generator to voltageSource@0x1D0, but every resolve through the +// stub returned NULL, so the ctor never calibrated the charge curve and +// TrackSeekVoltage never charged. The E7 one-frame force-charge that covered +// for it made every emitter recycle in ~0.3s instead of the authored 2-6s +// RechargeRate -- the actual root cause of the task-#10 heat runaway.) static int FUN_004ac9c8(void *) { return 0; } // heat-state query -static void *FUN_00417ab4(void *) { return 0; } // follow SharedData link static int FUN_00421070(void *, void *) { return 0; } // damage source resolve static void *ResolveSource(void *) { return 0; } // named source registry lookup @@ -157,7 +161,11 @@ Emitter::SharedData int Emitter::GetWeaponState() { return weaponAlarm.GetLevel(); } // this+0x364 int Emitter::GetVoltageState() { return electricalStateAlarm.GetLevel(); } // this+0x278 LWord Emitter::GetFlags() { return simulationFlags; } -int Emitter::GetFaultState() { return 0; } +// this+0x184 == the heatAlarm STATUS level (heat.hpp @0x170 + 0x14): the raw +// fault check @004baa88 (param_1[0x61] == 2) is the FailureHeat consumer for +// the emitter family (task #10) -- an overheated emitter resets its firing +// state and holds currentLevel at 0 until it cools below FailureTemperature. +int Emitter::GetFaultState() { return heatAlarm.GetLevel(); } void Emitter::SetDirty() { simulationFlags |= 0x1; } void Emitter::ClearDirty() { simulationFlags &= ~0x2; } @@ -227,7 +235,7 @@ void // BRING-UP verify (rate-limited): prove the real fire path executes + feeds the heat // sim (heatPortion -> pendingHeat -> flows to the central sink -> mech temp climbs). static int s_fireLog = 0; - if ((s_fireLog++ % 20) == 0) + if ((s_fireLog++ % 19) == 0) // 19 prime: rotates across the 5 emitters (20 aliased to one) DEBUG_STREAM << "[emitter] FIRED #" << s_fireLog << " damage=" << damagePortion << " heat=" << heatPortion << " pendingHeat=" << pendingHeat << "\n" << std::flush; @@ -382,17 +390,17 @@ void weaponAlarm.SetLevel(4); // TriggerDuringLoad blip weaponAlarm.SetLevel(3); } - if (GetVoltageState() == 4) // this+0x278 + // E7 RETIRED (task #10): the authentic charge path is live -- the + // PoweredSubsystem ctor attaches the authored generator, the Emitter + // ctor calibrates voltageScale/energyCoefficient, and TrackSeekVoltage + // integrates the charge over the authored RechargeRate seconds. The + // old unconditional snap-to-seek recharged every emitter in ONE frame + // (~0.3s cycle vs the authored 2-6s), multiplying the heat input ~16x + // (the "runaway": the routing itself was verified link-correct). + if (GetVoltageState() == 4) // this+0x278 == Ready { - TrackSeekVoltage(time_slice); // @004ba838 (inert: unresolved src) + TrackSeekVoltage(time_slice); // @004ba838 } - // E7 (bring-up): the real charge path is inert in the port -- the voltage-source - // plug (this+0x1d0) doesn't resolve, so TrackSeekVoltage can't raise currentLevel - // (and it runs whenever vState==4, so the old `else` force-charge never fired). - // Force the charge to the seek level UNCONDITIONALLY so the weapon reaches Loaded - // and the real fire path (E3/E4/E8 -> FireWeapon) executes. - if (currentLevel < seekVoltage[seekVoltageIndex]) - currentLevel = seekVoltage[seekVoltageIndex]; // -> ComputeOutputVoltage == 1.0 ComputeOutputVoltage(); // (*vtable+0x44)() if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged (== inherited @0x320) { @@ -418,26 +426,27 @@ void void Emitter::TrackSeekVoltage(Scalar time_slice) { - VoltageSource *src = (VoltageSource *)FUN_00417ab4(this + 0x1d0); - Scalar dtScale = FUN_004b0d50(this); // PoweredSubsystem dt scale - - // No voltage source resolved (the source plug at this+0x1d0 binds via the registry; - // not yet linked in the bring-up) or a zero dt scale -> can't charge this frame. - // Guard the deref instead of crashing; charging resumes once the source links. - if (src == 0 || dtScale == 0.0f) + // The powering Generator, attached to voltageSource@0x1D0 by the chained + // PoweredSubsystem ctor (authored "VoltageSource" roster index). The binary + // dereferences it unguarded; the port keeps charging inert if the link is + // gone (source destroyed) -- the electrical alarm handles the state. + Generator *src = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1d0) + if (src == 0) return; - seekRate = (src->voltage - currentLevel) / dtScale; // 0x45c = (src+0x1dc - 0x414)/.. + Scalar dtScale = ChargeTimeScale(); // @004b0d50 -- voltageScale + // stretched by the generator's temp rise + seekRate = (src->MeasuredVoltage() - currentLevel) / dtScale; // 0x45c = (src+0x1dc - 0x414)/.. currentLevel = (seekRate / energyCoefficient) * time_slice + currentLevel; // 0x414 if (FUN_004ad7d4(this)) // heatable/online { - // Raw @004ba838:7544 feeds rate^2*dt into the RESOLVED SOURCE's pendingHeat - // (src+0x1c8). The VoltageSource overlay here is a compact shortcut reading - // offsets 0/4/8/0xc, NOT the real 0x1dc/0x1c8 -- inert in bring-up (src never - // resolves, guarded above). FOLLOW-UP (Emitter re-base tail): type src as the - // real HeatSink-derived source and write src->pendingHeat@0x1c8. - src->heatLoad += seekRate * seekRate * dtScale * time_slice; + // @004ba838:7544 -- the charging I^2R loss lands in the GENERATOR's + // pendingHeat (src+0x1c8): recharging weapons is what heats generators + // (~3.5e8 per full PPC charge -> +~480K on a 726000-mass generator), + // which conduct to their authored condensers and slow further charging + // through ChargeTimeScale. This closes the heat/firepower feedback. + src->pendingHeat += seekRate * seekRate * dtScale * time_slice; } } @@ -558,18 +567,18 @@ int // current seekVoltage index and is itself in the "ready" state (source+0x210==2). // Logical - Emitter::ReadyToDischarge(VoltageSource *source) + Emitter::ReadyToDischarge(Generator *source) { if (source == 0) { - source = (VoltageSource *)FUN_00417ab4(this + 0x1d0); + source = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1d0) } if (source == 0) { return False; } - if (seekVoltage[seekVoltageIndex] <= source->voltage // 0x400[idx] <= src+0x1dc - && source->state == 2) // src+0x210 + if (seekVoltage[seekVoltageIndex] <= source->MeasuredVoltage() // 0x400[idx] <= src+0x1dc + && source->GeneratorStateOf() == 2) // src+0x210 { return True; } @@ -814,39 +823,66 @@ Emitter::Emitter( // energyTotal = (damageAmount + heatCostToFire) * 1.0e7 energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; // _DAT_004bb3ac - // SeekVoltage curve is only meaningful for an electrically-driven weapon - // ((flags & 0xC)==0 && (flags & 0x100)!=0): copy the per-index voltages, - // scaled by the linked voltage source's rated voltage, then size the curve. - if ((GetFlags() & 0xC) == 0 && (GetFlags() & 0x100) != 0) + // SeekVoltage curve is only meaningful for an electrically-driven MASTER. + // @004bb120:7872 reads the OWNER's simulationFlags (param_2+0x28) -- the + // same owner-gate every sibling subsystem ctor uses (the old this-flags + // read never armed, which is why the electrical block sat on the bring-up + // defaults seekV=1/EC=1 and needed the E7 force-charge at all). + if ((owner->simulationFlags & 0xC) == 0 + && (owner->simulationFlags & 0x100) != 0) { - VoltageSource *src = (VoltageSource *)FUN_00417ab4(this + 0x74); - for (int i = 0; i < 5; ++i) + // The powering Generator, attached by the chained PoweredSubsystem ctor + // (authored "VoltageSource" roster index; bhk1/madcat generators author + // ratedVoltage = 10000). @004bb120:7873 resolves it UNGUARDED; the + // port fails loud instead of crashing -- a master emitter without its + // generator cannot calibrate its charge curve. + Generator *src = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1D0) + if (src == 0) { - seekVoltage[i] = 0.0f; // 0x400[i] + Verify(False, "Master Emitter is missing its voltage source", __FILE__, __LINE__); } - for (int i = 0; i < 5; ++i) + else { - if (subsystem_resource->seekVoltage[i] == -1.0f) // _DAT_004bb3b0 sentinel + for (int i = 0; i < 5; ++i) { - seekVoltageCount = i - 1; // 0x3fc - break; + seekVoltage[i] = 0.0f; // 0x400[i] } - seekVoltage[i] = subsystem_resource->seekVoltage[i] * src->ratedVoltage; // src+0x1d8 + for (int i = 0; i < 5; ++i) + { + if (subsystem_resource->seekVoltage[i] == -1.0f) // _DAT_004bb3b0 sentinel + { + seekVoltageCount = i - 1; // 0x3fc + break; + } + // the authored values are FRACTIONS of the generator's rated + // voltage: [0.6, 0.7, 0.8, 0.99] x 10000 -> [6000..9900] + seekVoltage[i] = subsystem_resource->seekVoltage[i] + * src->RatedVoltageOf(); // src+0x1d8 + } + seekVoltageRecommendedIndex = subsystem_resource->seekVoltageRecommendedIndex; // 0x3f4 <- +0x1d8 + seekVoltageIndex = seekVoltageRecommendedIndex; // 0x3f0 + seekStepCounter = 0; // 0x3f8 + + // energyCoefficient = energyTotal / (seekVoltage[rec]^2 * 0.5) + // -> E = 0.5 * V^2 * EC lands exactly on energyTotal at V=seekV[rec] + Scalar v = seekVoltage[seekVoltageRecommendedIndex]; + energyCoefficient = energyTotal / (v * v * /*_DAT_004bb3b4*/ 0.5f); // 0x454 + + // THE RECHARGE CALIBRATION (@004bb120:7903, task #10). Lands in the + // BASE voltageScale@0x310 slot, which ChargeTimeScale (@004b0d50) + // reads back every charge tick (the old "never read back" note -- + // and the phantom `energyRampTime` local it justified -- were + // wrong). Sets the exponential charge + // level(t) = Vrated * (1 - exp(-t / (EC * voltageScale))) + // to reach seekV[rec] in exactly the authored RechargeRate seconds + // on a COLD generator (0.0001 == 1/Vrated; PPC 5s, ERLLaser 4s, + // ERMLaser 2s). Generator heat then stretches the scale by + // (1 + thermalResistivityCoefficient * srcTempRise). + voltageScale = (rechargeRate + / -logf(/*_DAT_004bb3c4*/ 1.0f + - /*_DAT_004bb3b8 (float80)*/ 0.0001f * v)) + / energyCoefficient; // 0x310 (base slot) } - seekVoltageRecommendedIndex = subsystem_resource->seekVoltageRecommendedIndex; // 0x3f4 <- +0x1d8 - seekVoltageIndex = seekVoltageRecommendedIndex; // 0x3f0 - seekStepCounter = 0; // 0x3f8 - - // energyCoefficient = energyTotal / (seekVoltage[rec]^2 * 0.5) - Scalar v = seekVoltage[seekVoltageRecommendedIndex]; - energyCoefficient = energyTotal / (v * v * /*_DAT_004bb3b4*/ 0.5f); // 0x454 - - // voltage-curve coefficient -> ramp time (expf of the seek curve). The binary - // stores this in the base slot voltageScale@0x310 and never reads it back, so the - // recon's own `energyRampTime` member was a phantom -> computed into a local. - Scalar curve = /*_DAT_004bb3c4 - _DAT_004bb3b8 * v*/ VoltageCurve(v); // best-effort - Scalar energyRampTime = (rechargeRate / -curve) / energyCoefficient; - (void)energyRampTime; } // (task #8 note: energyCoefficient deliberately stays on the bring-up diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index e30fb78..3b8afe0 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -112,14 +112,10 @@ class NotationFile; }; typedef Emitter__UpdateRecord UpdateRecord; - // Proxy for the linked voltage source (Generator segment) the decomp reads - // through the SharedData connection (voltage/state/ratedVoltage/heatLoad). - struct VoltageSource { - Scalar voltage; - int state; - Scalar ratedVoltage; - Scalar heatLoad; - }; + // (task #10: the old compact "VoltageSource" proxy struct is RETIRED -- + // the +0x1d0 plug resolves to the real Generator, whose named members + // carry the decomp offsets: outputVoltage@0x1dc, ratedVoltage@0x1d8, + // state@0x210, pendingHeat@0x1c8.) protected: // State / flag accessors the recovered bodies read (mapped onto the @@ -206,7 +202,7 @@ class NotationFile; // @004ba6e0 -- True when the supplied (or linked) voltage source can // satisfy the current seekVoltage index and is in the "ready" state (2). Logical - ReadyToDischarge(VoltageSource *source); // slot 16 + ReadyToDischarge(Generator *source); // slot 16 // @004ba738 -- recompute outputVoltage = currentLevel / seekVoltage[idx], // snapped to 1.0 near full and clamped to [0,1]. diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index b2d826e..e79fcd4 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -38,6 +38,7 @@ // #include +#include // BT_HEAT_LOG census (diag only) #pragma hdrstop #if !defined(HEAT_HPP) @@ -505,6 +506,15 @@ HeatSink::HeatSink( // @004adda0: "Bad subsystem resource ->heatSink" HEAT.CPP:0x25F Verify(False, "Bad subsystem resource ->heatSink", __FILE__, 0x25F); } + if (getenv("BT_HEAT_LOG")) + { + DEBUG_STREAM << "[heat-link] " << GetName() + << " sinkIdx=" << subsystem_resource->heatSinkIndex + << " linked=" << (linked ? linked->GetName() : "") + << " mass=" << subsystem_resource->thermalMass + << " k=" << subsystem_resource->thermalConductance + << std::endl; + } if ( (owner->simulationFlags & SegmentCopyMask) == 0 // param_2+0x28 & 0xc == 0 @@ -602,17 +612,25 @@ void currentTemperature = heatEnergy / thermalMass; UpdateHeatLoad(); - // BRING-UP verify (1 Hz, viewpoint mech only): show the mech heat climbing as - // weapons fire and relaxing when they stop. pendingHeat just absorbed above. - static Scalar s_heatLog = 0.0f; - s_heatLog += time_slice; - if (s_heatLog >= 1.0f && application != 0 - && (Entity *)owner == application->GetViewpointEntity() && pendingHeat > 0.0f) + // DIAG census (BT_HEAT_LOG, viewpoint mech): PER-INSTANCE 5-s timers -- + // the old shared static timer aliased to whichever instance crossed the + // tick (the diagnostic-sampler trap), which manufactured the task-#10 + // "heat pools in Condenser1" misread. + if (getenv("BT_HEAT_LOG") && application != 0 + && (Entity *)owner == application->GetViewpointEntity()) { - s_heatLog = 0.0f; - DEBUG_STREAM << "[heat] " << GetName() << " temp=" << currentTemperature - << " heatEnergy=" << heatEnergy << " absorbed=" << pendingHeat - << " heatLoad=" << heatLoad << "\n" << std::flush; + static std::map s_census; + Scalar &acc = s_census[this]; + acc += time_slice; + if (acc >= 5.0f) + { + acc = 0.0f; + DEBUG_STREAM << "[heat-t] " << GetName() + << " T=" << currentTemperature + << " absorbed=" << pendingHeat + << " cool=" << coolantLevel << "/" << thermalCapacity + << " load=" << heatLoad << "\n" << std::flush; + } } pendingHeat = 0.0f; diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index 640468e..d252b14 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -37,6 +37,7 @@ #endif #if !defined(HEATFAMILY_RESLICE_HPP) # include +# include // application (BT_HEAT_LOG census) #endif #define JM_CLOSE_ENOUGH 0.0001f // _DAT_004ae8ac / _DAT_004af3a0 @@ -769,8 +770,27 @@ void * (coolantLevel / thermalCapacity) * coolantFlowScale) / thermalMass); Scalar decay = 1.0f - (Scalar)exp(ex); // FUN_004dca38 - pendingHeat += -((currentTemperature * massScale - target) + Scalar shed = -((currentTemperature * massScale - target) * thermalMass * decay); + pendingHeat += shed; + + // DIAG census (BT_HEAT_LOG, viewpoint mech, 5 s) -- the bank runs THIS + // Performance instead of HeatSinkSimulation, so it needs its own line. + if (getenv("BT_HEAT_LOG") && application != 0 + && (Entity *)owner == application->GetViewpointEntity()) + { + static Scalar s_bankAcc = 0.0f; + s_bankAcc += time_slice; + if (s_bankAcc >= 5.0f) + { + s_bankAcc = 0.0f; + DEBUG_STREAM << "[heat-t] " << GetName() << " (bank)" + << " T=" << currentTemperature + << " shed=" << shed + << " cool=" << coolantLevel << "/" << thermalCapacity + << " load=" << heatLoad << "\n" << std::flush; + } + } } // coolant top-up from the reservoir (the binary tail: deficit > 0 and diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index fd9248f..695fdb3 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -341,6 +341,41 @@ void } +// +// @004b0d50 -- the charge time-scale (task #10, the heat/firepower feedback): +// rise = max(0, srcTemperature - srcStartingTemperature) (src+0x114 - src+0x13C) +// scale = max(voltageScale, +// (thermalResistivityCoefficient * rise + 1.0) * voltageScale) +// (_DAT_004b0dd0 = 0.0f, _DAT_004b0dd4 = 1.0f, both byte-verified.) The binary +// dereferences the resolved source UNGUARDED (the master ctor Verify guarantees +// the link); the port falls back to the unstretched voltageScale instead. +// +Scalar + PoweredSubsystem::ChargeTimeScale() +{ + Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1D0) + + Scalar rise = 0.0f; + if (source != 0) + { + rise = source->currentTemperature // src+0x114 + - source->startingTemperature; // src+0x13C + } + if (rise < 0.0f) // _DAT_004b0dd0 + { + rise = 0.0f; + } + + Scalar scale = (thermalResistivityCoefficient * rise + 1.0f) // this+0x30C, _DAT_004b0dd4 + * voltageScale; // this+0x310 + if (scale < voltageScale) + { + scale = voltageScale; + } + return scale; +} + + //############################################################################# // Subsystem virtual overrides // diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index 33ad713..30d2bfd 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -216,6 +216,15 @@ class Generator; Subsystem* ResolveVoltageSource() { return voltageSource.Resolve(); } + // @004b0d50 -- the charge time-scale for the powered charge integrators + // (Emitter::TrackSeekVoltage divides the source-voltage gap by this): + // voltageScale stretched by (1 + thermalResistivityCoefficient x the + // SOURCE generator's temperature rise) -- a hot generator recharges its + // weapons slower. Reads voltageScale@0x310 back every tick (task #10: + // the old "the binary never reads it back" claim was wrong). + Scalar + ChargeTimeScale(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data for the powered-subsystem class. // Offsets are byte offsets into the shipped object. @@ -455,6 +464,8 @@ class Generator; GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210 Scalar MeasuredVoltage() { return outputVoltage; } // source+0x1dc + Scalar + RatedVoltageOf() { return ratedVoltage; } // source+0x1d8 // The generator's "short" state is its stateAlarm level (GeneratorShorted==3) + // outputVoltage (IsSourceShorted @004b0b5c reads 0x210/0x1dc). There is NO // separate shortFlag field on the Generator (it ends at 0x250 == stateAlarm end). diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index c9a209c..5ef7011 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -529,6 +529,28 @@ void { Check(this); + // 0. The PoweredSubsystem step FIRST -- disasm @004bbd12 (task #10): + // `call 0x4b0bd0` opens the body, exactly like the emitter sibling. It + // runs HeatSink::HeatSinkSimulation (absorb pendingHeat -> temperature -> + // conduct to the authored condenser) and the electrical state machine. + // Without it the launcher's firing heat (task #9) accumulated in + // pendingHeat forever and never reached the mech's thermal network. + PoweredSubsystem::PoweredSubsystemSimulation(time_slice); // @004b0bd0 + + // 0b. The FAULT gate -- disasm @004bbd36-004bbd6e (task #10): flags==1, the + // weapon's own heatAlarm at FailureHeat (this+0x184 == 2 -- THE consumer + // of the authored FailureTemperature for the ballistic family), or the + // owning mech destroyed -> pin recoil to the full rechargeRate and latch + // alarm state 7 (unavailable). No return: the frame continues. + if (simulationFlags == 1 + || heatAlarm.GetLevel() == HeatSink::FailureHeat // this+0x184 == 2 + || (owner != 0 && owner->IsDerivedFrom(*Mech::GetClassDerivations()) + && ((Mech *)owner)->IsDisabled())) // FUN_0049fb54 + { + recoil = rechargeRate; // this+0x3E8 = this+0x3DC + weaponAlarm.SetLevel(7); // FUN_0041bbd8(this+0x350, 7) + } + // 1. Recover the firing charge (recoil counts down toward 0 in // DrawFiringCharge) and advance any in-progress magazine eject. DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8)