//===========================================================================//
// File:	projweap.cpp                                                        //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: ProjectileWeapon -- MechWeapon that spawns physical projectiles  //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  04/13/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 @004bbae0-@004bcff0 (file=bt/projweap.cpp).  Method and
// member names follow the surviving MISLANCH.HPP / PROJWEAP.TCP, the binary's own
// attribute strings (DAT_00512290) and resource tag strings, and the base
// mechweap.cpp.  Each non-trivial method cites the originating @ADDR.
//
// CONFIDENCE
//   confident (full body recovered & translated):
//       ctor @004bc3fc, dtor @004bc688, deleting-dtor @004bcbcf,
//       ammoBinLink ctor @004bcbb0, ReadUpdateRecord @004bbae0,
//       ResetToInitialState @004bbaf8, HandleMessage @004bcabc,
//       PrintState @004bc6c8, CreateStreamedSubsystem @004bc7cc,
//       UpdateEject @004bbb50, DrawFiringCharge @004bbc78,
//       CheckForJam @004bbfcc, ComputeTimeOfFlight @004bc06c.
//   best-effort (vtable slot proven, function prologue present, BODY NOT
//   recovered by the decompiler -- bodies below are reconstructed from context
//   and clearly marked):
//       FireWeapon @004bc104 (slot 18), ProjectileWeaponSimulation @004bbd04
//       (Performance), GetStatusFlags @004bbf88 (slot 12),
//       UpdateWeaponState @004bbc20 (slot 16).
//   excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
//       Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
//       MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
//       AmmoBin (HeatWatcher-derived) @004bd5c4/@004bd6b0/@004bd6f0 and its
//       helpers @004bd2c0-@004bd4f4.
//
// Resolved read-only constants (4 LE bytes from section_dump CODE):
//      _DAT_004bc100 = 0000803f =  1.0f
//      _DAT_004bc064 = 85ebd13e =  0.41f   (heat->jam-chance coefficient)
//      _DAT_004bc068 = 0000803f =  1.0f    (jam-chance clamp ceiling)
//      _DAT_004bb3b0 = 000080bf = -1.0f    (resource "unset" sentinel)
//      _DAT_004bb3b4 = 0000003f =  0.5f
//      DAT_004e0f74  = {0,0,0}             (zero vector)
//      DAT_004e0fd4  = {0,0,0}             (zero point)
//      0x40400000    =  3.0f               (TotalTimeToEject default)
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_004b99a8  MechWeapon base constructor
//      FUN_004b9b9c  ~MechWeapon
//      FUN_004b964c  MechWeapon::TakeDamage (vtable slot 6, inherited)
//      FUN_004b9690  MechWeapon::WriteUpdateRecord (slot 7, inherited)
//      FUN_004add6c  Subsystem::HandleMessage (slot 8 base)
//      FUN_004b96d4  MechWeapon::ReadUpdateRecord (slot 9 base)
//      FUN_004b96ec  MechWeapon::ResetToInitialState (slot 10 base)
//      FUN_004b9d00  MechWeapon::PrintState (slot 13 base)
//      FUN_004b9948  MechWeapon::GetMuzzlePoint     FUN_004b9bdc MechWeapon::UpdateTargeting
//      FUN_004b9cbc  MechWeapon::GetTargetPosition  FUN_004b0d50 voltage helper
//      FUN_004ad7d4  HeatableSubsystem present?     FUN_00417ab4 Connection::Resolve
//      FUN_0041bbd8  AlarmIndicator::SetLevel(n)    FUN_0041db7c Damage init
//      FUN_004179d4 / FUN_004179f8                  SharedData::Connection ctor/dtor
//      FUN_004022d0  operator delete                FUN_004215b0 resolve subsystem name->index
//      FUN_00408440  Vector copy   FUN_0040a7f4 Point copy   FUN_00408050 uniform random
//      FUN_004040d8  Read(int/bool)  FUN_00404088 Read(string)  FUN_00404118 Read(scalar)
//      FUN_004084fc  vectors-close?  FUN_00408944 parse vector
//      FUN_004dbb24  DebugStream <<  FUN_004d9c38 DebugStream flush
//      FUN_004b9d10  MechWeapon::CreateStreamedSubsystem
//      FUN_004bd588  AmmoBin::request-eject/dry     FUN_004bf8bc spawn projectile entity
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(PROJWEAP_HPP)
#	include <projweap.hpp>
#endif
#if !defined(AMMOBIN_HPP)
#	include <ammobin.hpp>
#endif
#if !defined(PROJTILE_HPP)
#	include <projtile.hpp>
#endif
#if !defined(TESTBT_HPP)
#	include <testbt.hpp>
#endif
#include <math.h>

// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp).
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
							 const Point3D &targetPos, Scalar speed, Scalar damage);


//#############################################################################
// Resolved read-only constants + unrecovered-helper stand-ins (see banner).
//
static const Scalar	_DAT_004bc100	= 1.0f;		// time-of-flight bias
static const Scalar	_DAT_004bc064	= 0.41f;	// heat -> jam-chance coefficient
static const Scalar	_DAT_004bc068	= 1.0f;		// jam-chance clamp ceiling
static const Scalar	_DAT_004bc678	= 1.0f;		// muzzle-speed term (unresolved)
static const Scalar	_DAT_004bc67c	= 1.0f;		// muzzle-speed term (unresolved)

// FUN_00408050 -- uniform [0,1) random (jam roll).  The old `return 0.0f` stub ALWAYS
// jammed (0 < any positive jam chance), so a projectile weapon could never fire.  A real
// pseudo-random (LCG) restores authentic behavior: it fires ~(1-jamChance) of the time.
static Scalar UniformRandom()
{
	static unsigned s = 0x12345678u;
	s = s * 1103515245u + 12345u;
	return (Scalar)((s >> 16) & 0x7fff) / 32768.0f;
}
static int    ResolveTracerModel(void*)	{ return 0; }		// explosion/tracer model handle
static int    ResolveSubsystemName(const ResourceDirectories*, const char*) { return -1; }	// FUN_004215b0


//#############################################################################
// Shared Data Support
//
Derivation
	ProjectileWeapon::ClassDerivations(
		&MechWeapon::ClassDerivations,
		"ProjectileWeapon"					// DAT_00512280
	);

Receiver::MessageHandlerSet		ProjectileWeapon::MessageHandlers;
Simulation::AttributeIndexSet	ProjectileWeapon::AttributeIndex;

ProjectileWeapon::SharedData
	ProjectileWeapon::DefaultData(
		&ProjectileWeapon::ClassDerivations,
		ProjectileWeapon::MessageHandlers,
		MechWeapon::GetAttributeIndex(),	// gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty)
		ProjectileWeapon::StateCount
	);


//#############################################################################
// Test Class Support  (PROJWEAP.TCP)
//
Logical
	ProjectileWeapon::TestClass(Mech &)
{
	return True;
}

Logical
	ProjectileWeapon::TestInstance() const
{
	return IsDerivedFrom(ClassDerivations);
}


//#############################################################################
// Cross-family isolation helpers (mech subsystem roster + cockpit controls).
// The owning Mech's subsystem roster and the cockpit "live fire" control flag
// are owned by the mech / controls families; these stubs let the recovered
// bodies compile and are wired up there.
//
int
	ProjectileWeapon::OwnerSubsystemCount(Mech *owner) const
{
	// Real roster (binary reads owner->subsystemCount @0x124).  The AmmoBin (0xBCB)
	// constructs before the projectile weapons (0xBCD/0xBD0) in the factory loop, so
	// its roster slot is populated by the time this weapon's ctor resolves it.
	return (owner != 0) ? owner->GetSubsystemCount() : 0;
}

Subsystem *
	ProjectileWeapon::OwnerSubsystem(Mech *owner, int index) const
{
	// Real roster lookup (binary: subsystemArray[index] @0x128).
	return (owner != 0) ? owner->GetSubsystem(index) : 0;
}

Logical
	ProjectileWeapon::LiveFireEnabled() const
{
	return True;
}

