//===========================================================================// // 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; // // Reconstruction helpers / data referenced by the decomp that have no direct // analog in the surviving WinTesla headers (CROSS-FAMILY / engine gap -- see // report). Declared here so the recovered bodies compile; the linker binds // them to the real engine/BT symbols. // // FUN_0041bbd8 AlarmIndicator::SetLevel (the engine GaugeAlarm exposes no // SetLevel in WinTesla) -- raise an alarm to a given level. // FUN_004b04d8 notify the objective subsystem that its mech was destroyed. // StatusMessagePool the BT status-message MemoryBlock pool (@00512f6c). // BT*VTable the BT vtable symbols installed by ctor/dtor. // SelfDestructName / ResetDelay canned strings / timing constants. // extern void Set_Alarm_Level(void *alarm_indicator, int level); extern void Notify_Objective_Reached(int *objective_subsystem, Mech *mech); extern MemoryBlock *StatusMessagePool; extern void *BTPlayerVTable; extern void *BTStatusMessageVTable; // Wire-format lock: BTPlayer::MakeMessage travels RAW over the network (entity // replication), so the inline string fields must sit at the binary's offsets. static_assert(offsetof(BTPlayer::MakeMessage, teamName) == 0x50, "BTPlayer::MakeMessage::teamName must be at wire offset 0x50"); static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90, "BTPlayer::MakeMessage::roleName must be at wire offset 0x90"); static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0, "BTPlayer::MakeMessage wire size must be 0xD0"); static const char *SelfDestructName = "self destruct"; // &DAT_00524b38 static const Scalar ResetDelay = 1.0f; // re-post delay static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation) // // Mech fields touched by the scoring code but not exposed by the reconstructed // mech.hpp (owned by the mech family). Accessed through the documented byte // offsets. CROSS-FAMILY -- ideally Mech would expose these accessors. // #define MECH_TONNAGE(m) (*(Scalar *)((char *)(m) + 0x4bc)) // +0x4bc #define MECH_DAMAGE_BIAS(m) (*(Scalar *)((char *)(m) + 0x354)) // +0x354 #define MECH_OWNING_PLAYER(m) (*(BTPlayer **)((char *)(m) + 0x190)) // +0x190 //############################################################################# //############################### BTPlayer ############################## //############################################################################# //############################################################################# // Message Support // // MESSAGE_ENTRY names recovered from the .data string block @005130c0. // (ScoreInflicted is reconstructed best-effort -- see header note.) // const Receiver::HandlerEntry BTPlayer::MessageHandlerEntries[] = { MESSAGE_ENTRY(BTPlayer, DropZoneReply), MESSAGE_ENTRY(BTPlayer, VehicleDead), MESSAGE_ENTRY(BTPlayer, Score), MESSAGE_ENTRY(BTPlayer, ScoreInflicted), MESSAGE_ENTRY(BTPlayer, ScoreUpdate), MESSAGE_ENTRY(BTPlayer, MissionStarting), MESSAGE_ENTRY(BTPlayer, MissionEnding) }; Receiver::MessageHandlerSet& BTPlayer::GetMessageHandlers() { static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(BTPlayer::MessageHandlerEntries), BTPlayer::MessageHandlerEntries, Player::GetMessageHandlers() ); return messageHandlers; } //############################################################################# // Shared Data Support // Derivation* BTPlayer::GetClassDerivations() { static Derivation classDerivations( Player::GetClassDerivations(), "BTPlayer" // @005130cb ); return &classDerivations; } BTPlayer::SharedData BTPlayer::DefaultData( BTPlayer::GetClassDerivations(), BTPlayer::GetMessageHandlers(), Player::GetAttributeIndex(), BTPlayer::StateCount, (BTPlayer::MakeHandler)BTPlayer::Make ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // VehicleDeadMessageHandler // // @004c012c (file=? in the index, but operates on the BTPlayer object and // is reached through the "VehicleDead" handler slot). Records the death, // stamps the outgoing message with our death count, debits one "return from // death" against the role, raises the console alarm, remembers the killer's // name and re-posts the message a short time later so we can hunt for a new // drop zone. // void BTPlayer::VehicleDeadMessageHandler(VehicleDeadMessage *message) { Check(this); Check(message); // // One more death; tell the message (so replicants stay in sync) and // debit a life against the scoring role. // ++deathCount; // this+0x200 message->deathCount = deathCount; // message+0x38 // TODO(bring-up): scenarioRole is the scoring role looked up from the mission's // role registry (btplayer.cpp:815, currently deferred), so it is NULL for the // minimal TEST.EGG dev mission. Only debit a life when a role is present. if (scenarioRole != 0) // this[0x208] { scenarioRole->SetReturnFromDeath( // --*(this[0x208]+0x28) scenarioRole->GetReturnFromDeath() - 1); } simulationFlags |= 0x1; // request a forced update Set_Alarm_Level((char *)this + 0x2c, 1); // FUN_0041bbd8(this+0x2c, 1) // // Remember who killed us (for the pilot HUD). If the kill came from a // real attacker the message carries its name (+0x1c); otherwise fall // back to the canned "self destruct" string (@00524b38). // if (showDamageInflicted == 0) // this+0x264 { // The BT VehicleDead message carried the killer's name at +0x1c; the // engine base Player__VehicleDeadMessage has no such field, so it is // read through the documented byte offset. BEST-EFFORT (message gap). killerName = *(const char *const *)((const char *)message + 0x1c); } else { killerName = SelfDestructName; // &DAT_00524b38 } // // Re-post the dead message to ourselves after a short delay. // Time when = Now(); // FUN_00414b60 + FUN_004dcd94 when += ResetDelay; application->Post(HighEventPriority, this, message, when); // app+0x60 deathPending = 0; // this+0x290 suppressConsole = 0; // this+0x258 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ScoreInflictedMessageHandler // // @004c0200 Awards points for damage *this* player inflicted on another // mech. The score is the tonnage-scaled inflicted value; if the target mech // turns out to be our own vehicle the award is negated (you don't score for // shooting yourself). // void BTPlayer::ScoreInflictedMessageHandler(ScoreMessage *message) { Check(this); Check(message); // // This handler is only ever fed DamageInflictedScore (type 0) messages. // if (message->scoreType != BTPlayer::ScoreMessage::DamageInflictedScore) { Verify( False, "BTPlayer::ScoreInflictedMessageHandler should only be " "given DamageInflictedScoreMessages!", // @00513110 "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051316a 0x18b ); } // // Resolve the mech that took the damage and our own vehicle. // Mech *target_mech = (Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID); // app+0x2c, msg+0x34 Mech *our_mech = (Mech *)playerVehicle; // this+0x1fc Scalar award = CalcInflictedScore(message->damageAmount, target_mech, 0.0f); if (target_mech == our_mech) { award = -award; } // // Scale by the tonnage ratio (target / self) and accumulate. (tonnage is // a Mech field at +0x4bc; not exposed by the reconstructed mech.hpp, so it // is read through the documented offset. CROSS-FAMILY -- see report.) // currentScore += (MECH_TONNAGE(target_mech) / MECH_TONNAGE(our_mech)) * award; // (+0x4bc / +0x4bc), this+0x278 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ScoreUpdateMessageHandler // // @004c02a8 A network score-sync message. Only the master instance owns the // authoritative score, so this must never run on a replicant; it then defers // to the base score handler which folds the carried scoreAward in. // void BTPlayer::ScoreUpdateMessageHandler(ScoreMessage *message) { if ((simulationFlags & 0xc) == 4) // this+0x28 -- replicant copy { Verify( False, "BTPlayer::ScoreUpdateMessageHandler should only " "run on Master instances!", // @00513186 "d:\\tesla_bt\\bt\\btplayer.cpp", // @005131cf 0x1d5 ); } Player::ScoreMessageHandler(message); // FUN_0042da20 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ScoreMessageHandler // // @004c02e4 The main combat scoring handler. Runs only on the master, and // only while not mission-ending. Handles damage received (type 1) and kills // (type 2); the computed award is written back into the message before the // base Player::ScoreMessageHandler applies it to currentScore. // void BTPlayer::ScoreMessageHandler(ScoreMessage *message) { if ((simulationFlags & 0xc) == 4) // replicant -- must not score { Verify( False, "BTPlayer::ScoreMessageHandler should only run on " "Master instances!", // @005131eb "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051322e 0x1e9 ); } // // Ignore everything once the mission is ending (state 4). // if (GetSimulationState() == MissionEndingState) // this+0x40 == 4 { return; } Mech *sender_mech = (Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID); // msg+0x34 Mech *our_mech = (Mech *)playerVehicle; // this+0x1fc Scalar award = 0.0f; switch (message->scoreType) // msg+0x20 { case BTPlayer::ScoreMessage::DamageInflictedScore: // 0 // // Inflicted-damage messages belong to ScoreInflictedMessageHandler. // Verify( False, "BTPlayer::ScoreMessageHandler should not be " "given DamageInflictedScoreMessages!", // @0051324a "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051329a 0x296 ); break; case BTPlayer::ScoreMessage::DamageReceivedScore: // 1 { // // Points lost for damage we took (unless we hit ourselves). // if (sender_mech != our_mech) { 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 { // // 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 to the attacking player. // ++killCount; // this+0x27c ++MECH_OWNING_PLAYER(sender_mech)->killCount; // *(sender+0x190)+0x27c } // // Pop a "destroyed by" status message onto the pilot HUD. // StatusMessage *status = (StatusMessage *)StatusMessagePool->New(); // FUN_00402f74(0x512f6c) if (status) { new (status) Player__StatusMessage( MECH_OWNING_PLAYER(sender_mech), // *(sender+0x190) 0, STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f ); // install the BT status-message vtable + clear its flag word // (Player__StatusMessage exposes neither in WinTesla -- raw). /* removed decompiler vtable artifact -- C++ ctor sets the real vtable (was: *(void**)status = &BTStatusMessageVTable). */ *(int *)((char *)status + 0x18) = 0; // status[6] = 0 } AddStatusMessage(status); // FUN_0042e580 // // If we just killed our designated objective mech, notify it. // (playerVehicle's objective subsystem link is at +0x128.) // if (sender_mech == objectiveMech) // this+0x284 { Notify_Objective_Reached( // FUN_004b04d8 *(int **)((char *)playerVehicle + 0x128), // *(this[0x1fc]+0x128) sender_mech ); } } break; } // // Hand the (now-scored) message to the base class to apply the award. // message->scoreAward = award; // msg+0x1c Player::ScoreMessageHandler(message); // FUN_0042da20 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CalcInflictedScore // // @004c052c Shared damage->points routine. Same-team hits (in a non-free- // for-all game) cost a fixed friendly-fire penalty; otherwise the points // scale with the target's damage-bias factor. The result is multiplied by // the role's damageInflictedModifier. // Scalar BTPlayer::CalcInflictedScore( const Scalar &damage_amount, Mech *other_mech, Scalar bias ) { 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, ...) Dispatch(&score_message); // (**(*this+0xc))(this, &msg) currentScore = 0; // this[0x9e] = 0 } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CreatePlayerVehicle // // @004bfcac (BTPlayer vtable slot +0x40). Makes the player's BattleMech at // the supplied drop-zone origin. Lets the base build any MUNGA-level vehicle // first (camera-ship observers); if no vehicle resulted, instantiates a Mech // from the mission's game-model resource and links it as our viewpoint. // // Structural model: RPPLAYER.cpp RPPlayer::CreatePlayerVehicle (which makes a // VTV the same way). BT substitutes Mech for VTV and carries the three mech // resource-name strings (badge / colour / patch) into the make message. // void BTPlayer::CreatePlayerVehicle(Origin mech_location) { Check(this); Check(application); ResourceFile *resources = application->GetResourceFile(); Check(resources); Check_Pointer(playerMission->GetGameModel()); HostManager *host_manager = application->GetHostManager(); Check(host_manager); // // Make any MUNGA-level vehicle (camera ship for an observer player). // Player::CreatePlayerVehicle(mech_location); // // If the player still has no vehicle, build him a mech and link to it. // if (!playerVehicle) { ResourceDescription *mech_res = resources->FindResourceDescription( playerMission->GetGameModel(), ResourceDescription::ModelListResourceType ); Check(mech_res); mech_res->Lock(); Mech::MakeMessage create_player( Mech::MakeMessageID, sizeof(Mech::MakeMessage), EntityID(host_manager->GetLocalHostID()), (Entity::ClassID)Mech::MechClassID, EntityID::Null, mech_res->resourceID, Mech::DefaultFlags, mech_location, Motion::Identity, Motion::Identity, playerMission->GetBadgeName(), // vehicle badge (+0x7c) playerMission->GetColorName(), // vehicle colour (+0x90) ((BTMission *)playerMission)->GetPatchName() // vehicle patch (+0xa4) ); mech_res->Unlock(); playerVehicle = application->MakeAndLinkViewpointEntity(&create_player); Register_Object(playerVehicle); } // --- BRING-UP: spawn a stationary target/enemy mech (gameplay scaffolding) --- // The mission system is networked (one pilot per host) and BT's bot/AI code // is absent, so a solo mission has nothing to fight. When BT_SPAWN_ENEMY is // set we drop a SECOND mech into the world via the SAME factory the registry // uses for remote/AI entities (Mech::Make on a MakeMessage) -- a non-viewpoint, // self-registering, rendering, ticking entity placed ahead of the player. It // has no AI yet (mech4.cpp gates the player-drive path to the viewpoint mech), // so it stands as a target dummy. First step toward actual combat. if (getenv("BT_SPAWN_ENEMY")) { ResourceDescription *enemy_res = resources->FindResourceDescription( playerMission->GetGameModel(), ResourceDescription::ModelListResourceType); if (enemy_res) { enemy_res->Lock(); // Place it ahead of the player's drop ALONG THE SPAWN FACING (the mech // faces local -Z; rotate that axis into world by the spawn orientation). // The old fixed "z -= 120" assumed the bring-up drive's forced heading=0; // the real-controls drive keeps the authentic spawn orientation, so the // dummy must follow the actual facing or the auto-walk leaves it behind. Origin enemy_origin = mech_location; { AffineMatrix facing; facing.BuildIdentity(); facing = mech_location.angularPosition; // rotation from the spawn pose UnitVector spawnZ; facing.GetFromAxis(Z_Axis, &spawnZ); // local Z basis in world enemy_origin.linearPosition.x -= spawnZ.x * 120.0f; // forward = -Z enemy_origin.linearPosition.y -= spawnZ.y * 120.0f; enemy_origin.linearPosition.z -= spawnZ.z * 120.0f; } 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 (!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 } else // camera ship (0x45) { SetPerformance((Performance)&BTPlayer::CameraShipSimulation); // perf @00513064 playerRanking = -1; // this+0x1cc } AlwaysExecute(); // param_1[10] &= ~2 (run every frame) deathCount = 0; // param_1[0x80] } else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80] { if (GetSimulationState() == MissionEndingState) // state == 4 { Check_Fpu(); return; } if (GetSimulationState() != DropZoneAcquiredState) // state != 1 { // // Reset-after-death: BT routes this through the death handler // (FUN_004bffd0 calls FUN_004c012c here). // VehicleDeadMessageHandler((VehicleDeadMessage *)message); return; } } else // stale / old message { return; } // // Place (or re-place) the mech at the drop-zone origin and raise the // "translocated" cockpit alarm. // Set_Alarm_Level((char *)this + 0x2c, 2); // FUN_0041bbd8(this+0x2c, 2) if (playerVehicle->GetClassID() == Mech::MechClassID) // 0xbb9 { Mech *mech = (Mech *)playerVehicle; mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74 } Check_Fpu(); } //############################################################################# // Construction and Destruction // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BTPlayer (constructor) // // @004c0bc8 // BTPlayer::BTPlayer( BTPlayer::MakeMessage *creation_message, SharedData &shared_data ): Player(creation_message, shared_data) // FUN_0042e328 { /* removed decompiler vtable artifact -- the C++ ctor already sets the real BTPlayer vtable; the original `*(void**)this = &BTPlayerVTable` overwrote it with a zeroed stub -> crash. */ // PTR_FUN_00513300 consoleAttached = 0; // this[0x95] suppressConsole = 0; // this[0x96] currentScore = 0; // this[0x9e] deathPending = 0; // this[0xa4] // // Remember our team name from the creation message. // strcpy(teamName, creation_message->teamName); // this+0x20c <- make+0x50 // // Resolve the BTTeam entity that owns this team name. // teamEntity = 0; // this[0x93] EntityGroup *teams = application->GetEntityManager()->FindGroup("Teams"); // app+0x24, @005132ee if (teams) { ChainIteratorOf 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) }