//===========================================================================//
// File:	btplayer.cpp                                                        //
// Project: BattleTech           Brick: Player / Scoring                      //
// Contents: BattleTech player & scoring entity (BT analog of RPPLAYER)       //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/96  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 bt/btplayer.cpp cluster (FUN_004c012c .. FUN_004c0f28);
// method/member names follow the surviving sibling RPPLAYER.cpp/.h, the
// MUNGA PLAYER.HPP base, and SCNROLE.HPP (the scoring "role" object).
// Each non-trivial method cites its originating @ADDR.
//
// d:\tesla_bt\bt\btplayer.cpp  is the original path baked into the asserts.
//
// Hex float constants converted to decimal:
//      0x3f800000 = 1.0f   0x40c00000 = 6.0f   0x41200000 = 10.0f
//
// Engine helper-function name mapping (internals referenced by the decomp):
//      FUN_0040385c  Verify()/assert(msg,file,line)
//      FUN_0041a1a4  IsDerivedFrom(classDerivations)
//      FUN_00402298  operator new (raw alloc)         FUN_004022d0  operator delete
//      FUN_004024d8  CString refcount free
//      FUN_00402460  CString::CString(const char*)
//      FUN_00402f74  MemoryBlock::New (status-message pool @00512f6c)
//      FUN_004078fc  Round_To_Int(Scalar)
//      FUN_00408440  CString::operator=(const char*)
//      FUN_00414b60  Now()  (clock)                   FUN_004dcd94  Now()  (Time)
//      FUN_0041acbc  Application::Post(priority,receiver,message,when)
//      FUN_0041bbd8  AlarmIndicator::SetLevel(n)
//      FUN_004212b0  EntityManager::FindGroup(name)
//      FUN_00421414  ChainIteratorOf<Node*>::ctor     FUN_00421452  ::dtor
//      FUN_004323c7  Registry iterator ctor            FUN_00432405  ::dtor
//      FUN_004d4b58  strcmp()
//      FUN_00429078  HostManager::GetConsoleHost()
//      FUN_00429b94  ScenarioRole::CalcDamageReceivedScore(&dmg)
//      FUN_0042d990  Player__StatusMessage::StatusMessage(player,type,time)
//      FUN_0042da20  Player::ScoreMessageHandler(message)        [base]
//      FUN_0042e168  Player::PlayerSimulation(time_slice)        [base]
//      FUN_0042e328  Player::Player(make,shared)                 [base ctor]
//      FUN_0042e524  Player::~Player()                           [base dtor]
//      FUN_0042e580  Player::AddStatusMessage(status_message)
//      FUN_004b04d8  Mech objective-reached notify (bt subsystem)
//      FUN_004c052c  BTPlayer::CalcInflictedScore  (this file)
//      FUN_004c1944  ConsolePlayerVTVDamagedMessage ctor (8 args)
//
// Global app singleton:  DAT_004efc94 == application
//      application+0x20  -> network sender (SendMessage slot @+0x18)
//      application+0x24  -> EntityManager
//      application+0x2c  -> HostManager   (GetEntityPointer slot @+0xc,
//                                          GetConsoleHost via host table +0x18)
//      application+0x60  -> event queue (Post)
//      application+0x88  -> application state    application+0xc8 -> mission/registry
//
// ScenarioRole (the scoring "role", inherited Player::scenarioRole @0x208):
//      +0x0c  damageInflictedModifier   +0x10  damageReceivedModifier
//      +0x14  friendlyFirePenalty       +0x18  damageBias
//      +0x1c  killBonus                 +0x20  specialCaseDeathPenalty
//      +0x24  roleName (CString)        +0x28  returnFromDeath
//
// Mech fields touched here:
//      +0x190 (400)  owningPlayer (BTPlayer*)     +0x4bc  tonnage / score scale
//      +0x354        per-target damage-bias factor    +0x128  (objective link)
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(BTPLAYER_HPP)
#	include <btplayer.hpp>
#endif
#if !defined(BTTEAM_HPP)
#	include <btteam.hpp>
#endif
#if !defined(BTMSSN_HPP)
#	include <btmssn.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(HOSTMGR_HPP)
#	include <hostmgr.hpp>
#endif
#if !defined(BTCNSL_HPP)
#	include <btcnsl.hpp>
#endif
#if !defined(NTTMGR_HPP)
#	include <nttmgr.hpp>		// EntityManager / EntityGroup / FindGroup
#endif
#if !defined(SCALAR_HPP)
#	include <scalar.hpp>		// Round()
#endif
#if !defined(DROPZONE_HPP)
#	include <dropzone.hpp>		// DropZone__ReplyMessage (spawn handshake)
#endif
#if !defined(MISSION_HPP)
#	include <mission.hpp>		// Mission::GetGameModel / GetBadgeName / GetColorName
#endif
#if !defined(RESOURCE_HPP)
#	include <resource.hpp>		// ResourceFile / ResourceDescription / ModelListResourceType
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>			// Mech::MakeMessage / MechClassID / Reset
#endif

#define CONSOLE_UPDATE_INTERVAL	10.0f	// _DAT_004c08fc
#define STATUS_DISPLAY_TIME		6.0f	// 0x40c00000

// BRING-UP: the spawned target/enemy mech (see CreatePlayerVehicle below).  The
// player mech's per-frame targeting step (mech4.cpp) locks onto this as its
// current target.  NULL when no enemy was spawned (BT_SPAWN_ENEMY unset).
Entity *gEnemyMech = 0;

//
// Reconstruction helpers / data referenced by the decomp that have no direct
// analog in the surviving WinTesla headers (CROSS-FAMILY / engine gap -- see
// report).  Declared here so the recovered bodies compile; the linker binds
// them to the real engine/BT symbols.
//
//   FUN_0041bbd8  AlarmIndicator::SetLevel (the engine GaugeAlarm exposes no
//                 SetLevel in WinTesla) -- raise an alarm to a given level.
//   FUN_004b04d8  notify the objective subsystem that its mech was destroyed.
//   StatusMessagePool  the BT status-message MemoryBlock pool (@00512f6c).
//   BT*VTable          the BT vtable symbols installed by ctor/dtor.
//   SelfDestructName / ResetDelay  canned strings / timing constants.
//
extern void			Set_Alarm_Level(void *alarm_indicator, int level);
extern void			Notify_Objective_Reached(int *objective_subsystem, Mech *mech);
extern MemoryBlock	*StatusMessagePool;
extern void			*BTPlayerVTable;
extern void			*BTStatusMessageVTable;