//
// ConsumeRound -- pull one round from the linked AmmoBin (FUN_004bd4f4
// AmmoBin::FeedAmmo).  Returns True when a round was dispensed; otherwise raises
// the NoAmmo alarm and returns False.  Shared by both FireWeapon spawn paths.
//
Logical
	ProjectileWeapon::ConsumeRound()
{
	AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve();		// FUN_00417ab4(this+0x43c)
	if (bin != 0 && bin->FeedAmmo() != 0)				// FUN_004bd4f4 -- dispensed?
	{
		return True;
	}
	weaponAlarm.SetLevel(NoAmmoState);					// FUN_0041bbd8(this+0x350, 7)
	return False;
}


//#############################################################################
// Construction / Destruction
//
// @004bc3fc -- chain to the MechWeapon ctor (@004b99a8, StateCount 8), install
// vtable PTR_FUN_0051242c, construct the embedded AmmoBin connection, copy the
// projectile parameters out of the resource record, build the launch-velocity
// seed from effectiveRange, prime the eject cycle, install the Performance
// simulation (unless the model is flagged non-simulated), and link to the
// AmmoBin subsystem named in the resource.
//
ProjectileWeapon::ProjectileWeapon(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	SharedData			&shared_data
):
	MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data)
{
	Check(owner);
	Check_Pointer(subsystem_resource);

	ammoBinLink.Construct();						// FUN_004bcbb0(this+0x43c, 0)

	tracerInterval			= subsystem_resource->tracerInterval;			// +0x1BC -> 0x438
	minTimeOfFlight			= subsystem_resource->minTimeOfFlight;			// +0x1C4 -> 0x40C
	minJamChance			= subsystem_resource->minJamChance;				// +0x1CC -> 0x3FC

	leadPosition			= Point3D(0,0,0);		// FUN_0040a7f4(this+0x420, DAT_004e0fd4)
	minVoltagePercentToFire	= subsystem_resource->minVoltagePercentToFire;	// +0x1C8 -> 0x404
	tracerCounter			= 0;					// 0x408

	// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed)
	launchVelocity			= Vector3D(0,0,0);		// FUN_00408440(this+0x410, DAT_004e0f74)
	Scalar muzzleSpeed		= (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678);	// FUN_004dd138
	tracerOrigin			= Vector3D(0,0,0);		// FUN_00408440(this+0x42c, DAT_004e0f74)
	launchVelocity.z		= -(effectiveRange / muzzleSpeed);	// this[0x106] = -(this[0xca]/speed)

	totalTimeToEject		= 3.0f;					// 0x3F0  (0x40400000)
	timeToEject				= totalTimeToEject;		// 0x3F4
	percentOfEject			= 0.0f;					// 0x3F8
	ejectState				= -1;					// 0x400  (idle)

	// Install Performance unless the owning Mech is a non-authoritative ghost.
	// @004bc3fc reads the OWNER's flags (param_2+0x28 == owner->simulationFlags),
	// NOT the resource flags -- the same authoritative-simulation gate the AmmoBin
	// uses (owner&0xC==0 && owner&0x100).  This is what arms the per-frame tick on
	// the bring-up's authoritative mech.
	Logical installPerf = (((owner->simulationFlags & 0xC) == 0) && ((owner->simulationFlags & 0x100) != 0));
	if (installPerf)
	{
		SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation);	// member-ptr {0x4bbd04,..}
	}
	else
	{
		simulationFlags |= 0x2;					// this[10] |= 2  (mark "not simulated")
	}
	if (getenv("BT_PROJ_LOG"))
		DEBUG_STREAM << "[projweap] CTOR name=" << GetName() << " ownerFlags="
			<< owner->simulationFlags << " installPerf=" << (int)installPerf << "\n" << std::flush;

	// Link to the AmmoBin subsystem referenced by the resource and copy the
	// initial display fields out of it.  (The Mech subsystem-roster lookup lives
	// in the mech family; isolated behind OwnerSubsystemCount/OwnerSubsystem.)
	if (subsystem_resource->ammoBinIndex < OwnerSubsystemCount(owner))		// +0x124
	{
		Subsystem *roster_bin = OwnerSubsystem(owner, subsystem_resource->ammoBinIndex);	// +0x128 table
		ammoBinLink.Connect(roster_bin);		// (**bin.vtbl[1])(...)
		// CROSS-FAMILY (mech/ammobin): the shipped ctor also primed the bin's HUD
		// display block (iVar1+0x1f0/0x1f4/0x1f8/0x210) from this weapon; wired in
		// the AmmoBin family.
	}

	AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve();	// FUN_00417ab4(this+0x43c)
	if (bin == 0)
	{
		DebugStream << GetName() << " Could not get ammo bin!" << endl;		// DAT_005122c0
	}

	// Look up / ref-count the explosion (tracer) model for the round.
	tracerModelHandle = ResolveTracerModel(bin);	// FUN_00407064(...,bin+0x1e8,0xf) -> this[0x107]

	Check_Fpu();
}

