Files
BT412/game/reconstructed/btplayer.cpp
T
arcattackandClaude Opus 4.8 a35f321ba1 Warp: exact POVTranslocateRenderable replica (collapse/throb/reveal + world mask)
Stop approximating -- replicate POVTranslocateRenderable::Execute() behaviorally,
byte-for-byte on the constants and transitions (engine T0, L4VIDRND.cpp:1763-2076),
per the user's "replicate the effect exactly".  The five prior piecemeal attempts
each fixed one facet and broke another because they left out the WHOLE mechanism.

Full state machine (btl4vid.cpp), driven by two decoupled events (NOT the
SimulationState trigger -- that dial also drives camera/POV+targeting, the f053535
regression; SetIsDead is a separate pure-render flag, verified safe):
  - LOCAL DEATH -> InitialCollapse: scale (pct_left*100)+1 over 1.3s (world visible),
    then SetIsDead(true) + WaitForReincarnate.  (btplayer VehicleDeadMessageHandler,
    deathCount==-1, local-guarded.)
  - WaitForReincarnate: world BLACK (mask), scale 1, Lissajous throb
    (cos(t*3.33), sin(t*2.5)) x2.0 -- the "dances around playfully".
  - LOCAL RESPAWN -> ExpandReveal: SetIsDead(false) + scale (pct_used*150)+1 over
    1.0s -- blasts open, world revealed ("shoots off into the distance").  (btplayer
    DropZoneReplyMessageHandler, local-guarded.)

Draw EXACTLY as the engine (isDeathDraw -> drawAsSky): PASS_SKY, OPAQUE,
CULLMODE=CW, z-test ON (L4VIDEO.cpp:7526/7568-7570/7693).  This kills the "glitchy
funnel" -- opaque = one write/pixel (no translucent double-blend), CW = one winding
of the double-sided mesh (no coincident z-fight), z-on = the expand shell is
occluded/revealed by the returning world.  Placement = Scale * inverse(view) with
NO recenter (the mesh is authored off-origin so the eye sits inside, authentic);
dropped the wrong mCullCenter recenter and the Z-off overlay hack.  Colour is the
"sky" ramp (per prior commit); no geometry spin (myRotateY is dead) -- swirl is the
texture scroll.  SetIsDead reached via a BTSetWorldDead bridge (L4VIDRND.cpp).

STUCK-BLACK SAFETY (the one new risk of decoupling mask-on from mask-off): a 12s
WaitForReincarnate render-loop timeout un-masks if no respawn arrives, plus
BTWarpForceUnmask on the mission-ending + no-DropZones abort paths.  Smoke-verified
2-node: collapse->wait(masked=1)->expand(masked=0), ends masked=0, no crash.

