//===========================================================================//
// 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);
}