// Wire-format lock: BTPlayer::MakeMessage travels RAW over the network (entity
// replication), so the inline string fields must sit at the binary's offsets.
static_assert(offsetof(BTPlayer::MakeMessage, teamName) == 0x50,
	"BTPlayer::MakeMessage::teamName must be at wire offset 0x50");
static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90,
	"BTPlayer::MakeMessage::roleName must be at wire offset 0x90");
static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
	"BTPlayer::MakeMessage wire size must be 0xD0");

static const char	*SelfDestructName	= "self destruct";	// &DAT_00524b38
static const Scalar	ResetDelay			= 1.0f;				// re-post delay
static const Scalar	TicksPerSecond		= 1.0f;				// (see note in PlayerSimulation)

//
// Mech fields touched by the scoring code but not exposed by the reconstructed
// mech.hpp (owned by the mech family).  Accessed through the documented byte
// offsets.  CROSS-FAMILY -- ideally Mech would expose these accessors.
//
// gauge scoring wave: the raw binary offsets are garbage in our compiled Mech
// (engine Entity base ~0x1BC vs the binary's 0x300; 0x638 Mech vs 0x854), and the
// scoring handlers deref them -> a fault the moment a ScoreMessage is dispatched.
// Reconcile: the owning player via the engine playerLink accessor (NULL for the
// uncontrolled BT_SPAWN_ENEMY dummy -- exactly what the null-guards below rely on);
// tonnage ratio stubbed 1.0 + damage bias stubbed 0.0 for bring-up (SCORE == raw
// damage, un-tonnage-scaled; the real per-mech tonnage/bias accessors are a follow-up).
#define MECH_TONNAGE(m)			(1.0f)											// bring-up: ratio == 1
#define MECH_DAMAGE_BIAS(m)		(0.0f)											// bring-up: factor = 0*bias+1 = 1
#define MECH_OWNING_PLAYER(m)	((BTPlayer *)((Mech *)(m))->GetPlayerLink())	// ENTITY.h:430 (NULL for the dummy)

//#############################################################################
//###############################    BTPlayer    ##############################
//#############################################################################

//#############################################################################
// Message Support
//
// MESSAGE_ENTRY names recovered from the .data string block @005130c0.
// (ScoreInflicted is reconstructed best-effort -- see header note.)
//
const Receiver::HandlerEntry
	BTPlayer::MessageHandlerEntries[] =
{
	MESSAGE_ENTRY(BTPlayer, DropZoneReply),
	MESSAGE_ENTRY(BTPlayer, VehicleDead),
	MESSAGE_ENTRY(BTPlayer, Score),
	MESSAGE_ENTRY(BTPlayer, ScoreInflicted),
	MESSAGE_ENTRY(BTPlayer, ScoreUpdate),
	MESSAGE_ENTRY(BTPlayer, MissionStarting),
	MESSAGE_ENTRY(BTPlayer, MissionEnding)
};

Receiver::MessageHandlerSet&
	BTPlayer::GetMessageHandlers()
{
	static Receiver::MessageHandlerSet messageHandlers(
		ELEMENTS(BTPlayer::MessageHandlerEntries),
		BTPlayer::MessageHandlerEntries,
		Player::GetMessageHandlers()
	);
	return messageHandlers;
}

//#############################################################################
// Shared Data Support
//
Derivation*
	BTPlayer::GetClassDerivations()
{
	static Derivation classDerivations(
		Player::GetClassDerivations(),
		"BTPlayer"					// @005130cb
	);
	return &classDerivations;
}

