BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival / expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating) keyed on the player's SimulationState dial. Drawn direct from the render loop by BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same accommodation the beams/reticle use. The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is why every resource-name search missed the effect for three rounds. Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL player (the authentic wiring builds it only for replicants + a POV fade for self, but peer player-attribute replication isn't wired -- on a replicant SimulationState/ DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for the engine's +1s drop-zone re-post) and writes the respawn origin into the DropZoneLocation attribute. Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn cycle un-regressed, no render errors. Visual appearance (colour/size) still needs a live look; the authentic see-others'-spheres path needs player replication. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1415 lines
51 KiB
C++
1415 lines
51 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)
|
|
|
|
// WARP (task #52): countdown from the respawn's DropZoneAcquired pulse to the
|
|
// VehicleTranslocated flip that makes the translocation sphere EXPAND (reveal).
|
|
// One local player per node, so a file-static holds it. <0 = idle.
|
|
static Scalar sWarpExpandTimer = -1.0f;
|
|
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)
|
|
{
|
|
Check_Fpu();
|
|
return;
|
|
}
|
|
|
|
if (message->deathCount != -1)
|
|
{
|
|
//
|
|
// The 5s respawn re-post (>=0) or the engine's 2s probe (-2). A
|
|
// vehicle in hand means the drop zone was acquired -> nothing to do.
|
|
// (This branch also kept the old solo bring-up honest: the initial
|
|
// spawn's -2 probes land here and are eaten.)
|
|
//
|
|
if (playerVehicle != 0)
|
|
{
|
|
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)
|
|
{
|
|
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
|
|
|
|
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
|
|
|
|
//
|
|
// Sever the vehicle link. @004c05c4 powers the roster down through
|
|
// @0049fe0c (the slot-11 DeathShutdown broadcast -- our mech's own death
|
|
// transition already runs that pass) and clears this+0x1fc. The wreck
|
|
// stays registered in the world; only the player's claim on it ends --
|
|
// which is what lets DropZoneReply create the replacement.
|
|
//
|
|
playerVehicle = 0; // this+0x1fc
|
|
|
|
//
|
|
// 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
|
|
|
|
// WARP (task #52): once the collapse has had time to play, flip the
|
|
// SimulationState dial off DropZoneAcquired so the translocation sphere
|
|
// leaves its "wait" phase and EXPANDS (reveals the reborn mech).
|
|
if (sWarpExpandTimer > 0.0f)
|
|
{
|
|
sWarpExpandTimer -= time_slice;
|
|
if (sWarpExpandTimer <= 0.0f)
|
|
{
|
|
sWarpExpandTimer = -1.0f;
|
|
SetSimulationState(VehicleTranslocatedState); // state 2 -> expand
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
//
|
|
// 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]
|
|
|
|
//
|
|
// WARP (task #52): fire the translocation sphere at our reinsertion.
|
|
// Publish the drop-zone position into our DropZoneLocation attribute (the
|
|
// sphere renderable draws there) and pulse the SimulationState dial to
|
|
// DropZoneAcquired -- state 1, the sphere's COLLAPSE trigger.
|
|
// PlayerSimulation flips it to VehicleTranslocated ~1.4 s later, which the
|
|
// sphere reads as "reincarnate" and EXPANDS to reveal the reborn mech.
|
|
// (Authentic trigger = the player's own SimulationState + DropZoneLocation
|
|
// attribute; the flip-timer stands in for the engine's +1 s drop-zone
|
|
// re-post choreography, which our sever-and-recreate respawn skips.)
|
|
//
|
|
{
|
|
Point3D *drop_attr = (Point3D *)GetAttributePointer("DropZoneLocation");
|
|
if (drop_attr != 0)
|
|
*drop_attr = message->dropZoneLocation.linearPosition;
|
|
}
|
|
SetSimulationState(DropZoneAcquiredState); // state 1 -> collapse
|
|
sWarpExpandTimer = 1.4f;
|
|
}
|
|
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);
|
|
}
|
|
}
|