//
// @004bc688 -- destroy the embedded AmmoBin connection, chain to ~MechWeapon,
// and (if the deleting-dtor bit is set) free the object.
//
ProjectileWeapon::~ProjectileWeapon()
{
	Check(this);
	ammoBinLink.Destruct();							// FUN_004bcbcf(this+0x43c, 2)
	// base chain (FUN_004b9b9c) handles the rest.
	Check_Fpu();
}


//#############################################################################
// Subsystem virtual overrides
//

//
// @004bcabc -- slot 8.  A "jam" event (message 2) latches the weapon alarm to
// the Jammed level; everything else defers to the Subsystem base handler.
//
Logical
	ProjectileWeapon::HandleMessage(int message)
{
	if (message == 2)
	{
		weaponAlarm.SetLevel(JammedState);			// FUN_0041bbd8(this+0x350, 5)
		return True;
	}
	return PoweredSubsystem::HandleMessage(message);	// FUN_004add6c
}

//
// @004bbae0 -- slot 9.  ProjectileWeapon adds nothing of its own; defers to the
// MechWeapon implementation.
//
void
	ProjectileWeapon::ReadUpdateRecord(UpdateRecord *message)
{
	MechWeapon::ReadUpdateRecord(message);			// FUN_004b96d4
}

//
// @004bbaf8 -- slot 10.  Reset the firing/eject state, re-arm the alarm to the
// "Loading" level, restore the eject countdown, and clear the jam flag, then
// chain to MechWeapon.  (PROJWEAP.TCP showed an empty early stub; this is the
// shipped body.)
//
void
	ProjectileWeapon::ResetToInitialState()
{
	MechWeapon::ResetToInitialState();				// FUN_004b96ec
	weaponAlarm.SetLevel(3);						// FUN_0041bbd8(this+0x350, 3) -- "Loading"

	recoil			= rechargeRate;					// this[0xfa] = this[0xf7]
	timeToEject		= totalTimeToEject;				// this[0xfd] = this[0xfc]
	percentOfEject	= 0.0f;							// this[0xfe] = 0
	ejectState		= -1;							// this[0x100] = -1

	simulationFlags |= 0x1;							// this[6] |= 1  (state dirty)
}

//
// @004bbf88 -- slot 12 (GetStatusFlags).  Body NOT recovered by the decompiler
// (function prologue present at 0x4bbf88).  Overrides MechWeapon's
// PoweredSubsystem::GetStatusFlags (@004b0f48); best-effort: ORs the weapon's
// ammo/jam status bits onto the base flags.
//
LWord
	ProjectileWeapon::GetStatusFlags()
{
	// BEST-EFFORT -- body unrecovered.  Defer to base until a human disassembles
	// 0x4bbf88.
	return MechWeapon::GetStatusFlags();
}

