//===========================================================================//
// File:	mechweap.cpp                                                        //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: MechWeapon -- abstract base for every mounted Mech weapon        //
//---------------------------------------------------------------------------//
//   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 @004b95ec-@004b9d10 (file=bt/mechweap.cpp)
// plus the pure-virtual stub @004ba45c (file=bt/mechweap.hpp).  Method and
// member names follow the surviving derived sources (PPC.CPP, GAUSS.CPP,
// MISLANCH.HPP), the resource-parser tag strings, and the RP WEAPSYS.h analog.
// Each non-trivial method cites the originating @ADDR.
//
// Resolved read-only constants (4 LE bytes from section_dump CODE):
//      _DAT_004b9648 = 00 00 00 00 =  0.0f   (fire-edge threshold)
//      _DAT_004b9b98 = 00 00 80 bf = -1.0f   (default pip-colour component)
//      _DAT_004b9c98 = 00 00 80 3f =  1.0f   (full-heat reference)
//      _DAT_004ba37c = 00 00 80 bf = -1.0f   (resource "unset" sentinel)
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_004b0f74  PoweredSubsystem base constructor
//      FUN_004b115c  PoweredSubsystem destructor
//      FUN_004b0e6c  PoweredSubsystem::ResetToInitialState (vtable slot 10)
//      FUN_004b0efc  PoweredSubsystem (vtable slot 9 -- ReadUpdateRecord)
//      FUN_004b0f48  PoweredSubsystem::GetStatusFlags (vtable slot 12)
//      FUN_004b1224  PoweredSubsystem::PrintState (vtable slot 13)
//      FUN_004b13ac  PoweredSubsystem::CreateStreamedSubsystem
//      FUN_0041bd34  Subsystem::TakeDamage (vtable slot 6, base)
//      FUN_0041c500  Subsystem::WriteUpdateRecord (vtable slot 7, base)
//      FUN_0041b9ec  AlarmIndicator(levels)        FUN_0041baa4  ~AlarmIndicator
//      FUN_0041bbd8  AlarmIndicator::SetLevel(n)
//      FUN_0041db7c  Damage::Damage() / init
//      FUN_0041a1a4  IsDerivedFrom(classDerivations)
//      FUN_00408440  Vector copy        FUN_0040a7f4  Point copy
//      FUN_00408644  Vector subtract (out = a - b)
//      FUN_004087f4  Vector negate/copy
//      FUN_0040aadc  Matrix identity    FUN_0040ad2c  Matrix from angles
//      FUN_00409a70  apply transform    FUN_00409968  Vector set
//      FUN_004dd138  sqrtf              FUN_004dc8ec  atan2f(double,double)
//      FUN_00408110  angle normalise
//      FUN_0041f216 / FUN_0041f235      segment-iterator construct / destruct
//      FUN_00424da8  resolve segment world point
//      FUN_0049b6d8  cockpit HUD: register pip / fire marker
//      FUN_004b13ac etc. (resource parser primitives):
//      FUN_00404118  NotationFile::Read(scalar)   FUN_00404088  Read(string)
//      FUN_004040d8  Read(int/bool)               FUN_00404190  Read(int)
//      FUN_00406824  parse colour       FUN_00406ff8  ResourceFile::FindResource
//      FUN_004d4b58  strcmp             FUN_004d4b14  memcmp   FUN_004d4c78  strstr
//      FUN_004dbb24  DebugStream <<     FUN_004d9c38  DebugStream flush
//      FUN_0040385c  Verify()/assert(msg,file,line)
//

#include <bt.hpp>
#include <messmgr.hpp>		// SubsystemMessageManager (task #8 damage submission)
#include <mechmppr.hpp>		// MechControlsMapper -- the config-session virtuals (task #6)
#pragma hdrstop

#if !defined(MECHWEAP_HPP)
#	include <mechweap.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
#	include <testbt.hpp>
#endif


//#############################################################################
// Shared Data Support
//
Derivation
	MechWeapon::ClassDerivations(
		PoweredSubsystem::GetClassDerivations(),
		"MechWeapon"
	);

// task #6 -- the binary's MechWeapon handler table @0x511860 (section_dump:
// {9, "ConfigureMappables"@0x511994, @004b9550}, {10, "ChooseButton"@0x5119A7,
// @004b95b8}); chained onto the PoweredSubsystem set.  Table INSIDE the
// accessor (function-local static) -- the cross-TU static-init order trap
// emptied the powersub table this same way (task #12); this one only worked
// by TU-order luck.
Receiver::MessageHandlerSet&
	MechWeapon::GetMessageHandlers()
{
	static const Receiver::HandlerEntry entries[]=
	{
		MESSAGE_ENTRY(MechWeapon, ConfigureMappables),	// id 9  @004b9550
		MESSAGE_ENTRY(MechWeapon, ChooseButton)			// id 10 @004b95b8
	};
	static Receiver::MessageHandlerSet messageHandlers(
		ELEMENTS(entries), entries,
		PoweredSubsystem::GetMessageHandlers()
	);
	return messageHandlers;
}

