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
@@ -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.
|
- 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).
|
- `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`.
|
- 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,14 @@ intact). The `overlay` port (SectorDisplay lives there) shares the `sec` physica
|
|||||||
different bit-plane (0x00C0). [T2]
|
different bit-plane (0x00C0). [T2]
|
||||||
|
|
||||||
## Authentically-static (do NOT "fix")
|
## 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
|
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);
|
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
|
RANK 1 solo; MessageBoard empty (no status messages exist in bring-up).
|
||||||
authentically NEAR-STATIC, not a live heat-accumulation display. [T2]
|
**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)
|
## Remaining = DATA FEEDS, not widgets (deferred systems)
|
||||||
The **condenser valve CONTROL** gates on the owning **BTPlayer**'s game-mode flag (`mech+0x190`,
|
The **condenser valve CONTROL** gates on the owning **BTPlayer**'s game-mode flag (`mech+0x190`,
|
||||||
|
|||||||
@@ -176,15 +176,44 @@ authentic path scoped.
|
|||||||
CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows,
|
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
|
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;
|
as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN;
|
||||||
heatLoad -> effectiveRange 0 = the authentic overheat cutout). **REMAINING DEFECT: the
|
heatLoad -> effectiveRange 0 = the authentic overheat cutout).
|
||||||
linked-sink ROUTING is scrambled** -- both PPCs' heat pools in Condenser1 (observed 6800 K)
|
- **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER
|
||||||
while their AUTHORED condensers (PPC_1->C4, PPC_2->C6) sit at 72 K; heat.hpp:186 already
|
scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink
|
||||||
notes the heat-stream parse reading neighbouring fields for thermalMass/heatSinkIndex --
|
exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5,
|
||||||
audit the type-0x11/heat-family stream offsets + the "+2-biased" index resolve. Also
|
condensers+reservoir->bank@slot2). The "pools in Condenser1" observation was the
|
||||||
[T4]: who reads FailureHeat to BLOCK firing; the HeatWatcher/AmmoBin cook-off watchedLink;
|
DIAGNOSTIC-SAMPLER ALIASING trap (one shared static 1-Hz timer, pendingHeat-gated); the
|
||||||
the mech-level heatAlarm@0x450 producer. KB sweep pending: the "Heat MFD authentically
|
census log is now per-instance. The REAL defect was the fire RATE: emitter.cpp's local
|
||||||
NEAR-STATIC" claim (gauges-hud.md) was computed at the degenerate scale -- at authentic
|
`FUN_00417ab4` stub returned NULL, so although the PoweredSubsystem ctor attaches the
|
||||||
scale the MFD is dynamic.
|
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)
|
## Locomotion / combat polish (non-gating)
|
||||||
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
|
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
|
||||||
|
|||||||
@@ -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 Negate(Point3D &o, const LinearMatrix &) { o = Point3D(0.0f,0.0f,0.0f); } // extract origin (best-effort)
|
||||||
static void Normalize(EulerAngles &) {}
|
static void Normalize(EulerAngles &) {}
|
||||||
static void MakeIdentity(LinearMatrix &m) { m.BuildIdentity(); }
|
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 *) {}
|
static void CopyColor(void *, const void *) {}
|
||||||
|
|
||||||
// engine-internal artifacts the decomp called by address (stand-ins)
|
// engine-internal artifacts the decomp called by address (stand-ins)
|
||||||
@@ -124,9 +123,14 @@ static int BTMechDestroyed(Entity *e)
|
|||||||
return 0;
|
return 0;
|
||||||
return ((Mech *)e)->IsDisabled() ? 1 : 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 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 int FUN_00421070(void *, void *) { return 0; } // damage source resolve
|
||||||
static void *ResolveSource(void *) { return 0; } // named source registry lookup
|
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::GetWeaponState() { return weaponAlarm.GetLevel(); } // this+0x364
|
||||||
int Emitter::GetVoltageState() { return electricalStateAlarm.GetLevel(); } // this+0x278
|
int Emitter::GetVoltageState() { return electricalStateAlarm.GetLevel(); } // this+0x278
|
||||||
LWord Emitter::GetFlags() { return simulationFlags; }
|
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::SetDirty() { simulationFlags |= 0x1; }
|
||||||
void Emitter::ClearDirty() { simulationFlags &= ~0x2; }
|
void Emitter::ClearDirty() { simulationFlags &= ~0x2; }
|
||||||
|
|
||||||
@@ -227,7 +235,7 @@ void
|
|||||||
// BRING-UP verify (rate-limited): prove the real fire path executes + feeds the heat
|
// 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).
|
// sim (heatPortion -> pendingHeat -> flows to the central sink -> mech temp climbs).
|
||||||
static int s_fireLog = 0;
|
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
|
DEBUG_STREAM << "[emitter] FIRED #" << s_fireLog << " damage=" << damagePortion
|
||||||
<< " heat=" << heatPortion << " pendingHeat=" << pendingHeat << "\n" << std::flush;
|
<< " heat=" << heatPortion << " pendingHeat=" << pendingHeat << "\n" << std::flush;
|
||||||
|
|
||||||
@@ -382,17 +390,17 @@ void
|
|||||||
weaponAlarm.SetLevel(4); // TriggerDuringLoad blip
|
weaponAlarm.SetLevel(4); // TriggerDuringLoad blip
|
||||||
weaponAlarm.SetLevel(3);
|
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)()
|
ComputeOutputVoltage(); // (*vtable+0x44)()
|
||||||
if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged (== inherited @0x320)
|
if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged (== inherited @0x320)
|
||||||
{
|
{
|
||||||
@@ -418,26 +426,27 @@ void
|
|||||||
void
|
void
|
||||||
Emitter::TrackSeekVoltage(Scalar time_slice)
|
Emitter::TrackSeekVoltage(Scalar time_slice)
|
||||||
{
|
{
|
||||||
VoltageSource *src = (VoltageSource *)FUN_00417ab4(this + 0x1d0);
|
// The powering Generator, attached to voltageSource@0x1D0 by the chained
|
||||||
Scalar dtScale = FUN_004b0d50(this); // PoweredSubsystem dt scale
|
// PoweredSubsystem ctor (authored "VoltageSource" roster index). The binary
|
||||||
|
// dereferences it unguarded; the port keeps charging inert if the link is
|
||||||
// No voltage source resolved (the source plug at this+0x1d0 binds via the registry;
|
// gone (source destroyed) -- the electrical alarm handles the state.
|
||||||
// not yet linked in the bring-up) or a zero dt scale -> can't charge this frame.
|
Generator *src = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1d0)
|
||||||
// Guard the deref instead of crashing; charging resumes once the source links.
|
if (src == 0)
|
||||||
if (src == 0 || dtScale == 0.0f)
|
|
||||||
return;
|
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
|
currentLevel = (seekRate / energyCoefficient) * time_slice + currentLevel; // 0x414
|
||||||
|
|
||||||
if (FUN_004ad7d4(this)) // heatable/online
|
if (FUN_004ad7d4(this)) // heatable/online
|
||||||
{
|
{
|
||||||
// Raw @004ba838:7544 feeds rate^2*dt into the RESOLVED SOURCE's pendingHeat
|
// @004ba838:7544 -- the charging I^2R loss lands in the GENERATOR's
|
||||||
// (src+0x1c8). The VoltageSource overlay here is a compact shortcut reading
|
// pendingHeat (src+0x1c8): recharging weapons is what heats generators
|
||||||
// offsets 0/4/8/0xc, NOT the real 0x1dc/0x1c8 -- inert in bring-up (src never
|
// (~3.5e8 per full PPC charge -> +~480K on a 726000-mass generator),
|
||||||
// resolves, guarded above). FOLLOW-UP (Emitter re-base tail): type src as the
|
// which conduct to their authored condensers and slow further charging
|
||||||
// real HeatSink-derived source and write src->pendingHeat@0x1c8.
|
// through ChargeTimeScale. This closes the heat/firepower feedback.
|
||||||
src->heatLoad += seekRate * seekRate * dtScale * time_slice;
|
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).
|
// current seekVoltage index and is itself in the "ready" state (source+0x210==2).
|
||||||
//
|
//
|
||||||
Logical
|
Logical
|
||||||
Emitter::ReadyToDischarge(VoltageSource *source)
|
Emitter::ReadyToDischarge(Generator *source)
|
||||||
{
|
{
|
||||||
if (source == 0)
|
if (source == 0)
|
||||||
{
|
{
|
||||||
source = (VoltageSource *)FUN_00417ab4(this + 0x1d0);
|
source = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1d0)
|
||||||
}
|
}
|
||||||
if (source == 0)
|
if (source == 0)
|
||||||
{
|
{
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
if (seekVoltage[seekVoltageIndex] <= source->voltage // 0x400[idx] <= src+0x1dc
|
if (seekVoltage[seekVoltageIndex] <= source->MeasuredVoltage() // 0x400[idx] <= src+0x1dc
|
||||||
&& source->state == 2) // src+0x210
|
&& source->GeneratorStateOf() == 2) // src+0x210
|
||||||
{
|
{
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
@@ -814,39 +823,66 @@ Emitter::Emitter(
|
|||||||
// energyTotal = (damageAmount + heatCostToFire) * 1.0e7
|
// energyTotal = (damageAmount + heatCostToFire) * 1.0e7
|
||||||
energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; // _DAT_004bb3ac
|
energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; // _DAT_004bb3ac
|
||||||
|
|
||||||
// SeekVoltage curve is only meaningful for an electrically-driven weapon
|
// SeekVoltage curve is only meaningful for an electrically-driven MASTER.
|
||||||
// ((flags & 0xC)==0 && (flags & 0x100)!=0): copy the per-index voltages,
|
// @004bb120:7872 reads the OWNER's simulationFlags (param_2+0x28) -- the
|
||||||
// scaled by the linked voltage source's rated voltage, then size the curve.
|
// same owner-gate every sibling subsystem ctor uses (the old this-flags
|
||||||
if ((GetFlags() & 0xC) == 0 && (GetFlags() & 0x100) != 0)
|
// 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);
|
// The powering Generator, attached by the chained PoweredSubsystem ctor
|
||||||
for (int i = 0; i < 5; ++i)
|
// (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
|
seekVoltage[i] = 0.0f; // 0x400[i]
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
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
|
// (task #8 note: energyCoefficient deliberately stays on the bring-up
|
||||||
|
|||||||
@@ -112,14 +112,10 @@ class NotationFile;
|
|||||||
};
|
};
|
||||||
typedef Emitter__UpdateRecord UpdateRecord;
|
typedef Emitter__UpdateRecord UpdateRecord;
|
||||||
|
|
||||||
// Proxy for the linked voltage source (Generator segment) the decomp reads
|
// (task #10: the old compact "VoltageSource" proxy struct is RETIRED --
|
||||||
// through the SharedData connection (voltage/state/ratedVoltage/heatLoad).
|
// the +0x1d0 plug resolves to the real Generator, whose named members
|
||||||
struct VoltageSource {
|
// carry the decomp offsets: outputVoltage@0x1dc, ratedVoltage@0x1d8,
|
||||||
Scalar voltage;
|
// state@0x210, pendingHeat@0x1c8.)
|
||||||
int state;
|
|
||||||
Scalar ratedVoltage;
|
|
||||||
Scalar heatLoad;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// State / flag accessors the recovered bodies read (mapped onto the
|
// 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
|
// @004ba6e0 -- True when the supplied (or linked) voltage source can
|
||||||
// satisfy the current seekVoltage index and is in the "ready" state (2).
|
// satisfy the current seekVoltage index and is in the "ready" state (2).
|
||||||
Logical
|
Logical
|
||||||
ReadyToDischarge(VoltageSource *source); // slot 16
|
ReadyToDischarge(Generator *source); // slot 16
|
||||||
|
|
||||||
// @004ba738 -- recompute outputVoltage = currentLevel / seekVoltage[idx],
|
// @004ba738 -- recompute outputVoltage = currentLevel / seekVoltage[idx],
|
||||||
// snapped to 1.0 near full and clamped to [0,1].
|
// snapped to 1.0 near full and clamped to [0,1].
|
||||||
|
|||||||
+28
-10
@@ -38,6 +38,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <bt.hpp>
|
#include <bt.hpp>
|
||||||
|
#include <map> // BT_HEAT_LOG census (diag only)
|
||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#if !defined(HEAT_HPP)
|
#if !defined(HEAT_HPP)
|
||||||
@@ -505,6 +506,15 @@ HeatSink::HeatSink(
|
|||||||
// @004adda0: "Bad subsystem resource ->heatSink" HEAT.CPP:0x25F
|
// @004adda0: "Bad subsystem resource ->heatSink" HEAT.CPP:0x25F
|
||||||
Verify(False, "Bad subsystem resource ->heatSink", __FILE__, 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() : "<NULL/not-built-yet>")
|
||||||
|
<< " mass=" << subsystem_resource->thermalMass
|
||||||
|
<< " k=" << subsystem_resource->thermalConductance
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(owner->simulationFlags & SegmentCopyMask) == 0 // param_2+0x28 & 0xc == 0
|
(owner->simulationFlags & SegmentCopyMask) == 0 // param_2+0x28 & 0xc == 0
|
||||||
@@ -602,17 +612,25 @@ void
|
|||||||
currentTemperature = heatEnergy / thermalMass;
|
currentTemperature = heatEnergy / thermalMass;
|
||||||
UpdateHeatLoad();
|
UpdateHeatLoad();
|
||||||
|
|
||||||
// BRING-UP verify (1 Hz, viewpoint mech only): show the mech heat climbing as
|
// DIAG census (BT_HEAT_LOG, viewpoint mech): PER-INSTANCE 5-s timers --
|
||||||
// weapons fire and relaxing when they stop. pendingHeat just absorbed above.
|
// the old shared static timer aliased to whichever instance crossed the
|
||||||
static Scalar s_heatLog = 0.0f;
|
// tick (the diagnostic-sampler trap), which manufactured the task-#10
|
||||||
s_heatLog += time_slice;
|
// "heat pools in Condenser1" misread.
|
||||||
if (s_heatLog >= 1.0f && application != 0
|
if (getenv("BT_HEAT_LOG") && application != 0
|
||||||
&& (Entity *)owner == application->GetViewpointEntity() && pendingHeat > 0.0f)
|
&& (Entity *)owner == application->GetViewpointEntity())
|
||||||
{
|
{
|
||||||
s_heatLog = 0.0f;
|
static std::map<const void *, Scalar> s_census;
|
||||||
DEBUG_STREAM << "[heat] " << GetName() << " temp=" << currentTemperature
|
Scalar &acc = s_census[this];
|
||||||
<< " heatEnergy=" << heatEnergy << " absorbed=" << pendingHeat
|
acc += time_slice;
|
||||||
<< " heatLoad=" << heatLoad << "\n" << std::flush;
|
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;
|
pendingHeat = 0.0f;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#if !defined(HEATFAMILY_RESLICE_HPP)
|
#if !defined(HEATFAMILY_RESLICE_HPP)
|
||||||
# include <heatfamily_reslice.hpp>
|
# include <heatfamily_reslice.hpp>
|
||||||
|
# include <app.hpp> // application (BT_HEAT_LOG census)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define JM_CLOSE_ENOUGH 0.0001f // _DAT_004ae8ac / _DAT_004af3a0
|
#define JM_CLOSE_ENOUGH 0.0001f // _DAT_004ae8ac / _DAT_004af3a0
|
||||||
@@ -769,8 +770,27 @@ void
|
|||||||
* (coolantLevel / thermalCapacity) * coolantFlowScale)
|
* (coolantLevel / thermalCapacity) * coolantFlowScale)
|
||||||
/ thermalMass);
|
/ thermalMass);
|
||||||
Scalar decay = 1.0f - (Scalar)exp(ex); // FUN_004dca38
|
Scalar decay = 1.0f - (Scalar)exp(ex); // FUN_004dca38
|
||||||
pendingHeat += -((currentTemperature * massScale - target)
|
Scalar shed = -((currentTemperature * massScale - target)
|
||||||
* thermalMass * decay);
|
* 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
|
// coolant top-up from the reservoir (the binary tail: deficit > 0 and
|
||||||
|
|||||||
@@ -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
|
// Subsystem virtual overrides
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -216,6 +216,15 @@ class Generator;
|
|||||||
Subsystem*
|
Subsystem*
|
||||||
ResolveVoltageSource() { return voltageSource.Resolve(); }
|
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.
|
// Local data for the powered-subsystem class.
|
||||||
// Offsets are byte offsets into the shipped object.
|
// Offsets are byte offsets into the shipped object.
|
||||||
@@ -455,6 +464,8 @@ class Generator;
|
|||||||
GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210
|
GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210
|
||||||
Scalar
|
Scalar
|
||||||
MeasuredVoltage() { return outputVoltage; } // source+0x1dc
|
MeasuredVoltage() { return outputVoltage; } // source+0x1dc
|
||||||
|
Scalar
|
||||||
|
RatedVoltageOf() { return ratedVoltage; } // source+0x1d8
|
||||||
// The generator's "short" state is its stateAlarm level (GeneratorShorted==3) +
|
// The generator's "short" state is its stateAlarm level (GeneratorShorted==3) +
|
||||||
// outputVoltage (IsSourceShorted @004b0b5c reads 0x210/0x1dc). There is NO
|
// outputVoltage (IsSourceShorted @004b0b5c reads 0x210/0x1dc). There is NO
|
||||||
// separate shortFlag field on the Generator (it ends at 0x250 == stateAlarm end).
|
// separate shortFlag field on the Generator (it ends at 0x250 == stateAlarm end).
|
||||||
|
|||||||
@@ -529,6 +529,28 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
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
|
// 1. Recover the firing charge (recoil counts down toward 0 in
|
||||||
// DrawFiringCharge) and advance any in-progress magazine eject.
|
// DrawFiringCharge) and advance any in-progress magazine eject.
|
||||||
DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8)
|
DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8)
|
||||||
|
|||||||
Reference in New Issue
Block a user