TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and live-verified (hold-configure -> tap fire button -> toggle membership -> release-commit): - MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept) + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/ MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch). - Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration / +0x40,+0x44 AddOrErase evt/dir. No "secondary vtable @0050f498", no "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP). L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO mapper implements the toggle (Thrustmaster no-ops = can't regroup). - MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the destination to &fireImpulse (@004b99a8). - ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table; buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale was wrong). Finding [T1]: the shipped binary NEVER enables this gauge (no SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable. - Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon); BT_CONFIG_TEST scripts a headless verify. @004afbc4 corrected to its real Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups). - Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong. TASK #11 -- the user-reported live-play regressions (both real bugs): - PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the victim on its OWN 0.3s cadence whenever fire was held -- desynced once the authentic recharges landed. Retired (the real impact visual flows from each discharge via the messmgr SubmitExplosion). - WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness, and the weapon sticks in Loading at level ~10000 forever (observed live). Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick -- also fixes the I^2R integral over-heating generators on big steps. NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap). - Duty-cycle measured (max-rate autofire): generators equilibrate in the degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker never trips solo, PPCs sustain ~11.8-damage full-charge fire. The slow kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's counters (this regrouping UI, generator reassignment, coolant valves) are the reconstruction queue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1055 lines
43 KiB
C++
1055 lines
43 KiB
C++
//===========================================================================//
|
|
// File: emitter.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Emitter -- energy/beam weapon base (PPC, GaussRifle derive) //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 JM Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
|
|
// pseudo-C for the module cluster @004ba478-@004bb478 (file=bt/emitter.cpp).
|
|
// Method and member names follow the surviving sources EMITTER.TCP, PPC.CPP,
|
|
// GAUSS.CPP/GAUSS.HPP and the reconstructed base mechweap.cpp; the remainder
|
|
// are inferred from usage and flagged best-effort. Each non-trivial method
|
|
// cites the originating @ADDR.
|
|
//
|
|
// Resolved read-only constants (4 LE bytes from section_dump CODE):
|
|
// _DAT_004ba818 = 00 00 00 00 = 0.0f (zero / lower clamp)
|
|
// _DAT_004ba81c = 17 b7 d1 38 = 1.0e-4 (currentLevel "non-zero" epsilon)
|
|
// _DAT_004ba820 = 00 00 80 3f = 1.0f (full-charge reference / upper clamp)
|
|
// _DAT_004ba824 = 0a d7 23 3c = 0.01f (snap-to-1.0 tolerance)
|
|
// _DAT_004ba828 = 00 00 00 00 = 0.0f (below-range clamp result)
|
|
// _DAT_004ba830 = 00 00 00 00 = 0.0f (above-range clamp result)
|
|
// _DAT_004ba9a4 = 00 00 00 00 = 0.0f (dischargeTimer expiry threshold)
|
|
// _DAT_004bac04 = 00 00 80 3f = 1.0f (outputVoltage "fully charged" test)
|
|
// _DAT_004bac08 = 00 00 00 00 = 0.0f (firing dischargeTimer expiry threshold)
|
|
// _DAT_004bafb8 = 00 00 00 3f = 0.5f (1/2 in the E = 1/2 k V^2 energy term)
|
|
// _DAT_004bb3ac = 80 96 18 4b = 1.0e7 (energyTotal unit scale)
|
|
// _DAT_004bb3b0 = 00 00 80 bf = -1.0f (SeekVoltage "unset" sentinel)
|
|
// _DAT_004bb3b4 = 00 00 00 3f = 0.5f (energyCoefficient denominator term)
|
|
// _DAT_004bb3c4 = 00 00 80 3f = 1.0f (voltage-curve constant)
|
|
// _DAT_004bb428 = 00 00 00 3f = 0.5f
|
|
// _DAT_004bb798 = 00 00 80 bf = -1.0f (resource "unset" sentinel)
|
|
// (the firing-physics multipliers _DAT_004bafbc, _DAT_004bb3b8 sit inside
|
|
// this TU's literal pool and resolve to non-finite singles in the dump;
|
|
// they are part of larger double-precision operands and are left
|
|
// symbolic below -- a human should re-derive them from the FPU stream.)
|
|
//
|
|
// Helper-function name mapping (engine internals referenced by the decomp):
|
|
// FUN_004b99a8 MechWeapon base constructor FUN_004b9b9c ~MechWeapon
|
|
// FUN_004b96ec MechWeapon::ResetToInitialState FUN_004b96d4 MechWeapon::ReadUpdateRecord
|
|
// FUN_004b964c MechWeapon::TakeDamage FUN_004b9690 MechWeapon::WriteUpdateRecord
|
|
// FUN_004b9d00 MechWeapon::PrintState FUN_004b9d10 MechWeapon::CreateStreamedSubsystem
|
|
// FUN_004b9608 MechWeapon::CheckFireEdge FUN_004b9bdc MechWeapon::UpdateTargeting
|
|
// FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9864 MechWeapon::ComputeAimOrientation
|
|
// FUN_004b9728 MechWeapon::DrawWeaponPip FUN_004b9cbc MechWeapon::GetTargetPosition
|
|
// FUN_004b0bd0 PoweredSubsystem simulation step FUN_004b0d50 PoweredSubsystem scalar query
|
|
// FUN_004ad7d4 HeatableSubsystem "is online/active" FUN_004ac9c8 HeatableSubsystem heat-state query
|
|
// FUN_00417ab4 follow a SharedData link (voltage source / ammo bin)
|
|
// FUN_0049fb54 Mech::IsDestroyed (movementMode 2||9) FUN_0041a1a4 IsDerivedFrom(classDerivations)
|
|
// FUN_0041bbd8 AlarmIndicator::SetLevel(n) (weaponAlarm @0x350, state @0x364)
|
|
// FUN_0041c350 Subsystem::SendMessage(template, a, b)
|
|
// FUN_00408440 Vector copy FUN_00409968 Vector set FUN_0040a968 Vector negate/copy
|
|
// FUN_00408644 Vector subtract FUN_00408bf8 Vector subtract (pt - pt) FUN_0040a138 normalise
|
|
// FUN_0040aadc Matrix identity FUN_00424da8 resolve segment world point
|
|
// FUN_0041f216 / FUN_0041f235 segment-iterator construct / destruct
|
|
// FUN_004dd138 sqrtf FUN_004dce24 expf/logf FUN_004dcd00 fabsf
|
|
// FUN_00404118 Read(scalar) FUN_00404720 Read(list) FUN_00403b60 list-empty?
|
|
// FUN_004dbb24 DebugStream<< FUN_004d9c38 DebugStream flush FUN_0040385c Verify()
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(EMITTER_HPP)
|
|
# include <emitter.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
#include <hostmgr.hpp> // HostManager::GetEntityPointer -- the update-record target resolve (task #51)
|
|
|
|
// E8 (bring-up): the player fire input. The controls mapper that would normally write
|
|
// the weapon's fireImpulse is bypassed (mech4.cpp), so EmitterSimulation reads this
|
|
// global directly. Defined in btl4main.cpp; layout must match (see mech4.cpp).
|
|
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
|
|
int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
|
|
extern BTDriveInput gBTDrive;
|
|
|
|
|
|
//#############################################################################
|
|
// Reconstruction support: artifact constants, globals and helper stand-ins.
|
|
//
|
|
static const Scalar kDamageScale = 1.0e-7f; // _DAT_004bafbc -- an x87 80-bit constant
|
|
// (bytes bc427ae5d594bfd6e73f @0x4bafbc)
|
|
// = 1e-7 EXACTLY: it cancels the ctor's
|
|
// energyTotal x1e7, so at recommended
|
|
// charge damagePortion == the AUTHORED
|
|
// DamageAmount verbatim (task #8; the old
|
|
// 1.0f "best-effort" was the whole
|
|
// damage-economy mystery)
|
|
static const Vector3D DAT_004e0fa4(0.0f, 0.0f, 0.0f); // zero vector
|
|
static char DAT_00522524[4] = { 0 }; // default colour tag
|
|
static int PTR_LAB_00511e6c, DAT_00511e70, DAT_00511e74; // beam-keepalive message template
|
|
static int PTR_LAB_00511e78, DAT_00511e7c, DAT_00511e80; // beam-keepalive message template
|
|
|
|
static Scalar Fabs(Scalar x) { return (x < 0.0f) ? -x : x; }
|
|
static void Copy(Vector3D &o, const Vector3D &v) { o = v; }
|
|
static void Set(Vector3D &o, const Vector3D *v) { o = *v; }
|
|
static void Subtract(Vector3D &o, const Point3D &a, const Point3D &b) { o.x=a.x-b.x; o.y=a.y-b.y; o.z=a.z-b.z; }
|
|
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 void CopyColor(void *, const void *) {}
|
|
|
|
// engine-internal artifacts the decomp called by address (stand-ins)
|
|
static int FUN_004ad7d4(void *) { return 1; } // HeatableSubsystem active?
|
|
// FUN_0049fb54 == Mech::IsDestroyed (movementMode 2||9; reconstructed as
|
|
// Mech::IsDisabled). Was a mislabeled "cockpit/HUD query" no-op stub -- so a
|
|
// live beam never CUT when its target mech died (@004ba8d0/@004baa20 check
|
|
// this every discharge frame), and a destroyed mech's own weapons kept firing
|
|
// (@004baa88). The binary's call sites first verify the object IS a Mech
|
|
// (IsDerivedFrom vs 0x50bdb4 = Mech::ClassDerivations), reproduced here.
|
|
static int BTMechDestroyed(Entity *e)
|
|
{
|
|
if (e == 0)
|
|
return 0;
|
|
if (!e->IsDerivedFrom(*Mech::GetClassDerivations()))
|
|
return 0;
|
|
return ((Mech *)e)->IsDisabled() ? 1 : 0;
|
|
}
|
|
// (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_00421070(void *, void *) { return 0; } // damage source resolve
|
|
static void *ResolveSource(void *) { return 0; } // named source registry lookup
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation
|
|
Emitter::ClassDerivations(
|
|
&MechWeapon::ClassDerivations,
|
|
"Emitter"
|
|
);
|
|
|
|
// task #6: inherit MechWeapon's handler set (ids 9/10 ConfigureMappables/
|
|
// ChooseButton) -- dispatch resolves through the CONCRETE class's SharedData,
|
|
// so an empty own-set here silently swallowed the config messages.
|
|
Receiver::MessageHandlerSet&
|
|
Emitter::GetMessageHandlers()
|
|
{
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
MechWeapon::GetMessageHandlers()); // copy-inherit
|
|
return messageHandlers;
|
|
}
|
|
Simulation::AttributeIndexSet Emitter::AttributeIndex;
|
|
|
|
Emitter::SharedData
|
|
Emitter::DefaultData(
|
|
&Emitter::ClassDerivations,
|
|
Emitter::GetMessageHandlers(),
|
|
MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty)
|
|
Emitter::StateCount
|
|
);
|
|
|
|
|
|
//#############################################################################
|
|
// State / flag accessors (recovered reads mapped onto the engine members).
|
|
//
|
|
int Emitter::GetWeaponState() { return weaponAlarm.GetLevel(); } // this+0x364
|
|
int Emitter::GetVoltageState() { return electricalStateAlarm.GetLevel(); } // this+0x278
|
|
LWord Emitter::GetFlags() { return simulationFlags; }
|
|
// 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; }
|
|
|
|
|
|
//#############################################################################
|
|
// Firing -- the real discharge
|
|
//
|
|
// @004bace8 -- vtable slot 18. Only fires when the owning Mech actually has a
|
|
// target (entity+0x388 != 0). It converts the stored charge into a damage
|
|
// portion and a self-heat portion (E = 1/2 k V^2, split by damageFraction),
|
|
// applies the heat to itself, drains the charge, then builds the beam geometry
|
|
// (muzzle -> target), aims it, registers the hit/pip and marks the weapon dirty
|
|
// for replication. PPC::FireWeapon (@004bb878) tail-calls straight into this.
|
|
//
|
|
void
|
|
Emitter::FireWeapon()
|
|
{
|
|
if (!HasActiveTarget()) // entity+0x388 == 0
|
|
{
|
|
return;
|
|
}
|
|
|
|
// re-arm the beam-on countdown
|
|
dischargeTimer = dischargeTime; // 0x440 = 0x43c
|
|
|
|
// E = 1/2 * currentLevel^2 * energyCoefficient
|
|
Scalar energy = 0.5f * currentLevel * currentLevel * energyCoefficient; // _DAT_004bafb8
|
|
|
|
// THE AUTHENTIC PER-SHOT DAMAGE (task #8) -- the closed form of the
|
|
// binary's energy algebra (@004bb120 ctor x @004bace8 fire):
|
|
// damagePortion = dF x (0.5 V^2 EC) x 1e-7
|
|
// = authored DamageAmount x (charge / seekV[rec])^2
|
|
// (kDamageScale = _DAT_004bafbc = 1e-7 EXACTLY cancels the ctor's x1e7;
|
|
// derived here from the STORED damageFraction/energyTotal so the authored
|
|
// base never degrades, and the bring-up charge cycle -- calibrated to the
|
|
// degenerate EC=1 electrical model -- stays untouched. At full charge:
|
|
// the authored value verbatim; madcat ERLLaser=6, bhk1 PPC=12.)
|
|
{
|
|
Scalar vRec = seekVoltage[seekVoltageRecommendedIndex];
|
|
Scalar chargeRatio = (vRec > 0.0f) ? (currentLevel / vRec) : 1.0f;
|
|
damagePortion = (damageFraction * energyTotal * kDamageScale)
|
|
* chargeRatio * chargeRatio; // 0x44c
|
|
}
|
|
// THE AUTHENTIC HEAT (task #9; the calibration audit verified the whole
|
|
// HeatSink chain is 1e7-unit-native and formula-exact): the closed form
|
|
// mirroring the damage fix -- heatPortion = (1 - dF) x energyTotal x
|
|
// (charge/seekV)^2 = heatCostToFire x 1e7 at full charge (PPC = 1.1e8 ->
|
|
// +632 K on its own 174000-mass sink, conducting to the 1.39e6 bank;
|
|
// authored thresholds 77 start / 1000 degradation / 2000 failure).
|
|
{
|
|
Scalar vRecH = seekVoltage[seekVoltageRecommendedIndex];
|
|
Scalar chargeRatioH = (vRecH > 0.0f) ? (currentLevel / vRecH) : 1.0f;
|
|
heatPortion = (1.0f - damageFraction) * energyTotal
|
|
* chargeRatioH * chargeRatioH; // 0x450
|
|
}
|
|
(void)energy;
|
|
|
|
// dump the heat portion into our INHERITED thermal accumulator if heatable/online.
|
|
// E5: pendingHeat (HeatSink @0x1C8) is the field the heat sim absorbs each frame
|
|
// (heatEnergy += pendingHeat -> temperature); the old Emitter-local heatAccumulator
|
|
// was a SHADOW at the wrong offset, so firing never reached the heat model.
|
|
if (FUN_004ad7d4(this)) // HeatableSubsystem active
|
|
{
|
|
pendingHeat += heatPortion; // inherited HeatSink::pendingHeat @0x1c8
|
|
}
|
|
|
|
// 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++ % 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;
|
|
|
|
// recompute output voltage now that the charge is about to be spent
|
|
ComputeOutputVoltage(); // (*vtable+0x44)()
|
|
currentLevel = 0.0f; // 0x414 -- discharge
|
|
firingActive = 1; // 0x418
|
|
|
|
// --- build the beam: muzzle point -> target point ---
|
|
Point3D muzzlePoint;
|
|
GetMuzzlePoint(muzzlePoint); // FUN_004b9948 (cached muzzle)
|
|
Point3D targetPoint;
|
|
GetTargetPosition(targetPoint); // entity+0x37c (cached target)
|
|
|
|
beamFlag = 1; // 0x46c
|
|
|
|
Vector3D beamVector;
|
|
beamVector.Subtract(targetPoint, muzzlePoint); // FUN_00408bf8
|
|
LinearMatrix aimTransform; // was the phantom beamHitData member
|
|
ComputeAimOrientation(aimTransform, beamVector); // FUN_004b9864 -> aim matrix
|
|
|
|
Vector3D delta;
|
|
delta.Subtract(targetPoint, muzzlePoint); // FUN_00408644
|
|
Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138
|
|
beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438
|
|
|
|
if (dist <= effectiveRange) // this+0x328
|
|
{
|
|
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
|
|
// fill the inherited Damage record -- amount = this shot's
|
|
// damagePortion (== authored DamageAmount at full charge),
|
|
// damageForce = target - muzzle (ALSO the gyro's directional
|
|
// hit-bounce source, retiring its random fallback), impactPoint =
|
|
// the target point -- then submit through SendDamageMessage
|
|
// (@004b9728) into the owner's SubsystemMessageManager.
|
|
damageData.damageAmount = damagePortion; // @0x3AC
|
|
damageData.damageForce.Subtract(targetPoint, muzzlePoint); // @0x3B0
|
|
damageData.impactPoint = targetPoint; // @0x3C8
|
|
damageData.burstCount = 1;
|
|
Entity *submitTarget = (owner != 0)
|
|
? *(Entity **)((char *)owner + 0x388) : 0; // mech target slot
|
|
SendDamageMessage(submitTarget); // FUN_004b9728
|
|
}
|
|
|
|
// stash the beam endpoint for replication (world hit point) + the beam's
|
|
// TARGET (0x474 -- the discharge checks read it every frame: the beam cuts
|
|
// the moment this mech is destroyed). The owner's target slot is the same
|
|
// raw 0x388 the rest of the weapon path uses (HasActiveTarget/mech4).
|
|
beamEndpoint = targetPoint; // 0x460 <- targetPoint
|
|
targetEntity = (owner != 0)
|
|
? *(Entity **)((char *)owner + 0x388) : 0; // 0x474 <- mech target entity
|
|
targetLocalFlag = 1; // 0x470
|
|
|
|
SetDirty(); // this+6 |= 1 (needs replication)
|
|
}
|
|
|
|
//
|
|
// @004bb878 -- PPC::FireWeapon, reproduced from PPC.CPP for context: it simply
|
|
// chains to Emitter::FireWeapon(). GaussRifle::FireWeapon overrides instead
|
|
// (GAUSS.CPP: outputVoltage = 0.0f).
|
|
//
|
|
// void PPC::FireWeapon() { Emitter::FireWeapon(); }
|
|
//
|
|
|
|
|
|
//#############################################################################
|
|
// Simulation -- the installed per-frame "performance"
|
|
//
|
|
// @004baa88 -- run the PoweredSubsystem step, sample the fire-edge and the
|
|
// target range, then advance the Loading -> Loaded -> Firing state machine.
|
|
// The "state" is carried in the weapon alarm level (this+0x364):
|
|
// 0 = Firing 2 = Loaded(ready) 3 = Loading 4 = TriggerDuringLoad.
|
|
//
|
|
void
|
|
Emitter::EmitterSimulation(Scalar time_slice)
|
|
{
|
|
// Real PoweredSubsystem step (@004b0bd0): runs HeatSink::HeatSinkSimulation
|
|
// (absorb pendingHeat -> heatEnergy/temperature, then ConductHeat flows it to
|
|
// the linked central sink) and advances the electrical state machine. This is
|
|
// what makes weapon self-heat propagate to the mech's heat sinks.
|
|
PoweredSubsystem::PoweredSubsystemSimulation(time_slice);
|
|
|
|
// AUTHENTIC TRIGGER (task #5): fireImpulse IS the published TriggerState
|
|
// attribute (id 0x13); the streamed per-mech controls map (type-6 resource,
|
|
// installed at MakeViewpointEntity) binds each grouped fire BUTTON directly
|
|
// onto it, and the keyboard/harness pushes press/release edges into the
|
|
// LBE4 buttonGroup channels (mech4). The old gBT*Trigger bypass + the
|
|
// per-type keyboard split are retired -- weapons sharing a button now fire
|
|
// TOGETHER (the pod's authored groups: e.g. madcat Trigger = 4 weapons).
|
|
Logical fireEdge = CheckFireEdge(); // @004b9608
|
|
// task #8 diag: the discharge-chain probe (button->TriggerState->edge->FSM)
|
|
if (getenv("BT_FIRE_LOG"))
|
|
{
|
|
static int s_fp = 0;
|
|
if ((++s_fp % 97) == 0 || fireEdge) // 97 prime: rotates through all weapons
|
|
DEBUG_STREAM << "[fire-probe] " << GetName()
|
|
<< " owner=" << (void *)owner
|
|
<< " trigState=" << (float)fireImpulse
|
|
<< " edge=" << (int)fireEdge
|
|
<< " alarm=" << (int)weaponAlarm.GetState()
|
|
<< " level=" << (float)currentLevel
|
|
<< " tgt=" << (void *)((owner != 0)
|
|
? *(Entity **)((char *)owner + 0x388) : 0)
|
|
<< std::endl;
|
|
}
|
|
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
|
|
|
// hard failure: powered-off, faulted, or the OWNING MECH destroyed (@004baa88
|
|
// checks IsDestroyed on the owner -- a dead mech's weapons drop everything)
|
|
if (GetFlags() == 1 || GetFaultState() == 2 || BTMechDestroyed((Entity *)owner))
|
|
{
|
|
ResetFiringState(); // @004ba9a8
|
|
currentLevel = 0.0f; // 0x414
|
|
ComputeOutputVoltage(); // (*vtable+0x44)()
|
|
return;
|
|
}
|
|
|
|
switch (GetWeaponState()) // this+0x364
|
|
{
|
|
case 0: // Firing -- count the beam-on timer down
|
|
dischargeTimer -= time_slice; // 0x440
|
|
if (dischargeTimer <= 0.0f) // _DAT_004bac08
|
|
{
|
|
ResetFiringState();
|
|
}
|
|
else
|
|
{
|
|
ContinueDischarge(); // @004baa20
|
|
}
|
|
break;
|
|
|
|
case 2: // Loaded -- ready; fire on the trigger's rising edge
|
|
if (fireEdge)
|
|
{
|
|
if (useConfiguredPip && HasActiveTarget()) // this+0x3E0 (Loaded->Firing gate) && entity+0x388
|
|
{
|
|
weaponAlarm.SetLevel(0); // -> Firing
|
|
FireWeapon(); // (*vtable+0x48)()
|
|
ContinueDischarge();
|
|
}
|
|
else
|
|
{
|
|
weaponAlarm.SetLevel(4); // TriggerDuringLoad
|
|
weaponAlarm.SetLevel(2); // back to Loaded
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 3: // Loading -- keep charging
|
|
if (fireEdge)
|
|
{
|
|
weaponAlarm.SetLevel(4); // TriggerDuringLoad blip
|
|
weaponAlarm.SetLevel(3);
|
|
}
|
|
// E7 RETIRED (task #10): the authentic charge path is live (authored
|
|
// generator + calibrated voltageScale/EC; TrackSeekVoltage integrates
|
|
// the charge over the authored RechargeRate seconds).
|
|
//
|
|
// POD-FRAME SUB-STEPPING (task #11, THE WEAPON-BRICK FIX): the binary's
|
|
// Loading tick assumes the pod's LOCKED 60 fps -- the charge integrates
|
|
// toward the generator's 10000V and the Loaded transition only fires
|
|
// while rechargeLevel crosses the +-0.01 snap window around seekV
|
|
// (level in [7920, 8080]; ~15 pod frames wide). Under the port's
|
|
// variable dt, one spike frame (dt >= ~0.25s) jumps the whole window:
|
|
// the level overshoots, ComputeOutputVoltage's byte-verified >1.0
|
|
// clamp (_DAT_004ba830 = 0.0) zeroes rechargeLevel, and the weapon is
|
|
// PERMANENTLY stuck Loading at level ~10000 (observed live:
|
|
// "level=9999.68 alarm=3 edge=1"). Fix: run the binary's own
|
|
// tick -- TrackSeekVoltage + ComputeOutputVoltage + the Loaded test --
|
|
// at the binary's own frame rate. This also keeps the I^2R generator
|
|
// feed integral accurate (big steps evaluate seekRate at a stale level
|
|
// and OVER-heat the generators). Sub-step count is bounded; leftover
|
|
// time under extreme throttling just resumes next frame.
|
|
{
|
|
static const Scalar kPodFrame = 1.0f / 60.0f;
|
|
Scalar remaining = time_slice;
|
|
int guard = 600; // bound: 10s of sim per frame
|
|
while (remaining > 0.0f && guard-- > 0)
|
|
{
|
|
Scalar slice = (remaining < kPodFrame) ? remaining : kPodFrame;
|
|
remaining -= slice;
|
|
if (GetVoltageState() == 4) // this+0x278 == Ready
|
|
{
|
|
TrackSeekVoltage(slice); // @004ba838
|
|
}
|
|
ComputeOutputVoltage(); // (*vtable+0x44)()
|
|
if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged
|
|
{
|
|
weaponAlarm.SetLevel(2); // -> Loaded
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Charge / discharge helpers
|
|
//
|
|
|
|
//
|
|
// @004ba838 -- integrate the charge from the linked voltage source. Derive a
|
|
// seek rate from (sourceVoltage - currentLevel)/dt, step currentLevel by
|
|
// rate/energyCoefficient * dt, and (if heatable) feed rate^2 * dt back as heat.
|
|
//
|
|
void
|
|
Emitter::TrackSeekVoltage(Scalar time_slice)
|
|
{
|
|
// 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;
|
|
|
|
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
|
|
{
|
|
// @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;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004ba8d0 -- service an active discharge / beam. When the firing flag is
|
|
// clear, just mark for replication. Otherwise decrement dischargeTimer; on
|
|
// expiry (or when the HUD refuses the beam) tear the beam down and re-arm to
|
|
// the Loaded state, restoring dischargeTimer from dischargeTime.
|
|
//
|
|
void
|
|
Emitter::ServiceDischarge(Scalar time_slice)
|
|
{
|
|
if (firingActive == 0) // 0x418
|
|
{
|
|
SetDirty(); // this+0x28 |= 2
|
|
return;
|
|
}
|
|
|
|
weaponAlarm.SetLevel(0); // Firing
|
|
dischargeTimer -= time_slice; // 0x440
|
|
if (dischargeTimer <= 0.0f // _DAT_004ba9a4
|
|
|| BTMechDestroyed(targetEntity)) // the beam CUTS when its target mech dies
|
|
{
|
|
firingActive = 0;
|
|
weaponAlarm.SetLevel(2); // Loaded
|
|
beamFlag = 0; // 0x46c
|
|
dischargeTimer = dischargeTime; // 0x440 = 0x43c
|
|
SetDirty();
|
|
// replicate the beam END: one final Emitter update record turns the
|
|
// peer's copy off (the 1995 flush wrote idle-flagged subsystems too).
|
|
ForceUpdate();
|
|
return;
|
|
}
|
|
|
|
// keep the beam alive for another frame -- BEAM REPLICATION (task #51):
|
|
// the 1995 keepalive (FUN_0041c350, template @0x511e6c) marked the
|
|
// subsystem dirty + queued the local deferred beam-effect callback; the
|
|
// local draw is our per-weapon render walk, and the dirty-mark maps to the
|
|
// engine's updateModel bit -- the next PerformAndWatch serializes THIS
|
|
// emitter's record (@004ba65c) into the mech's update stream, and the
|
|
// peer's replicant emitter applies it (@004ba568) so its own walk draws
|
|
// the beam.
|
|
ForceUpdate();
|
|
(void)PTR_LAB_00511e6c; (void)DAT_00511e70; (void)DAT_00511e74;
|
|
}
|
|
|
|
//
|
|
// @004ba9a8 -- reset the transient firing/beam fields and re-arm to Loading.
|
|
//
|
|
void
|
|
Emitter::ResetFiringState()
|
|
{
|
|
beamOrientation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); // 0x41c <- (0,0,0)
|
|
beamScale = Vector3D(1.0f, 1.0f, 1.0f); // 0x42c <- (1,1,1)
|
|
targetEntity = 0; // 0x474
|
|
firingActive = 0; // 0x418
|
|
beamFlag = 0; // 0x46c
|
|
weaponAlarm.SetLevel(3); // Loading
|
|
}
|
|
|
|
//
|
|
// @004baa20 -- keep an active beam alive for another frame. If the targeted
|
|
// subsystem has gone away, reset; otherwise re-send the beam message.
|
|
//
|
|
void
|
|
Emitter::ContinueDischarge()
|
|
{
|
|
if (beamFlag != 0) // 0x46c
|
|
{
|
|
if (BTMechDestroyed(targetEntity)) // the beam CUTS when its target mech dies
|
|
{
|
|
ResetFiringState(); // @004ba9a8
|
|
return;
|
|
}
|
|
// re-send the beam keepalive -- the engine's updateModel bit (task #51,
|
|
// same mapping as ServiceDischarge above; 1995 template @0x511e78).
|
|
ForceUpdate();
|
|
(void)PTR_LAB_00511e78; (void)DAT_00511e7c; (void)DAT_00511e80;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004ba478 -- advance the seek-voltage index, wrapping modulo (count+1). If
|
|
// the index wrapped past the recommended one, re-arm via ResetFiringState.
|
|
// Reached only when the heat-state query is clear and the message has payload.
|
|
// (best-effort -- exact role of param +0xc unconfirmed.)
|
|
//
|
|
int
|
|
Emitter::AdvanceSeekVoltage(UpdateRecord *message)
|
|
{
|
|
if (FUN_004ac9c8(this)) // heat-state set -> skip
|
|
{
|
|
return /*non-zero status*/ 1;
|
|
}
|
|
if (!FUN_004ad7d4(this) || message->payloadCount <= 0) // message+0xc
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int prev = seekVoltageIndex; // 0x3f0
|
|
int modulus = seekVoltageCount + 1; // 0x3fc + 1
|
|
int next = (prev + 1) % modulus;
|
|
seekVoltageIndex = next; // 0x3f0
|
|
if (prev < next)
|
|
{
|
|
ResetFiringState(); // @004ba9a8
|
|
}
|
|
return next;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Simulation virtual overrides (slots 16, 17)
|
|
//
|
|
|
|
//
|
|
// @004ba6e0 -- True if the (supplied or linked) voltage source can satisfy the
|
|
// current seekVoltage index and is itself in the "ready" state (source+0x210==2).
|
|
//
|
|
Logical
|
|
Emitter::ReadyToDischarge(Generator *source)
|
|
{
|
|
if (source == 0)
|
|
{
|
|
source = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1d0)
|
|
}
|
|
if (source == 0)
|
|
{
|
|
return False;
|
|
}
|
|
if (seekVoltage[seekVoltageIndex] <= source->MeasuredVoltage() // 0x400[idx] <= src+0x1dc
|
|
&& source->GeneratorStateOf() == 2) // src+0x210
|
|
{
|
|
return True;
|
|
}
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// @004ba738 -- recompute outputVoltage from the accumulated charge:
|
|
// outputVoltage = currentLevel / seekVoltage[index] (0 if charge ~= 0)
|
|
// snap to 1.0 within 0.01, then clamp to [0,1]. (outputVoltage aliases the
|
|
// MechWeapon recharge-level slot @0x320.)
|
|
//
|
|
void
|
|
Emitter::ComputeOutputVoltage()
|
|
{
|
|
// outputVoltage IS the inherited MechWeapon::rechargeLevel@0x320 (the binary keeps
|
|
// no separate Emitter slot; the recon's own `outputVoltage` was a duplicate).
|
|
if (Fabs(currentLevel - 0.0f) > 1.0e-4f) // _DAT_004ba818 / _DAT_004ba81c
|
|
{
|
|
rechargeLevel = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320
|
|
}
|
|
else
|
|
{
|
|
rechargeLevel = 0.0f;
|
|
}
|
|
|
|
if (Fabs(rechargeLevel - 1.0f) <= 0.01f) // _DAT_004ba820 / _DAT_004ba824
|
|
{
|
|
rechargeLevel = 1.0f;
|
|
}
|
|
|
|
if (rechargeLevel < 0.0f) // _DAT_004ba818
|
|
{
|
|
rechargeLevel = 0.0f; // _DAT_004ba828
|
|
}
|
|
else if (rechargeLevel > 1.0f) // _DAT_004ba820
|
|
{
|
|
rechargeLevel = 0.0f; // _DAT_004ba830 (NB: also 0.0f)
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Subsystem virtual overrides
|
|
//
|
|
|
|
//
|
|
// @004ba568 -- slot 6 (TASK #51 RENAME: was mistranscribed as TakeDamage; the
|
|
// body is unambiguous RECORD semantics -- it compares rec+0x30 against
|
|
// EntityID::Null (DAT_00522524), resolves it through the entity index, and
|
|
// applies alarm/beam fields). Apply an Emitter update record on the
|
|
// REPLICANT: resolve the beam TARGET id (drop the record when a non-null id is
|
|
// unknown on this host), chain the MechWeapon read (@004b964c, alarm apply),
|
|
// then the beam-replication fields. The tail `this+0x10 = this+0x14` is the
|
|
// 1995 Simulation lastUpdate=lastPerformance resync (the earlier transcription
|
|
// misread it as rechargeLevel=effectiveRange) -- the engine base read already
|
|
// maintains lastUpdate.
|
|
//
|
|
void
|
|
Emitter::ReadUpdateRecord(Simulation__UpdateRecord *message)
|
|
{
|
|
Emitter__UpdateRecord *rec = (Emitter__UpdateRecord *)message;
|
|
|
|
if (rec->targetID == EntityID::Null) // FUN_00421070 vs DAT_00522524
|
|
{
|
|
targetEntity = 0; // 0x474
|
|
}
|
|
else
|
|
{
|
|
targetEntity = (application != 0)
|
|
? application->GetHostManager()->GetEntityPointer(rec->targetID)
|
|
: 0;
|
|
if (targetEntity == 0)
|
|
{
|
|
return; // unknown target -> drop
|
|
}
|
|
}
|
|
|
|
MechWeapon::ReadUpdateRecord(message); // FUN_004b964c (alarm apply)
|
|
|
|
firingActive = rec->firingActive; // 0x418 <- rec+0x18
|
|
weaponAlarm.SetLevel(firingActive != 0 ? 0 : 2); // 0=Firing / 2=Loaded
|
|
beamFlag = rec->beamFlag; // 0x46c <- rec+0x28
|
|
beamEndpoint = rec->beamEndpoint; // 0x460 <- rec+0x1c
|
|
targetLocalFlag = rec->targetLocalFlag; // 0x470 <- rec+0x2c
|
|
ClearDirty(); // this+0x28 &= ~2 (idle bit clear)
|
|
if (getenv("BT_BEAM_LOG"))
|
|
{
|
|
static int s_rd = 0;
|
|
if ((s_rd++ % 60) == 0)
|
|
DEBUG_STREAM << "[emit-rd] " << (void*)this << " firing=" << firingActive
|
|
<< " beamFlag=" << beamFlag << "\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004ba65c -- slot 7. Append the Emitter beam-replication fields to the
|
|
// update record (record type 0x38): firingActive, targetLocalFlag, beam
|
|
// endpoint, beamFlag and the target's colour tag.
|
|
//
|
|
void
|
|
Emitter::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
|
|
{
|
|
MechWeapon::WriteUpdateRecord(message, update_model); // FUN_004b9690
|
|
|
|
Emitter__UpdateRecord *rec = (Emitter__UpdateRecord *)message;
|
|
// DISASM CORRECTIONS (task #51): `*param_2 = 0x38` writes the record
|
|
// LENGTH, not recordID; and rec+0x30 receives the TARGET's EntityID
|
|
// (FUN_00420ef4 copies from targetEntity+0x184 = the 1995 entityID --
|
|
// the old `CopyColor(...targetEntity+0x184)` was both a misread AND a
|
|
// databinding trap in our compiled Entity layout).
|
|
rec->recordLength = sizeof(Emitter__UpdateRecord); // *message = 0x38
|
|
rec->firingActive = firingActive; // rec+0x18 <- 0x418
|
|
rec->beamEndpoint = beamEndpoint; // rec+0x1c <- 0x460
|
|
rec->beamFlag = beamFlag; // rec+0x28 <- 0x46c
|
|
rec->targetLocalFlag = targetLocalFlag; // rec+0x2c <- 0x470
|
|
rec->targetID = (targetEntity != 0)
|
|
? ((Entity *)targetEntity)->GetEntityID()
|
|
: EntityID::Null; // rec+0x30
|
|
if (getenv("BT_BEAM_LOG"))
|
|
{
|
|
static int s_wr = 0;
|
|
if ((s_wr++ % 60) == 0)
|
|
DEBUG_STREAM << "[emit-wr] " << (void*)this << " firing=" << firingActive
|
|
<< " beamFlag=" << beamFlag << " subsysID=" << (int)rec->subsystemID
|
|
<< " len=" << (int)rec->recordLength << "\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004ba4d0 -- slot 10. Reset to Loading: chain to MechWeapon, set the alarm
|
|
// to 3 (Loading), clear the firing/beam transients and re-prime beamScale to
|
|
// (1,1,1). Matches EMITTER.TCP ResetToInitialState (SetSimulationState(Loaded)
|
|
// then base reset).
|
|
//
|
|
void
|
|
Emitter::ResetToInitialState()
|
|
{
|
|
MechWeapon::ResetToInitialState(); // FUN_004b96ec
|
|
weaponAlarm.SetLevel(3); // Loading
|
|
currentLevel = 0.0f; // 0x414
|
|
beamFlag = 0; // 0x46c
|
|
targetLocalFlag = 0; // 0x470
|
|
targetEntity = 0; // 0x474
|
|
beamScale = Vector3D(1.0f, 1.0f, 1.0f); // 0x42c
|
|
beamOrientation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); // 0x41c
|
|
firingActive = 0; // 0x418
|
|
SetDirty(); // this+6 |= 1
|
|
}
|
|
|
|
//
|
|
// @004bb014 -- slot 13. Print the base state, then the Emitter weapon state
|
|
// from the alarm level (this+0x364): 0 "Firing", 2 "Loaded", 3 "Loading",
|
|
// 4 "TriggerDuringLoad".
|
|
//
|
|
void
|
|
Emitter::PrintState()
|
|
{
|
|
MechWeapon::PrintState(); // FUN_004b9d00
|
|
|
|
switch (GetWeaponState()) // this+0x364
|
|
{
|
|
case 0: DebugStream << GetName() << " Firing"; break;
|
|
case 2: DebugStream << GetName() << " Loaded"; break;
|
|
case 3: DebugStream << GetName() << " Loading"; break;
|
|
case 4: DebugStream << GetName() << " TriggerDuringLoad"; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Construction / Destruction
|
|
//
|
|
// @004bb120 -- chain to the MechWeapon ctor (subsystem kind 5), install the
|
|
// Emitter vtable PTR_FUN_00512078, choose the firing-message template (the
|
|
// (flags & 0xC)==4 "external model" variant vs. the default), then seed the
|
|
// firing state machine to Loading and derive the discharge-energy constants
|
|
// from the resource's SeekVoltage curve.
|
|
//
|
|
Emitter::Emitter(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data)
|
|
{
|
|
// vtable installed by the compiler (PTR_FUN_00512078)
|
|
|
|
// @004bb120 -- install the per-frame "performance". this[7..9] is the
|
|
// Simulation::activePerformance member pointer (the same slot every sibling
|
|
// subsystem fills via SetPerformance), and the ctor picks one of two variants:
|
|
// default this[7..9] = {0x004baa88,0,0} == EmitterSimulation
|
|
// external model this[7..9] = {0x004ba8d0,0,0} == ServiceDischarge
|
|
// (member-pointer constants @00511e84 / @00511e90, resolved from section_dump:
|
|
// 511e84: 88 aa 4b 00 -> 0x004baa88 ; 511e90: d0 a8 4b 00 -> 0x004ba8d0).
|
|
// Installing it here is what makes the weapon tick: without it the chained
|
|
// PoweredSubsystem ctor's PoweredSubsystemSimulation would run instead and the
|
|
// Loading->Loaded->Firing charge/fire machine below would never execute.
|
|
if ((GetFlags() & 0xC) == 4) // resource+0x28 -- external model
|
|
{
|
|
SetPerformance(&Emitter::ServiceDischarge); // this[7..9] = {0x004ba8d0,0,0}
|
|
simulationFlags |= 0x2; // this[10] |= 2 (external model)
|
|
}
|
|
else
|
|
{
|
|
SetPerformance(&Emitter::EmitterSimulation); // this[7..9] = {0x004baa88,0,0}
|
|
}
|
|
|
|
// (E4 removed) the former `firingArmed = 1` wrote this+0x3e8 == MechWeapon::recoil,
|
|
// NOT the Loaded->Firing gate: the binary gate reads useConfiguredPip@0x3E0 (set by
|
|
// the MechWeapon ctor from usesExternalModel), so no separate arming is needed.
|
|
weaponAlarm.SetLevel(3); // Loading
|
|
beamEndpoint = Point3D(0.0f, 0.0f, 0.0f); // 0x460 <- (0,0,0)
|
|
beamFlag = 0; // 0x46c
|
|
targetEntity = 0; // 0x474
|
|
targetLocalFlag = 0; // 0x470
|
|
beamScale = Vector3D(1.0f, 1.0f, 1.0f); // 0x42c
|
|
beamOrientation = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); // 0x41c
|
|
firingActive = 0; // 0x418
|
|
currentLevel = 0.0f; // 0x414
|
|
damagePortion = 0.0f; // 0x44c
|
|
heatPortion = 0.0f; // 0x450
|
|
seekReserved = 0.0f; // 0x458
|
|
|
|
// E2 (bring-up): pre-init the seek-voltage table + energy coefficient so they are
|
|
// valid even when the voltage-source plug below doesn't resolve (the electrical
|
|
// block overwrites these with real values when src is live). Nominal seekVoltage=1
|
|
// lets the E7 force-charge reach Loaded (currentLevel=seek -> outputVoltage=1.0).
|
|
energyCoefficient = 1.0f; // 0x454
|
|
seekVoltageIndex = 0; // 0x3f0
|
|
seekVoltageRecommendedIndex = 0; // 0x3f4
|
|
seekVoltageCount = 0; // 0x3fc
|
|
for (int sv = 0; sv < 5; ++sv)
|
|
seekVoltage[sv] = 1.0f; // 0x400[]
|
|
|
|
graphicLength = subsystem_resource->graphicLength; // 0x438 <- +0x1bc
|
|
dischargeTime = subsystem_resource->dischargeTime; // 0x43c <- +0x1c0
|
|
dischargeTimer = dischargeTime; // 0x440
|
|
|
|
// energyTotal = (damageAmount + heatCostToFire) * 1.0e7
|
|
energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; // _DAT_004bb3ac
|
|
|
|
// 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)
|
|
{
|
|
// 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)
|
|
{
|
|
Verify(False, "Master Emitter is missing its voltage source", __FILE__, __LINE__);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
seekVoltage[i] = 0.0f; // 0x400[i]
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
// (task #8 note: energyCoefficient deliberately stays on the bring-up
|
|
// electrical model -- the charge cycle is calibrated to it; the AUTHENTIC
|
|
// damage algebra is applied as a closed form at fire time instead.)
|
|
|
|
// damageFraction = damageAmount / (damageAmount + heatCostToFire)
|
|
damageFraction = damageData.damageAmount /
|
|
(damageData.damageAmount + heatCostToFire); // 0x444
|
|
seekStepCounter = 0; // 0x3f0/0x45c bookkeeping
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @004bb3c8 -- reinstall the Emitter vtable, chain to ~MechWeapon, then (when
|
|
// the deleting-dtor bit is set) free the object.
|
|
//
|
|
Emitter::~Emitter()
|
|
{
|
|
Check(this);
|
|
// vtable reset to PTR_FUN_00512078; FUN_004b9b9c handles the base chain.
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Test Class Support (EMITTER.TCP)
|
|
//
|
|
Logical
|
|
Emitter::TestClass(Mech &mech)
|
|
{
|
|
return True;
|
|
}
|
|
|
|
Logical
|
|
Emitter::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(ClassDerivations);
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// CreateStreamedSubsystem
|
|
//
|
|
// @004bb478 -- parse the Emitter-specific resource fields after the MechWeapon
|
|
// base parser. Stamps subsystemModelSize = 0x1DC and classID = EmitterClassID
|
|
// (0xBC8). On the first pass GraphicLength/DischargeTime are primed to the
|
|
// -1.0 sentinel and the SeekVoltage curve to -1.0; then DischargeTime,
|
|
// GraphicLength, the SeekVoltage list and its RecommendedIndex are read.
|
|
//
|
|
int
|
|
Emitter::CreateStreamedSubsystem(
|
|
ResourceFile *resource_file,
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
if (
|
|
!MechWeapon::CreateStreamedSubsystem( // FUN_004b9d10
|
|
resource_file, model_file, model_name, subsystem_name,
|
|
subsystem_resource, subsystem_file, directories, passes
|
|
)
|
|
)
|
|
{
|
|
return False;
|
|
}
|
|
|
|
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource); // 0x1DC
|
|
subsystem_resource->classID = (RegisteredClass::ClassID)Mech::WeaponEmitterClassID; // 0xBC8
|
|
|
|
if (passes == 1)
|
|
{
|
|
subsystem_resource->graphicLength = -1.0f; // +0x1bc _DAT_004bb798
|
|
subsystem_resource->dischargeTime = -1.0f; // +0x1c0
|
|
for (int i = 0; i < 5; ++i)
|
|
{
|
|
subsystem_resource->seekVoltage[i] = -1.0f; // +0x1c4..
|
|
}
|
|
subsystem_resource->seekVoltageRecommendedIndex = -1; // +0x1d8
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "DischargeTime", &subsystem_resource->dischargeTime)
|
|
&& subsystem_resource->dischargeTime == -1.0f
|
|
)
|
|
{
|
|
DebugStream << subsystem_name << " missing DischargeTime!";
|
|
return False;
|
|
}
|
|
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "GraphicLength", &subsystem_resource->graphicLength)
|
|
&& subsystem_resource->graphicLength == -1.0f
|
|
)
|
|
{
|
|
DebugStream << subsystem_name << " missing GraphicLength!";
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// SeekVoltage: in the shipped build this is a NotationList of named scalar
|
|
// entries -- curve samples plus a "SeekVoltageRecommendedIndex" tag. The list
|
|
// iterator (FUN_00404720) is an engine artifact; here we read the recommended
|
|
// index and the first curve sample directly via the keyed notation API.
|
|
//
|
|
if (
|
|
!model_file->GetEntry(subsystem_name, "SeekVoltageRecommendedIndex",
|
|
&subsystem_resource->seekVoltageRecommendedIndex)
|
|
&& subsystem_resource->seekVoltageRecommendedIndex == -1
|
|
)
|
|
{
|
|
DebugStream << subsystem_name << " missing SeekVoltage!";
|
|
return False;
|
|
}
|
|
|
|
model_file->GetEntry(subsystem_name, "SeekVoltage", &subsystem_resource->seekVoltage[0]);
|
|
|
|
return True;
|
|
}
|
|
|
|
//===========================================================================//
|
|
// WAVE 3b factory bridge -- the Emitter beam weapon. Used for BOTH 0xBC8
|
|
// (Emitter) and 0xBD4 (PPC): PPC's FireWeapon tail-calls Emitter::FireWeapon
|
|
// and the renderer keys off the streamed classID, so behaviour is identical.
|
|
//===========================================================================//
|
|
Subsystem *CreateEmitterSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
Check(sizeof(Emitter) <= 0x478);
|
|
return (Subsystem *) new (Memory::Allocate(0x478))
|
|
Emitter(owner, id, (Emitter::SubsystemResource *)seg, Emitter::DefaultData);
|
|
}
|