//
// @004bc6c8 -- slot 13.  Print the MechWeapon state, then append the
// ammo-specific weapon-alarm state.
//
void
	ProjectileWeapon::PrintState()
{
	MechWeapon::PrintState();						// FUN_004b9d00

	switch (weaponAlarm.GetState())					// *(this+0x364)
	{
	case JammedState:			DebugStream << GetName() << " = Jammed";			break;	// 5
	case TriggerDuringJamState:	DebugStream << GetName() << " = TriggerDuringJam";	break;	// 6
	case NoAmmoState:			DebugStream << GetName() << " = NoAmmo";			break;	// 7
	default:					DebugStream << GetName() << "Unknown weapon state!";	break;
	}
	DebugStream.Flush();
}

//
// @004bbc20 -- slot 16, MechWeapon-family per-frame hook (overrides MechWeapon
// @004b0b5c).  Body NOT recovered (prologue present at 0x4bbc20).
//
void
	ProjectileWeapon::UpdateWeaponState()
{
	// BEST-EFFORT -- body unrecovered.  TODO: disassemble 0x4bbc20.
}


//#############################################################################
// Internal model helpers
//

//
// @004bbb50 -- advance the eject/reload cycle.  While ejecting (ejectState==0)
// run the TimeToEject countdown (clamped >= 0) and recompute PercentOfEject;
// when the countdown reaches ~0 ask the AmmoBin for the next round, raising the
// NoAmmo alarm and parking the weapon if the bin is dry.
//
void
	ProjectileWeapon::UpdateEject(Scalar time_slice)
{
	if (ejectState == 0)
	{
		timeToEject -= time_slice;								// this+0x3f4
		if (timeToEject < 0.0f)									// _DAT_004bbc18 == 0.0f
			timeToEject = 0.0f;
		percentOfEject = (totalTimeToEject - timeToEject) / totalTimeToEject;	// this+0x3f8
	}

	if (Close_Enough(timeToEject, 0.0f))						// FUN_004dcd00 .. _DAT_004bbc1c
	{
		AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve();			// FUN_00417ab4(this+0x43c)
		if (bin != 0)
		{
			bin->DumpAmmo();									// FUN_004bd588
			weaponAlarm.SetLevel(NoAmmoState);					// FUN_0041bbd8(this+0x350, 7)
		}
		ejectState = -1;										// this+0x400 = -1 (done)
	}
}

//
// @004bbc78 -- bleed the firing charge (MechWeapon recoil @0x3E8) at a rate set
// by the available bus voltage and the AmmoBin's remaining supply ratio.
//
void
	ProjectileWeapon::DrawFiringCharge(Scalar time_slice)
{
	// bus voltage scale (FUN_004b0d50) -- clamp to >= 1.0
	Scalar voltage = 1.0f;
	if (voltage < 1.0f)											// _DAT_004bbd00 == 1.0f
		voltage = 1.0f;

	// available supply ratio from the linked voltage source.  The shipped code
	// read source->outputVoltage / source->ratedVoltage off the resolved Generator
	// segment; here we use the inherited PoweredSubsystem voltageScale, which is
	// that same normalised available-voltage figure.
	Scalar supply = voltageScale;								// (+0x1dc)/(+0x1d8)
	if (supply < minVoltagePercentToFire)						// this+0x404
		supply = 0.0f;

	recoil -= (time_slice * supply) / voltage;					// *(this+0x3e8)
}

