The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control, wreck-never-disappears) all traced to one architectural divergence: our respawn SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a permanent wreck and building a duplicate viewpoint whose old render tree was never torn down. The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on respawn Mech::Reset heals it and moves it in place. Implemented faithfully, adapted to our layout (the 1995 raw offsets map to different 2007 engine fields, so reset the equivalent named members, not the offsets): - Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait accumulators) so the replicant stops lerping to the death spot; clear the death latch (movementMode=1, graphicAlarm=0); Heal every damage zone (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset (vtable+0x28) every subsystem; ForceUpdate to broadcast. - btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post gates on the mech still being dead; DropZoneReply resets the EXISTING mech in place (heal+move) instead of creating a new one, then fires the warp. Warp moved to the shared placement (initial drop-in + respawn). Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new 3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE mech (no wreck+new pair). Warp fires each respawn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1448 lines
52 KiB
C++
1448 lines
52 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)
|
|
{
|
|
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)
|
|
{
|
|
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
|
|
|
|
//
|
|
// 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): the translocation sphere at our reinsertion. A
|
|
// self-contained render one-shot -- it does NOT touch SimulationState
|
|
// (that dial drives the camera/POV + targeting; pulsing it regressed all
|
|
// of those). The effect plays its own collapse->expand.
|
|
const Point3D &p = message->dropZoneLocation.linearPosition;
|
|
extern void BTStartWarpEffect(float x, float y, float z);
|
|
BTStartWarpEffect((float)p.x, (float)p.y, (float)p.z);
|
|
}
|
|
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);
|
|
}
|
|
}
|