Files
BT412/game/reconstructed/btplayer.cpp
T
arcattackandClaude Fable 5 a9c3e96f20 MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry
(BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects
every mech -- player, dummy, and peer replicants. The boresight
world-pick walks BTGetTargetCandidates (all mechs != shooter, closest
ray hit) and the whole fire/damage block retargets from the solo
gEnemyMech to the picked hotTarget; missile/projectile validation
generalised via BTIsRegisteredMech. Verified one-box: instance A
enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3).
Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED).

STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and
invoked with a valid owner, but the dispatched message doesn't reach
the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's
msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A
dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6
damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute
FIRES; the break is downstream in the transport/delivery of a
dispatched entity-message (update records flow fine -- net-rx works).
Next MP task = that wire delivery. Diagnostics BT_MP_LOG /
BT_MP_FORCE_DMG retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 10:13:39 -05:00

1273 lines
46 KiB
C++

//===========================================================================//
// 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;
//
// LIVE-MECH REGISTRY (task #46): every Mech (player, dummy, peer replicants)
// (de)registers here from its ctor/dtor. The boresight world-pick walks it to
// find target candidates -- generalising the solo gEnemyMech to MP peers. A
// plain fixed array (mech counts are tiny -- <= a few per arena) avoids any
// allocator interaction in the hot ctor path.
//
static Entity *gBTMechRegistry[32];
static int gBTMechCount = 0;
void BTRegisterMech(Entity *m)
{
if (m == 0) return;
for (int i = 0; i < gBTMechCount; ++i) // idempotent
if (gBTMechRegistry[i] == m) return;
if (gBTMechCount < 32)
gBTMechRegistry[gBTMechCount++] = m;
}
void BTDeregisterMech(Entity *m)
{
for (int i = 0; i < gBTMechCount; ++i)
{
if (gBTMechRegistry[i] == m)
{
gBTMechRegistry[i] = gBTMechRegistry[--gBTMechCount];
gBTMechRegistry[gBTMechCount] = 0;
return;
}
}
}
// The target-candidate list for a given shooter: every registered mech that is
// NOT the shooter itself. (Alive/pick filtering is the caller's -- mech4's
// world-pick already tests IsMechDestroyed + the collision-box ray.)
int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut)
{
int n = 0;
for (int i = 0; i < gBTMechCount && n < maxOut; ++i)
{
Entity *m = gBTMechRegistry[i];
if (m != 0 && m != shooter)
out[n++] = m;
}
return n;
}
// Is this entity a live registered mech? (Projectile-impact target validation
// -- replaces the solo `== gEnemyMech` guard.)
int BTIsRegisteredMech(Entity *e)
{
for (int i = 0; i < gBTMechCount; ++i)
if (gBTMechRegistry[i] == e)
return 1;
return 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);
}
}