//
// @004bbfcc -- roll a heat-scaled jam chance.  Returns True (jam!) when a
// uniform random sample falls under the current jam probability:
//      p = clamp(0.41 * currentTemperature / failureTemperature,
//                minJamChance, 1.0)
// Gated on the weapon being heat-active (this+0x184) and the owning Mech's
// "live fire" flag (mech.controls+0x25c).
//
Logical
	ProjectileWeapon::CheckForJam()
{
	if (!LiveFireEnabled())										// *(*(this+0xd0)+400)+0x25c
		return False;

	// BRING-UP: the reconstructed heat model holds currentTemperature at a constant ~77
	// (thermalMass is huge, so temp doesn't yet rise/fall with fire), which makes this
	// heat-scaled jam roll fire spuriously EVERY shot -- and since JammedState clears only
	// on ResetToInitialState, a weapon that jams STAYS jammed for the whole mission (you'd
	// fire one salvo, then only lasers).  The authentic rule is "jam risk scales with heat",
	// so gate the roll on the weapon actually being in a degraded HEAT STATE (heatAlarm
	// level >= 1) instead of the raw heatLoad.  In bring-up the alarm stays 0 (temp <<
	// degradation threshold) -> weapons fire reliably; real jams return once the heat model
	// drives true temps.  (FOLLOW-UP: make JammedState recoverable via an unjam delay so
	// even a genuine overheat jam clears.)
	if (heatAlarm.GetLevel() < HeatSink::DegradationHeat)		// was: if (heatLoad <= 0.0f)
		return False;

	Scalar p = (_DAT_004bc064 * currentTemperature) / failureTemperature;	// 0.41 * temp/failTemp
	if (minJamChance <= p)										// this+0x3fc
	{
		if (_DAT_004bc068 < p)									// clamp to 1.0
			p = _DAT_004bc068;
	}
	else
	{
		p = minJamChance;
	}

	return (p > UniformRandom()) ? True : False;				// FUN_00408050
}

//
// @004bc06c -- lead-solution time-of-flight.  Sample the current target
// position, advance it by launchVelocity, and re-run targeting.  With no target
// in range, return minTimeOfFlight plus a small bias; otherwise return
// rangeToTarget / projectileSpeed.
//
Scalar
	ProjectileWeapon::ComputeTimeOfFlight()
{
	GetTargetPosition(leadPosition);							// FUN_004b9cbc(this, this+0x420)
	leadPosition.x += launchVelocity.x;							// this+0x420 += this+0x410
	leadPosition.y += launchVelocity.y;
	leadPosition.z  = launchVelocity.z;							// (z copied, not accumulated)

	if (!UpdateTargeting())										// FUN_004b9bdc -- target in range?
	{
		return minTimeOfFlight + _DAT_004bc100;					// +0x40c + 1.0
	}

	Scalar speed = Sqrt(										// FUN_004dd138
		leadPosition.z * leadPosition.z +
		leadPosition.y * leadPosition.y +
		leadPosition.x * leadPosition.x);
	return rangeToTarget / speed;								// this+0x324 / speed
}


//#############################################################################
// Simulation / Firing
//

