//===========================================================================// // 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::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 #pragma hdrstop #if !defined(BTPLAYER_HPP) # include #endif #if !defined(BTTEAM_HPP) # include #endif #if !defined(BTMSSN_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(HOSTMGR_HPP) # include #endif #if !defined(BTCNSL_HPP) # include #endif #if !defined(NTTMGR_HPP) # include // EntityManager / EntityGroup / FindGroup #endif #if !defined(SCALAR_HPP) # include // Round() #endif #if !defined(DROPZONE_HPP) # include // DropZone__ReplyMessage (spawn handshake) #endif #if !defined(MISSION_HPP) # include // Mission::GetGameModel / GetBadgeName / GetColorName #endif #if !defined(RESOURCE_HPP) # include // ResourceFile / ResourceDescription / ModelListResourceType #endif #if !defined(MECH_HPP) # include // Mech::MakeMessage / MechClassID / Reset #endif #define CONSOLE_UPDATE_INTERVAL 10.0f // _DAT_004c08fc #define STATUS_DISPLAY_TIME 6.0f // 0x40c00000 // BRING-UP: the spawned target/enemy mech (see CreatePlayerVehicle below). The // player mech's per-frame targeting step (mech4.cpp) locks onto this as its // current target. NULL when no enemy was spawned (BT_SPAWN_ENEMY unset). Entity *gEnemyMech = 0; // // LIVE-MECH REGISTRY (task #46): every Mech (player, dummy, peer replicants) // (de)registers here from its ctor/dtor. The boresight world-pick walks it to // find target candidates -- generalising the solo gEnemyMech to MP peers. A // plain fixed array (mech counts are tiny -- <= a few per arena) avoids any // allocator interaction in the hot ctor path. // static Entity *gBTMechRegistry[32]; static int gBTMechCount = 0; void BTRegisterMech(Entity *m) { if (m == 0) return; for (int i = 0; i < gBTMechCount; ++i) // idempotent if (gBTMechRegistry[i] == m) return; if (gBTMechCount < 32) gBTMechRegistry[gBTMechCount++] = m; } void BTDeregisterMech(Entity *m) { for (int i = 0; i < gBTMechCount; ++i) { if (gBTMechRegistry[i] == m) { gBTMechRegistry[i] = gBTMechRegistry[--gBTMechCount]; gBTMechRegistry[gBTMechCount] = 0; return; } } } // The target-candidate list for a given shooter: every registered mech that is // NOT the shooter itself. (Alive/pick filtering is the caller's -- mech4's // world-pick already tests IsMechDestroyed + the collision-box ray.) int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut) { int n = 0; for (int i = 0; i < gBTMechCount && n < maxOut; ++i) { Entity *m = gBTMechRegistry[i]; if (m != 0 && m != shooter) out[n++] = m; } return n; } // Is this entity a live registered mech? (Projectile-impact target validation // -- replaces the solo `== gEnemyMech` guard.) int BTIsRegisteredMech(Entity *e) { for (int i = 0; i < gBTMechCount; ++i) if (gBTMechRegistry[i] == e) return 1; return 0; } // // Reconstruction helpers / data referenced by the decomp that have no direct // analog in the surviving WinTesla headers (CROSS-FAMILY / engine gap -- see // report). Declared here so the recovered bodies compile; the linker binds // them to the real engine/BT symbols. // // FUN_0041bbd8 AlarmIndicator::SetLevel (the engine GaugeAlarm exposes no // SetLevel in WinTesla) -- raise an alarm to a given level. // FUN_004b04d8 notify the objective subsystem that its mech was destroyed. // StatusMessagePool the BT status-message MemoryBlock pool (@00512f6c). // BT*VTable the BT vtable symbols installed by ctor/dtor. // SelfDestructName / ResetDelay canned strings / timing constants. // extern void Set_Alarm_Level(void *alarm_indicator, int level); extern void Notify_Objective_Reached(int *objective_subsystem, Mech *mech); extern MemoryBlock *StatusMessagePool; extern void *BTPlayerVTable; extern void *BTStatusMessageVTable; // Wire-format lock: BTPlayer::MakeMessage travels RAW over the network (entity // replication), so the inline string fields must sit at the binary's offsets. static_assert(offsetof(BTPlayer::MakeMessage, teamName) == 0x50, "BTPlayer::MakeMessage::teamName must be at wire offset 0x50"); static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90, "BTPlayer::MakeMessage::roleName must be at wire offset 0x90"); static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0, "BTPlayer::MakeMessage wire size must be 0xD0"); static const char *SelfDestructName = "self destruct"; // &DAT_00524b38 static const Scalar RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830) static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation) // // Mech fields touched by the scoring code but not exposed by the reconstructed // mech.hpp (owned by the mech family). Accessed through the documented byte // offsets. CROSS-FAMILY -- ideally Mech would expose these accessors. // // gauge scoring wave: the raw binary offsets are garbage in our compiled Mech // (engine Entity base ~0x1BC vs the binary's 0x300; 0x638 Mech vs 0x854), and the // scoring handlers deref them -> a fault the moment a ScoreMessage is dispatched. // Reconcile: the owning player via the engine playerLink accessor (NULL for the // uncontrolled BT_SPAWN_ENEMY dummy -- exactly what the null-guards below rely on); // tonnage ratio stubbed 1.0 + damage bias stubbed 0.0 for bring-up (SCORE == raw // damage, un-tonnage-scaled; the real per-mech tonnage/bias accessors are a follow-up). #define MECH_TONNAGE(m) (1.0f) // bring-up: ratio == 1 #define MECH_DAMAGE_BIAS(m) (0.0f) // bring-up: factor = 0*bias+1 = 1 #define MECH_OWNING_PLAYER(m) ((BTPlayer *)((Mech *)(m))->GetPlayerLink()) // ENTITY.h:430 (NULL for the dummy) //############################################################################# //############################### BTPlayer ############################## //############################################################################# //############################################################################# // Message Support // // MESSAGE_ENTRY names recovered from the .data string block @005130c0. // (ScoreInflicted is reconstructed best-effort -- see header note.) // const Receiver::HandlerEntry BTPlayer::MessageHandlerEntries[] = { MESSAGE_ENTRY(BTPlayer, DropZoneReply), MESSAGE_ENTRY(BTPlayer, VehicleDead), MESSAGE_ENTRY(BTPlayer, Score), MESSAGE_ENTRY(BTPlayer, ScoreInflicted), MESSAGE_ENTRY(BTPlayer, ScoreUpdate), MESSAGE_ENTRY(BTPlayer, MissionStarting), MESSAGE_ENTRY(BTPlayer, MissionEnding) }; Receiver::MessageHandlerSet& BTPlayer::GetMessageHandlers() { static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(BTPlayer::MessageHandlerEntries), BTPlayer::MessageHandlerEntries, Player::GetMessageHandlers() ); return messageHandlers; } //############################################################################# // Shared Data Support // Derivation* BTPlayer::GetClassDerivations() { static Derivation classDerivations( Player::GetClassDerivations(), "BTPlayer" // @005130cb ); return &classDerivations; } BTPlayer::SharedData BTPlayer::DefaultData( BTPlayer::GetClassDerivations(), BTPlayer::GetMessageHandlers(), Player::GetAttributeIndex(), BTPlayer::StateCount, (BTPlayer::MakeHandler)BTPlayer::Make ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // VehicleDeadMessageHandler // // @004c05c4 The BT death/respawn cycle (task #52). Three message flavors // arrive here, keyed on message->deathCount: // // -1 The immediate DEATH NOTIFICATION (the ctor default), dispatched // by the vehicle's death transition (mech4.cpp). Bookkeeping, // sever playerVehicle (the wreck entity STAYS in the world) and // re-post this message to ourselves at +5 seconds (5.0f // @004c0830) stamped with the new death count. // >= 0 Our own 5-second re-post: hand it to the engine base handler // (@0042db80, PLAYER.cpp:214) which hunts the closest drop zone, // dispatches AssignDropZone and posts the 2s -2 probe. The // DropZoneReply then CREATEs the replacement mech. // -2 The engine's 2s "did the reply arrive?" probe. If the player // has a vehicle again the drop zone WAS acquired -> moot. // // The binary also runs killer/victim score bumps (+0x280 via @0042e580) here // -- our kill scoring already flows through BTPostKillScore at the death // transition, so it is not duplicated -- and, when the role is out of lives, // posts the mission-review message (id 0x18) at +10 s instead of respawning // (deferred with the mission-flow work). @004c012c is a sibling retry // helper reached from DropZoneReply. // void BTPlayer::VehicleDeadMessageHandler(VehicleDeadMessage *message) { Check(this); Check(message); // // Mission teardown swallows the whole cycle. ([this+0x40] == 4 skip.) // if (GetSimulationState() == MissionEndingState) { // If our death raised the warp world-mask but the mission is tearing down // before we respawn, drop it now so the world can never stay black. if (this == (BTPlayer *)application->GetMissionPlayer()) { extern void BTWarpForceUnmask(); BTWarpForceUnmask(); } Check_Fpu(); return; } if (message->deathCount != -1) { // // The 5s respawn re-post (>=0) or the engine's 2s probe (-2). RESET-BASED // respawn: the mech STAYS as playerVehicle through death (we no longer // sever it), so "has a vehicle" no longer means "acquired a drop zone". // Gate on whether the mech is still DEAD -- if it's alive again (already // reset by a prior reply), this re-post is moot; if still dead, fall // through to the drop-zone hunt so it gets a spot to reset into. // { Mech *mech = (Mech *)playerVehicle; Logical mech_dead = (mech != 0 && mech->GetClassID() == Mech::MechClassID && mech->IsMechDestroyed()); if (!mech_dead) { deathPending = 0; // this+0x290 suppressConsole = 0; // this+0x258 Check_Fpu(); return; } } if (getenv("BT_SCORE_LOG")) DEBUG_STREAM << "[score] VehicleDead: drop-zone hunt (msgDeath=" << message->deathCount << " deathCount=" << deathCount << ")\n" << std::flush; // // Respawning needs a drop zone. A dev mission without a "DropZones" // group would hit the engine base's ACTIVE Check(dropzones) -> an // abort() dialog; no zones -> no respawn (stay dead), honestly logged. // { EntityManager *entity_manager = application->GetEntityManager(); if (entity_manager == 0 || entity_manager->FindGroup("DropZones") == 0) { // No respawn possible -> the expand will never fire -> drop the warp // world-mask now so we don't stay black waiting for a reincarnation. if (this == (BTPlayer *)application->GetMissionPlayer()) { extern void BTWarpForceUnmask(); BTWarpForceUnmask(); } if (getenv("BT_SCORE_LOG")) DEBUG_STREAM << "[score] VehicleDead: no DropZones group -- " "respawn unavailable in this mission\n" << std::flush; Check_Fpu(); return; } } // // No vehicle: find/ask a drop zone for a spot. The base gates on // message->deathCount == deathCount, so stale re-posts from an // earlier death fall through harmlessly. // Player::VehicleDeadMessageHandler(message); // engine base @0042db80 return; } // // ======== deathCount == -1: the immediate death notification ========== // Dedup on deathPending -- one death, one cycle (cleared when a vehicle // is back in hand, above). // if (deathPending != 0) { Check_Fpu(); return; } deathPending = 1; // this+0x290 // WARP (task #52): our OWN death = the engine's Idle -> InitialCollapse (trigger // becomes == control state, POVTranslocateRenderable). Collapse the eye-centred // sphere 100x -> 1x, then it raises the SetIsDead world mask and throbs until the // respawn (DropZoneReply) releases the expand-reveal. LOCAL player only (POV). if (this == (BTPlayer *)application->GetMissionPlayer()) { extern void BTStartWarpCollapsePOV(); BTStartWarpCollapsePOV(); } if (getenv("BT_SCORE_LOG")) DEBUG_STREAM << "[score] VehicleDead(-1): death #" << (deathCount + 1) << ", respawn hunt in 5s\n" << std::flush; // // One more death; stamp the message (the base handler's identity gate) // and debit a life against the scoring role. // ++deathCount; // this+0x200 message->deathCount = deathCount; // message+0x38 // TODO(bring-up): scenarioRole is the scoring role looked up from the mission's // role registry (btplayer.cpp:815, currently deferred), so it is NULL for the // minimal dev missions. Only debit a life when a role is present. if (scenarioRole != 0) // this[0x208] { scenarioRole->SetReturnFromDeath( // --*(this[0x208]+0x28) scenarioRole->GetReturnFromDeath() - 1); } simulationFlags |= 0x1; // request a forced update Set_Alarm_Level((char *)this + 0x2c, 1); // FUN_0041bbd8(this+0x2c, 1) // // Remember who killed us (for the pilot HUD). The 1995 message is an // extended VehicleDeadMessage carrying the killer's name (the binary // reads message+0x1c); our compiled Entity::Message header is a // different size, so that raw offset lands inside the header -- a // garbage pointer. Until the pilot-roster wave defines the extended // message, every death reads as the canned string (@00524b38). // killerName = SelfDestructName; // this+0x1d0 // // RESET-BASED respawn: do NOT sever playerVehicle. The authentic respawn // REUSES the same mech entity -- it stays our vehicle (a dead wreck) and is // later healed + moved in place by Mech::Reset (DropZoneReply). Severing it // (the old model) orphaned the wreck as a second entity and made the reply // build a NEW viewpoint mech, which is what produced the "2 mechs / camera- // inside / on-fire respawn" glitches. The ghost-drive is already gated by // the input zero on IsMechDestroyed (mech4.cpp), so keeping the pointer is // safe. (The dead mech keeps ticking as a frozen wreck until reset.) // // // If lives remain (solo/dev: no role -> always), re-post to ourselves at // now + 5 seconds: the pilot watches the wreck, then the recovery drop. // if (scenarioRole == 0 || scenarioRole->GetReturnFromDeath() > 0) { Time when = Now(); // FUN_00414b60 + FUN_004dcd94 when += RespawnDelay; // 5.0f @004c0830 application->Post(HighEventPriority, this, message, when); // app+0x60 } // else: out of lives -> the +10s mission-review post (id 0x18). Deferred. suppressConsole = 0; // this+0x258 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ScoreInflictedMessageHandler // // @004c0200 Awards points for damage *this* player inflicted on another // mech. The score is the tonnage-scaled inflicted value; if the target mech // turns out to be our own vehicle the award is negated (you don't score for // shooting yourself). // void BTPlayer::ScoreInflictedMessageHandler(ScoreMessage *message) { Check(this); Check(message); // // This handler is only ever fed DamageInflictedScore (type 0) messages. // if (message->scoreType != BTPlayer::ScoreMessage::DamageInflictedScore) { Verify( False, "BTPlayer::ScoreInflictedMessageHandler should only be " "given DamageInflictedScoreMessages!", // @00513110 "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051316a 0x18b ); } // // Resolve the mech that took the damage and our own vehicle. // Mech *target_mech = (Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID); // app+0x2c, msg+0x34 Mech *our_mech = (Mech *)playerVehicle; // this+0x1fc // // Respawn-cycle guard (task #52): during the 5-second dead window // playerVehicle is severed; the tonnage ratio below is undefined without // our own mech, so a straggler inflicted-score is dropped. // if (our_mech == 0) { Check_Fpu(); return; } Scalar award = CalcInflictedScore(message->damageAmount, target_mech, 0.0f); if (target_mech == our_mech) { award = -award; } // // Scale by the tonnage ratio (target / self) and accumulate. (tonnage is // a Mech field at +0x4bc; not exposed by the reconstructed mech.hpp, so it // is read through the documented offset. CROSS-FAMILY -- see report.) // currentScore += (MECH_TONNAGE(target_mech) / MECH_TONNAGE(our_mech)) * award; // (+0x4bc / +0x4bc), this+0x278 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ScoreUpdateMessageHandler // // @004c02a8 A network score-sync message. Only the master instance owns the // authoritative score, so this must never run on a replicant; it then defers // to the base score handler which folds the carried scoreAward in. // void BTPlayer::ScoreUpdateMessageHandler(ScoreMessage *message) { if ((simulationFlags & 0xc) == 4) // this+0x28 -- replicant copy { Verify( False, "BTPlayer::ScoreUpdateMessageHandler should only " "run on Master instances!", // @00513186 "d:\\tesla_bt\\bt\\btplayer.cpp", // @005131cf 0x1d5 ); } Player::ScoreMessageHandler(message); // FUN_0042da20 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ScoreMessageHandler // // @004c02e4 The main combat scoring handler. Runs only on the master, and // only while not mission-ending. Handles damage received (type 1) and kills // (type 2); the computed award is written back into the message before the // base Player::ScoreMessageHandler applies it to currentScore. // void BTPlayer::ScoreMessageHandler(ScoreMessage *message) { if ((simulationFlags & 0xc) == 4) // replicant -- must not score { Verify( False, "BTPlayer::ScoreMessageHandler should only run on " "Master instances!", // @005131eb "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051322e 0x1e9 ); } // // Ignore everything once the mission is ending (state 4). // if (GetSimulationState() == MissionEndingState) // this+0x40 == 4 { return; } Mech *sender_mech = (Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID); // msg+0x34 Mech *our_mech = (Mech *)playerVehicle; // this+0x1fc Scalar award = 0.0f; switch (message->scoreType) // msg+0x20 { case BTPlayer::ScoreMessage::DamageInflictedScore: // 0 // // Inflicted-damage messages belong to ScoreInflictedMessageHandler. // Verify( False, "BTPlayer::ScoreMessageHandler should not be " "given DamageInflictedScoreMessages!", // @0051324a "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051329a 0x296 ); break; case BTPlayer::ScoreMessage::DamageReceivedScore: // 1 { // // Points lost for damage we took (unless we hit ourselves). // if (sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role { Scalar received = scenarioRole->CalcDamageReceivedScore(message->damageAmount); // FUN_00429b94 award = (MECH_TONNAGE(our_mech) / MECH_TONNAGE(sender_mech)) * received; } // // Tell the console (if any, and not suppressed) that our VTV // was damaged. // Host *console_host = application->GetHostManager()->GetConsoleHost(); // FUN_00429078 if (console_host && suppressConsole == 0) // this+0x258 { ConsolePlayerVTVDamagedMessage damaged_message( ownerID, // this+0x18c MECH_OWNING_PLAYER(sender_mech)->ownerID, // *(sender+0x190)+0x18c Round(message->damageAmount), // FUN_004078fc message->pointSenderLo, // msg+0x2c message->pointSenderHi, // msg+0x28 (int)Now().ticks, // FUN_004dcd94 message->auxID // msg+0x30 ); application->SendMessage( // app+0x20, slot+0x18 console_host->GetHostID(), // console_host+0xc NetworkClient::ConsoleClientID, // 5 &damaged_message ); } } break; case BTPlayer::ScoreMessage::KillScore: // 2 { // gauge scoring wave: the attacking mech's owner (NULL for the ownerless // BT_SPAWN_ENEMY dummy). Resolved once, guarded everywhere below. BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech); // *(sender+0x190), NULL for the dummy // // A kill (or a self-kill). Inflicted value scaled by the // sender/self tonnage ratio. // award = (MECH_TONNAGE(sender_mech) / MECH_TONNAGE(our_mech)) * CalcInflictedScore(message->damageAmount, sender_mech, message->scoreAward); if (sender_mech == our_mech) { award = -award; // suicide -- no credit } else { // // Credit a kill to us and (if it has one) to the attacking player. // The solo demo posts this to the LOCAL player with senderMechID=the // victim, so `this->killCount++` fires (KILLS -> 1); the ownerless // dummy's sender_owner is NULL -> the second increment is guarded away. // ++killCount; // this+0x27c if (sender_owner) ++sender_owner->killCount; // *(sender+0x190)+0x27c } // // Pop a "destroyed by " status message onto the pilot HUD. // Gated on a real attacker player (+ the status pool, a NULL stub in // bring-up), so the ownerless-dummy solo path skips it (no player to name). // if (sender_owner && StatusMessagePool) { StatusMessage *status = (StatusMessage *)StatusMessagePool->New(); // FUN_00402f74(0x512f6c) if (status) { new (status) Player__StatusMessage( sender_owner, // *(sender+0x190) 0, STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f ); *(int *)((char *)status + 0x18) = 0; // status[6] = 0 AddStatusMessage(status); // FUN_0042e580 } } // // If we just killed our designated objective mech, notify it. // (playerVehicle's objective subsystem link is at +0x128.) // if (sender_mech == objectiveMech) // this+0x284 { Notify_Objective_Reached( // FUN_004b04d8 *(int **)((char *)playerVehicle + 0x128), // *(this[0x1fc]+0x128) sender_mech ); } } break; } // // Hand the (now-scored) message to the base class to apply the award. // Respawn-cycle guard (task #52): during the 5-second dead window // playerVehicle is severed, and the engine base does // playerVehicle->RespondToScoreMessage unguarded (Check aborts) -- so a // straggler score lands the award without the vehicle response. // message->scoreAward = award; // msg+0x1c if (playerVehicle == 0) { currentScore += message->scoreAward; // the base's award apply return; } Player::ScoreMessageHandler(message); // FUN_0042da20 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CalcInflictedScore // // @004c052c Shared damage->points routine. Same-team hits (in a non-free- // for-all game) cost a fixed friendly-fire penalty; otherwise the points // scale with the target's damage-bias factor. The result is multiplied by // the role's damageInflictedModifier. // Scalar BTPlayer::CalcInflictedScore( const Scalar &damage_amount, Mech *other_mech, Scalar bias ) { // gauge scoring wave: scenarioRole is NULL in bring-up (the BTMission role // registry has no WinTesla analog -- ctor lookup stubbed), and every modifier // deref below would fault the moment a score message is dispatched. A NULL // role == the neutral role (all modifiers 1, no penalties, bias 0), so the // award is just (damage + bias). Real per-role modifiers are a follow-up. if (scenarioRole == 0) { return damage_amount + bias; } Scalar factor; // // Team game: a hit on a team-mate is friendly fire. // if (freeForAll == 0 // this+0x250 && strcmp(MECH_OWNING_PLAYER(other_mech)->teamName, teamName) == 0) // (other+0x190)+0x20c vs this+0x20c { factor = -scenarioRole->GetFriendlyFirePenalty(); // -*(this[0x208]+0x14) } else { factor = MECH_DAMAGE_BIAS(other_mech) // other+0x354 * scenarioRole->GetDamageBias() // *(this[0x208]+0x18) + 1.0f; // _DAT_004c05c0 == 1.0f } return (damage_amount + bias) * scenarioRole->GetDamageInflictedModifier() // *(this[0x208]+0x0c) * factor; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // PlayerSimulation // // @004c083c Registered as the Performance for a console/replicant copy // (set in the ctor when (simulationFlags & 0xc) == 4). Runs the base sim, // then pushes our score to the console once every CONSOLE_UPDATE_INTERVAL // seconds (or immediately if the application is shutting the mission down). // void BTPlayer::PlayerSimulation(Scalar time_slice) { Check(this); Player::PlayerSimulation(time_slice); // FUN_0042e168 if ( (lastPerformance - lastConsoleUpdate) / TicksPerSecond >= CONSOLE_UPDATE_INTERVAL // _DAT_004c08fc || application->GetApplicationState() == Application::EndingMission // app+0x88 == 6 ) { lastConsoleUpdate = lastPerformance; // this+0x28c = this+0x10 // // Only bother if our score actually changed since last time. // if ((Scalar)currentScore != 0.0f) // this[0x9e] != _DAT_004c0900 (0.0f) { int score = (int)currentScore; ConsolePlayerVTVScoreUpdateMessage score_message( ownerID, score ); // FUN_00420ea4(0x20, 0x1a, 1, ...) // gauge scoring wave: the binary's currentScore is a console DELTA that is // flushed to the operator console then zeroed. Our SCORE/RANK gauges read // currentScore as the RUNNING total, so only flush+zero when a console host // is actually present (MP / pod); in solo there is no console -> keep the // running score so the SCORE gauge + CalcRanking don't reset every 10s. Host *console_host = application->GetHostManager()->GetConsoleHost(); // FUN_00429078 if (console_host) { // A console message is a NetworkClient::Message, NOT an // Entity::Message -- it must reach the console over the // NETWORK stream (as the VTV-damaged push above does), // not through the player's Entity::Dispatch. Entity::Dispatch // stamps entityID/interestZoneID at the (larger) Entity::Message // offsets, writing PAST this small stack message -> an RTC stack // overflow (caught live on the respawned player's first score // flush, task #52). Route it through SendMessage like the // damaged-message push. application->SendMessage( // app+0x20, slot+0x18 console_host->GetHostID(), // console_host+0xc NetworkClient::ConsoleClientID, // 5 &score_message ); currentScore = 0; // this[0x9e] = 0 } } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CreatePlayerVehicle // // @004bfcac (BTPlayer vtable slot +0x40). Makes the player's BattleMech at // the supplied drop-zone origin. Lets the base build any MUNGA-level vehicle // first (camera-ship observers); if no vehicle resulted, instantiates a Mech // from the mission's game-model resource and links it as our viewpoint. // // Structural model: RPPLAYER.cpp RPPlayer::CreatePlayerVehicle (which makes a // VTV the same way). BT substitutes Mech for VTV and carries the three mech // resource-name strings (badge / colour / patch) into the make message. // void BTPlayer::CreatePlayerVehicle(Origin mech_location) { Check(this); Check(application); ResourceFile *resources = application->GetResourceFile(); Check(resources); Check_Pointer(playerMission->GetGameModel()); HostManager *host_manager = application->GetHostManager(); Check(host_manager); // TEST HOOK (BT_SPAWN_XZ="x,z"): force the spawn/respawn to a fixed ground // position (X/Z; Y stays the drop-zone ground height, the ground model // snaps it). The map's drop zones are picked ~randomly and land the two // -net instances ~1500u apart -- hard to find each other. Set the same // base on both pods with a small offset so they spawn side by side. { const char *xz = getenv("BT_SPAWN_XZ"); if (xz != 0) { float sx = 0.0f, sz = 0.0f; if (sscanf(xz, "%f,%f", &sx, &sz) == 2) { mech_location.linearPosition.x = sx; mech_location.linearPosition.z = sz; if (getenv("BT_MP_LOG")) DEBUG_STREAM << "[spawn] BT_SPAWN_XZ override -> (" << sx << ", " << mech_location.linearPosition.y << ", " << sz << ")\n" << std::flush; } } } // // Make any MUNGA-level vehicle (camera ship for an observer player). // Player::CreatePlayerVehicle(mech_location); // // If the player still has no vehicle, build him a mech and link to it. // if (!playerVehicle) { ResourceDescription *mech_res = resources->FindResourceDescription( playerMission->GetGameModel(), ResourceDescription::ModelListResourceType ); Check(mech_res); mech_res->Lock(); Mech::MakeMessage create_player( Mech::MakeMessageID, sizeof(Mech::MakeMessage), EntityID(host_manager->GetLocalHostID()), (Entity::ClassID)Mech::MechClassID, EntityID::Null, mech_res->resourceID, Mech::DefaultFlags, mech_location, Motion::Identity, Motion::Identity, playerMission->GetBadgeName(), // vehicle badge (+0x7c) playerMission->GetColorName(), // vehicle colour (+0x90) ((BTMission *)playerMission)->GetPatchName() // vehicle patch (+0xa4) ); mech_res->Unlock(); playerVehicle = application->MakeAndLinkViewpointEntity(&create_player); Register_Object(playerVehicle); } // --- BRING-UP: spawn a stationary target/enemy mech (gameplay scaffolding) --- // The mission system is networked (one pilot per host) and BT's bot/AI code // is absent, so a solo mission has nothing to fight. When BT_SPAWN_ENEMY is // set we drop a SECOND mech into the world via the SAME factory the registry // uses for remote/AI entities (Mech::Make on a MakeMessage) -- a non-viewpoint, // self-registering, rendering, ticking entity placed ahead of the player. It // has no AI yet (mech4.cpp gates the player-drive path to the viewpoint mech), // so it stands as a target dummy. First step toward actual combat. if (getenv("BT_SPAWN_ENEMY")) { ResourceDescription *enemy_res = resources->FindResourceDescription( playerMission->GetGameModel(), ResourceDescription::ModelListResourceType); if (enemy_res) { enemy_res->Lock(); // Place it ahead of the player's drop ALONG THE SPAWN FACING (the mech // faces local -Z; rotate that axis into world by the spawn orientation). // The old fixed "z -= 120" assumed the bring-up drive's forced heading=0; // the real-controls drive keeps the authentic spawn orientation, so the // dummy must follow the actual facing or the auto-walk leaves it behind. Origin enemy_origin = mech_location; { AffineMatrix facing; facing.BuildIdentity(); facing = mech_location.angularPosition; // rotation from the spawn pose UnitVector spawnZ; facing.GetFromAxis(Z_Axis, &spawnZ); // local Z basis in world enemy_origin.linearPosition.x -= spawnZ.x * 120.0f; // forward = -Z enemy_origin.linearPosition.y -= spawnZ.y * 120.0f; enemy_origin.linearPosition.z -= spawnZ.z * 120.0f; // FACE THE PLAYER: copying the spawn pose left BOTH mechs facing // the same way (the player stared at the enemy's back). Flip the // enemy's yaw 180 deg about Y so the two face each other. Engine // yaw convention (MATRIX.cpp:196-209): the Z basis = (sin y, 0, // cos y) -> yaw = atan2(z.x, z.z); enemy yaw = that + pi. Scalar spawnYaw = (Scalar)atan2((double)spawnZ.x, (double)spawnZ.z); enemy_origin.angularPosition = EulerAngles(0.0f, spawnYaw + 3.14159265f, 0.0f); } Mech::MakeMessage create_enemy( Mech::MakeMessageID, sizeof(Mech::MakeMessage), host_manager->MakeUniqueEntityID(), (Entity::ClassID)Mech::MechClassID, EntityID::Null, enemy_res->resourceID, Mech::DefaultFlags, enemy_origin, Motion::Identity, Motion::Identity, playerMission->GetBadgeName(), playerMission->GetColorName(), ((BTMission *)playerMission)->GetPatchName()); enemy_res->Unlock(); Mech *enemy = Mech::Make(&create_enemy); if (enemy) { Register_Object(enemy); // Mark it a VALID master so Entity::Dispatch delivers messages // SYNCHRONOUSLY (Receiver::Receive) instead of posting them as deferred // "entity invalid" events that never fire -- otherwise TakeDamage never // lands. (The player gets validated via MakeViewpointEntity's CheckLoad // handshake; a manually-spawned entity must set it itself. Its resources // are already loaded -- it renders + its zones are built.) Also force // pre-run + interest so it ticks like a live entity. enemy->SetValidFlag(); enemy->SetPreRunFlag(); if (enemy->interestCount == 0) enemy->interestCount = 1; // GROUND MODEL (task #15): a MASTER mech needs a CollisionAssistant // for GetCurrentCollisions (the engine iterates it unchecked, // MOVER.cpp:894). The player gets one in MakeViewpointEntity // (btl4app.cpp:591); this dummy is a master too, so start its own // -- without it the authentic ground block skips its collision half. enemy->StartCollisionAssistant(); gEnemyMech = enemy; // the player's targeting step locks onto this DEBUG_STREAM << "[enemy] spawned target mech at (" << enemy_origin.linearPosition.x << ", " << enemy_origin.linearPosition.y << ", " << enemy_origin.linearPosition.z << ")\n" << std::flush; } else { DEBUG_STREAM << "[enemy] Mech::Make returned NULL\n" << std::flush; } } else { DEBUG_STREAM << "[enemy] model resource not found\n" << std::flush; } } Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // InitializePlayerLink // // @004bfee0 Tells our freshly-made vehicle (and its replicants) which player // owns it, by dispatching a PlayerLink message carrying our EntityID. // void BTPlayer::InitializePlayerLink() { Check(this); Check(playerVehicle); PlayerLinkMessage player_link_message( PlayerLinkMessageID, sizeof(PlayerLinkMessage), GetEntityID() ); playerVehicle->Dispatch(&player_link_message); playerVehicle->DispatchToReplicants(&player_link_message); Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // DropZoneReplyMessageHandler // // @004bffd0 The drop zone has answered our spawn request with a location. // // * No vehicle yet -> make the mech there, link it, choose the per-vehicle // simulation Performance from its class, start running. // * Same death -> if the mission is ending, ignore; if we have not yet // acquired this drop zone, fold into the death/reset // path (VehicleDeadMessageHandler, FUN_004c012c). // * Stale message -> ignore. // // Then (re)place the mech at the drop-zone origin. Reconstructed faithfully // from FUN_004bffd0; structural cross-check = Blocker::DropZoneReplyMessageHandler. // void BTPlayer::DropZoneReplyMessageHandler(DropZone__ReplyMessage *message) { Check(this); Check(message); if (getenv("BT_SCORE_LOG")) DEBUG_STREAM << "[score] DropZoneReply: hasVehicle=" << (playerVehicle != 0) << " deathCount=" << deathCount << " msgDeath=" << message->deathCount << " state=" << (int)GetSimulationState() << "\n" << std::flush; // TEST HOOK (BT_SPAWN_XZ="x,z"): force the drop-zone X/Z so two -net instances // land in the same area (map zones are ~random, ~1500u apart -- hard to find // each other). Applied to the MESSAGE so BOTH the CreatePlayerVehicle make // AND the Mech::Reset placement below use it (Reset re-places at // message->dropZoneLocation, which would otherwise undo the override). Y // stays the drop-zone ground height; the ground model snaps it. { const char *xz = getenv("BT_SPAWN_XZ"); if (xz != 0) { float sx = 0.0f, sz = 0.0f; if (sscanf(xz, "%f,%f", &sx, &sz) == 2) { message->dropZoneLocation.linearPosition.x = sx; message->dropZoneLocation.linearPosition.z = sz; } } } if (!playerVehicle) // param_1[0x7f] == 0 { CreatePlayerVehicle(message->dropZoneLocation); // (**vtable+0x40)(origin) InitializePlayerLink(); // FUN_004bfee0 // // Drive the right per-vehicle simulation. A mech runs the combat // PlayerSimulation; a camera-ship observer runs CameraShipSimulation // and never ranks. (classID @ playerVehicle+4.) // if (playerVehicle->GetClassID() == Mech::MechClassID) // 0xbb9 { SetPerformance(&BTPlayer::PlayerSimulation); // perf @00513058 // scoring wave (RANK fix): an active mech combatant is a SCORING player. // Player::DefaultFlags creates every player NonScoring (rank -1 until // activated); CalcRanking only ranks IsScoringPlayer()==true. The camera // ship (below) stays non-scoring; a piloted mech becomes scoring here. SetScoringPlayerFlag(); // clear NonScoringPlayerFlag } else // camera ship (0x45) { SetPerformance((Performance)&BTPlayer::CameraShipSimulation); // perf @00513064 playerRanking = -1; // this+0x1cc } AlwaysExecute(); // param_1[10] &= ~2 (run every frame) deathCount = 0; // param_1[0x80] // (warp fired in the shared placement below -- initial drop-in + respawn) } else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80] { if (GetSimulationState() == MissionEndingState) // state == 4 { Check_Fpu(); return; } // // RESPAWN (reset-based): the mech STAYS ours through death; this reply // (deathCount matches) resets the EXISTING dead mech in place -- fall // through to the placement below (Mech::Reset heals + moves it). Guard // against a duplicate reply / the 2s probe: if the mech is already alive // again, there is nothing to reset. (The old path pulsed // DropZoneAcquiredState and looped through the dead handler to build a // NEW mech -- removed; that state drives our camera/targeting.) // Mech *mech = (Mech *)playerVehicle; if (!(mech != 0 && mech->GetClassID() == Mech::MechClassID && mech->IsMechDestroyed())) { Check_Fpu(); return; } // else: still dead -> fall through to placement (reset + warp) } else // stale / old message { return; } // // Place the mech at the drop-zone origin: heal + move the SAME entity // (Mech::Reset), raise the "translocated" cockpit alarm, and fire the // translocation-sphere warp. Runs for the initial drop-in (fresh mech) and // for a respawn (dead mech healed) alike. // Set_Alarm_Level((char *)this + 0x2c, 2); // FUN_0041bbd8(this+0x2c, 2) if (playerVehicle->GetClassID() == Mech::MechClassID) // 0xbb9 { Mech *mech = (Mech *)playerVehicle; mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74 (heal+move) // WARP (task #52): our OWN respawn = the engine's WaitForReincarnate -> Expand // release (POVTranslocateRenderable): drop the SetIsDead world mask and blast // the eye-centred sphere open to reveal the reborn world. LOCAL player only (a // POV effect); a self-contained render one-shot that does NOT touch // SimulationState (that dial drives the camera/POV + targeting -- pulsing it // regressed all of those; SetIsDead is a separate pure-render flag). if (this == (BTPlayer *)application->GetMissionPlayer()) { extern void BTStartWarpExpandPOV(); BTStartWarpExpandPOV(); } } Check_Fpu(); } //############################################################################# // Construction and Destruction // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BTPlayer (constructor) // // @004c0bc8 // BTPlayer::BTPlayer( BTPlayer::MakeMessage *creation_message, SharedData &shared_data ): Player(creation_message, shared_data) // FUN_0042e328 { /* removed decompiler vtable artifact -- the C++ ctor already sets the real BTPlayer vtable; the original `*(void**)this = &BTPlayerVTable` overwrote it with a zeroed stub -> crash. */ // PTR_FUN_00513300 consoleAttached = 0; // this[0x95] suppressConsole = 0; // this[0x96] currentScore = 0; // this[0x9e] deathPending = 0; // this[0xa4] // // Remember our team name from the creation message. // strcpy(teamName, creation_message->teamName); // this+0x20c <- make+0x50 // // Resolve the BTTeam entity that owns this team name. // teamEntity = 0; // this[0x93] EntityGroup *teams = application->GetEntityManager()->FindGroup("Teams"); // app+0x24, @005132ee if (teams) { ChainIteratorOf 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); } } //############################################################################# // BTPlayerRoleLocksAdvanced -- complete-type bridge (task #12) // // FUN_004ac9c8 [T1]: `return *(*(*(sub+0xD0) + 0x190) + 0x274) == 0` -- the // subsystem's owner Mech -> the owning BTPlayer (mech+0x190, GetPlayerLink) // -> roleClassIndex (+0x274, role resource +0xE4). TRUE = the ROOKIE role // (class 0) LOCKS the advanced cockpit systems (generator routing, coolant // valves); nonzero role classes unlock them. The old reconstruction // mislabeled this fn "Subsystem::IsDamaged" -- with healthy subsystems at // simulationState==1, that stand-in gated the power-routing handlers OFF // permanently. A NULL player (the target dummy / unbound solo mech) would AV // in the binary (every pod mech has a player); the port reads NULL as // UNLOCKED so dev rigs work [T3]. // int BTPlayerRoleLocksAdvanced(void *owner_mech) { if (owner_mech == 0) return 0; BTPlayer *player = MECH_OWNING_PLAYER(owner_mech); if (player == 0) return 0; // dev-permissive [T3] return (player->roleClassIndex == 0) ? 1 : 0; // player+0x274 == 0 }