The peer-observer warp stays a world-anchored translucent alpha draw (port
extension; the authentic effect is POV-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:43:54 -05:00

1470 lines
54 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 RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830)
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
//
// @004c05c4 The BT death/respawn cycle (task #52). Three message flavors
// arrive here, keyed on message->deathCount:
//
// -1 The immediate DEATH NOTIFICATION (the ctor default), dispatched
// by the vehicle's death transition (mech4.cpp). Bookkeeping,
// sever playerVehicle (the wreck entity STAYS in the world) and
// re-post this message to ourselves at +5 seconds (5.0f
// @004c0830) stamped with the new death count.
// >= 0 Our own 5-second re-post: hand it to the engine base handler
// (@0042db80, PLAYER.cpp:214) which hunts the closest drop zone,
// dispatches AssignDropZone and posts the 2s -2 probe. The
// DropZoneReply then CREATEs the replacement mech.
// -2 The engine's 2s "did the reply arrive?" probe. If the player
// has a vehicle again the drop zone WAS acquired -> moot.
//
// The binary also runs killer/victim score bumps (+0x280 via @0042e580) here
// -- our kill scoring already flows through BTPostKillScore at the death
// transition, so it is not duplicated -- and, when the role is out of lives,
// posts the mission-review message (id 0x18) at +10 s instead of respawning
// (deferred with the mission-flow work). @004c012c is a sibling retry
// helper reached from DropZoneReply.
//
void
BTPlayer::VehicleDeadMessageHandler(VehicleDeadMessage *message)
{
Check(this);
Check(message);
//
// Mission teardown swallows the whole cycle. ([this+0x40] == 4 skip.)
//
if (GetSimulationState() == MissionEndingState)
{
// If our death raised the warp world-mask but the mission is tearing down
// before we respawn, drop it now so the world can never stay black.
if (this == (BTPlayer *)application->GetMissionPlayer())
{ extern void BTWarpForceUnmask(); BTWarpForceUnmask(); }
Check_Fpu();
return;
}
if (message->deathCount != -1)
{
//
// The 5s respawn re-post (>=0) or the engine's 2s probe (-2). RESET-BASED
// respawn: the mech STAYS as playerVehicle through death (we no longer
// sever it), so "has a vehicle" no longer means "acquired a drop zone".
// Gate on whether the mech is still DEAD -- if it's alive again (already
// reset by a prior reply), this re-post is moot; if still dead, fall
// through to the drop-zone hunt so it gets a spot to reset into.
//
{
Mech *mech = (Mech *)playerVehicle;
Logical mech_dead = (mech != 0
&& mech->GetClassID() == Mech::MechClassID
&& mech->IsMechDestroyed());
if (!mech_dead)
{
deathPending = 0; // this+0x290
suppressConsole = 0; // this+0x258
Check_Fpu();
return;
}
}
if (getenv("BT_SCORE_LOG"))
DEBUG_STREAM << "[score] VehicleDead: drop-zone hunt (msgDeath="
<< message->deathCount << " deathCount=" << deathCount
<< ")\n" << std::flush;
//
// Respawning needs a drop zone. A dev mission without a "DropZones"
// group would hit the engine base's ACTIVE Check(dropzones) -> an
// abort() dialog; no zones -> no respawn (stay dead), honestly logged.
//
{
EntityManager *entity_manager = application->GetEntityManager();
if (entity_manager == 0
|| entity_manager->FindGroup("DropZones") == 0)
{
// No respawn possible -> the expand will never fire -> drop the warp
// world-mask now so we don't stay black waiting for a reincarnation.
if (this == (BTPlayer *)application->GetMissionPlayer())
{ extern void BTWarpForceUnmask(); BTWarpForceUnmask(); }
if (getenv("BT_SCORE_LOG"))
DEBUG_STREAM << "[score] VehicleDead: no DropZones group -- "
"respawn unavailable in this mission\n" << std::flush;
Check_Fpu();
return;
}
}
//
// No vehicle: find/ask a drop zone for a spot. The base gates on
// message->deathCount == deathCount, so stale re-posts from an
// earlier death fall through harmlessly.
//
Player::VehicleDeadMessageHandler(message); // engine base @0042db80
return;
}
//
// ======== deathCount == -1: the immediate death notification ==========
// Dedup on deathPending -- one death, one cycle (cleared when a vehicle
// is back in hand, above).
//
if (deathPending != 0)
{
Check_Fpu();
return;
}
deathPending = 1; // this+0x290
// WARP (task #52): our OWN death = the engine's Idle -> InitialCollapse (trigger
// becomes == control state, POVTranslocateRenderable). Collapse the eye-centred
// sphere 100x -> 1x, then it raises the SetIsDead world mask and throbs until the
// respawn (DropZoneReply) releases the expand-reveal. LOCAL player only (POV).
if (this == (BTPlayer *)application->GetMissionPlayer())
{
extern void BTStartWarpCollapsePOV();
BTStartWarpCollapsePOV();
}
if (getenv("BT_SCORE_LOG"))
DEBUG_STREAM << "[score] VehicleDead(-1): death #" << (deathCount + 1)
<< ", respawn hunt in 5s\n" << std::flush;
//
// One more death; stamp the message (the base handler's identity gate)
// 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 dev missions. 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). The 1995 message is an
// extended VehicleDeadMessage carrying the killer's name (the binary
// reads message+0x1c); our compiled Entity::Message header is a
// different size, so that raw offset lands inside the header -- a
// garbage pointer. Until the pilot-roster wave defines the extended
// message, every death reads as the canned string (@00524b38).
//
killerName = SelfDestructName; // this+0x1d0
//
// RESET-BASED respawn: do NOT sever playerVehicle. The authentic respawn
// REUSES the same mech entity -- it stays our vehicle (a dead wreck) and is
// later healed + moved in place by Mech::Reset (DropZoneReply). Severing it
// (the old model) orphaned the wreck as a second entity and made the reply
// build a NEW viewpoint mech, which is what produced the "2 mechs / camera-
// inside / on-fire respawn" glitches. The ghost-drive is already gated by
// the input zero on IsMechDestroyed (mech4.cpp), so keeping the pointer is
// safe. (The dead mech keeps ticking as a frozen wreck until reset.)
//
//
// If lives remain (solo/dev: no role -> always), re-post to ourselves at
// now + 5 seconds: the pilot watches the wreck, then the recovery drop.
//
if (scenarioRole == 0 || scenarioRole->GetReturnFromDeath() > 0)
{
Time when = Now(); // FUN_00414b60 + FUN_004dcd94
when += RespawnDelay; // 5.0f @004c0830
application->Post(HighEventPriority, this, message, when); // app+0x60
}
// else: out of lives -> the +10s mission-review post (id 0x18). Deferred.
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
//
// Respawn-cycle guard (task #52): during the 5-second dead window
// playerVehicle is severed; the tonnage ratio below is undefined without
// our own mech, so a straggler inflicted-score is dropped.
//
if (our_mech == 0)
{
Check_Fpu();
return;
}
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.
// Respawn-cycle guard (task #52): during the 5-second dead window
// playerVehicle is severed, and the engine base does
// playerVehicle->RespondToScoreMessage unguarded (Check aborts) -- so a
// straggler score lands the award without the vehicle response.
//
message->scoreAward = award; // msg+0x1c
if (playerVehicle == 0)
{
currentScore += message->scoreAward; // the base's award apply
return;
}
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)
{
// A console message is a NetworkClient::Message, NOT an
// Entity::Message -- it must reach the console over the
// NETWORK stream (as the VTV-damaged push above does),
// not through the player's Entity::Dispatch. Entity::Dispatch
// stamps entityID/interestZoneID at the (larger) Entity::Message
// offsets, writing PAST this small stack message -> an RTC stack
// overflow (caught live on the respawned player's first score
// flush, task #52). Route it through SendMessage like the
// damaged-message push.
application->SendMessage( // app+0x20, slot+0x18
console_host->GetHostID(), // console_host+0xc
NetworkClient::ConsoleClientID, // 5
&score_message
);
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);
// TEST HOOK (BT_SPAWN_XZ="x,z"): force the spawn/respawn to a fixed ground
// position (X/Z; Y stays the drop-zone ground height, the ground model
// snaps it). The map's drop zones are picked ~randomly and land the two
// -net instances ~1500u apart -- hard to find each other. Set the same
// base on both pods with a small offset so they spawn side by side.
{
const char *xz = getenv("BT_SPAWN_XZ");
if (xz != 0)
{
float sx = 0.0f, sz = 0.0f;
if (sscanf(xz, "%f,%f", &sx, &sz) == 2)
{
mech_location.linearPosition.x = sx;
mech_location.linearPosition.z = sz;
if (getenv("BT_MP_LOG"))
DEBUG_STREAM << "[spawn] BT_SPAWN_XZ override -> ("
<< sx << ", " << mech_location.linearPosition.y
<< ", " << sz << ")\n" << std::flush;
}
}
}
//
// 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;
// TEST HOOK (BT_SPAWN_XZ="x,z"): force the drop-zone X/Z so two -net instances
// land in the same area (map zones are ~random, ~1500u apart -- hard to find
// each other). Applied to the MESSAGE so BOTH the CreatePlayerVehicle make
// AND the Mech::Reset placement below use it (Reset re-places at
// message->dropZoneLocation, which would otherwise undo the override). Y
// stays the drop-zone ground height; the ground model snaps it.
{
const char *xz = getenv("BT_SPAWN_XZ");
if (xz != 0)
{
float sx = 0.0f, sz = 0.0f;
if (sscanf(xz, "%f,%f", &sx, &sz) == 2)
{
message->dropZoneLocation.linearPosition.x = sx;
message->dropZoneLocation.linearPosition.z = sz;
}
}
}
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]
// (warp fired in the shared placement below -- initial drop-in + respawn)
}
else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80]
{
if (GetSimulationState() == MissionEndingState) // state == 4
{
Check_Fpu();
return;
}
//
// RESPAWN (reset-based): the mech STAYS ours through death; this reply
// (deathCount matches) resets the EXISTING dead mech in place -- fall
// through to the placement below (Mech::Reset heals + moves it). Guard
// against a duplicate reply / the 2s probe: if the mech is already alive
// again, there is nothing to reset. (The old path pulsed
// DropZoneAcquiredState and looped through the dead handler to build a
// NEW mech -- removed; that state drives our camera/targeting.)
//
Mech *mech = (Mech *)playerVehicle;
if (!(mech != 0 && mech->GetClassID() == Mech::MechClassID
&& mech->IsMechDestroyed()))
{
Check_Fpu();
return;
}
// else: still dead -> fall through to placement (reset + warp)
}
else // stale / old message
{
return;
}
//
// Place the mech at the drop-zone origin: heal + move the SAME entity
// (Mech::Reset), raise the "translocated" cockpit alarm, and fire the
// translocation-sphere warp. Runs for the initial drop-in (fresh mech) and
// for a respawn (dead mech healed) alike.
//
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 (heal+move)
// WARP (task #52): our OWN respawn = the engine's WaitForReincarnate -> Expand
// release (POVTranslocateRenderable): drop the SetIsDead world mask and blast
// the eye-centred sphere open to reveal the reborn world. LOCAL player only (a
// POV effect); a self-contained render one-shot that does NOT touch
// SimulationState (that dial drives the camera/POV + targeting -- pulsing it
// regressed all of those; SetIsDead is a separate pure-render flag).
if (this == (BTPlayer *)application->GetMissionPlayer())
{
extern void BTStartWarpExpandPOV();
BTStartWarpExpandPOV();
}
}
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);
}
}