//
// @004bbd04 -- Performance: per-frame ProjectileWeapon update.  Body NOT
// recovered by the decompiler (prologue confirmed at 0x4bbd04).  From context it
// sequences DrawFiringCharge / UpdateEject / CheckForJam / ComputeTimeOfFlight
// and dispatches FireWeapon (vtable slot 18) when the trigger + charge + ammo
// conditions are met.
//
void
	ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice)
{
	Check(this);

	// 1. Recover the firing charge (recoil counts down toward 0 in
	//    DrawFiringCharge) and advance any in-progress magazine eject.
	DrawFiringCharge(time_slice);						// @004bbc78 -- bleed recoil (+0x3E8)
	UpdateEject(time_slice);							// @004bbb50 -- idle unless ejectState==0

	// 2. Rising-edge trigger from the owning Mech's discharge signal.
	//    Bring-up: the controls mapper is bypassed, so drive fireImpulse from the
	//    per-frame trigger pulse -- else CheckFireEdge never sees an edge and a
	//    projectile weapon never fires.  SPLIT CONTROLS (interim, toward the real
	//    controls-mapper weapon groups): projectile weapons (the missile
	//    launchers, cannon) fire on the MISSILE channel (CTRL), not the
	//    energy-weapon channel (SPACE) -- one key no longer fires everything.
	extern int gBTMissileTrigger;
	fireImpulse = (Scalar)gBTMissileTrigger;			// this+0x31C
	Logical trigger = CheckFireEdge();					// MechWeapon @004b9608

	static int s_pwtick = 0;
	if (getenv("BT_PROJ_LOG") && (((++s_pwtick) % 240) == 0 || trigger))
		DEBUG_STREAM << "[projweap] tick " << GetName() << " trig=" << (int)trigger
			<< " gTrig=" << gBTMissileTrigger << " ready=" << (int)ReadyToFire()
			<< " state=" << weaponAlarm.GetState() << " recoil=" << recoil << " x\n" << std::flush;

	// 3. Firing state machine (weaponAlarm @0x350).  Modelled on the RP analog
	//    RivetGun::RivetGunSimulation (WEAPSYS.cpp): the Jammed / NoAmmo latches
	//    persist and only escalate on a trigger pull; the default (ready) branch
	//    rolls the heat-scaled jam chance and -- on a clean shot with charge and
	//    ammo -- dispatches FireWeapon through the vtable (slot 18) so the
	//    MissileLauncher override spawns Missiles.
	switch (weaponAlarm.GetState())						// *(this+0x364)
	{
	case JammedState:									// 5 -- cleared only by ResetToInitialState
		if (trigger)
			weaponAlarm.SetLevel(TriggerDuringJamState);	// 6
		break;

	case NoAmmoState:									// 7 -- re-assert on a dry trigger pull
		if (trigger)
			weaponAlarm.SetLevel(NoAmmoState);
		break;

	default:											// Loaded / Loading / Firing / idle
		if (trigger && ReadyToFire())
		{
			if (CheckForJam())							// @004bbfcc -- heat-scaled jam roll
			{
				weaponAlarm.SetLevel(JammedState);		// 5
				break;
			}

			// refresh the lead solution (updates leadPosition / targeting), then
			// fire.  FireWeapon() consumes a round + spawns the projectile/missile
			// and (on a dry bin) latches NoAmmo itself.
			ComputeTimeOfFlight();						// @004bc06c
			FireWeapon();								// vtable slot 18 -> spawn path
			if (weaponAlarm.GetState() != NoAmmoState && weaponAlarm.GetState() != JammedState)
				weaponAlarm.SetLevel(0);				// Firing
		}
		break;
	}

	Check_Fpu();
}

//
// @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap
// @004ba45c).  Body NOT recovered by the decompiler (prologue confirmed at
// 0x4bc104, immediately following the 1.0f literal @0x4bc100).  Spawns the
// generic projectile/tracer and starts the eject cycle.  MissileLauncher
// overrides this again at @004bcc60 to launch Missile entities.
//
void
	ProjectileWeapon::FireWeapon()
{
	Check(this);

	// Pull a round; a dry / not-ready bin latches NoAmmo and aborts the shot.
	if (!ConsumeRound())								// FUN_004bd4f4 (AmmoBin::FeedAmmo)
	{
		return;
	}

	// Begin the per-shot recharge cooldown (recoil bleeds back down to 0 in
	// DrawFiringCharge before the weapon is ReadyToFire again).
	recoil = rechargeRate;								// this[0xfa] = this[0xf7]

	// Resolve the live muzzle.
	Point3D muzzle;
	GetMuzzlePoint(muzzle);								// MechWeapon @004b9948

	// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
	// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
	// Entity base mismatch -- see BTPushProjectile / mech4.cpp).  Speed = |launchVelocity|.
	char *o = (char *)owner;							// inherited MechSubsystem::owner (the Mech)
	// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
	// targeting block writes; a consistent producer/consumer pair on our
	// compiled object).  Task #41: the slots hold the WORLD PICK -- the enemy
	// mech under the boresight, or the terrain downrange (a missile fired at
	// the scenery flies to the ground point and detonates without damage), or
	// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
	// wouldn't have fired either).
	void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
	Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
	Scalar  speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
								+ launchVelocity.y*launchVelocity.y
								+ launchVelocity.z*launchVelocity.z);
	BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount);

	simulationFlags |= 0x1;								// replication-dirty
	Check_Fpu();
}


