Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,868 @@
|
||||
//===========================================================================//
|
||||
// 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 cockpit/HUD query 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
|
||||
|
||||
// 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.0f; // _DAT_004bafbc firing-physics scale (best-effort)
|
||||
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 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)
|
||||
static int FUN_004ad7d4(void *) { return 1; } // HeatableSubsystem active?
|
||||
static int FUN_0049fb54(int) { return 0; } // cockpit/HUD query
|
||||
static Scalar FUN_004b0d50(void *) { return 1.0f; }// PoweredSubsystem dt scale
|
||||
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
|
||||
|
||||
//#############################################################################
|
||||
// Shared Data Support
|
||||
//
|
||||
Derivation
|
||||
Emitter::ClassDerivations(
|
||||
&MechWeapon::ClassDerivations,
|
||||
"Emitter"
|
||||
);
|
||||
|
||||
Receiver::MessageHandlerSet Emitter::MessageHandlers;
|
||||
Simulation::AttributeIndexSet Emitter::AttributeIndex;
|
||||
|
||||
Emitter::SharedData
|
||||
Emitter::DefaultData(
|
||||
&Emitter::ClassDerivations,
|
||||
Emitter::MessageHandlers,
|
||||
Emitter::AttributeIndex,
|
||||
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; }
|
||||
int Emitter::GetFaultState() { return 0; }
|
||||
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
|
||||
damagePortion = damageFraction * energy; // 0x44c
|
||||
heatPortion = energy - damagePortion; // 0x450
|
||||
damagePortion = kDamageScale * damagePortion; // firing-physics scale (best-effort)
|
||||
|
||||
// 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++ % 20) == 0)
|
||||
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
|
||||
ComputeAimOrientation(beamHitData, 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
|
||||
beamLengthRatio = dist / graphicLength; // 0x434 = dist / 0x438
|
||||
|
||||
if (dist <= effectiveRange) // this+0x328
|
||||
{
|
||||
// record the hit point + impact and register the pip / fire marker
|
||||
beamHitPoint = targetPoint; // 0xf2 <- targetPoint
|
||||
beamImpact = delta; // 0xec <- delta
|
||||
beamImpactScalar = damagePortion; // 0xeb <- 0x113
|
||||
DrawWeaponPip(beamHitData); // FUN_004b9728
|
||||
}
|
||||
|
||||
// stash the beam endpoint for replication (world hit point)
|
||||
beamEndpoint = targetPoint; // 0x460 <- targetPoint
|
||||
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);
|
||||
|
||||
// E8 (bring-up trigger): write the weapon's fireImpulse from the player fire input
|
||||
// (the controls mapper is bypassed). gBTWeaponTrigger is pulsed per-FRAME by mech4
|
||||
// (1,0,1,0...) so CheckFireEdge sees clean rising edges for repeated auto-fire under a
|
||||
// held/forced trigger. HasActiveTarget (E3) limits firing to the mech with a locked
|
||||
// target (the player) -- enemy emitters read this too but never fire (no target).
|
||||
extern int gBTWeaponTrigger;
|
||||
fireImpulse = (Scalar)gBTWeaponTrigger;
|
||||
|
||||
Logical fireEdge = CheckFireEdge(); // @004b9608
|
||||
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
||||
|
||||
// hard failure: powered-off, faulted, or destroyed -> drop everything
|
||||
if (GetFlags() == 1 || GetFaultState() == 2 || FUN_0049fb54(0))
|
||||
{
|
||||
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 (firingArmed && HasActiveTarget()) // this+0x3e8 && 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);
|
||||
}
|
||||
if (GetVoltageState() == 4) // this+0x278
|
||||
{
|
||||
TrackSeekVoltage(time_slice); // @004ba838 (inert: unresolved src)
|
||||
}
|
||||
// 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 (outputVoltage == 1.0f) // _DAT_004bac04 -- fully charged
|
||||
{
|
||||
weaponAlarm.SetLevel(2); // -> Loaded
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)
|
||||
return;
|
||||
|
||||
seekRate = (src->voltage - currentLevel) / dtScale; // 0x45c = (src+0x1dc - 0x414)/..
|
||||
currentLevel = (seekRate / energyCoefficient) * time_slice + currentLevel; // 0x414
|
||||
|
||||
if (FUN_004ad7d4(this)) // heatable/online
|
||||
{
|
||||
src->heatLoad += seekRate * seekRate * dtScale * time_slice; // src+0x1c8
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @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
|
||||
|| (targetEntity != 0 && FUN_0049fb54(0))) // or the targeted subsystem is gone
|
||||
{
|
||||
firingActive = 0;
|
||||
weaponAlarm.SetLevel(2); // Loaded
|
||||
beamFlag = 0; // 0x46c
|
||||
dischargeTimer = dischargeTime; // 0x440 = 0x43c
|
||||
SetDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
// keep the beam alive for another frame (re-send the beam message).
|
||||
// CROSS-FAMILY (messaging): SendMessage(PTR_LAB_00511e6c,..) -- FUN_0041c350.
|
||||
(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 (targetEntity != 0 && FUN_0049fb54(0)) // targeted subsystem gone
|
||||
{
|
||||
ResetFiringState(); // @004ba9a8
|
||||
return;
|
||||
}
|
||||
// re-send the beam-keepalive message (CROSS-FAMILY messaging, FUN_0041c350)
|
||||
(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(VoltageSource *source)
|
||||
{
|
||||
if (source == 0)
|
||||
{
|
||||
source = (VoltageSource *)FUN_00417ab4(this + 0x1d0);
|
||||
}
|
||||
if (source == 0)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
if (seekVoltage[seekVoltageIndex] <= source->voltage // 0x400[idx] <= src+0x1dc
|
||||
&& source->state == 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()
|
||||
{
|
||||
if (Fabs(currentLevel - 0.0f) > 1.0e-4f) // _DAT_004ba818 / _DAT_004ba81c
|
||||
{
|
||||
outputVoltage = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320
|
||||
}
|
||||
else
|
||||
{
|
||||
outputVoltage = 0.0f;
|
||||
}
|
||||
|
||||
if (Fabs(outputVoltage - 1.0f) <= 0.01f) // _DAT_004ba820 / _DAT_004ba824
|
||||
{
|
||||
outputVoltage = 1.0f;
|
||||
}
|
||||
|
||||
if (outputVoltage < 0.0f) // _DAT_004ba818
|
||||
{
|
||||
outputVoltage = 0.0f; // _DAT_004ba828
|
||||
}
|
||||
else if (outputVoltage > 1.0f) // _DAT_004ba820
|
||||
{
|
||||
outputVoltage = 0.0f; // _DAT_004ba830 (NB: also 0.0f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// Subsystem virtual overrides
|
||||
//
|
||||
|
||||
//
|
||||
// @004ba568 -- slot 6. Resolve the incoming damage's source subsystem (the
|
||||
// VoltageSource link), then apply the base MechWeapon damage and replicate the
|
||||
// extra Emitter fields (firingActive, beam endpoint, target). A "no-source"
|
||||
// damage clears the link and sets the alarm to 2 (Loaded) / 0 (active).
|
||||
//
|
||||
void
|
||||
Emitter::TakeDamage(Damage &damage)
|
||||
{
|
||||
int src = FUN_00421070(&damage, DAT_00522524); // damage source (damage+0x30)
|
||||
if (src == 0)
|
||||
{
|
||||
targetEntity = 0; // 0x474
|
||||
}
|
||||
else
|
||||
{
|
||||
// look up the named source through the global subsystem registry
|
||||
targetEntity = (Entity *)ResolveSource(&damage); // this+0x474
|
||||
if (targetEntity == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MechWeapon::TakeDamage(damage); // FUN_004b964c
|
||||
|
||||
// CROSS-FAMILY (replication): the shipped Emitter also pulled its beam state
|
||||
// (firingActive, beamFlag, beamEndpoint, beamColor, targetLocalFlag) out of the
|
||||
// networked damage record. Those fields are not present on the engine Damage;
|
||||
// they arrive through the update-record path (WriteUpdateRecord) instead.
|
||||
if (firingActive == 0)
|
||||
{
|
||||
weaponAlarm.SetLevel(2); // Loaded
|
||||
}
|
||||
else
|
||||
{
|
||||
weaponAlarm.SetLevel(0); // Firing
|
||||
}
|
||||
ClearDirty(); // this+0x28 &= ~2
|
||||
rechargeLevel = effectiveRange; // this+0x10 = this+0x14 (best-effort)
|
||||
}
|
||||
|
||||
//
|
||||
// @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(UpdateRecord *message, int update_model)
|
||||
{
|
||||
MechWeapon::WriteUpdateRecord(message, update_model); // FUN_004b9690
|
||||
|
||||
message->recordID = 0x38; // *message
|
||||
message->firingActive = firingActive; // message[6] <- 0x418
|
||||
message->targetLocalFlag= targetLocalFlag; // message[10] <- 0x470
|
||||
message->beamEndpoint = beamEndpoint; // message[7] <- 0x460
|
||||
message->beamFlag = beamFlag; // message[11] <- 0x46c
|
||||
|
||||
if (targetEntity == 0) // 0x474
|
||||
{
|
||||
CopyColor(&message->color, DAT_00522524); // FUN_00420ef4
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyColor(&message->color, (void*)((char*)targetEntity + 0x184));
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @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}
|
||||
}
|
||||
|
||||
firingArmed = 1; // E4: arm the Loaded->Firing gate (this+0x3e8;
|
||||
// real semantic is a weapon-selected flag from
|
||||
// the controls path -- bypassed in bring-up).
|
||||
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 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)
|
||||
{
|
||||
VoltageSource *src = (VoltageSource *)FUN_00417ab4(this + 0x74);
|
||||
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;
|
||||
}
|
||||
seekVoltage[i] = subsystem_resource->seekVoltage[i] * src->ratedVoltage; // src+0x1d8
|
||||
}
|
||||
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)
|
||||
Scalar curve = /*_DAT_004bb3c4 - _DAT_004bb3b8 * v*/ VoltageCurve(v); // best-effort
|
||||
energyRampTime = (rechargeRate / -curve) / energyCoefficient; // this+0x328 working term
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user