//===========================================================================// // File: btplayer.cpp // // Project: BattleTech // // Contents: Implementation details for the BT player // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(BTPLAYER_HPP) # include #endif #if !defined(BTTEAM_HPP) # include #endif #if !defined(BTMSSN_HPP) # include #endif #if !defined(NTTMGR_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(DROPZONE_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(RESOURCE_HPP) # include #endif #if !defined(HOSTMGR_HPP) # include #endif #if !defined(MISSION_HPP) # include #endif #if !defined(APPMSG_HPP) # include #endif #if !defined(SCNROLE_HPP) # include #endif // //############################################################################# // Shared data support //############################################################################# // Derivation BTPlayer::ClassDerivations( Player::ClassDerivations, "BTPlayer" ); 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) }; BTPlayer::MessageHandlerSet BTPlayer::MessageHandlers( ELEMENTS(BTPlayer::MessageHandlerEntries), BTPlayer::MessageHandlerEntries, Player::MessageHandlers ); BTPlayer::SharedData BTPlayer::DefaultData( BTPlayer::ClassDerivations, BTPlayer::MessageHandlers, Player::AttributeIndex, 1, (Entity::MakeHandler)BTPlayer::Make ); // //############################################################################# // Constructor // // Copies the team name out of the creation message, resolves the owning // BTTeam, caches the mission, and derives the game-mode display / heat-model // flags from the mission's experience level and advanced-damage flag. //############################################################################# // BTPlayer::BTPlayer( MakeMessage *creation_message, SharedData &shared_data ): Player(creation_message, shared_data) { consoleAttached = False; suppressConsole = False; currentScore = 0; deathPending = False; // // Remember our team name from the creation message. // strcpy(teamName, creation_message->teamName); // // Resolve the BTTeam entity that owns this team name. // teamEntity = NULL; EntityGroup *teams = application->GetEntityManager()->FindGroup("Teams"); if (teams) { ChainIteratorOf iterator(teams->groupMembers); BTTeam *team; while ((team = (BTTeam *)iterator.ReadAndNext()) != NULL) { if (strcmp(team->teamName, teamName) == 0) { teamEntity = team; break; } } } freeForAll = True; // // Cache the mission (experience level / advanced-damage source). // BTMission *bt_mission = Cast_Object(BTMission *, application->GetCurrentMission()); btMission = bt_mission; if (GetInstance() == ReplicantInstance) { // // Replicant: driven by console-update messages. // SetPerformance(&BTPlayer::PlayerSimulation); } else { // // Master: derive the game-mode flags from the mission experience level // (the egg's per-pilot "experience" token). The binary rows for // (simLive, heatModelOn, levelFlag26c, levelFlag270): // nov 0,0,0,0 / std 1,0,1,1 / vet 1,1,1,1 / exp 1,1,0,1. // Standard mode is LIVE but has no heat model -- authentic. // Check(bt_mission); experienceLevel = bt_mission->ExperienceLevel(); switch (experienceLevel) { case BTMission::NoviceMode: simLive = 0; heatModelOn = 0; levelFlag26c = 0; levelFlag270 = 0; break; case BTMission::StandardMode: simLive = 1; heatModelOn = 0; levelFlag26c = 1; levelFlag270 = 1; break; case BTMission::VeteranMode: simLive = 1; heatModelOn = 1; levelFlag26c = 1; levelFlag270 = 1; break; case BTMission::ExpertMode: simLive = 1; heatModelOn = 1; levelFlag26c = 0; levelFlag270 = 1; break; } advancedDamageOn = bt_mission->AdvancedDamageOn(); advancedDamageOn2 = advancedDamageOn; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[exp] master player experience=" << experienceLevel << " simLive=" << (int)simLive << " heatModelOn=" << (int)heatModelOn << " advDamage=" << (int)advancedDamageOn << endl << flush; } } killCount = 0; padScore = 0; objectiveMech = NULL; lastPerformance = lastConsoleUpdate = lastUpdate; // // Bind the SCORING ROLE (the binary ctor tail @004c0bc8: look the role // up in the mission's role dictionary by the MakeMessage roleName and // stash it at this+0x208). BTL4Mission::SetPlayerData populated the // dictionary before any player exists; the role carries the score // modifiers AND the lives counter (returnFromDeath). A missing page // leaves the base ctor's NULL (dev shape: infinite lives, raw-damage // scores). // if (btMission != NULL) { scenarioRole = Cast_Object(BTMission *, btMission)->GetScenarioRole( creation_message->roleName); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[role] '" << creation_message->roleName << "'"; if (scenarioRole != NULL) { DEBUG_STREAM << " lives=" << scenarioRole->GetReturnFromDeath() << " killBonus=" << scenarioRole->GetKillBonus() << " dmgMod=" << scenarioRole->GetDamageInflictedModifier(); } else { DEBUG_STREAM << " NOT FOUND (role-less: infinite lives)"; } DEBUG_STREAM << endl << flush; } } Check_Fpu(); } // //############################################################################# //############################################################################# // BTPlayer::~BTPlayer() { Check(this); Check_Fpu(); } // //############################################################################# // Make //############################################################################# // BTPlayer* BTPlayer::Make(MakeMessage *creation_message) { return new BTPlayer(creation_message); } // //############################################################################# //############################################################################# // Logical BTPlayer::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // GetExperienceLevel //############################################################################# // Enumeration BTPlayer::GetExperienceLevel() { Check(this); Check(btMission); return Cast_Object(BTMission *, btMission)->ExperienceLevel(); } // //############################################################################# // DropZoneReplyMessageHandler The spawn / respawn handshake: when the drop // zone replies with our spawn location we create (or, on respawn, reset) the // player's vehicle there and bind it to us. // // The mech placement / respawn-reset tail depends on the Mech subsystem // (Mech::MechClassID / Mech::Reset), which is not yet reconstructed; the // dispatch skeleton below halts at CreatePlayerVehicle -> Mech::Make on the // first (fresh-drop) reply. See BTPLAYER.NOTES.md. //############################################################################# // void BTPlayer::DropZoneReplyMessageHandler(DropZone__ReplyMessage *message) { Check(this); Check(message); if (!playerVehicle) { CreatePlayerVehicle(message->dropZoneLocation); InitializePlayerLink(); // // Choose the per-vehicle Performance from the vehicle's class. A piloted // mech runs the combat PlayerSimulation and is a scoring player; a // camera-ship observer runs CameraShipSimulation and never ranks. This // is load-bearing for mission flow: switching off HuntForDropZone onto // PlayerSimulation is what starts the launch-fade countdown that advances // the application to RunningMission (so the world begins executing). // if (playerVehicle->GetClassID() == (Entity::ClassID)MechClassID) { SetPerformance(&BTPlayer::PlayerSimulation); SetScoringPlayerFlag(); } else { SetPerformance((Performance)&Player::CameraShipSimulation); playerRanking = -1; } // // (Mech placement -- Mech::Reset heal+move to the drop-zone origin, the // translocation cockpit alarm, and the warp-sphere reveal -- is deferred // with the mech per-frame simulation; the fresh mech sits at its ctor // origin for now.) // AlwaysExecute(); deathCount = 0; // // DEV harness (BT_SPAWN_ENEMY=1): spawn a second, unowned mech 150 u // down +Z from the drop zone through the SAME make machinery (its // Entity ctor self-registers with the host manager; no player link, so // its gates read dev-permissive) and hand it to our mech as the // TARGET. The authentic target writer is the reticle pick (the // render/mech4 wave); this gives the fire/damage path a live opponent // headlessly. // if (getenv("BT_SPAWN_ENEMY") && playerVehicle->GetClassID() == (Entity::ClassID)MechClassID) { HostManager *host_manager = application->GetHostManager(); Check(host_manager); ResourceDescription *mech_res = application->GetResourceFile()->FindResourceDescription( playerMission->GetGameModel(), ResourceDescription::ModelListResourceType ); Check(mech_res); mech_res->Lock(); Origin enemy_location = message->dropZoneLocation; enemy_location.linearPosition.z += 150.0f; Mech::MakeMessage create_enemy( Mech::MakeMessageID, sizeof(Mech::MakeMessage), EntityID(host_manager->GetLocalHostID()), (Entity::ClassID)MechClassID, EntityID::Null, mech_res->resourceID, Mech::DefaultFlags, enemy_location, Motion::Identity, Motion::Identity, playerMission->GetBadgeName(), playerMission->GetColorName(), ((BTMission *)playerMission)->GetPatchName() ); mech_res->Unlock(); Mech *enemy = Mech::Make(&create_enemy); Register_Object(enemy); ((Mech *)playerVehicle)->SetTargetEntity(enemy); // // Mutual: the enemy targets us back -- with BT_FORCE_FIRE its // weapons return fire (its gates read dev-permissive, no player // link), giving the damage cascade a live INCOMING side too. // enemy->SetTargetEntity(playerVehicle); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[enemy] spawned " << (void *)enemy << " at z+150, zones=" << enemy->damageZoneCount << " -> target locked" << endl << flush; } } } else if (deathCount == message->deathCount) { if (GetSimulationState() == MissionEndingState) { Check_Fpu(); return; } // // RESPAWN: reset the existing dead mech in place (the authentic // reset-based respawn -- the same entity heals and teleports; the // old sever-and-recreate model was the 1995 port's "two mechs" // glitch family). A duplicate reply / 2s probe on a mech that is // already alive again has nothing to do. The level-2 translocation // cockpit alarm + warp reveal join the cockpit/render waves. // if (playerVehicle != NULL && playerVehicle->GetClassID() == (Entity::ClassID)MechClassID && ((Mech *)playerVehicle)->IsMechDestroyed()) { ((Mech *)playerVehicle)->Reset(message->dropZoneLocation, True); deathPending = False; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[respawn] death #" << deathCount << " -- mech reset at the drop zone" << endl << flush; } } } Check_Fpu(); } // //############################################################################# // CreatePlayerVehicle Build the player's mech from the mission game-model // resource and bind it as the viewpoint entity. (An observer player gets a // MUNGA camera ship from the base Player::CreatePlayerVehicle instead.) // // Constructs the Mech::MakeMessage and hands it to MakeAndLinkViewpointEntity, // which routes through the registry to Mech::Make -> the Mech ctor (the mech // subsystem, not yet reconstructed -- current boot frontier). //############################################################################# // 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 (a 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)MechClassID, EntityID::Null, mech_res->resourceID, Mech::DefaultFlags, mech_location, Motion::Identity, Motion::Identity, playerMission->GetBadgeName(), playerMission->GetColorName(), ((BTMission *)playerMission)->GetPatchName() ); mech_res->Unlock(); playerVehicle = application->MakeAndLinkViewpointEntity(&create_player); Register_Object(playerVehicle); } } // //############################################################################# // InitializePlayerLink Tell our freshly-made vehicle (and its network // replicants) which player owns it, by dispatching a PlayerLinkMessage that // carries our own EntityID. Identical to the engine's own // CameraDirector::CreateCameraShip tail (DIRECTOR.CPP): BTPlayer, Player and // CameraDirector all derive from Entity, so PlayerLinkMessage / // PlayerLinkMessageID / GetEntityID resolve through the Entity base. //############################################################################# // 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); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[link] InitializePlayerLink -> vehicle playerLink=" << (void *)playerVehicle->GetPlayerLink() << endl << flush; } Check_Fpu(); } // //############################################################################# // Scoring / death handlers -- exercised in combat, not during bring-up boot. //############################################################################# // // //############################################################################# // VehicleDeadMessageHandler The BT death / respawn cycle. Three message // flavours arrive here, keyed on message->deathCount: // -1 the immediate death notification dispatched by the mech's death // transition (mech4.cpp) -- the full cycle (scoring-role life debit, // +5s respawn re-post, warp collapse) is downstream of the per-frame // mech simulation + scoring roles and is staged for now; // >= 0 our own 5s respawn re-post, and // -2 the engine's 2s "did the drop-zone reply arrive?" probe that // Player::HuntForDropZone posts during a normal spawn. // For everything but -1 the authentic engine base handler does exactly the // right thing: it re-hunts a drop zone only when this death is still current // AND one has not been acquired, so a freshly-spawned player (bring-up boot: // the -2 probe lands after DropZoneReply already made the mech) falls straight // through. So delegate the non-death flavours to the base. //############################################################################# // void BTPlayer::VehicleDeadMessageHandler(VehicleDeadMessage *message) { Check(this); Check(message); if (GetSimulationState() == MissionEndingState) { Check_Fpu(); return; } if (message->deathCount != -1) { // // The +5s re-post / the 2s probes: if the mech LIVES again the drop // zone was acquired and the cycle is moot (1995 semantics: "if the // player has a vehicle again ... moot"; reset-based, so the gate is // the mech's own death latch). // if (playerVehicle != NULL && playerVehicle->GetClassID() == (Entity::ClassID)MechClassID && !((Mech *)playerVehicle)->IsMechDestroyed()) { deathPending = False; Check_Fpu(); return; } Player::VehicleDeadMessageHandler(message); Check_Fpu(); return; } // // The IMMEDIATE death notification (deathCount == -1, dispatched by the // mech's once-per-death transition). Binary @004c05c4: // dedup on deathPending (one death, one cycle), bump + stamp deathCount // (the base handler's identity gate), debit a life from the scoring // role, and post the +5s respawn re-post (RespawnDelay @004c0830) -- // which falls into the base handler's drop-zone hunt. Out of lives -> // the mission-review post (deferred with the scoring wave). The warp // collapse + cockpit death alarm join the render/cockpit waves. // if (deathPending) { Check_Fpu(); return; } deathPending = True; ++deathCount; message->deathCount = deathCount; if (scenarioRole != NULL) { scenarioRole->SetReturnFromDeath( scenarioRole->GetReturnFromDeath() - 1); } if (scenarioRole == NULL || scenarioRole->GetReturnFromDeath() > 0) { Time when = Now(); when += 5.0f; application->Post(HighEventPriority, this, message, when); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[vdead] death #" << deathCount << " lives=" << ((scenarioRole != NULL) ? scenarioRole->GetReturnFromDeath() : -1) << " -- respawn posted +5s" << endl << flush; } } else { // // OUT OF LIVES. The binary posts an id-0x18 message at +10s (the // mission-review kick; its exact receiver sits in the one decomp // gap 4c05c4..4c083c). STAGED stand-in with the engine's own // end-of-mission entry: StopMissionMessage at +10s -- the full // authentic cascade runs from there (EndingMission -> the player's // 3s fade -> the second Stop -> Application::Stop -> RunMissions // exits -> GAME-RC). The review/playback launch joins the // mission-review wave. // Application::StopMissionMessage stop_mission(NullExitCodeID); Time when = Now(); when += 10.0f; application->Post(HighEventPriority, application, &stop_mission, when); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[vdead] death #" << deathCount << " OUT OF LIVES -- mission end posted +10s" << endl << flush; } } } void BTPlayer::ScoreMessageHandler(ScoreMessage *message) { Check(this); Check(message); // // Binary @004c02e4. Master-only; everything is ignored once the // mission is ending. Type 0 has its own handler; type 1 = points LOST // for damage taken; type 2 = the kill credit. The tonnage ratio // (mech+0x4bc) and mech damage bias (+0x354) are STAGED at 1.0/0.0 // (members not reconstructed): awards degrade to the role-scaled // damage figures, raw damage with no role (the dev shape). // if (GetSimulationState() == MissionEndingState) { return; } HostManager *host_manager = application->GetHostManager(); Entity *sender_mech = (Entity *)host_manager->GetEntityPointer(message->senderMechID); switch (message->scoreType) { case DamageReceivedScore: if (sender_mech != NULL && sender_mech != playerVehicle && scenarioRole != NULL) { message->scoreAward = scenarioRole->CalcDamageReceivedScore(message->damageAmount); // (the console VTV-damaged feed joins the console wave) Player::ScoreMessageHandler(message); } break; case KillScore: { Scalar award = CalcInflictedScore( message->damageAmount, sender_mech, message->scoreAward); if (sender_mech != NULL && sender_mech == playerVehicle) { // // Suicide: no credit, no kill count. // award = -award; } else { ++killCount; // (the victim-side status ticker joins the console wave) } message->scoreAward = award; Player::ScoreMessageHandler(message); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[score] KILL award=" << award << " kills=" << killCount << " score=" << currentScore << endl << flush; } } break; default: // // Type 0 belongs to the ScoreInflicted handler (the binary asserts // here); drop it quietly in the opt shape. // break; } Check_Fpu(); } void BTPlayer::ScoreInflictedMessageHandler(ScoreMessage *message) { Check(this); Check(message); // // Binary @004c0200 (type 0 only): points EARNED for damage delivered. // Accumulates DIRECTLY into currentScore (not via the base handler); // negated for self-hits. Tonnage ratio staged 1.0. // if (GetSimulationState() == MissionEndingState) { return; } HostManager *host_manager = application->GetHostManager(); Entity *target_mech = (Entity *)host_manager->GetEntityPointer(message->senderMechID); Scalar award = CalcInflictedScore(message->damageAmount, target_mech, 0.0f); if (target_mech != NULL && target_mech == playerVehicle) { award = -award; } currentScore += award; Check_Fpu(); } void BTPlayer::ScoreUpdateMessageHandler(ScoreMessage *message) { Check(this); Check(message); // // Binary @004c02a8: the network score resync -- fold the carried award // straight through the base handler. // Player::ScoreMessageHandler(message); Check_Fpu(); } // //############################################################################# // CalcInflictedScore (binary @004c052c) -- the shared damage -> points // converter. factor = -friendlyFirePenalty on a team hit, else // (mechDamageBias x roleDamageBias + 1); STAGED: mech damage bias (+0x354) // unreconstructed = 0, so factor degrades to 1 (or the friendly penalty). //############################################################################# // Scalar BTPlayer::CalcInflictedScore( Scalar damage_amount, Entity * /*other_mech -- the team / bias source, staged*/, Scalar bias ) { Check(this); if (scenarioRole == NULL) { return damage_amount + bias; } return (damage_amount + bias) * scenarioRole->GetDamageInflictedModifier(); } // //############################################################################# // PlayerSimulation -- the master player's per-frame Performance (installed by // DropZoneReply once the mech exists, and by the replicant ctor). Chains the // authentic engine base Player::PlayerSimulation, which is what actually drives // mission flow: CalcRanking + ManageApplicationStatus (counts down the launch // fade and, when it expires, dispatches the second RunMissionMessage that // advances LaunchingMission -> RunningMission so the world starts executing) + // vehicle-position copy + status-message service. // // The BT-specific tail is the periodic console SCORE-delta flush // (ConsolePlayerVTVScoreUpdateMessage over the network to the operator // console). It is a no-op without a live console host and needs the networked // console-message type; it is deferred to the scoring wave. //############################################################################# // void BTPlayer::PlayerSimulation(Scalar time_slice) { Check(this); Player::PlayerSimulation(time_slice); Check_Fpu(); }