//#############################################################################
// CreateStreamedSubsystem
//
// @004bc7cc -- parse the ProjectileWeapon-specific resource fields after the
// MechWeapon base parser.  Stamps subsystemModelSize = 0x1D0.  (The classID
// stamp here equals the MechWeapon class id 0xBCD -- ProjectileWeapon is an
// abstract base with no separately-registered ClassID; the concrete
// MissileLauncher::CreateStreamedSubsystem @004bd08c overwrites the stamp with
// 0x1E0 / classID 0xBD0.)
//
int
	ProjectileWeapon::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);	// 0x1D0
	subsystem_resource->classID            = (RegisteredClass::ClassID)Mech::MechWeaponClassID;	// 0xBCD (abstract)

	if (passes == 1)
	{
		// prime all projectile fields to "unset"
		subsystem_resource->tracerInterval			= -1;			// +0x1BC
		subsystem_resource->ammoBinIndex			= -1;			// +0x1C0
		subsystem_resource->minTimeOfFlight			= -1.0f;		// +0x1C4
		subsystem_resource->minVoltagePercentToFire	= -1.0f;		// +0x1C8
		subsystem_resource->minJamChance			= -1.0f;		// +0x1CC
	}

	if (
		!model_file->GetEntry(subsystem_name, "TracerInterval", &subsystem_resource->tracerInterval)
		&& subsystem_resource->tracerInterval == -1
	)
	{
		DebugStream << subsystem_name << " missing TracerInterval!" << endl;
		return False;
	}

	//
	// AmmoBin: a subsystem name; "Unspecified" leaves the resource value,
	// otherwise it is resolved to a subsystem index (+2 bias applied when not
	// "Unspecified").
	//
	const char *ammoBinName = "Unspecified";
	if (
		!model_file->GetEntry(subsystem_name, "AmmoBin", &ammoBinName)
		&& subsystem_resource->ammoBinIndex == -1
	)
	{
		DebugStream << subsystem_name << " missing AmmoBin!" << endl;
		return False;
	}
	if (strcmp(ammoBinName, "Unspecified") != 0)
	{
		subsystem_resource->ammoBinIndex = ResolveSubsystemName(directories, ammoBinName);	// FUN_004215b0
	}
	if (subsystem_resource->ammoBinIndex < 0)
	{
		DebugStream << subsystem_name << " has an invalid Ammo Bin!" << endl;
		return False;
	}
	if (strcmp(ammoBinName, "Unspecified") != 0)
	{
		subsystem_resource->ammoBinIndex += 2;				// segment-table +2 bias
	}

	if (
		!model_file->GetEntry(subsystem_name, "MinTimeOfFlight", &subsystem_resource->minTimeOfFlight)
		&& subsystem_resource->minTimeOfFlight == -1.0f		// _DAT_004bca9c
	)
	{
		DebugStream << subsystem_name << " missing MinTimeOfFlight !" << endl;
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "MinJamChance", &subsystem_resource->minJamChance)
		&& subsystem_resource->minJamChance == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing MinJamChance !" << endl;
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "MinVoltagePercentToFire", &subsystem_resource->minVoltagePercentToFire)
		&& subsystem_resource->minVoltagePercentToFire == -1.0f
	)
	{
		DebugStream << subsystem_name << " missing MinVoltagePercentToFire !" << endl;
		return False;
	}

	return True;
}


//===========================================================================//
// WAVE 7 factory bridge -- ProjectileWeapon (factory case 0xBCD, "MechWeapon"
//   mislabel).  The real class at 0xBCD (ctor @004bc3fc) is ProjectileWeapon
//   (the autocannon/ballistic ammo weapon); the factory built the base MechWeapon
//   stub (whose FireWeapon is the pure-virtual trap @004ba45c).  alloc 0x448 (==
//   the byte-exact ProjectileWeapon sizeof).  Constructs the AmmoBin connection +
//   ticks its real ProjectileWeaponSimulation (charge/ammo/jam/eject); FireWeapon
//   consumes a round + recoils but DEFERS the projectile spawn (Phase B).
//===========================================================================//
Subsystem *CreateProjectileWeaponSubsystem(Mech *owner, int id, void *seg)
{
	Check(sizeof(ProjectileWeapon) <= 0x448);
	return (Subsystem *) new (Memory::Allocate(0x448))
		ProjectileWeapon(owner, id, (ProjectileWeapon::SubsystemResource *)seg, ProjectileWeapon::DefaultData);
}