BTPlayer::SharedData
	BTPlayer::DefaultData(
		BTPlayer::GetClassDerivations(),
		BTPlayer::GetMessageHandlers(),
		Player::GetAttributeIndex(),
		BTPlayer::StateCount,
		(BTPlayer::MakeHandler)BTPlayer::Make
	);


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					VehicleDeadMessageHandler
//
// @004c012c  (file=? in the index, but operates on the BTPlayer object and
// is reached through the "VehicleDead" handler slot).  Records the death,
// stamps the outgoing message with our death count, debits one "return from
// death" against the role, raises the console alarm, remembers the killer's
// name and re-posts the message a short time later so we can hunt for a new
// drop zone.
//
void
	BTPlayer::VehicleDeadMessageHandler(VehicleDeadMessage *message)
{
	Check(this);
	Check(message);

	//
	// scoring wave (DEATHS fix): @004c012c is the drop-zone RESPAWN-RETRY helper --
	// the engine's RequestDropZone (PLAYER.cpp:400) posts a 2s-delayed VehicleDead
	// (deathCount=-2) as a "did the drop zone reply?" timer.  If the player already
	// has a vehicle, the drop zone WAS acquired (the DropZoneReply created it) -> the
	// retry is moot: don't count a death and don't re-post.  The prior unconditional
	// ++deathCount + re-post was an INFINITE loop in solo (the drop-zone handshake
	// never "completes" in the bring-up), climbing deathCount ~1/s.  (A real MP death
	// -- vehicle destroyed -> playerVehicle cleared/the @004c05c4 path -- is deferred.)
	//
	if (playerVehicle != 0)
	{
		deathPending    = 0;						// this+0x290
		suppressConsole = 0;						// this+0x258
		Check_Fpu();
		return;
	}

	if (getenv("BT_SCORE_LOG"))
		DEBUG_STREAM << "[score] VehicleDeadMessageHandler: deathCount " << deathCount
			<< " -> " << (deathCount + 1) << "\n" << std::flush;

	//
	// One more death; tell the message (so replicants stay in sync) and
	// debit a life against the scoring role.
	//
	++deathCount;								// this+0x200
	message->deathCount = deathCount;			// message+0x38
	// TODO(bring-up): scenarioRole is the scoring role looked up from the mission's
	// role registry (btplayer.cpp:815, currently deferred), so it is NULL for the
	// minimal TEST.EGG dev mission.  Only debit a life when a role is present.
	if (scenarioRole != 0)						// this[0x208]
	{
		scenarioRole->SetReturnFromDeath(		// --*(this[0x208]+0x28)
			scenarioRole->GetReturnFromDeath() - 1);
	}

	simulationFlags |= 0x1;						// request a forced update
	Set_Alarm_Level((char *)this + 0x2c, 1);	// FUN_0041bbd8(this+0x2c, 1)

	//
	// Remember who killed us (for the pilot HUD).  If the kill came from a
	// real attacker the message carries its name (+0x1c); otherwise fall
	// back to the canned "self destruct" string (@00524b38).
	//
	if (showDamageInflicted == 0)				// this+0x264
	{
		// The BT VehicleDead message carried the killer's name at +0x1c; the
		// engine base Player__VehicleDeadMessage has no such field, so it is
		// read through the documented byte offset.  BEST-EFFORT (message gap).
		killerName = *(const char *const *)((const char *)message + 0x1c);
	}
	else
	{
		killerName = SelfDestructName;			// &DAT_00524b38
	}

	//
	// Re-post the dead message to ourselves after a short delay.
	//
	Time when = Now();							// FUN_00414b60 + FUN_004dcd94
	when += ResetDelay;
	application->Post(HighEventPriority, this, message, when);	// app+0x60

	deathPending    = 0;						// this+0x290
	suppressConsole = 0;						// this+0x258
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					ScoreInflictedMessageHandler
//
// @004c0200  Awards points for damage *this* player inflicted on another
// mech.  The score is the tonnage-scaled inflicted value; if the target mech
// turns out to be our own vehicle the award is negated (you don't score for
// shooting yourself).
//
void
	BTPlayer::ScoreInflictedMessageHandler(ScoreMessage *message)
{
	Check(this);
	Check(message);

	//
	// This handler is only ever fed DamageInflictedScore (type 0) messages.
	//
	if (message->scoreType != BTPlayer::ScoreMessage::DamageInflictedScore)
	{
		Verify(
			False,
			"BTPlayer::ScoreInflictedMessageHandler should only be "
				"given DamageInflictedScoreMessages!",			// @00513110
			"d:\\tesla_bt\\bt\\btplayer.cpp",					// @0051316a
			0x18b
		);
	}

	//
	// Resolve the mech that took the damage and our own vehicle.
	//
	Mech *target_mech =
		(Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID);	// app+0x2c, msg+0x34
	Mech *our_mech = (Mech *)playerVehicle;										// this+0x1fc

	Scalar award = CalcInflictedScore(message->damageAmount, target_mech, 0.0f);
	if (target_mech == our_mech)
	{
		award = -award;
	}

	//
	// Scale by the tonnage ratio (target / self) and accumulate.  (tonnage is
	// a Mech field at +0x4bc; not exposed by the reconstructed mech.hpp, so it
	// is read through the documented offset.  CROSS-FAMILY -- see report.)
	//
	currentScore +=
		(MECH_TONNAGE(target_mech) / MECH_TONNAGE(our_mech)) * award;	// (+0x4bc / +0x4bc), this+0x278
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					ScoreUpdateMessageHandler
//
// @004c02a8  A network score-sync message.  Only the master instance owns the
// authoritative score, so this must never run on a replicant; it then defers
// to the base score handler which folds the carried scoreAward in.
//
void
	BTPlayer::ScoreUpdateMessageHandler(ScoreMessage *message)
{
	if ((simulationFlags & 0xc) == 4)			// this+0x28 -- replicant copy
	{
		Verify(
			False,
			"BTPlayer::ScoreUpdateMessageHandler should only "
				"run on Master instances!",						// @00513186
			"d:\\tesla_bt\\bt\\btplayer.cpp",					// @005131cf
			0x1d5
		);
	}

	Player::ScoreMessageHandler(message);		// FUN_0042da20
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					ScoreMessageHandler
//
// @004c02e4  The main combat scoring handler.  Runs only on the master, and
// only while not mission-ending.  Handles damage received (type 1) and kills
// (type 2); the computed award is written back into the message before the
// base Player::ScoreMessageHandler applies it to currentScore.
//
void
	BTPlayer::ScoreMessageHandler(ScoreMessage *message)
{
	if ((simulationFlags & 0xc) == 4)			// replicant -- must not score
	{
		Verify(
			False,
			"BTPlayer::ScoreMessageHandler should only run on "
				"Master instances!",							// @005131eb
			"d:\\tesla_bt\\bt\\btplayer.cpp",					// @0051322e
			0x1e9
		);
	}

	//
	// Ignore everything once the mission is ending (state 4).
	//
	if (GetSimulationState() == MissionEndingState)				// this+0x40 == 4
	{
		return;
	}

	Mech *sender_mech =
		(Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID);	// msg+0x34
	Mech *our_mech = (Mech *)playerVehicle;										// this+0x1fc

	Scalar award = 0.0f;

	switch (message->scoreType)									// msg+0x20
	{
	case BTPlayer::ScoreMessage::DamageInflictedScore:			// 0
		//
		// Inflicted-damage messages belong to ScoreInflictedMessageHandler.
		//
		Verify(
			False,
			"BTPlayer::ScoreMessageHandler should not be "
				"given DamageInflictedScoreMessages!",			// @0051324a
			"d:\\tesla_bt\\bt\\btplayer.cpp",					// @0051329a
			0x296
		);
		break;

	case BTPlayer::ScoreMessage::DamageReceivedScore:			// 1
		{
			//
			// Points lost for damage we took (unless we hit ourselves).
			//
			if (sender_mech != our_mech && scenarioRole != 0)	// scoring wave: guard the NULL bring-up role
			{
				Scalar received =
					scenarioRole->CalcDamageReceivedScore(message->damageAmount);	// FUN_00429b94
				award = (MECH_TONNAGE(our_mech) / MECH_TONNAGE(sender_mech)) * received;
			}

			//
			// Tell the console (if any, and not suppressed) that our VTV
			// was damaged.
			//
			Host *console_host =
				application->GetHostManager()->GetConsoleHost();	// FUN_00429078
			if (console_host && suppressConsole == 0)				// this+0x258
			{
				ConsolePlayerVTVDamagedMessage damaged_message(
					ownerID,									// this+0x18c
					MECH_OWNING_PLAYER(sender_mech)->ownerID,	// *(sender+0x190)+0x18c
					Round(message->damageAmount),				// FUN_004078fc
					message->pointSenderLo,						// msg+0x2c
					message->pointSenderHi,						// msg+0x28
					(int)Now().ticks,							// FUN_004dcd94
					message->auxID								// msg+0x30
				);

				application->SendMessage(						// app+0x20, slot+0x18
					console_host->GetHostID(),					// console_host+0xc
					NetworkClient::ConsoleClientID,				// 5
					&damaged_message
				);
			}
		}
		break;

	case BTPlayer::ScoreMessage::KillScore:						// 2
		{
			// gauge scoring wave: the attacking mech's owner (NULL for the ownerless
			// BT_SPAWN_ENEMY dummy).  Resolved once, guarded everywhere below.
			BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech);	// *(sender+0x190), NULL for the dummy

			//
			// A kill (or a self-kill).  Inflicted value scaled by the
			// sender/self tonnage ratio.
			//
			award =
				(MECH_TONNAGE(sender_mech) / MECH_TONNAGE(our_mech))
					* CalcInflictedScore(message->damageAmount, sender_mech, message->scoreAward);

			if (sender_mech == our_mech)
			{
				award = -award;								// suicide -- no credit
			}
			else
			{
				//
				// Credit a kill to us and (if it has one) to the attacking player.
				// The solo demo posts this to the LOCAL player with senderMechID=the
				// victim, so `this->killCount++` fires (KILLS -> 1); the ownerless
				// dummy's sender_owner is NULL -> the second increment is guarded away.
				//
				++killCount;								// this+0x27c
				if (sender_owner)
					++sender_owner->killCount;				// *(sender+0x190)+0x27c
			}

			//
			// Pop a "destroyed by <killer>" status message onto the pilot HUD.
			// Gated on a real attacker player (+ the status pool, a NULL stub in
			// bring-up), so the ownerless-dummy solo path skips it (no player to name).
			//
			if (sender_owner && StatusMessagePool)
			{
				StatusMessage *status = (StatusMessage *)StatusMessagePool->New();	// FUN_00402f74(0x512f6c)
				if (status)
				{
					new (status) Player__StatusMessage(
						sender_owner,							// *(sender+0x190)
						0,
						STATUS_DISPLAY_TIME						// 0x40c00000 == 6.0f
					);
					*(int *)((char *)status + 0x18) = 0;				// status[6] = 0
					AddStatusMessage(status);						// FUN_0042e580
				}
			}

			//
			// If we just killed our designated objective mech, notify it.
			// (playerVehicle's objective subsystem link is at +0x128.)
			//
			if (sender_mech == objectiveMech)				// this+0x284
			{
				Notify_Objective_Reached(					// FUN_004b04d8
					*(int **)((char *)playerVehicle + 0x128),	// *(this[0x1fc]+0x128)
					sender_mech
				);
			}
		}
		break;
	}

	//
	// Hand the (now-scored) message to the base class to apply the award.
	//
	message->scoreAward = award;					// msg+0x1c
	Player::ScoreMessageHandler(message);			// FUN_0042da20
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					CalcInflictedScore
//
// @004c052c  Shared damage->points routine.  Same-team hits (in a non-free-
// for-all game) cost a fixed friendly-fire penalty; otherwise the points
// scale with the target's damage-bias factor.  The result is multiplied by
// the role's damageInflictedModifier.
//
Scalar
	BTPlayer::CalcInflictedScore(
		const Scalar	&damage_amount,
		Mech			*other_mech,
		Scalar			bias
	)
{
	// gauge scoring wave: scenarioRole is NULL in bring-up (the BTMission role
	// registry has no WinTesla analog -- ctor lookup stubbed), and every modifier
	// deref below would fault the moment a score message is dispatched.  A NULL
	// role == the neutral role (all modifiers 1, no penalties, bias 0), so the
	// award is just (damage + bias).  Real per-role modifiers are a follow-up.
	if (scenarioRole == 0)
	{
		return damage_amount + bias;
	}

	Scalar factor;

	//
	// Team game: a hit on a team-mate is friendly fire.
	//
	if (freeForAll == 0											// this+0x250
		&& strcmp(MECH_OWNING_PLAYER(other_mech)->teamName, teamName) == 0)	// (other+0x190)+0x20c vs this+0x20c
	{
		factor = -scenarioRole->GetFriendlyFirePenalty();	// -*(this[0x208]+0x14)
	}
	else
	{
		factor =
			MECH_DAMAGE_BIAS(other_mech)					// other+0x354
				* scenarioRole->GetDamageBias()				// *(this[0x208]+0x18)
			+ 1.0f;											// _DAT_004c05c0 == 1.0f
	}

	return (damage_amount + bias)
			* scenarioRole->GetDamageInflictedModifier()	// *(this[0x208]+0x0c)
			* factor;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					PlayerSimulation
//
// @004c083c  Registered as the Performance for a console/replicant copy
// (set in the ctor when (simulationFlags & 0xc) == 4).  Runs the base sim,
// then pushes our score to the console once every CONSOLE_UPDATE_INTERVAL
// seconds (or immediately if the application is shutting the mission down).
//
void
	BTPlayer::PlayerSimulation(Scalar time_slice)
{
	Check(this);

	Player::PlayerSimulation(time_slice);			// FUN_0042e168

	if (
		(lastPerformance - lastConsoleUpdate) / TicksPerSecond >= CONSOLE_UPDATE_INTERVAL	// _DAT_004c08fc
			|| application->GetApplicationState() == Application::EndingMission				// app+0x88 == 6
	)
	{
		lastConsoleUpdate = lastPerformance;		// this+0x28c = this+0x10

		//
		// Only bother if our score actually changed since last time.
		//
		if ((Scalar)currentScore != 0.0f)			// this[0x9e] != _DAT_004c0900 (0.0f)
		{
			int score = (int)currentScore;

			ConsolePlayerVTVScoreUpdateMessage score_message(
				ownerID,
				score
			);											// FUN_00420ea4(0x20, 0x1a, 1, ...)

			// gauge scoring wave: the binary's currentScore is a console DELTA that is
			// flushed to the operator console then zeroed.  Our SCORE/RANK gauges read
			// currentScore as the RUNNING total, so only flush+zero when a console host
			// is actually present (MP / pod); in solo there is no console -> keep the
			// running score so the SCORE gauge + CalcRanking don't reset every 10s.
			Host *console_host =
				application->GetHostManager()->GetConsoleHost();	// FUN_00429078
			if (console_host)
			{
				Dispatch(&score_message);				// (**(*this+0xc))(this, &msg)
				currentScore = 0;						// this[0x9e] = 0
			}
		}
	}
}


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					CreatePlayerVehicle
//
// @004bfcac (BTPlayer vtable slot +0x40).  Makes the player's BattleMech at
// the supplied drop-zone origin.  Lets the base build any MUNGA-level vehicle
// first (camera-ship observers); if no vehicle resulted, instantiates a Mech
// from the mission's game-model resource and links it as our viewpoint.
//
// Structural model: RPPLAYER.cpp RPPlayer::CreatePlayerVehicle (which makes a
// VTV the same way).  BT substitutes Mech for VTV and carries the three mech
// resource-name strings (badge / colour / patch) into the make message.
//
void
	BTPlayer::CreatePlayerVehicle(Origin mech_location)
{
	Check(this);
	Check(application);

	ResourceFile *resources = application->GetResourceFile();
	Check(resources);
	Check_Pointer(playerMission->GetGameModel());

	HostManager *host_manager = application->GetHostManager();
	Check(host_manager);

	//
	// Make any MUNGA-level vehicle (camera ship for an observer player).
	//
	Player::CreatePlayerVehicle(mech_location);

	//
	// If the player still has no vehicle, build him a mech and link to it.
	//
	if (!playerVehicle)
	{
		ResourceDescription *mech_res =
			resources->FindResourceDescription(
				playerMission->GetGameModel(),
				ResourceDescription::ModelListResourceType
			);
		Check(mech_res);
		mech_res->Lock();

		Mech::MakeMessage
			create_player(
				Mech::MakeMessageID,
				sizeof(Mech::MakeMessage),
				EntityID(host_manager->GetLocalHostID()),
				(Entity::ClassID)Mech::MechClassID,
				EntityID::Null,
				mech_res->resourceID,
				Mech::DefaultFlags,
				mech_location,
				Motion::Identity,
				Motion::Identity,
				playerMission->GetBadgeName(),			// vehicle badge  (+0x7c)
				playerMission->GetColorName(),			// vehicle colour (+0x90)
				((BTMission *)playerMission)->GetPatchName()	// vehicle patch  (+0xa4)
			);

		mech_res->Unlock();

		playerVehicle = application->MakeAndLinkViewpointEntity(&create_player);
		Register_Object(playerVehicle);
	}

	// --- BRING-UP: spawn a stationary target/enemy mech (gameplay scaffolding) ---
	//   The mission system is networked (one pilot per host) and BT's bot/AI code
	//   is absent, so a solo mission has nothing to fight.  When BT_SPAWN_ENEMY is
	//   set we drop a SECOND mech into the world via the SAME factory the registry
	//   uses for remote/AI entities (Mech::Make on a MakeMessage) -- a non-viewpoint,
	//   self-registering, rendering, ticking entity placed ahead of the player.  It
	//   has no AI yet (mech4.cpp gates the player-drive path to the viewpoint mech),
	//   so it stands as a target dummy.  First step toward actual combat.
	if (getenv("BT_SPAWN_ENEMY"))
	{
		ResourceDescription *enemy_res =
			resources->FindResourceDescription(
				playerMission->GetGameModel(),
				ResourceDescription::ModelListResourceType);
		if (enemy_res)
		{
			enemy_res->Lock();

			// Place it ahead of the player's drop ALONG THE SPAWN FACING (the mech
			// faces local -Z; rotate that axis into world by the spawn orientation).
			// The old fixed "z -= 120" assumed the bring-up drive's forced heading=0;
			// the real-controls drive keeps the authentic spawn orientation, so the
			// dummy must follow the actual facing or the auto-walk leaves it behind.
			Origin enemy_origin = mech_location;
			{
				AffineMatrix facing;
				facing.BuildIdentity();
				facing = mech_location.angularPosition;			// rotation from the spawn pose
				UnitVector spawnZ;
				facing.GetFromAxis(Z_Axis, &spawnZ);			// local Z basis in world
				enemy_origin.linearPosition.x -= spawnZ.x * 120.0f;	// forward = -Z
				enemy_origin.linearPosition.y -= spawnZ.y * 120.0f;
				enemy_origin.linearPosition.z -= spawnZ.z * 120.0f;
				// FACE THE PLAYER: copying the spawn pose left BOTH mechs facing
				// the same way (the player stared at the enemy's back).  Flip the
				// enemy's yaw 180 deg about Y so the two face each other.  Engine
				// yaw convention (MATRIX.cpp:196-209): the Z basis = (sin y, 0,
				// cos y) -> yaw = atan2(z.x, z.z); enemy yaw = that + pi.
				Scalar spawnYaw = (Scalar)atan2((double)spawnZ.x, (double)spawnZ.z);
				enemy_origin.angularPosition =
					EulerAngles(0.0f, spawnYaw + 3.14159265f, 0.0f);
			}

			Mech::MakeMessage
				create_enemy(
					Mech::MakeMessageID,
					sizeof(Mech::MakeMessage),
					host_manager->MakeUniqueEntityID(),
					(Entity::ClassID)Mech::MechClassID,
					EntityID::Null,
					enemy_res->resourceID,
					Mech::DefaultFlags,
					enemy_origin,
					Motion::Identity,
					Motion::Identity,
					playerMission->GetBadgeName(),
					playerMission->GetColorName(),
					((BTMission *)playerMission)->GetPatchName());

			enemy_res->Unlock();

			Mech *enemy = Mech::Make(&create_enemy);
			if (enemy)
			{
				Register_Object(enemy);
				// Mark it a VALID master so Entity::Dispatch delivers messages
				// SYNCHRONOUSLY (Receiver::Receive) instead of posting them as deferred
				// "entity invalid" events that never fire -- otherwise TakeDamage never
				// lands.  (The player gets validated via MakeViewpointEntity's CheckLoad
				// handshake; a manually-spawned entity must set it itself.  Its resources
				// are already loaded -- it renders + its zones are built.)  Also force
				// pre-run + interest so it ticks like a live entity.
				enemy->SetValidFlag();
				enemy->SetPreRunFlag();
				if (enemy->interestCount == 0) enemy->interestCount = 1;
				// GROUND MODEL (task #15): a MASTER mech needs a CollisionAssistant
				// for GetCurrentCollisions (the engine iterates it unchecked,
				// MOVER.cpp:894).  The player gets one in MakeViewpointEntity
				// (btl4app.cpp:591); this dummy is a master too, so start its own
				// -- without it the authentic ground block skips its collision half.
				enemy->StartCollisionAssistant();
				gEnemyMech = enemy;			// the player's targeting step locks onto this
				DEBUG_STREAM << "[enemy] spawned target mech at ("
					<< enemy_origin.linearPosition.x << ", "
					<< enemy_origin.linearPosition.y << ", "
					<< enemy_origin.linearPosition.z << ")\n" << std::flush;
			}
			else
			{
				DEBUG_STREAM << "[enemy] Mech::Make returned NULL\n" << std::flush;
			}
		}
		else
		{
			DEBUG_STREAM << "[enemy] model resource not found\n" << std::flush;
		}
	}

	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					InitializePlayerLink
//
// @004bfee0  Tells our freshly-made vehicle (and its replicants) which player
// owns it, by dispatching a PlayerLink message carrying our EntityID.
//
void
	BTPlayer::InitializePlayerLink()
{
	Check(this);
	Check(playerVehicle);

	PlayerLinkMessage
		player_link_message(
			PlayerLinkMessageID,
			sizeof(PlayerLinkMessage),
			GetEntityID()
		);

	playerVehicle->Dispatch(&player_link_message);
	playerVehicle->DispatchToReplicants(&player_link_message);
	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					DropZoneReplyMessageHandler
//
// @004bffd0  The drop zone has answered our spawn request with a location.
//
//   * No vehicle yet  -> make the mech there, link it, choose the per-vehicle
//                        simulation Performance from its class, start running.
//   * Same death      -> if the mission is ending, ignore; if we have not yet
//                        acquired this drop zone, fold into the death/reset
//                        path (VehicleDeadMessageHandler, FUN_004c012c).
//   * Stale message   -> ignore.
//
// Then (re)place the mech at the drop-zone origin.  Reconstructed faithfully
// from FUN_004bffd0; structural cross-check = Blocker::DropZoneReplyMessageHandler.
//
void
	BTPlayer::DropZoneReplyMessageHandler(DropZone__ReplyMessage *message)
{
	Check(this);
	Check(message);

	if (getenv("BT_SCORE_LOG"))
		DEBUG_STREAM << "[score] DropZoneReply: hasVehicle=" << (playerVehicle != 0)
			<< " deathCount=" << deathCount << " msgDeath=" << message->deathCount
			<< " state=" << (int)GetSimulationState() << "\n" << std::flush;

	if (!playerVehicle)							// param_1[0x7f] == 0
	{
		CreatePlayerVehicle(message->dropZoneLocation);	// (**vtable+0x40)(origin)
		InitializePlayerLink();							// FUN_004bfee0

		//
		// Drive the right per-vehicle simulation.  A mech runs the combat
		// PlayerSimulation; a camera-ship observer runs CameraShipSimulation
		// and never ranks.  (classID @ playerVehicle+4.)
		//
		if (playerVehicle->GetClassID() == Mech::MechClassID)	// 0xbb9
		{
			SetPerformance(&BTPlayer::PlayerSimulation);		// perf @00513058
			// scoring wave (RANK fix): an active mech combatant is a SCORING player.
			// Player::DefaultFlags creates every player NonScoring (rank -1 until
			// activated); CalcRanking only ranks IsScoringPlayer()==true.  The camera
			// ship (below) stays non-scoring; a piloted mech becomes scoring here.
			SetScoringPlayerFlag();								// clear NonScoringPlayerFlag
		}
		else													// camera ship (0x45)
		{
			SetPerformance((Performance)&BTPlayer::CameraShipSimulation);	// perf @00513064
			playerRanking = -1;									// this+0x1cc
		}

		AlwaysExecute();						// param_1[10] &= ~2  (run every frame)
		deathCount = 0;							// param_1[0x80]
	}
	else if (deathCount == message->deathCount)	// param_2[0xe] == param_1[0x80]
	{
		if (GetSimulationState() == MissionEndingState)		// state == 4
		{
			Check_Fpu();
			return;
		}
		if (GetSimulationState() != DropZoneAcquiredState)	// state != 1
		{
			//
			// Reset-after-death: BT routes this through the death handler
			// (FUN_004bffd0 calls FUN_004c012c here).
			//
			VehicleDeadMessageHandler((VehicleDeadMessage *)message);
			return;
		}
	}
	else										// stale / old message
	{
		return;
	}

	//
	// Place (or re-place) the mech at the drop-zone origin and raise the
	// "translocated" cockpit alarm.
	//
	Set_Alarm_Level((char *)this + 0x2c, 2);	// FUN_0041bbd8(this+0x2c, 2)
	if (playerVehicle->GetClassID() == Mech::MechClassID)	// 0xbb9
	{
		Mech *mech = (Mech *)playerVehicle;
		mech->Reset(message->dropZoneLocation, 1 /* True */);	// FUN_0049fb74
	}
	Check_Fpu();
}


//#############################################################################
// Construction and Destruction
//

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					BTPlayer  (constructor)
//
// @004c0bc8
//
BTPlayer::BTPlayer(
	BTPlayer::MakeMessage	*creation_message,
	SharedData				&shared_data
):
	Player(creation_message, shared_data)			// FUN_0042e328
{
	/* removed decompiler vtable artifact -- the C++ ctor already sets the real BTPlayer vtable;
   the original `*(void**)this = &BTPlayerVTable` overwrote it with a zeroed stub -> crash. */				// PTR_FUN_00513300

	consoleAttached    = 0;							// this[0x95]
	suppressConsole    = 0;							// this[0x96]
	currentScore       = 0;							// this[0x9e]
	deathPending       = 0;							// this[0xa4]

	//
	// Remember our team name from the creation message.
	//
	strcpy(teamName, creation_message->teamName);	// this+0x20c <- make+0x50

	//
	// Resolve the BTTeam entity that owns this team name.
	//
	teamEntity = 0;									// this[0x93]
	EntityGroup *teams =
		application->GetEntityManager()->FindGroup("Teams");	// app+0x24, @005132ee
	if (teams)
	{
		ChainIteratorOf<Node*> iterator(teams->groupMembers);
		BTTeam *team;
		while ((team = (BTTeam *)iterator.ReadAndNext()) != 0)
		{
			if (strcmp(team->teamName, teamName) == 0)	// team+0x1c4 vs this+0x20c
			{
				teamEntity = team;
				break;
			}
		}
	}

	//
	// Game-type flag.  (Both arms of the shipped code set this to 1; the
	// "freeforall" string compare result is effectively ignored -- looks
	// like a left-over from a half-finished edit.)
	//
	freeForAll = 1;									// this[0x94]
	// The shipped code compared the mission's game model to "freeforall" but
	// discarded the result (both arms set freeForAll=1 -- a half-finished
	// edit).  Application::GetMission()/Mission::GetGameModel() have no
	// WinTesla analog, so the dead compare is dropped.  @005132f4

	//
	// On a replicant copy, cache the application's mission/registry pointer
	// (application+0xc8 in the binary; no GetMissionRegistry accessor in the
	// WinTesla Application, so read through the documented offset).
	//
	if ((simulationFlags & 0xc) == 4 && btMission == 0)		// this+0x28, this[0x7e]
	{
		btMission = (Mission *)*(void **)((char *)application + 0xc8);	// app+0xc8
	}

	//
	// Look the scoring role up in the role registry (keyed by the role name
	// in the creation message, +0x90) and stash it as our scenarioRole.  The
	// BT role registry (BTMission::GetRoleRegistry()->Lookup) has no WinTesla
	// analog, so the scenarioRole set by the base Player ctor stands.
	// CROSS-FAMILY: needs BTMission role-registry access.  BEST-EFFORT.
	//
	CString role_key(creation_message->roleName);			// make+0x90
	(void)role_key;
	// scenarioRole = btMission->GetRoleRegistry()->Lookup(&role_key);	// this[0x7e]+0x50, this[0x82]

	if ((simulationFlags & 0xc) == 4)
	{
		//
		// Replicant: drive the console-update Performance.
		//
		SetPerformance(&BTPlayer::PlayerSimulation);		// this[7..9] = PTR_FUN_00513070
	}
	else
	{
		//
		// Master: choose the HUD display toggles from the role class index.
		//
		// TODO(bring-up): scenarioRole is NULL here because the role lookup above
		// (btMission->GetRoleRegistry()->Lookup) is still stubbed out. Guard the deref
		// so mission load proceeds; default index 2 = show all stats (freeforall).
		roleClassIndex = scenarioRole ? scenarioRole->GetReturnFromDeath() : 2;	// this[0x9d] <- role+0xe4
		switch (roleClassIndex)
		{
		case 0:
			showKills = showDamageReceived = showDamageInflicted = showScore = 0;
			break;
		case 1:
			showKills = 0;
			showDamageReceived = showDamageInflicted = showScore = 1;
			break;
		case 2:
			showKills = showDamageReceived = showDamageInflicted = showScore = 1;
			break;
		case 3:
			showKills = showDamageReceived = showDamageInflicted = 1;
			showScore = 0;
			break;
		}

		// role+0xf0 ("returnDelay") has no field in the WinTesla ScenarioRole;
		// initialised to 0 here.  CROSS-FAMILY -- see report.  BEST-EFFORT.
		roleReturnDelay = roleReturnDelay2 = 0.0f;		// this[0x99],[0x9a] <- role+0xf0
	}

	killCount       = 0;							// this[0x9f]
	pad_0x280       = 0;							// this[0xa0]
	objectiveMech   = 0;							// this[0xa1]
	lastPerformance = lastConsoleUpdate = lastUpdate;	// this[0xa2],[0xa3] = this[4]

	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					Make
//
// @004c0ecc  Allocate a 0x294-byte BTPlayer and construct it with the
// default shared data (@00512fc4).
//
BTPlayer*
	BTPlayer::Make(BTPlayer::MakeMessage *creation_message)
{
	BTPlayer *player = new BTPlayer(creation_message);
	// P6 bring-up: validate replicant players at creation (see Mech::Make -- an
	// invalid replicant defers every network message forever).
	if (player != 0 && player->GetInstance() == Entity::ReplicantInstance)
	{
		player->SetValidFlag();
	}
	return player;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					~BTPlayer
//
// @004c0efc  (scalar-deleting destructor thunk).  Chains to ~Player.
//
BTPlayer::~BTPlayer()
{
	/* removed decompiler vtable artifact -- the C++ ctor already sets the real BTPlayer vtable;
   the original `*(void**)this = &BTPlayerVTable` overwrote it with a zeroed stub -> crash. */		// PTR_FUN_00513300
	// (no BT-specific owned resources to release; base chain handles teardown)
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//					TestInstance
//
// @004c0f28
//
Logical
	BTPlayer::TestInstance() const
{
	return IsDerivedFrom(*GetClassDerivations());	// FUN_0041a1a4(**this[3], 0x512f94)
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// gauge scoring wave: scoreboard bridges for the PilotList gauge (btl4gau3.cpp).
// The pilot roster entries are BTPlayer* delivered as void* (BTResolveRosterPilot);
// these read the COMPILED named members the scoring handlers write, replacing the
// earlier raw-offset reads (pilot+0x27c / +0x200) that don't match our layout.
//
int BTPilotKills(void *pilot)
{
	int k = pilot ? ((BTPlayer *)pilot)->GetKillCount() : 0;
	static int s_n = 0;
	if (getenv("BT_SCORE_LOG") && (s_n++ % 120) == 0)		// ~throttled
		DEBUG_STREAM << "[score] gauge read: pilot=" << pilot << " kills=" << k << "\n" << std::flush;
	return k;
}
int BTPilotDeaths(void *pilot)  { return pilot ? ((BTPlayer *)pilot)->GetDeaths()    : 0; }

// Is `pilot` the owner of `local_player`'s current target? (the SELECT-TARGET row
// highlight).  Computed here via accessors -- the PilotList's raw-offset version
// (local+0x284 objectiveMech, then deref target+0x190) reads garbage in our layout
// and AV'd inside the SEH-guarded Execute, silently aborting it BEFORE the KILLS/
// DEATHS draw.  GetPlayerLink() is NULL for the ownerless dummy target -> not selected.
int BTPilotIsSelected(void *pilot, void *local_player)
{
	if (pilot == 0 || local_player == 0)
	{
		return 0;
	}
	Mech *target = ((BTPlayer *)local_player)->GetObjectiveMech();		// @0x284
	if (target == 0)
	{
		return 0;
	}
	return ((void *)((BTPlayer *)pilot) == (void *)target->GetPlayerLink()) ? 1 : 0;
}

// MessageBoard data bridge (consumed by btl4gau3.cpp MessageBoard::Execute).  DEFERRED:
// the per-player status queue (StatusMessagePool, btstubs.cpp:62) is a NULL stub and
// BTPlayer+0x1dc is never populated, so there are no messages -> return False (empty
// board).  Real impl (when StatusMessagePool is wired): owningPlayer (mech+0x190) ->
// status record (+0x1dc) -> {messageId@+0xc, nameEntity@+0x10 -> name bitmap} via the
// compiled BTPlayer/Player__StatusMessage members (decode AddStatusMessage @0042e580).
// A real (stub) definition is REQUIRED or the /FORCE link AVs MessageBoard::Execute's call.
class BitMap;
Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap **nameBitmap)
{
	*messageId  = -1;
	*nameBitmap = 0;
	return False;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed
// the scoreboard.  The inflictor is always the viewpoint mech in bring-up, so the
// local (crediting) player is application->GetMissionPlayer().  Bridges (not inline
// in mech4.cpp) so the message construction lives in this complete-BTPlayer TU.
//
void BTPostDamageScore(Entity *victim, Scalar damage)		// Step 6: per-hit SCORE
{
	if (application == 0 || victim == 0)
	{
		return;
	}
	BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer();
	if (local_player == 0)
	{
		return;
	}
	// ScoreInflicted (scoreType 0): senderMechID = the mech that TOOK the damage.
	// -> ScoreInflictedMessageHandler: currentScore += tonnageRatio*CalcInflictedScore.
	BTPlayer::ScoreMessage message(
		BTPlayer::ScoreInflictedMessageID,				// 0x16
		sizeof(BTPlayer::ScoreMessage),
		BTPlayer::ScoreMessage::DamageInflictedScore,	// 0
		0.0f,											// scoreAward (unused for inflicted)
		damage,											// damageAmount
		victim->GetEntityID());							// senderMechID = victim
	local_player->Dispatch(&message);
	if (getenv("BT_SCORE_LOG"))
		DEBUG_STREAM << "[score] +damage " << damage << " -> currentScore="
			<< (Scalar)local_player->GetScore() << "\n" << std::flush;
}

void BTPostKillScore(Entity *victim, Scalar damage)			// Step 7: KILL (+ MP death)
{
	if (application == 0 || victim == 0)
	{
		return;
	}
	BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer();
	if (local_player != 0)
	{
		// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= our mech) so
		// the local player is credited via `this->killCount++` -- if it were our own
		// mech, ScoreMessageHandler takes the suicide branch (award negated, no kill).
		BTPlayer::ScoreMessage kill(
			Player::ScoreMessageID,						// 0x12
			sizeof(BTPlayer::ScoreMessage),
			BTPlayer::ScoreMessage::KillScore,			// 2
			0.0f,										// scoreAward (killBonus; 0 for bring-up)
			damage,										// damageAmount (killing-blow)
			victim->GetEntityID());						// senderMechID = victim
		local_player->Dispatch(&kill);
		if (getenv("BT_SCORE_LOG"))
			DEBUG_STREAM << "[score] *** KILL *** localPlayer=" << (void *)local_player
				<< " killCount=" << local_player->GetKillCount()
				<< " deaths=" << local_player->GetDeaths()
				<< " score=" << (Scalar)local_player->GetScore()
				<< " rank=" << local_player->GetRanking() << "\n" << std::flush;
	}

	// MP DEATH: credit a death to the VICTIM's own player.  NULL for the solo
	// BT_SPAWN_ENEMY dummy (GetPlayerLink()==0) -> skipped, so DEATHS stays 0 in
	// solo (authentic -- DEATHS only lands on a real pilot in multiplayer).
	BTPlayer *victim_player = (BTPlayer *)((Mech *)victim)->GetPlayerLink();
	if (victim_player != 0)
	{
		Player::VehicleDeadMessage dead(
			Player::VehicleDeadMessageID,				// 0x13
			sizeof(Player::VehicleDeadMessage));
		victim_player->Dispatch(&dead);
	}
}