Simulation::AttributeIndexSet	MechWeapon::AttributeIndex;	// legacy empty member (superseded by GetAttributeIndex)

// gauge data-binding wave: OutputVoltage/PercentDone -> rechargeLevel (1.0 ==
// ready; ComputeOutputVoltage/the charge state machine drive it, so both animate
// as the weapon recharges).  Chained to PoweredSubsystem's dense index.
// task #16: the ID GAP between our parent chain (ends 0x0D) and the pinned
// binary ids MUST be padded -- see the enum note.  Locked so a future parent
// publish shows up here instead of silently double-assigning ids:
static_assert((int)PoweredSubsystem::NextAttributeID
	== (int)MechWeapon::MechWeaponPadFirstAttributeID,
	"MechWeapon pad base != PoweredSubsystem::NextAttributeID -- re-count the pads");

const MechWeapon::IndexEntry
	MechWeapon::AttributePointers[]=
{
	// task #16: the FULL binary table @0x511890 (eleven entries) + the
	// port-extra OutputVoltage alias.  Every member offset matches the
	// binary's (layout static_assert-locked); TriggerState stays the streamed
	// fire-button binding (id 0x13 PINNED).
	//
	// The five PADS fill our chain-vs-binary id gap (0x0D..0x11) with valid,
	// named, never-bound entries (the binary's own ids here are the powered/
	// aux-screen family we don't publish); target = rechargeLevel, harmless.
	{ (int)MechWeapon::MechWeaponPadFirstAttributeID + 0, "MechWeaponPad0D",
		(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
	{ (int)MechWeapon::MechWeaponPadFirstAttributeID + 1, "MechWeaponPad0E",
		(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
	{ (int)MechWeapon::MechWeaponPadFirstAttributeID + 2, "MechWeaponPad0F",
		(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
	{ (int)MechWeapon::MechWeaponPadFirstAttributeID + 3, "MechWeaponPad10",
		(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
	{ (int)MechWeapon::MechWeaponPadFirstAttributeID + 4, "MechWeaponPad11",
		(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
	ATTRIBUTE_ENTRY(MechWeapon, PercentDone,        rechargeLevel),		// 0x12 @0x320
	ATTRIBUTE_ENTRY(MechWeapon, TriggerState,       fireImpulse),		// 0x13 @0x31C
	ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget,   rangeToTarget),		// 0x14 @0x324
	ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange,  targetWithinRange),	// 0x15 @0x34C
	ATTRIBUTE_ENTRY(MechWeapon, WeaponRange,        effectiveRange),	// 0x16 @0x328 (heat-degraded)
	ATTRIBUTE_ENTRY(MechWeapon, PipPosition,        pipPosition),		// 0x17 @0x338
	ATTRIBUTE_ENTRY(MechWeapon, PipColor,           pipColor),			// 0x18 @0x33C
	ATTRIBUTE_ENTRY(MechWeapon, PipExtendedRange,   pipExtendedRange),	// 0x19 @0x348
	ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A @0x330
	ATTRIBUTE_ENTRY(MechWeapon, RearFiring,         usesExternalModel),	// 0x1B @0x334
	ATTRIBUTE_ENTRY(MechWeapon, WeaponState,        weaponAlarm),		// 0x1C @0x350
	ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage,      rechargeLevel)		// 0x1D (port alias)
};

MechWeapon::AttributeIndexSet&
	MechWeapon::GetAttributeIndex()
{
	static MechWeapon::AttributeIndexSet attributeIndex(
		ELEMENTS(MechWeapon::AttributePointers),
		MechWeapon::AttributePointers,
		PoweredSubsystem::GetAttributeIndex()
	);
	return attributeIndex;
}

MechWeapon::SharedData
	MechWeapon::DefaultData(
		&MechWeapon::ClassDerivations,
		MechWeapon::GetMessageHandlers(),
		MechWeapon::GetAttributeIndex(),
		MechWeapon::StateCount
	);


//#############################################################################
// The CONFIG-MODE session handlers (task #6)
//

//
// @004b9550 -- id 9 "ConfigureMappables": the press-and-HOLD session bracket.
// dataContents = +-(element+1) (press/release, the L4 button value convention).
// Press: open the session on THIS weapon -- the roster-slot-0 mapper flips the
// global mode NonMapping->Mapping, latches &fireImpulse as the active configure
// target, and temp-maps the fire buttons to send ChooseButton (id 10) here.
// Release: close the session (mode restored, temp layer stripped).
// The +0x110 write (0 open / -1 idle) is the binary's config-session flag
// (ctor default -1, part_012.c:15775); the port field carries the older name
// vitalSubsystemIndex -- no confirmed reader yet (read-site survey pending).
//
void
	MechWeapon::ConfigureMappablesMessageHandler(
		ReceiverDataMessageOf<ControlsButton> *message
	)
{
	Check(this);
	Check_Pointer(message);

	int value   = message->dataContents;				// +-(element+1)
	int element = ((value < 0) ? -value : value) - 1;

	MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper();	// **(mech+0x128)
	if (mapper == 0)
	{
		return;											// (binary assumes slot 0 is live)
	}

	if (value < 1)
	{
		mapper->ExitConfiguration(element);				// vtbl+0x3C
		vitalSubsystemIndex = -1;						// this+0x110 -> idle
	}
	else
	{
		vitalSubsystemIndex = 0;						// this+0x110 -> session open
		mapper->EnterConfiguration(						// vtbl+0x38
			element,
			(ControlsButton *)&fireImpulse,				// this+0x31C (TriggerState)
			this,
			ChooseButtonMessageID,						// 10
			ConfigureMappablesMessageID,				// 9
			0);											// active_message_id (direct bind)
	}
}

//
// @004b95b8 -- id 10 "ChooseButton": while the session is open, a tapped fire
// button sends its RAW 1-based value here; the mapper's direct AddOrErase
// TOGGLES buttonGroup[value-1]'s permanent (0x10000) mapping onto
// &fireImpulse -- join the group if absent, leave it if present.  Press only.
//
void
	MechWeapon::ChooseButtonMessageHandler(
		ReceiverDataMessageOf<ControlsButton> *message
	)
{
	Check(this);
	Check_Pointer(message);

	if (message->dataContents > 0)
	{
		MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper();
		if (mapper != 0)
		{
			mapper->AddOrErase(							// vtbl+0x44 (direct overload)
				(unsigned int)message->dataContents,	// RAW 1-based
				(ControlsButton *)&fireImpulse);
		}
	}
}


//#############################################################################
// Firing -- abstract base body
//
// @004b95ec -- MechWeapon is abstract; the base FireWeapon() must never run.
// Exact analog of RP GenericGun::Fire()'s "Should not be here!" stub.  Real
// firing lives in Emitter::FireWeapon (PPC/Gauss) and ProjectileWeapon
// (MissileLauncher).  Assert tag: MECHWEAP.CPP : 0xC9.
//
void
	MechWeapon::FireWeapon()
{
	Fail("MechWeapon abstract class should not be fired");
}

//
// @004ba45c -- a second pure-virtual placeholder emitted from MECHWEAP.HPP:343
// ("ERROR: Accessing Pure Virtual Method").  Compiled into the next translation
// unit (Emitter); reproduced here for completeness.  A human should reconcile
// which header-declared pure virtual this guards.
//
// void MechWeapon::<pure-virtual>() {
//     Verify(False, "ERROR: Accessing Pure Virtual Method",
//         "d:\\tesla\\bt\\bt\\mechweap.hpp", 0x157);
// }


//#############################################################################
// Construction / Destruction
//
// @004b99a8 -- chains to the PoweredSubsystem ctor (@004b0f74), installs the
// MechWeapon vtable (PTR_FUN_00511d2c), constructs the alarm indicator and the
// Damage member, then copies the weapon parameters out of the resource record.
//
MechWeapon::MechWeapon(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	SharedData			&shared_data
):
	PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data),
	weaponAlarm(/*levels=*/ 3),								// FUN_0041b9ec(this+0xd4, levels)
	damageData()											// FUN_0041db7c(this+0xea)
{
	Check(owner);
	Check_Pointer(subsystem_resource);

	// pip colour defaults to (-1,-1,-1) -- "use the configured/team colour".
	pipColor.Red			= -1.0f;	// _DAT_004b9b98
	pipColor.Green			= -1.0f;
	pipColor.Blue			= -1.0f;

	// --- copy weapon parameters out of the resource ---
	rechargeRate			= subsystem_resource->rechargeRate;			// +0x190
	explosionResourceID		= subsystem_resource->explosionResourceID;	// +0x198
	damageData.damageType	= subsystem_resource->damageType;			// +0x1A0
	damageData.damageAmount	= subsystem_resource->damageAmount;			// +0x19C
	segmentPageIndex		= subsystem_resource->segmentIndex;			// +0x28
	weaponRange				= subsystem_resource->weaponRange;			// +0x194
	heatCostToFire			= subsystem_resource->heatCostToFire;		// +0x1A4
	pipPosition				= subsystem_resource->pipPosition;			// +0x1A8
	pipColor				= subsystem_resource->pipColor;				// +0x1AC..+0x1B4
	pipExtendedRange		= subsystem_resource->pipExtendedRange;		// +0x1B8
	effectiveRange			= weaponRange;	// 0x328 primed to weaponRange (recomputed in UpdateTargeting)

	simulationFlags |= 0x8;			// this[10] |= 8  (mark "weapon" subsystem)

	rangeToTarget			= 0.0f;		// 0x324
	targetWithinRange		= False;	// 0x34C
	// segmentReference/pipSegment were phantom tail members (past the binary 0x3F0 end);
	// removed.  The muzzle segment index is the inherited base slot this+0xdc (used by
	// GetMuzzlePoint); the pip mount is gated by useConfiguredPip in DrawWeaponPip.

	// usesExternalModel: True if the model name contains the marker substring
	// (DAT_00511aa2); selects whether the configured pip is drawn.
	usesExternalModel = (strstr(GetName(), "EXT") != 0) ? True : False;

	fireImpulse				= 0.0f;		// 0x31C
	// task #6: the binary MechWeapon ctor defaults its controls-destination
	// pass-through to &fireImpulse when the caller passes NULL (@004b99a8
	// `if (param_7==0) param_7 = this+0x31C`); the chain stores it at
	// MechSubsystem+0xE8/+0xEC where ConfigMapGauge's GetMapState reads it.
	controlDestination		= (void *)&fireImpulse;		// 0xE8
	controlMessageID		= 0;						// 0xEC
	rechargeLevel			= 1.0f;		// 0x320  (fully charged)
	previousFireImpulse		= 0.0f;		// 0x3A4
	recoil					= 0.0f;		// 0x3E8
	useConfiguredPip		= (usesExternalModel == 0);	// 0x3E0
	estimatedReadyTime		= 0;		// 0x330 (binary attr "EstimatedReadyTime")

	Check_Fpu();
}

//
// @004b9b9c -- destroys the alarm indicator, chains to ~PoweredSubsystem, and
// (if the deleting-dtor bit is set) frees the object.
//
MechWeapon::~MechWeapon()
{
	Check(this);
	// weaponAlarm (@0xd4) destroyed by its own dtor (FUN_0041baa4(this+0xd4, 2));
	// base chain (FUN_004b115c) handles the rest.
	Check_Fpu();
}


//#############################################################################
// Test Class Support
//
Logical
	MechWeapon::TestClass(Mech &)
{
	return True;
}

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


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

//
// @004b96d4 -- slot 9 (TASK #51 RENAME: this body was mislabeled slot-6
// ReadUpdateRecord; @004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage,
// which dispatches on *param==4 == Damage::damageType -- damage semantics).
// The PoweredSubsystem::TakeDamage body (electrical-short handling) is not yet
// reconstructed; chain to the engine Subsystem base, and keep the damage-zone
// alarm tracking this port has shipped with (best-effort; the binary's alarm
// display on weapon damage flows through the unreconstructed @004b0efc chain).
//
void
	MechWeapon::TakeDamage(Damage &damage)
{
	Subsystem::TakeDamage(damage);					// engine base (real chain: @004b0efc)

	// Port behavior (kept): a destroyed weapon clears its display alarm;
	// otherwise the alarm tracks the resolved damage-zone state.
	::DamageZone *dz = this->Subsystem::damageZone;
	if (dz != 0
		&& dz->GetGraphicState() == DamageZone::DestroyedGraphicState)
	{
		weaponAlarm.SetLevel(0);
	}
	else
	{
		weaponAlarm.SetLevel(
			(dz != 0 && dz->damageLevel > 0.0f) ? 1 : 0
		);
	}
}

//
// @004b9690 -- slot 7.  Append the weapon's replication fields to the update
// record.  DISASM CORRECTIONS (task #51): `*message = 0x18` writes the record
// LENGTH (record[0] = recordLength), not recordID; and message[5] derives from
// this+0x360 == weaponAlarm+0x10 (LevelCountB), not from the level.  BASE-TYPED
// param: with the class's own UpdateRecord typedef this silently failed to
// override the engine virtual, so the engine base ran and no weapon field
// ever serialized.
//
void
	MechWeapon::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
{
	Subsystem::WriteUpdateRecord(message, update_model);	// FUN_0041c500 (header + subsystemID)

	MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message;
	rec->recordLength = sizeof(MechWeapon__UpdateRecord);	// *message = 0x18
	rec->alarmState   = weaponAlarm.GetLevel();				// message[4] = *(this+0x364)
	rec->weaponIdle   = (weaponAlarm.LevelCountB() == 0) ? 1 : 0;	// message[5] = (*(this+0x360)==0)
}

//
// @004b964c -- slot 6 (TASK #51 RENAME: was mislabeled TakeDamage).  Apply a
// weapon update record on the REPLICANT: chain the Simulation base
// (@0041bd34 -- lastUpdate + simulationState), then the weapon alarm --
// reset-through-0 first when the record's weaponIdle flag is set, then the
// replicated alarm level.
//
void
	MechWeapon::ReadUpdateRecord(Simulation__UpdateRecord *message)
{
	Simulation::ReadUpdateRecord(message);			// FUN_0041bd34

	MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message;
	if (rec->weaponIdle != 0)						// record+0x14
	{
		weaponAlarm.SetLevel(0);
	}
	weaponAlarm.SetLevel(rec->alarmState);			// record+0x10
}

//
// @004b96ec -- slot 10.  Reset the firing/recharge state, then chain to base.
//
void
	MechWeapon::ResetToInitialState()
{
	PoweredSubsystem::ResetToInitialState();		// FUN_004b0e6c
	recoil				= 0.0f;		// this[0xfa] = 0
	rechargeLevel		= 1.0f;		// this[200]  = 0x3f800000
	fireImpulse			= 0.0f;		// this[199]  = 0
	previousFireImpulse	= 0.0f;		// this[0xe9] = 0
}

//
// @004b9d00 -- slot 13.  Defers to the PoweredSubsystem print routine.
//
void
	MechWeapon::PrintState()
{
	PoweredSubsystem::PrintState();					// FUN_004b1224
}


//#############################################################################
// Targeting / aiming helpers
//

//
// @004b9bdc -- recompute the range to the owning Mech's current target and the
// heat-degraded effective range, then decide whether the target is in range.
//
//   no target (entity+0x388 == 0):
//       rangeToTarget = -1;  targetWithinRange = False
//   else:
//       v = entity->targetPoint(+0x37c) - entity->muzzlePoint(+0x100)
//       rangeToTarget  = |v|
//       effectiveRange = (1.0 - heatSource->heatLoad(+0x158)) * weaponRange
//       targetWithinRange = (effectiveRange > rangeToTarget)
//
Logical
	MechWeapon::UpdateTargeting()
{
	if (!HasActiveTarget())							// *(entity+0x388) == 0
	{
		rangeToTarget		= -1.0f;				// 0xbf800000
		targetWithinRange	= False;
	}
	else
	{
		Point3D target, muzzle;
		GetTargetPosition(target);
		GetMuzzlePoint(muzzle);

		Vector3D delta;
		delta.Subtract(target, muzzle);				// FUN_00408644
		rangeToTarget = (Scalar)Sqrt(				// FUN_004dd138
			delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);

		// 1.0f == _DAT_004b9c98; heatLoad is the inherited HeatableSubsystem load.
		effectiveRange = (1.0f - heatLoad) * weaponRange;	// *0x32c

		targetWithinRange = (effectiveRange > rangeToTarget) ? True : False;
	}
	return targetWithinRange;
}

//
// @004b9608 -- rising-edge detector on fireImpulse (threshold _DAT_004b9648 == 0.0f).
// Returns True the frame fireImpulse becomes positive while the previous sample
// was non-positive, then stores the current sample for next time.
//
// ⚠ BIT-PATTERN COMPARE (task #8 root-cause): TriggerState carries the raw
// ControlsButton INT bit-copied by the streamed direct mapping (+(button+1)
// press / -(button+1) release) -- the release value (e.g. -65 = 0xFFFFFFBF)
// reads as a NEGATIVE NaN in float terms.  The binary's x87 compare (FNSTSW/
// SAHF + JBE: unordered sets CF|ZF => branch TAKEN) effectively treated the
// NaN as "released", so its edge re-armed; an IEEE-correct float compare
// makes NaN<=0 FALSE and the detector LATCHES SHUT after the first release
// (observed live: one startup edge, then never again).  Compare the BIT
// PATTERNS as signed ints instead -- sign-correct for the button ints AND
// for genuine float values (0.0f = 0x0, positives > 0, negatives/NaNs < 0),
// reproducing the binary's effective behavior exactly.
//
Logical
	MechWeapon::CheckFireEdge()
{
	int current  = *(int *)&fireImpulse;
	int previous = *(int *)&previousFireImpulse;
	Logical edge = (current > 0 && previous <= 0) ? True : False;
	previousFireImpulse = fireImpulse;
	return edge;
}

//
// @004b9cbc -- copy the owning Mech's current target position (entity+0x1c4).
//
void
	MechWeapon::GetTargetPosition(Point3D &position)
{
	// E3: read the owning Mech's current target position (mech+0x37c, set by mech4's
	// targeting step) -- the local `targetPoint` member isn't refreshed in the port.
	char *o = (char *)owner;						// inherited MechSubsystem::owner (the Mech)
	if (o != 0)
		position = *(Point3D *)(o + 0x37c);			// MECH_TARGET_POS
	// (no owner -> leave position unchanged; the phantom `targetPoint` member is gone --
	//  binary GetTargetPosition @004b9cbc unconditionally reads the owner target slot.)
}

// Faithful FUN_004b9948 muzzle resolve, defined in mech4.cpp (a complete-Mech TU);
// mechweap.cpp reaches the owner Mech only as a raw pointer, so it bridges via void*.
extern void BTResolveWeaponMuzzle(void *ownerMech, int segIndex, Point3D &out);

//
// @004b9cdc -- direction from a supplied point to the weapon mount (entity+0x37c).
//
void
	MechWeapon::GetVectorToWeapon(const Point3D &from, Vector3D &direction)
{
	Point3D muzzle;
	GetMuzzlePoint(muzzle);							// resolve live (phantom muzzlePoint member removed)
	direction.Subtract(muzzle, from);				// FUN_00408644
}

//
// @004b9948 -- resolve the weapon's muzzle/site world position by looking up its
// segment in the owning Mech's segment table and transforming it.
//
void
	MechWeapon::GetMuzzlePoint(Point3D &point)
{
	// @004b9948 -- resolve the weapon's mount segment in the owning Mech's
	// segment table and transform it to world (FUN_00424da8).
	// DATABINDING FIX (muzzle wave, 2026-07-12): the binary's this+0xdc IS the
	// engine Subsystem's segmentIndex -- but the old raw `*(this+0xdc)` read
	// OUR compiled layout at that offset (garbage), so every weapon resolved a
	// RANDOM segment or none: missiles launched from heads/feet/below terrain
	// (user-hit on the Avatar) and the beam/missile paths grew gun-port
	// name-lookup hacks to compensate.  GetSegmentIndex() is the named member
	// [T0 SUBSYSTM.h:108] -- authentic mounts on every chassis.
	int segIndex = GetSegmentIndex();
	BTResolveWeaponMuzzle(owner, segIndex, point);	// mech4.cpp bridge (null-guarded)
}

//
// @004b9864 -- build an aim orientation from a direction vector (yaw/pitch via
// atan2), producing a rotation matrix.
//
void
	MechWeapon::ComputeAimOrientation(LinearMatrix &orientation, const Vector3D &direction)
{
	// Build a yaw/pitch aim orientation from a direction vector (atan2), roll 0.
	Scalar yaw   = (Scalar)atan2(-direction.x, -direction.z);	// FUN_004dc8ec
	Scalar len   = (Scalar)Sqrt(direction.z * direction.z + direction.x * direction.x);
	Scalar pitch = (Scalar)atan2(direction.y, len);				// FUN_004dc8ec

	EulerAngles angles(
		Radian(Radian::Normalize(pitch)),
		Radian(Radian::Normalize(yaw)),
		Radian(0.0f)
	);
	orientation = angles;							// AffineMatrix::operator=(EulerAngles)
}

//
// ⚠ IDENTITY CORRECTION (task #7): @004b9728 is MechWeapon::SendDamageMessage
// -- the weapon-fire damage submission into the owning mech's
// SubsystemMessageManager (mech+0x434 -> AddDamageMessage @0049b6d8), NOT a
// HUD pip.  The "HUD element record" reading below was the same 0x434
// misattribution the mapper suffered.  This body is the LIVE per-shot
// beam-damage path (called from Emitter::FireWeapon, emitter.cpp:289; decomp
// part_013.c:7758-7764) carrying the authentic per-weapon damageData +
// inflictingSubsystemID -- the damage-economy reconciliation (task #60)
// CONFIRMED the authored-DamageAmount economy; kShotDamage is retired to the
// kill-score and env-gated diag hooks (task #60).
//
void
	MechWeapon::SendDamageMessage(Entity *target)
{
	//
	// @004b9728 -- package this weapon's just-fired damageData as an
	// Entity::TakeDamageMessage and submit it to the owning mech's
	// SubsystemMessageManager for per-frame consolidation (AddDamageMessage
	// @0049b6d8) -- the authentic beam damage delivery (task #8; the caller
	// Emitter::FireWeapon fills damageAmount/damageForce/impactPoint first).
	//
	// Binary gate (part_013.c:6746): a Mech victim with NO designated zone
	// (owner+0x38C == -1) is SKIPPED.  The port PASSES -1 through [T3
	// divergence]: our Mech::TakeDamageMessageHandler cylinder-resolves the
	// unaimed zone (STEP 6), preserving the verified aiming model.
	//
	Mech *mech = (Mech *)GetEntity();					// weapon+0xD0
	if (mech == 0 || target == 0)
	{
		return;
	}

	extern int BTMechTargetZone(void *mech);			// mech+0x38C (mech4 TU)
	int zone = BTMechTargetZone(mech);

	Entity::TakeDamageMessage message(
		Entity::TakeDamageMessageID,
		sizeof(Entity::TakeDamageMessage),
		mech->GetEntityID(),							// inflicting = owner (+0x184)
		zone,											// designated zone (or -1 -> cylinder)
		damageData,										// the 12-dword Damage @0x3A8
		subsystemID);									// inflictingSubsystemID (+0xD8) --
														// lights up the messmgr's per-weapon
														// explosion bundling (weapon+0x3E4)
	SubsystemMessageManager *manager =
		(SubsystemMessageManager *)mech->GetMessageManager();	// mech+0x434
	if (manager != 0)
	{
		manager->AddDamageMessage(target, &message);	// FUN_0049b6d8
	}
	else
	{
		target->Dispatch(&message);						// no manager: direct (bring-up)
	}

	// PORT bookkeeping (score lived in the retired mech4 fire block): credit
	// the delivered amount to the shooter-side score exactly as before.
	extern void BTPostDamageScore(Entity *victim, Scalar damage);
	extern Logical BTIsRegisteredMech(Entity *e);
	if (BTIsRegisteredMech(target)
		&& !((Mech *)target)->IsMechDestroyed())
	{
		BTPostDamageScore(target, damageData.damageAmount);
	}
}


//#############################################################################
// CreateStreamedSubsystem
//
// @004b9d10 -- parse the MechWeapon-specific resource fields after the
// PoweredSubsystem base parser.  On the first pass every field is primed to the
// "unset" sentinel (-1.0f == _DAT_004ba37c, or -1 for ints); a field is an error
// only if both the read fails AND the field is still unset.  Stamps
// subsystemModelSize = 0x1BC and classID = MechWeaponClassID.
//
int
	MechWeapon::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 (
		!PoweredSubsystem::CreateStreamedSubsystem(		// FUN_004b13ac
			model_file, model_name, subsystem_name,
			subsystem_resource, subsystem_file, directories, passes
		)
	)
	{
		return False;
	}

	subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);	// 0x1BC
	subsystem_resource->classID            = (RegisteredClass::ClassID)Mech::MechWeaponClassID;	// 0xBCD

	if (passes == 1)
	{
		// prime all weapon fields to "unset"
		subsystem_resource->rechargeRate		= -1.0f;
		subsystem_resource->weaponRange			= -1.0f;
		subsystem_resource->explosionResourceID	= -1;
		subsystem_resource->damageAmount		= -1.0f;
		subsystem_resource->heatCostToFire		= -1.0f;
		subsystem_resource->pipPosition			= -1;
		subsystem_resource->pipColor.Red		= -1.0f;
		subsystem_resource->pipColor.Green		= -1.0f;
		subsystem_resource->pipColor.Blue		= -1.0f;
		subsystem_resource->pipExtendedRange	= -1;
	}

	// "SegmentPageName" must be present on streaming passes after the first.
	if (subsystem_resource->segmentIndex == -1 && passes > 1)	// +0x28
	{
		DEBUG_STREAM << subsystem_name << " missing SegmentPageName!";
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "DamageAmount", &subsystem_resource->damageAmount)
		&& subsystem_resource->damageAmount == -1.0f
	)
	{
		DEBUG_STREAM << model_name << " Missing DamageAmount!" << std::endl;
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "HeatCostToFire", &subsystem_resource->heatCostToFire)
		&& subsystem_resource->heatCostToFire == -1.0f
	)
	{
		DEBUG_STREAM << subsystem_name << " missing HeatCostToFire!";
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "PipPosition", &subsystem_resource->pipPosition)
		&& subsystem_resource->pipPosition == -1
	)
	{
		DEBUG_STREAM << subsystem_name << " missing PipPosition!";
		return False;
	}

	//
	// PipColor: optional named colour; "Unspecified" leaves the resource value,
	// otherwise it is parsed.  A missing/short value is a hard error.
	//
	const char *pipColorName = "Unspecified";
	if (!model_file->GetEntry(subsystem_name, "PipColor", &pipColorName))
	{
		// no value supplied -- require the resource already to hold one
		RGBColor probe(-1.0f, -1.0f, -1.0f);
		if (probe == subsystem_resource->pipColor)
		{
			DEBUG_STREAM << subsystem_name << " missing PipColor!";
			return False;
		}
	}
	if (strcmp(pipColorName, "Unspecified") != 0)
	{
		// FUN_00406824 -- the shipped build mapped a colour token to an RGB triple;
		// accept an explicit "R G B" triple here (named-colour table is data-driven).
		float r = -1.0f, g = -1.0f, b = -1.0f;
		if (sscanf(pipColorName, "%f %f %f", &r, &g, &b) == 3)
		{
			subsystem_resource->pipColor.Red   = r;
			subsystem_resource->pipColor.Green = g;
			subsystem_resource->pipColor.Blue  = b;
		}
	}

	if (
		!model_file->GetEntry(subsystem_name, "PipExtendedRange", &subsystem_resource->pipExtendedRange)
		&& subsystem_resource->pipExtendedRange == -1
	)
	{
		DEBUG_STREAM << subsystem_name << " missing PipExtendedRange!" << std::endl;
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "RechargeRate", &subsystem_resource->rechargeRate)
		&& subsystem_resource->rechargeRate == -1.0f
	)
	{
		DEBUG_STREAM << model_name << " Missing RechargeRate!" << std::endl;
		return False;
	}

	if (
		!model_file->GetEntry(subsystem_name, "WeaponRange", &subsystem_resource->weaponRange)
		&& subsystem_resource->weaponRange == -1.0f
	)
	{
		DEBUG_STREAM << model_name << " Missing WeaponRange!" << std::endl;
		return False;
	}

	//
	// DamageType: maps a name onto the Damage::DamageType enum.
	//
	const char *damageTypeName = "Unspecified";
	if (
		!model_file->GetEntry(subsystem_name, "DamageType", &damageTypeName)
		&& subsystem_resource->damageAmount == -1.0f
	)
	{
		DEBUG_STREAM << model_name << " missing DamageType!";
		return False;
	}
	if (strcmp(damageTypeName, "Unspecified") != 0)
	{
		if (strcmp(damageTypeName, "CollisionDamage") == 0)
			subsystem_resource->damageType = Damage::CollisionDamageType;	// 0
		else if (strcmp(damageTypeName, "BallisticDamage") == 0)
			subsystem_resource->damageType = Damage::BallisticDamageType;	// 1
		else if (strcmp(damageTypeName, "ExplosiveDamage") == 0)
			subsystem_resource->damageType = Damage::ExplosiveDamageType;	// 2
		else if (strcmp(damageTypeName, "LaserDamage") == 0)
			subsystem_resource->damageType = Damage::LaserDamageType;		// 3
		else if (strcmp(damageTypeName, "EnergyDamage") == 0)
			subsystem_resource->damageType = Damage::EnergyDamageType;		// 4
		else
		{
			DEBUG_STREAM << model_name << " has an unknown DamageType of "
						<< damageTypeName << std::endl;
			return False;
		}
	}

	//
	// ExplosionModelFile: optional; "Unspecified" leaves it unset, otherwise the
	// named model is looked up in the resource file.
	//
	const char *explosionModelName = "Unspecified";
	if (
		!model_file->GetEntry(subsystem_name, "ExplosionModelFile", &explosionModelName)
		&& subsystem_resource->explosionResourceID == -1
	)
	{
		DEBUG_STREAM << model_name << " missing ExplosionModelFile!";
		return False;
	}

	ResourceDescription *explosion = 0;
	if (strcmp(explosionModelName, "Unspecified") != 0)
	{
		explosion = resource_file->FindResourceDescription(			// FUN_00406ff8
			explosionModelName, (ResourceDescription::ResourceType)1, -1);
	}

	if (explosion == 0 && subsystem_resource->explosionResourceID == -1)
	{
		DEBUG_STREAM << model_name << " cannot find " << explosionModelName
					<< " in resource file!";
		return False;
	}

	if (explosion != 0)
	{
		subsystem_resource->explosionResourceID = explosion->resourceID;
	}

	return True;
}
