//===========================================================================//
// 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
				<< " pct=" << (float)rechargeLevel
				<< " 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);
}
