Heat: THE AUTHENTIC ELECTRICAL MODEL -- weapons recharge from generators (task #10)
Task #10 set out to fix "scrambled linked-sink routing"; a [heat-link] attach log proved the routing was NEVER scrambled (every subsystem links its authored condenser exactly; the "pools in Condenser1" read was the diagnostic-sampler aliasing trap). The real defect: emitter.cpp's local FUN_00417ab4 stub returned NULL, so the whole electrical model was inert and the E7 force-charge recharged every emitter in ONE frame (~0.3s cycle, 1501 fires/90s, ~1.7e9 heat/s -- the "runaway"). Landed authentically [T1: disasm + byte-verified constants]: - Emitter ctor @004bb120: seekVoltage = authored fraction x generator ratedVoltage (10000); EC = energyTotal/(seekV^2 x 0.5); voltageScale@0x310 = (RechargeRate / -ln(1 - 1e-4 x seekV)) / EC -- charge reaches seekV[rec] in EXACTLY the authored RechargeRate (PPC 5s, ERL 4s, SRM 3s, ERM 2s) cold. Owner-flags ctor gate (the usual gotcha; the this-flags read never armed). - PoweredSubsystem::ChargeTimeScale (@004b0d50, was a =1.0 stub): voltageScale x (1 + thermalResistivity x srcTempRise) -- hot generators charge slower. ("voltageScale is never read back" was wrong; corrected + swept.) - TrackSeekVoltage @004ba838: charging I^2R -> the GENERATOR's pendingHeat (~3.5e8/full PPC charge) -- generators self-heat, conduct to their authored condensers, and throttle further charging. The feedback economy closes. - FailureHeat consumers found: this+0x184 == 2 gates BOTH weapon families (@004baa88 emitter: reset firing + hold charge 0; @004bbd36 ballistic: recoil=rechargeRate + alarm 7). Emitter::GetFaultState un-stubbed. - ProjectileWeaponSimulation @004bbd04 opens with call 0x4b0bd0 (disasm) -- launchers now run the powered/heat step (their firing heat previously accumulated in pendingHeat forever). - heat.cpp: per-instance BT_HEAT_LOG census (the old shared-static 1-Hz sampler aliased); [heat-link] attach log; the stolen-else Verify restored. Verified live (120s max-rate autofire): PPC ~470-500 (was 55,000), bank plateaus ~600 and sheds to ambient, generators 1100-1400, ERMLaser self-regulates at the authored 2000 failure threshold (shutdown-cool-resume). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4ed2bbc293
commit
4e63a7b6c3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user