From 623c872c92fe0649e970790c16341522ec309400 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 24 Jul 2026 16:35:05 -0500 Subject: [PATCH] BT410 Phase 5.3.24: the death -> respawn cycle -- mechs DIE and COME BACK The full circuit, workflow-researched (5 parallel dossiers over BT411 + the engine) and adversarially reviewed (3 lenses) before landing: Mech side: the once-per-death transition in Simulate (freeze, motion kill, DeathShutdown sweep, MASTER-only VehicleDead(-1) to the player link); Mech::Reset @0049fb74 (reposition + heal every hull zone + the roster DeathReset sweep + PreRun -- the reset-based respawn that REUSES the entity); dead-owner terms in both weapon hard gates (wrecks fall silent). Player side: the VehicleDead(-1) branch (deathPending dedup, deathCount bump+stamp, scenario-role life debit, +5s re-post -> the engine drop-zone hunt) and the DropZoneReply respawn branch (reset the dead mech at the replied drop zone; probes on a live mech are moot). Subsystem side: the engine base virtual DeathReset implemented across the family -- MechSubsystem (zone heal + DestroyedState clear), MechWeapon (full powered/thermal restore + fresh Loading cycle), AmmoBin (restock from the ctor-cached count), HeatSink/HeatWatcher/PowerWatcher/Generator/Sensor/ MissileLauncher. Review catches fixed before commit: AmmoBin restocked through the FREED padBuffer pointer (use-after-free -> initialAmmoCount; MechSubsystem:: resource is now documented as never-deref-post-ctor); died-hot weapons respawned at FailureHeat (now full thermal re-init); Generator tap counts desynced across reset (preserved); Sensor skipped the base heal; PowerWatcher inherited a statically-bound reset; cooling toggle + connect mode restored. Verified: kill -> wreck (0 shots while dead) -> +5s -> drop-zone hunt -> HEALED + placed at a real drop zone -> 134 shots after respawn incl. 12 SRM (restock proof); unowned enemy wreck settles silently; fight/smoke/novice regressions green, zero faults. Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/AMMOBIN.CPP | 186 +- restoration/source410/BT/AMMOBIN.HPP | 230 +- restoration/source410/BT/BTPLAYER.CPP | 1210 ++++++----- restoration/source410/BT/BTPLAYER.NOTES.md | 14 + restoration/source410/BT/EMITTER.CPP | 3 +- restoration/source410/BT/HEAT.CPP | 2198 ++++++++++---------- restoration/source410/BT/HEAT.HPP | 1003 ++++----- restoration/source410/BT/MECH.CPP | 141 ++ restoration/source410/BT/MECH.HPP | 13 +- restoration/source410/BT/MECH.NOTES.md | 34 + restoration/source410/BT/MECHSUB.CPP | 648 +++--- restoration/source410/BT/MECHSUB.HPP | 394 ++-- restoration/source410/BT/MECHWEAP.CPP | 28 + restoration/source410/BT/MECHWEAP.HPP | 534 ++--- restoration/source410/BT/MISLANCH.CPP | 905 ++++---- restoration/source410/BT/POWERSUB.CPP | 1512 +++++++------- restoration/source410/BT/POWERSUB.HPP | 831 ++++---- restoration/source410/BT/PROJWEAP.CPP | 877 ++++---- restoration/source410/BT/SENSOR.CPP | 459 ++-- 19 files changed, 5857 insertions(+), 5363 deletions(-) diff --git a/restoration/source410/BT/AMMOBIN.CPP b/restoration/source410/BT/AMMOBIN.CPP index 4d11c1df..187b1b02 100644 --- a/restoration/source410/BT/AMMOBIN.CPP +++ b/restoration/source410/BT/AMMOBIN.CPP @@ -1,82 +1,104 @@ -//===========================================================================// -// File: ammobin.cpp // -// Project: BattleTech Brick: Mech weapons // -// Contents: AmmoBin -- ammunition store with cook-off heat // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(AMMOBIN_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -Derivation - AmmoBin::ClassDerivations( - HeatWatcher::ClassDerivations, - "AmmoBin" - ); - -AmmoBin::SharedData - AmmoBin::DefaultData( - AmmoBin::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -AmmoBin::AmmoBin( - Mech *owner, - int subsystem_ID, - SubsystemResource *resource, - SharedData &shared_data -): - HeatWatcher(owner, subsystem_ID, resource, shared_data), - ammoAlarm(6) -{ - Check(owner); - Check_Pointer(resource); - - ammoCount = resource->ammoCount; - initialAmmoCount = resource->ammoCount; - feedRate = resource->feedRate; - feedTimer = 0.0f; - ammoClassID = resource->ammoClassID; - ammoModelFile = resource->ammoModelFile; - explosionModelFile = resource->explosionModelFile; - heatPerRound = resource->heatPerRound; - cookOffArmed = 0; - cookOffTime = 0; - reserved = 0; - - for (int i = 0; i < 12; ++i) - { - cookOffState[i] = 0; - } - - Check_Fpu(); -} - -AmmoBin::~AmmoBin() -{ -} - -Logical - AmmoBin::TestClass(Mech &) -{ - return True; -} - -Logical - AmmoBin::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} +//===========================================================================// +// File: ammobin.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: AmmoBin -- ammunition store with cook-off heat // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(AMMOBIN_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +Derivation + AmmoBin::ClassDerivations( + HeatWatcher::ClassDerivations, + "AmmoBin" + ); + +AmmoBin::SharedData + AmmoBin::DefaultData( + AmmoBin::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +AmmoBin::AmmoBin( + Mech *owner, + int subsystem_ID, + SubsystemResource *resource, + SharedData &shared_data +): + HeatWatcher(owner, subsystem_ID, resource, shared_data), + ammoAlarm(6) +{ + Check(owner); + Check_Pointer(resource); + + ammoCount = resource->ammoCount; + initialAmmoCount = resource->ammoCount; + feedRate = resource->feedRate; + feedTimer = 0.0f; + ammoClassID = resource->ammoClassID; + ammoModelFile = resource->ammoModelFile; + explosionModelFile = resource->explosionModelFile; + heatPerRound = resource->heatPerRound; + cookOffArmed = 0; + cookOffTime = 0; + reserved = 0; + + for (int i = 0; i < 12; ++i) + { + cookOffState[i] = 0; + } + + Check_Fpu(); +} + +AmmoBin::~AmmoBin() +{ +} + +Logical + AmmoBin::TestClass(Mech &) +{ + return True; +} + +Logical + AmmoBin::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// DeathReset -- the respawn restock: base heal + refill from the authored +// resource count (the launchers' NoAmmo state releases in their own reset). +//############################################################################# +// +void + AmmoBin::DeathReset(Logical full_reset) +{ + Check(this); + + HeatWatcher::DeathReset(full_reset); + + // + // Restock from the CTOR-CACHED authored count -- NEVER through the + // `resource` member: it points into the Mech ctor's padded stream copy, + // which is delete[]d before the ctor returns (a respawn-time deref + // reads freed heap -- caught by the 5.3.24 adversarial review). + // + ammoCount = initialAmmoCount; +} diff --git a/restoration/source410/BT/AMMOBIN.HPP b/restoration/source410/BT/AMMOBIN.HPP index a541ac42..fe125814 100644 --- a/restoration/source410/BT/AMMOBIN.HPP +++ b/restoration/source410/BT/AMMOBIN.HPP @@ -1,112 +1,118 @@ -//===========================================================================// -// File: ammobin.hpp // -// Project: BattleTech Brick: Mech weapons // -// Contents: AmmoBin -- an ammunition store (with cook-off heat) // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(AMMOBIN_HPP) -# define AMMOBIN_HPP - -# if !defined(HEAT_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - -//########################################################################### -//###################### AmmoBin Model Resource ######################## -//########################################################################### - - struct AmmoBin__SubsystemResource: - public HeatWatcher::SubsystemResource - { - int ammoClassID; - int ammoModelFile; - int explosionModelFile; - int ammoCount; - Scalar feedRate; - Scalar heatPerRound; - }; - -//########################################################################### -//############################## AmmoBin ############################### -//########################################################################### - - class AmmoBin: - public HeatWatcher - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - - int GetAmmoCount() const { return ammoCount; } - - // - // The feed interface the ballistic fire path consumes. Ammo states - // (the 1995 feed alarm): 1 = a round is chambered/ready, 2 = EMPTY. - // FeedAmmo pulls one round (the launcher calls it on a granted shot); - // returns 0 when dry. (Feed-rate pacing + the cook-off heat source - // join with the heat wave; the launcher's own recharge already paces - // shots.) - // - enum { - AmmoLoadedState = 1, - AmmoEmptyState = 2 - }; - int - GetAmmoState() const - { Check(this); return (ammoCount > 0) ? AmmoLoadedState : AmmoEmptyState; } - int - FeedAmmo() - { - Check(this); - if (ammoCount <= 0) - { - return 0; - } - --ammoCount; - return 1; - } - - public: - typedef AmmoBin__SubsystemResource SubsystemResource; - - AmmoBin( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - ~AmmoBin(); - - protected: - int ammoCount; - int initialAmmoCount; - Scalar feedTimer; - Scalar feedRate; - int ammoClassID; - int ammoModelFile; - int explosionModelFile; - Scalar heatPerRound; - int cookOffArmed; - int cookOffTime; - int reserved; - AlarmIndicator ammoAlarm; - // - // Cook-off heat-source registration state (advanced by the heat sim). - // Reserved until the per-frame path is reconstructed. - // - int cookOffState[12]; - }; - -#endif +//===========================================================================// +// File: ammobin.hpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: AmmoBin -- an ammunition store (with cook-off heat) // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(AMMOBIN_HPP) +# define AMMOBIN_HPP + +# if !defined(HEAT_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + +//########################################################################### +//###################### AmmoBin Model Resource ######################## +//########################################################################### + + struct AmmoBin__SubsystemResource: + public HeatWatcher::SubsystemResource + { + int ammoClassID; + int ammoModelFile; + int explosionModelFile; + int ammoCount; + Scalar feedRate; + Scalar heatPerRound; + }; + +//########################################################################### +//############################## AmmoBin ############################### +//########################################################################### + + class AmmoBin: + public HeatWatcher + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + int GetAmmoCount() const { return ammoCount; } + + // + // The feed interface the ballistic fire path consumes. Ammo states + // (the 1995 feed alarm): 1 = a round is chambered/ready, 2 = EMPTY. + // FeedAmmo pulls one round (the launcher calls it on a granted shot); + // returns 0 when dry. (Feed-rate pacing + the cook-off heat source + // join with the heat wave; the launcher's own recharge already paces + // shots.) + // + enum { + AmmoLoadedState = 1, + AmmoEmptyState = 2 + }; + int + GetAmmoState() const + { Check(this); return (ammoCount > 0) ? AmmoLoadedState : AmmoEmptyState; } + // + // Respawn restock: refill the magazine from the authored count. + // + virtual void + DeathReset(Logical full_reset); + + int + FeedAmmo() + { + Check(this); + if (ammoCount <= 0) + { + return 0; + } + --ammoCount; + return 1; + } + + public: + typedef AmmoBin__SubsystemResource SubsystemResource; + + AmmoBin( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + ~AmmoBin(); + + protected: + int ammoCount; + int initialAmmoCount; + Scalar feedTimer; + Scalar feedRate; + int ammoClassID; + int ammoModelFile; + int explosionModelFile; + Scalar heatPerRound; + int cookOffArmed; + int cookOffTime; + int reserved; + AlarmIndicator ammoAlarm; + // + // Cook-off heat-source registration state (advanced by the heat sim). + // Reserved until the per-frame path is reconstructed. + // + int cookOffState[12]; + }; + +#endif diff --git a/restoration/source410/BT/BTPLAYER.CPP b/restoration/source410/BT/BTPLAYER.CPP index 9435233f..fa120c21 100644 --- a/restoration/source410/BT/BTPLAYER.CPP +++ b/restoration/source410/BT/BTPLAYER.CPP @@ -1,568 +1,642 @@ -//===========================================================================// -// 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 - -// -//############################################################################# -// 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; - - 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 (deferred with the - // Mech subsystem). - // - } - - 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 (message->deathCount != -1) - { - Player::VehicleDeadMessageHandler(message); - Check_Fpu(); - return; - } - - Fail("BTPlayer::VehicleDeadMessageHandler(-1) -- death/respawn cycle deferred (mech4/scoring)"); -} - -void - BTPlayer::ScoreMessageHandler(ScoreMessage *) -{ - Fail("BTPlayer::ScoreMessageHandler -- btplayer.cpp not yet reconstructed"); -} - -void - BTPlayer::ScoreInflictedMessageHandler(ScoreMessage *) -{ - Fail("BTPlayer::ScoreInflictedMessageHandler -- btplayer.cpp not yet reconstructed"); -} - -void - BTPlayer::ScoreUpdateMessageHandler(ScoreMessage *) -{ - Fail("BTPlayer::ScoreUpdateMessageHandler -- btplayer.cpp not yet reconstructed"); -} - -// -//############################################################################# -// 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(); -} +//===========================================================================// +// 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 + +// +//############################################################################# +// 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; + + 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 (GetScenarioRole() != NULL) + { + GetScenarioRole()->SetReturnFromDeath( + GetScenarioRole()->GetReturnFromDeath() - 1); + } + + if (GetScenarioRole() == NULL + || GetScenarioRole()->GetReturnFromDeath() > 0) + { + Time when = Now(); + when += 5.0f; + application->Post(HighEventPriority, this, message, when); + } + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[vdead] death #" << deathCount + << " -- respawn posted +5s" << endl << flush; + } +} + +void + BTPlayer::ScoreMessageHandler(ScoreMessage *) +{ + Fail("BTPlayer::ScoreMessageHandler -- btplayer.cpp not yet reconstructed"); +} + +void + BTPlayer::ScoreInflictedMessageHandler(ScoreMessage *) +{ + Fail("BTPlayer::ScoreInflictedMessageHandler -- btplayer.cpp not yet reconstructed"); +} + +void + BTPlayer::ScoreUpdateMessageHandler(ScoreMessage *) +{ + Fail("BTPlayer::ScoreUpdateMessageHandler -- btplayer.cpp not yet reconstructed"); +} + +// +//############################################################################# +// 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(); +} diff --git a/restoration/source410/BT/BTPLAYER.NOTES.md b/restoration/source410/BT/BTPLAYER.NOTES.md index a609081c..b250d4c6 100644 --- a/restoration/source410/BT/BTPLAYER.NOTES.md +++ b/restoration/source410/BT/BTPLAYER.NOTES.md @@ -179,3 +179,17 @@ The harness also sets the enemy's target back to the player: under BT_FORCE_FIRE the enemy returns fire (its gates read dev-permissive with no player link), so the damage cascade gets a live incoming side — used to verify the player's own weapons fall silent when destroyed. + +## 5.3.24 — the VehicleDead(-1) cycle + respawn branch LIVE + +The -1 immediate-death branch (binary @004c05c4): MissionEndingState swallow, +deathPending dedup (one death one cycle), ++deathCount + stamp into the +message (the base handler's identity gate), scenario-role life debit +(NULL-safe), +5s re-post (RespawnDelay @004c0830) -> the engine base handler +runs the drop-zone hunt -> reply -> the respawn branch resets the SAME mech +(reset-based respawn; severing was the "two mechs" glitch family). The >=0 +re-posts/probes are MOOT once the mech lives again (clear deathPending, +return -- ends the 2s probe loop). Out-of-lives -> mission review post = +scoring wave. The fresh-spawn branch does NOT yet run the shared +Reset/translocation-alarm tail (mech already placed by the MakeMessage; +joins the cockpit wave). diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index dd0a942e..860c17f7 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -460,7 +460,8 @@ void // resumes from Loading. (The owning-mech-disabled half joins with the // damage wave.) // - if (GetSimulationState() == 1 || GetHeatState() == FailureHeat) + if (GetSimulationState() == 1 || GetHeatState() == FailureHeat + || (owner != NULL && owner->IsMechDestroyed())) { if (getenv("BT_MECH_LOG") && GetWeaponState() != LoadingState) { diff --git a/restoration/source410/BT/HEAT.CPP b/restoration/source410/BT/HEAT.CPP index 48af947d..c53b3121 100644 --- a/restoration/source410/BT/HEAT.CPP +++ b/restoration/source410/BT/HEAT.CPP @@ -1,1083 +1,1115 @@ -//===========================================================================// -// File: heat.cpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: HeatableSubsystem -- a MechSubsystem with a thermal state // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(HEAT_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -#if !defined(BTPLAYER_HPP) -# include -#endif - -#include - -// -//############################################################################# -// Tuning constants (byte-verified against the shipped image in the BT411 RE: -// .data / literal-pool reads). -//############################################################################# -// -static const Scalar HeatLoadScale = 0.002f; // heat-load normalising scale -> band [0,1] -static const Scalar HeatLoadMinimum = 0.0f; -static const Scalar HeatLoadMaximum = 1.0f; -static const Scalar HeatEqualizeEpsilon = 1.0e-4f; // BalanceCoolant no-op band -static const Scalar CoolantDrawGate = 1.0e-4f; // DrawCoolant |amount| gate -static const Scalar CoolantDrawFloor = 0.0025f; // draw zero-floor + ACTIVE-off hysteresis -static const Scalar CoolantActiveOn = 0.003f; // coolantActive ON threshold - -// -//############################################################################# -// Shared data support -- reuses the base Subsystem sets (no boot-critical -// handlers / attributes of its own). -//############################################################################# -// -Derivation - HeatableSubsystem::ClassDerivations( - MechSubsystem::ClassDerivations, - "HeatableSubsystem" - ); - -HeatableSubsystem::SharedData - HeatableSubsystem::DefaultData( - HeatableSubsystem::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -//############################################################################# -// -HeatableSubsystem::HeatableSubsystem( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - MechSubsystem( - owner, - subsystem_ID, - (MechSubsystem::SubsystemResource *)subsystem_resource, - shared_data - ) -{ - Check(owner); - Check_Pointer(subsystem_resource); - ResetToInitialState(); - Check_Fpu(); -} - -// -//############################################################################# -//############################################################################# -// -HeatableSubsystem::~HeatableSubsystem() -{ -} - -// -//############################################################################# -//############################################################################# -// -void - HeatableSubsystem::ResetToInitialState() -{ - Check(this); - currentTemperature = 300.0f; - heatLoad = 0.0f; -} - -// -//############################################################################# -//############################################################################# -// -Logical - HeatableSubsystem::TestClass(Mech &) -{ - return True; -} - -Logical - HeatableSubsystem::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// CreateStreamedSubsystem Model-load-time construction (thermal resource + -// damage-zone stream). Not yet reconstructed (see MECHSUB.NOTES.md). -//############################################################################# -// -int - HeatableSubsystem::CreateStreamedSubsystem( - NotationFile *, - const char *, - const char *, - SubsystemResource *, - NotationFile *, - const ResourceDirectories *, - int - ) -{ - Fail("HeatableSubsystem::CreateStreamedSubsystem -- heat.cpp not yet reconstructed"); - return 0; -} - -//########################################################################### -//############################## HeatSink ############################### -//########################################################################### - -// -//############################################################################# -// Shared data support -//############################################################################# -// -Derivation - HeatSink::ClassDerivations( - HeatableSubsystem::ClassDerivations, - "HeatSink" - ); - -// -// The cockpit cooling toggle (id 3 -- the first subsystem-family message). -// -const HeatSink::HandlerEntry - HeatSink::MessageHandlerEntries[]= -{ - MESSAGE_ENTRY(HeatSink, ToggleCooling) -}; - -HeatSink::MessageHandlerSet - HeatSink::MessageHandlers( - ELEMENTS(HeatSink::MessageHandlerEntries), - HeatSink::MessageHandlerEntries, - Subsystem::MessageHandlers - ); - -HeatSink::SharedData - HeatSink::DefaultData( - HeatSink::ClassDerivations, - HeatSink::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -// The heat sink -- a thermal mass with a coolant loop. A master sink drives -// the per-frame thermal simulation. -//############################################################################# -// -HeatSink::HeatSink( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data), - linkedSinks(), - heatAlarm(3) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - currentTemperature = subsystem_resource->startingTemperature; - degradationTemperature = subsystem_resource->degradationTemperature; - failureTemperature = subsystem_resource->failureTemperature; - - heatLoad = 0.0f; - coolantEfficiency = 0.5f; - thermalCapacity = 1.0f; - coolantLevel = thermalCapacity; - coolantDraw = 0.0f; - coolantAvailable = 1; - coolantActive = 0; - - startingTemperature = currentTemperature; - thermalConductance = subsystem_resource->thermalConductance; - - heatFilter.SetSize(15, 0.0f); - filterDecay = 0.4f; - thermalMass = subsystem_resource->thermalMass; - heatEnergy = thermalMass * startingTemperature; - coolantFlowScale = 1.0f; - massScale = 1.0f; - - pendingHeat = 0.0f; - radiatedHeat = 0.0f; - - // - // Wire the heat-conduction link: the resource names the roster slot of the - // sink this one drains into (weapons/equipment -> the Condenser bank, the - // Condensers -> the central HeatSink). The shipped stream orders sinks so - // the target is already constructed; an unresolvable index leaves the sink - // standalone (its own thermal mass only). - // - { - Subsystem *linked = NULL; - if (subsystem_resource->linkedSinkIndex >= 0 - && subsystem_resource->linkedSinkIndex < owner->GetSubsystemCount()) - { - linked = owner->GetSubsystem(subsystem_resource->linkedSinkIndex); - } - if (linked != NULL) - { - linkedSinks.Add(linked); - } - if (getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[heat] '" << GetName() - << "' linkedSinkIdx=" << subsystem_resource->linkedSinkIndex - << " -> "; - if (linked != NULL) - { - DEBUG_STREAM << linked->GetName(); - } - else - { - DEBUG_STREAM << ""; - } - DEBUG_STREAM << " thermalMass=" << thermalMass - << " T0=" << currentTemperature - << " degrade=" << degradationTemperature - << " fail=" << failureTemperature - << " conduct=" << thermalConductance << endl << flush; - } - } - - // - // Install the per-frame thermal Performance (replicant copies are driven by - // console updates instead). Derived classes (PoweredSubsystem, Generator, - // the weapons) override with their own Performance in their ctors, each of - // which chains this step. - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&HeatSink::HeatSinkSimulation); - } - - Check_Fpu(); -} - -// -//############################################################################# -//############################################################################# -// -HeatSink::~HeatSink() -{ -} - -// -//############################################################################# -//############################################################################# -// -void - HeatSink::ResetToInitialState(Logical /*powered*/) -{ - Check(this); - currentTemperature = startingTemperature; - heatLoad = 0.0f; - coolantLevel = thermalCapacity; - coolantDraw = 0.0f; - coolantActive = 0; - heatEnergy = thermalMass * startingTemperature; - pendingHeat = 0.0f; - radiatedHeat = 0.0f; - heatAlarm.SetLevel(0); -} - -// -//############################################################################# -//############################################################################# -// -Logical - HeatSink::TestClass(Mech &) -{ - return True; -} - -Logical - HeatSink::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// HeatModelActive -- the heat-model master switch (binary FUN_004ad7d4): -// owner mech -> Entity::playerLink -> BTPlayer::heatModelOn, ON only for -// veteran / expert experience. Standard mode has NO heat consequences -- -// authentic, not a gap. A NULL player link (unlinked dev mech / target dummy) -// reads ON, matching the permissive dev-rig behavior; the binary derefs -// unguarded (a linked player always exists in pod missions). -//############################################################################# -// -Logical - HeatSink::HeatModelActive() -{ - Check(this); - - if (owner == NULL) - { - return True; - } - BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); - if (player == NULL) - { - return True; - } - return player->IsHeatModelOn(); -} - -// -//############################################################################# -// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924). -// Under the heat-model experience gate: absorb the pending heat into the -// thermal mass, recompute the temperature and the smoothed heat-load reading, -// conduct into the linked sink, and run the coolant draw. The degradation / -// failure alarm drive is OUTSIDE the gate (authentic -- with the model off the -// temperature never moves, so the alarm just holds Normal). -//############################################################################# -// -void - HeatSink::HeatSinkSimulation(Scalar time_slice) -{ - Check(this); - - if (HeatModelActive()) - { - heatEnergy += pendingHeat; - currentTemperature = heatEnergy / thermalMass; - UpdateHeatLoad(); - pendingHeat = 0.0f; - ConductHeat(time_slice); - } - - if (HeatModelActive()) - { - UpdateCoolant(time_slice); - } - - // - // Drive the degradation / failure alarm. - // - if (currentTemperature > failureTemperature) - { - heatAlarm.SetLevel(FailureHeat); - } - else if (currentTemperature > degradationTemperature) - { - heatAlarm.SetLevel(DegradationHeat); - } - else - { - heatAlarm.SetLevel(NormalHeat); - } - - // - // BT_HEAT_LOG: a 5-second per-sink census (temperature / coolant / load). - // - if (getenv("BT_HEAT_LOG")) - { - static Scalar censusAccum = 0.0f; - censusAccum += time_slice; - if (censusAccum >= 5.0f) - { - censusAccum = 0.0f; - DEBUG_STREAM << "[heat-t] " << GetName() - << " T=" << currentTemperature - << " cool=" << coolantLevel << "/" << thermalCapacity - << " load=" << heatLoad << endl << flush; - } - } - - Check_Fpu(); -} - -// -//############################################################################# -// UpdateHeatLoad -- recompute the radiated heat and feed the normalised sample -// through the 15-sample running-average filter to produce the smoothed -// heatLoad reading in the [0,1] band (binary @004ad7f0; the shaping constants -// are byte-verified from the image). -//############################################################################# -// -void - HeatSink::UpdateHeatLoad() -{ - Check(this); - - radiatedHeat = currentTemperature * coolantLevel; - - Scalar sample = HeatLoadScale * radiatedHeat; - if (sample < HeatLoadMinimum) - { - sample = HeatLoadMinimum; - } - else if (sample > HeatLoadMaximum) - { - sample = HeatLoadMaximum; - } - - heatFilter.Add(sample); - heatLoad = heatFilter.CalculateAverage(); -} - -// -//############################################################################# -// ConductHeat -- conduct heat into the linked sink, then rebalance coolant so -// the hotter side sheds load (binary @004ad8ac). -//############################################################################# -// -void - HeatSink::ConductHeat(Scalar time_slice) -{ - Check(this); - - HeatSink *other = (HeatSink *)linkedSinks.Resolve(); - if (other != NULL && coolantAvailable != 0) - { - Scalar flow = ComputeHeatFlow(other, time_slice); - other->pendingHeat += flow; - pendingHeat -= flow; - BalanceCoolant(time_slice); - } -} - -// -//############################################################################# -// BalanceCoolant -- move coolant between this sink and its linked sink so that -// the hotter side sheds load (binary @004ada94). Clamped on both ends so -// neither sink goes below empty or above its capacity. -//############################################################################# -// -void - HeatSink::BalanceCoolant(Scalar time_slice) -{ - Check(this); - - HeatSink *other = (HeatSink *)linkedSinks.Resolve(); - if (other == NULL) - { - return; - } - - Scalar spread = radiatedHeat - other->radiatedHeat; - if (spread < 0.0f) - { - spread = -spread; - } - if (spread <= HeatEqualizeEpsilon) - { - return; - } - - Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel; - - // - // Clamp delta to +/- (thermalCapacity * dt). - // - Scalar limit = thermalCapacity * time_slice; - if (delta < -limit) - { - delta = -limit; - } - else if (delta > limit) - { - delta = limit; - } - - // - // Clamp so this sink stays within [0, thermalCapacity]. - // - Scalar hi = thermalCapacity - coolantLevel; - Scalar lo = -coolantLevel; - if (delta < lo) delta = lo; - else if (delta > hi) delta = hi; - - // - // Clamp so the other sink stays within its own [0, thermalCapacity]. - // - Scalar otherLo = -(other->thermalCapacity - other->coolantLevel); - if (delta < otherLo) delta = otherLo; - else if (delta > other->coolantLevel) delta = other->coolantLevel; - - delta = coolantFlowScale * delta; - coolantLevel += delta; - other->coolantLevel -= delta; -} - -// -//############################################################################# -// UpdateCoolant -- consume coolant proportional to the current load, request a -// top-up from the central cooling system, and update the draw state machine -// (binary @004adbf8). The draw scales with THIS subsystem's own structural -// damage: an undamaged subsystem leaks nothing (the coolant bars stay full on -// a pristine mech); the draw rises only as the sink itself takes battle -// damage. -//############################################################################# -// -void - HeatSink::UpdateCoolant(Scalar time_slice) -{ - Check(this); - - coolantDraw = GetSubsystemDamageLevel() * heatLoad; - if (coolantDraw < CoolantDrawFloor) - { - coolantDraw = 0.0f; - } - - Scalar amount = coolantDraw * time_slice; - if (coolantLevel < amount) - { - amount = coolantLevel; - } - coolantLevel -= amount; - - if (amount > CoolantDrawGate || amount < -CoolantDrawGate) - { - coolantLevel += DrawCoolant(amount); - } - - // - // The draw state machine (hysteresis: ON above 0.003, OFF below 0.0025). - // - if (coolantActive == 0 && coolantDraw > CoolantActiveOn) - { - coolantActive = 1; - } - else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor) - { - coolantActive = 0; - } -} - -// -//############################################################################# -// ComputeHeatFlow -- conductive heat exchange between this sink and 'other' -// (binary @004ad9ec): -// tau = thermalMass / massScale -// denom = tau + other->thermalMass -// q = (currentTemperature*massScale -// - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom) -// * tau -// * (1 - exp( -dt * thermalConductance -// * (coolantLevel / thermalCapacity) -// * coolantFlowScale / denom )) -//############################################################################# -// -Scalar - HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice) -{ - Check(this); - Check(other); - - Scalar tau = thermalMass / massScale; - Scalar denom = tau + other->thermalMass; - - Scalar equilibrium = - currentTemperature * massScale - - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom; - - Scalar response = 1.0f - (Scalar)exp( - -time_slice * thermalConductance - * (coolantLevel / thermalCapacity) - * coolantFlowScale / denom - ); - - return equilibrium * tau * response; -} - -// -//############################################################################# -// DrawCoolant -- ask the central cooling system for coolant and return how -// much was actually supplied. Base sinks supply nothing on their own; the -// Reservoir (the coolant store) overrides this as the source. -//############################################################################# -// -Scalar - HeatSink::DrawCoolant(Scalar) -{ - Check(this); - return 0.0f; -} - -// -//############################################################################# -// ToggleCoolingMessageHandler -- the cockpit cooling on/off switch (binary -// @004ad6f8, id 3): novice-locked, press-only; toggles the coolant supply and -// the conduction flow together (OFF = this sink stops conducting into the -// bank -- its heat strands until cooling is switched back on). -//############################################################################# -// -void - HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf *message) -{ - Check(this); - Check(message); - - if (NoviceLockout()) - { - return; - } - if (message->dataContents <= 0) // press only - { - return; - } - - if (coolantAvailable != 0) - { - coolantAvailable = 0; - coolantFlowScale = 0.0f; - } - else - { - coolantAvailable = 1; - coolantFlowScale = 1.0f; - } - - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[cool] '" << GetName() - << "' cooling toggled -> " << coolantAvailable << endl << flush; - } -} - -//########################################################################### -//############################# HeatWatcher ############################# -//########################################################################### - -Derivation - HeatWatcher::ClassDerivations( - MechSubsystem::ClassDerivations, - "HeatWatcher" - ); - -HeatWatcher::SharedData - HeatWatcher::DefaultData( - HeatWatcher::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -HeatWatcher::HeatWatcher( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - MechSubsystem( - owner, - subsystem_ID, - (MechSubsystem::SubsystemResource *)subsystem_resource, - shared_data - ), - watchedLink(), - heatAlarm(3) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - degradationTemperature = subsystem_resource->degradationTemperature; - failureTemperature = subsystem_resource->failureTemperature; - watchedSubsystem = subsystem_resource->watchedSubsystem; - - // - // The master instance runs WatchSimulation per-frame; the install is - // deferred with that (staged) method. - // - Check_Fpu(); -} - -HeatWatcher::~HeatWatcher() -{ -} - -Logical - HeatWatcher::TestClass(Mech &) -{ - return True; -} - -Logical - HeatWatcher::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -void - HeatWatcher::ResetToInitialState(Logical /*powered*/) -{ - Check(this); - heatAlarm.SetLevel(0); -} - -// -// Per-frame: resolve the watched subsystem, read its temperature, drive the -// 3-level alarm. Not yet reconstructed. -// -void - HeatWatcher::WatchSimulation(Scalar) -{ - Fail("HeatWatcher::WatchSimulation -- heat.cpp not yet reconstructed"); -} - -//########################################################################### -//############################## Condenser ############################# -//########################################################################### - -Derivation - Condenser::ClassDerivations( - HeatSink::ClassDerivations, - "Condenser" - ); - -// -// The cockpit condenser-valve button (the binary's Condenser handler table -// has exactly ONE entry: id 4, "MoveValve"). ToggleCooling (id 3) is -// inherited from the HeatSink chain. -// -const Condenser::HandlerEntry - Condenser::MessageHandlerEntries[]= -{ - MESSAGE_ENTRY(Condenser, MoveValve) -}; - -Condenser::MessageHandlerSet - Condenser::MessageHandlers( - ELEMENTS(Condenser::MessageHandlerEntries), - Condenser::MessageHandlerEntries, - HeatSink::MessageHandlers - ); - -Condenser::SharedData - Condenser::DefaultData( - Condenser::ClassDerivations, - Condenser::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -Condenser::Condenser( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - HeatSink(owner, subsystem_ID, subsystem_resource, shared_data) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - // - // The authentic ctor (binary @4ae568): the valve opens at 1, the - // refrigeration output rides the inherited massScale slot, and the - // condenser's own coolantFlowScale starts 0 -- the valve recompute - // (RecomputeValves: MoveValve presses, and once at mech spawn) assigns - // each condenser its share of the total valve opening. - // - valveState = 1; - coolantFlowScale = 0.0f; - refrigerationFactor = subsystem_resource->refrigerationFactor; - massScale = refrigerationFactor; - - // - // Condenser number from the segment-name DIGIT suffix ("Condenser1" -> 1; - // the letter form -0x40 is the GENERATOR convention). - // - const char *name = GetName(); - condenserNumber = name[strlen(name) - 1] - '0'; - - if (getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[cond] '" << GetName() - << "' refrigFactor=" << refrigerationFactor - << " #" << condenserNumber << endl << flush; - } - - // - // Install the per-frame refrigeration Performance. - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&Condenser::RefrigerationSimulation); - } - - Check_Fpu(); -} - -Condenser::~Condenser() -{ -} - -Logical - Condenser::TestClass(Mech &) -{ - return True; -} - -Logical - Condenser::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// RefrigerationSimulation -- the condenser's per-frame step (binary @4ae4d8): -// recompute the refrigeration output as (1 - own structural damage) * -// refrigerationFactor, clamped >= 1, into the inherited massScale slot -- an -// undamaged condenser behaves refrigerationFactor-times "hotter" in the -// conduction exchange, actively pumping heat toward the central bank and -// chilling itself below ambient -- then run the base HeatSink step. -//############################################################################# -// -void - Condenser::RefrigerationSimulation(Scalar time_slice) -{ - Check(this); - - massScale = (1.0f - GetSubsystemDamageLevel()) * refrigerationFactor; - if (massScale < 1.0f) - { - massScale = 1.0f; - } - - HeatSink::HeatSinkSimulation(time_slice); -} - -// -//############################################################################# -// MoveValveMessageHandler -- the cockpit condenser-valve button (binary -// @4ae464, id 4): novice-locked, press-only; cycles THIS condenser's valve -// 1 -> 5 -> 50 -> 0 -> 1, then recomputes every condenser's flow share. -//############################################################################# -// -void - Condenser::MoveValveMessageHandler(ReceiverDataMessageOf *message) -{ - Check(this); - Check(message); - - if (NoviceLockout()) - { - return; - } - if (message->dataContents <= 0) // press only - { - return; - } - - switch (valveState) - { - case 0: valveState = 1; break; - case 1: valveState = 5; break; - case 5: valveState = 50; break; - case 50: valveState = 0; break; - default: return; // unknown -> no change, no recompute - } - - RecomputeValves((Entity *)owner); -} - -// -//############################################################################# -// RecomputeValves -- assign every condenser its coolant flow share -// (valve / sum-of-valves) after a valve move (binary @0049f788). All valves -// at the spawn default (1) yield equal shares. (The condenser-alarm change -// pulse joins with the gauge wave.) -//############################################################################# -// -void - Condenser::RecomputeValves(Entity *owner_mech) -{ - Check(owner_mech); - - int count = owner_mech->GetSubsystemCount(); - int total = 0; - int i; - - for (i = 2; i < count; ++i) - { - Subsystem *s = owner_mech->GetSubsystem(i); - if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) - { - total += ((Condenser *)s)->valveState; - } - } - - for (i = 2; i < count; ++i) - { - Subsystem *s = owner_mech->GetSubsystem(i); - if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) - { - Condenser *condenser = (Condenser *)s; - condenser->coolantFlowScale = (total > 0) - ? ((Scalar)condenser->valveState / (Scalar)total) - : 0.0f; - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[valve] '" << condenser->GetName() - << "' valve=" << condenser->valveState - << " flow=" << condenser->coolantFlowScale << endl; - } - } - } - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << flush; - } -} - -//########################################################################### -//######################### AggregateHeatSink ########################### -//########################################################################### - -Derivation - AggregateHeatSink::ClassDerivations( - HeatSink::ClassDerivations, - "HeatSinkBank" - ); - -AggregateHeatSink::SharedData - AggregateHeatSink::DefaultData( - AggregateHeatSink::ClassDerivations, - HeatSink::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -// The heat-sink bank (binary ctor @4ae8d0): the aggregate count scales the -// conductance (0.1 x count, byte-verified), the ambient setpoint defaults -// 300 K (the mission's [mission] temperature overwrites it in the PlayerLink -// pass -- that override joins the Mech-PlayerLink wave), and a master runs -// the RADIATOR Performance instead of the base heat step. -//############################################################################# -// -AggregateHeatSink::AggregateHeatSink( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - HeatSink(owner, subsystem_ID, subsystem_resource, shared_data) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - heatSinkCount = subsystem_resource->heatSinkCount; - ambientTemperature = 300.0f; - - thermalConductance = 0.1f * (Scalar)heatSinkCount * thermalConductance; - - if (getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[bank] '" << GetName() - << "' heatSinkCount=" << heatSinkCount - << " conductance=" << thermalConductance << endl << flush; - } - - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&AggregateHeatSink::RadiatorSimulation); - } - - Check_Fpu(); -} - -AggregateHeatSink::~AggregateHeatSink() -{ -} - -Logical - AggregateHeatSink::TestClass(Mech &) -{ - return True; -} - -Logical - AggregateHeatSink::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// RadiatorSimulation -- the bank's per-frame step (binary @4ae73c), replacing -// the base HeatSinkSimulation: under the heat-model gate, absorb + recompute -// the load, then the AMBIENT RADIATOR -- relax the bank toward the setpoint -// target (300 - (300 - ambient) x 3) with rate k = conductance x (1 - own -// damage) x (coolant/capacity) x flowScale / mass. This is the system's ONLY -// heat exit. Tail: top the bank's coolant up from the attached store via the -// DrawCoolant virtual (the reservoir-attach routing joins that wave; the base -// supplies 0 until then, a harmless no-op). -//############################################################################# -// -void - AggregateHeatSink::RadiatorSimulation(Scalar time_slice) -{ - Check(this); - - if (HeatModelActive()) - { - heatEnergy += pendingHeat; - currentTemperature = heatEnergy / thermalMass; - UpdateHeatLoad(); - pendingHeat = 0.0f; - - Scalar target = 300.0f - (300.0f - ambientTemperature) * 3.0f; - Scalar decay = 1.0f - (Scalar)exp( - (double)(-(time_slice * thermalConductance - * (1.0f - GetSubsystemDamageLevel()) - * (coolantLevel / thermalCapacity) * coolantFlowScale) - / thermalMass) - ); - Scalar shed = -((currentTemperature * massScale - target) - * thermalMass * decay); - pendingHeat += shed; - - if (getenv("BT_HEAT_LOG")) - { - static Scalar bankAccum = 0.0f; - bankAccum += time_slice; - if (bankAccum >= 5.0f) - { - bankAccum = 0.0f; - DEBUG_STREAM << "[heat-t] " << GetName() << " (bank)" - << " T=" << currentTemperature - << " shed=" << shed - << " cool=" << coolantLevel << "/" << thermalCapacity - << " load=" << heatLoad << endl << flush; - } - } - } - - // - // Coolant top-up from the attached store. - // - Scalar deficit = thermalCapacity - coolantLevel; - if (deficit > 1.0e-4f) - { - coolantLevel += DrawCoolant(deficit); - } -} +//===========================================================================// +// File: heat.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: HeatableSubsystem -- a MechSubsystem with a thermal state // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(HEAT_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(BTPLAYER_HPP) +# include +#endif + +#include + +// +//############################################################################# +// Tuning constants (byte-verified against the shipped image in the BT411 RE: +// .data / literal-pool reads). +//############################################################################# +// +static const Scalar HeatLoadScale = 0.002f; // heat-load normalising scale -> band [0,1] +static const Scalar HeatLoadMinimum = 0.0f; +static const Scalar HeatLoadMaximum = 1.0f; +static const Scalar HeatEqualizeEpsilon = 1.0e-4f; // BalanceCoolant no-op band +static const Scalar CoolantDrawGate = 1.0e-4f; // DrawCoolant |amount| gate +static const Scalar CoolantDrawFloor = 0.0025f; // draw zero-floor + ACTIVE-off hysteresis +static const Scalar CoolantActiveOn = 0.003f; // coolantActive ON threshold + +// +//############################################################################# +// Shared data support -- reuses the base Subsystem sets (no boot-critical +// handlers / attributes of its own). +//############################################################################# +// +Derivation + HeatableSubsystem::ClassDerivations( + MechSubsystem::ClassDerivations, + "HeatableSubsystem" + ); + +HeatableSubsystem::SharedData + HeatableSubsystem::DefaultData( + HeatableSubsystem::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +//############################################################################# +// +HeatableSubsystem::HeatableSubsystem( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + MechSubsystem( + owner, + subsystem_ID, + (MechSubsystem::SubsystemResource *)subsystem_resource, + shared_data + ) +{ + Check(owner); + Check_Pointer(subsystem_resource); + ResetToInitialState(); + Check_Fpu(); +} + +// +//############################################################################# +//############################################################################# +// +HeatableSubsystem::~HeatableSubsystem() +{ +} + +// +//############################################################################# +//############################################################################# +// +void + HeatableSubsystem::ResetToInitialState() +{ + Check(this); + currentTemperature = 300.0f; + heatLoad = 0.0f; +} + +// +//############################################################################# +//############################################################################# +// +Logical + HeatableSubsystem::TestClass(Mech &) +{ + return True; +} + +Logical + HeatableSubsystem::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// CreateStreamedSubsystem Model-load-time construction (thermal resource + +// damage-zone stream). Not yet reconstructed (see MECHSUB.NOTES.md). +//############################################################################# +// +int + HeatableSubsystem::CreateStreamedSubsystem( + NotationFile *, + const char *, + const char *, + SubsystemResource *, + NotationFile *, + const ResourceDirectories *, + int + ) +{ + Fail("HeatableSubsystem::CreateStreamedSubsystem -- heat.cpp not yet reconstructed"); + return 0; +} + +//########################################################################### +//############################## HeatSink ############################### +//########################################################################### + +// +//############################################################################# +// Shared data support +//############################################################################# +// +Derivation + HeatSink::ClassDerivations( + HeatableSubsystem::ClassDerivations, + "HeatSink" + ); + +// +// The cockpit cooling toggle (id 3 -- the first subsystem-family message). +// +const HeatSink::HandlerEntry + HeatSink::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(HeatSink, ToggleCooling) +}; + +HeatSink::MessageHandlerSet + HeatSink::MessageHandlers( + ELEMENTS(HeatSink::MessageHandlerEntries), + HeatSink::MessageHandlerEntries, + Subsystem::MessageHandlers + ); + +HeatSink::SharedData + HeatSink::DefaultData( + HeatSink::ClassDerivations, + HeatSink::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// The heat sink -- a thermal mass with a coolant loop. A master sink drives +// the per-frame thermal simulation. +//############################################################################# +// +HeatSink::HeatSink( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data), + linkedSinks(), + heatAlarm(3) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + currentTemperature = subsystem_resource->startingTemperature; + degradationTemperature = subsystem_resource->degradationTemperature; + failureTemperature = subsystem_resource->failureTemperature; + + heatLoad = 0.0f; + coolantEfficiency = 0.5f; + thermalCapacity = 1.0f; + coolantLevel = thermalCapacity; + coolantDraw = 0.0f; + coolantAvailable = 1; + coolantActive = 0; + + startingTemperature = currentTemperature; + thermalConductance = subsystem_resource->thermalConductance; + + heatFilter.SetSize(15, 0.0f); + filterDecay = 0.4f; + thermalMass = subsystem_resource->thermalMass; + heatEnergy = thermalMass * startingTemperature; + coolantFlowScale = 1.0f; + massScale = 1.0f; + + pendingHeat = 0.0f; + radiatedHeat = 0.0f; + + // + // Wire the heat-conduction link: the resource names the roster slot of the + // sink this one drains into (weapons/equipment -> the Condenser bank, the + // Condensers -> the central HeatSink). The shipped stream orders sinks so + // the target is already constructed; an unresolvable index leaves the sink + // standalone (its own thermal mass only). + // + { + Subsystem *linked = NULL; + if (subsystem_resource->linkedSinkIndex >= 0 + && subsystem_resource->linkedSinkIndex < owner->GetSubsystemCount()) + { + linked = owner->GetSubsystem(subsystem_resource->linkedSinkIndex); + } + if (linked != NULL) + { + linkedSinks.Add(linked); + } + if (getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[heat] '" << GetName() + << "' linkedSinkIdx=" << subsystem_resource->linkedSinkIndex + << " -> "; + if (linked != NULL) + { + DEBUG_STREAM << linked->GetName(); + } + else + { + DEBUG_STREAM << ""; + } + DEBUG_STREAM << " thermalMass=" << thermalMass + << " T0=" << currentTemperature + << " degrade=" << degradationTemperature + << " fail=" << failureTemperature + << " conduct=" << thermalConductance << endl << flush; + } + } + + // + // Install the per-frame thermal Performance (replicant copies are driven by + // console updates instead). Derived classes (PoweredSubsystem, Generator, + // the weapons) override with their own Performance in their ctors, each of + // which chains this step. + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&HeatSink::HeatSinkSimulation); + } + + Check_Fpu(); +} + +// +//############################################################################# +//############################################################################# +// +HeatSink::~HeatSink() +{ +} + +// +//############################################################################# +//############################################################################# +// +void + HeatSink::ResetToInitialState(Logical /*powered*/) +{ + Check(this); + currentTemperature = startingTemperature; + heatLoad = 0.0f; + coolantLevel = thermalCapacity; + coolantDraw = 0.0f; + coolantActive = 0; + heatEnergy = thermalMass * startingTemperature; + pendingHeat = 0.0f; + radiatedHeat = 0.0f; + heatAlarm.SetLevel(0); +} + +// +//############################################################################# +// DeathReset -- the respawn restore: base heal + the full thermal re-init. +//############################################################################# +// +void + HeatSink::DeathReset(Logical full_reset) +{ + Check(this); + MechSubsystem::DeathReset(full_reset); + ResetToInitialState(True); + // + // The cockpit cooling toggle is not part of ResetToInitialState; a sink + // switched OFF before death must respawn conducting again. + // + coolantAvailable = 1; + coolantFlowScale = 1.0f; +} + +// +//############################################################################# +//############################################################################# +// +Logical + HeatSink::TestClass(Mech &) +{ + return True; +} + +Logical + HeatSink::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// HeatModelActive -- the heat-model master switch (binary FUN_004ad7d4): +// owner mech -> Entity::playerLink -> BTPlayer::heatModelOn, ON only for +// veteran / expert experience. Standard mode has NO heat consequences -- +// authentic, not a gap. A NULL player link (unlinked dev mech / target dummy) +// reads ON, matching the permissive dev-rig behavior; the binary derefs +// unguarded (a linked player always exists in pod missions). +//############################################################################# +// +Logical + HeatSink::HeatModelActive() +{ + Check(this); + + if (owner == NULL) + { + return True; + } + BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); + if (player == NULL) + { + return True; + } + return player->IsHeatModelOn(); +} + +// +//############################################################################# +// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924). +// Under the heat-model experience gate: absorb the pending heat into the +// thermal mass, recompute the temperature and the smoothed heat-load reading, +// conduct into the linked sink, and run the coolant draw. The degradation / +// failure alarm drive is OUTSIDE the gate (authentic -- with the model off the +// temperature never moves, so the alarm just holds Normal). +//############################################################################# +// +void + HeatSink::HeatSinkSimulation(Scalar time_slice) +{ + Check(this); + + if (HeatModelActive()) + { + heatEnergy += pendingHeat; + currentTemperature = heatEnergy / thermalMass; + UpdateHeatLoad(); + pendingHeat = 0.0f; + ConductHeat(time_slice); + } + + if (HeatModelActive()) + { + UpdateCoolant(time_slice); + } + + // + // Drive the degradation / failure alarm. + // + if (currentTemperature > failureTemperature) + { + heatAlarm.SetLevel(FailureHeat); + } + else if (currentTemperature > degradationTemperature) + { + heatAlarm.SetLevel(DegradationHeat); + } + else + { + heatAlarm.SetLevel(NormalHeat); + } + + // + // BT_HEAT_LOG: a 5-second per-sink census (temperature / coolant / load). + // + if (getenv("BT_HEAT_LOG")) + { + static Scalar censusAccum = 0.0f; + censusAccum += time_slice; + if (censusAccum >= 5.0f) + { + censusAccum = 0.0f; + DEBUG_STREAM << "[heat-t] " << GetName() + << " T=" << currentTemperature + << " cool=" << coolantLevel << "/" << thermalCapacity + << " load=" << heatLoad << endl << flush; + } + } + + Check_Fpu(); +} + +// +//############################################################################# +// UpdateHeatLoad -- recompute the radiated heat and feed the normalised sample +// through the 15-sample running-average filter to produce the smoothed +// heatLoad reading in the [0,1] band (binary @004ad7f0; the shaping constants +// are byte-verified from the image). +//############################################################################# +// +void + HeatSink::UpdateHeatLoad() +{ + Check(this); + + radiatedHeat = currentTemperature * coolantLevel; + + Scalar sample = HeatLoadScale * radiatedHeat; + if (sample < HeatLoadMinimum) + { + sample = HeatLoadMinimum; + } + else if (sample > HeatLoadMaximum) + { + sample = HeatLoadMaximum; + } + + heatFilter.Add(sample); + heatLoad = heatFilter.CalculateAverage(); +} + +// +//############################################################################# +// ConductHeat -- conduct heat into the linked sink, then rebalance coolant so +// the hotter side sheds load (binary @004ad8ac). +//############################################################################# +// +void + HeatSink::ConductHeat(Scalar time_slice) +{ + Check(this); + + HeatSink *other = (HeatSink *)linkedSinks.Resolve(); + if (other != NULL && coolantAvailable != 0) + { + Scalar flow = ComputeHeatFlow(other, time_slice); + other->pendingHeat += flow; + pendingHeat -= flow; + BalanceCoolant(time_slice); + } +} + +// +//############################################################################# +// BalanceCoolant -- move coolant between this sink and its linked sink so that +// the hotter side sheds load (binary @004ada94). Clamped on both ends so +// neither sink goes below empty or above its capacity. +//############################################################################# +// +void + HeatSink::BalanceCoolant(Scalar time_slice) +{ + Check(this); + + HeatSink *other = (HeatSink *)linkedSinks.Resolve(); + if (other == NULL) + { + return; + } + + Scalar spread = radiatedHeat - other->radiatedHeat; + if (spread < 0.0f) + { + spread = -spread; + } + if (spread <= HeatEqualizeEpsilon) + { + return; + } + + Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel; + + // + // Clamp delta to +/- (thermalCapacity * dt). + // + Scalar limit = thermalCapacity * time_slice; + if (delta < -limit) + { + delta = -limit; + } + else if (delta > limit) + { + delta = limit; + } + + // + // Clamp so this sink stays within [0, thermalCapacity]. + // + Scalar hi = thermalCapacity - coolantLevel; + Scalar lo = -coolantLevel; + if (delta < lo) delta = lo; + else if (delta > hi) delta = hi; + + // + // Clamp so the other sink stays within its own [0, thermalCapacity]. + // + Scalar otherLo = -(other->thermalCapacity - other->coolantLevel); + if (delta < otherLo) delta = otherLo; + else if (delta > other->coolantLevel) delta = other->coolantLevel; + + delta = coolantFlowScale * delta; + coolantLevel += delta; + other->coolantLevel -= delta; +} + +// +//############################################################################# +// UpdateCoolant -- consume coolant proportional to the current load, request a +// top-up from the central cooling system, and update the draw state machine +// (binary @004adbf8). The draw scales with THIS subsystem's own structural +// damage: an undamaged subsystem leaks nothing (the coolant bars stay full on +// a pristine mech); the draw rises only as the sink itself takes battle +// damage. +//############################################################################# +// +void + HeatSink::UpdateCoolant(Scalar time_slice) +{ + Check(this); + + coolantDraw = GetSubsystemDamageLevel() * heatLoad; + if (coolantDraw < CoolantDrawFloor) + { + coolantDraw = 0.0f; + } + + Scalar amount = coolantDraw * time_slice; + if (coolantLevel < amount) + { + amount = coolantLevel; + } + coolantLevel -= amount; + + if (amount > CoolantDrawGate || amount < -CoolantDrawGate) + { + coolantLevel += DrawCoolant(amount); + } + + // + // The draw state machine (hysteresis: ON above 0.003, OFF below 0.0025). + // + if (coolantActive == 0 && coolantDraw > CoolantActiveOn) + { + coolantActive = 1; + } + else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor) + { + coolantActive = 0; + } +} + +// +//############################################################################# +// ComputeHeatFlow -- conductive heat exchange between this sink and 'other' +// (binary @004ad9ec): +// tau = thermalMass / massScale +// denom = tau + other->thermalMass +// q = (currentTemperature*massScale +// - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom) +// * tau +// * (1 - exp( -dt * thermalConductance +// * (coolantLevel / thermalCapacity) +// * coolantFlowScale / denom )) +//############################################################################# +// +Scalar + HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice) +{ + Check(this); + Check(other); + + Scalar tau = thermalMass / massScale; + Scalar denom = tau + other->thermalMass; + + Scalar equilibrium = + currentTemperature * massScale + - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom; + + Scalar response = 1.0f - (Scalar)exp( + -time_slice * thermalConductance + * (coolantLevel / thermalCapacity) + * coolantFlowScale / denom + ); + + return equilibrium * tau * response; +} + +// +//############################################################################# +// DrawCoolant -- ask the central cooling system for coolant and return how +// much was actually supplied. Base sinks supply nothing on their own; the +// Reservoir (the coolant store) overrides this as the source. +//############################################################################# +// +Scalar + HeatSink::DrawCoolant(Scalar) +{ + Check(this); + return 0.0f; +} + +// +//############################################################################# +// ToggleCoolingMessageHandler -- the cockpit cooling on/off switch (binary +// @004ad6f8, id 3): novice-locked, press-only; toggles the coolant supply and +// the conduction flow together (OFF = this sink stops conducting into the +// bank -- its heat strands until cooling is switched back on). +//############################################################################# +// +void + HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf *message) +{ + Check(this); + Check(message); + + if (NoviceLockout()) + { + return; + } + if (message->dataContents <= 0) // press only + { + return; + } + + if (coolantAvailable != 0) + { + coolantAvailable = 0; + coolantFlowScale = 0.0f; + } + else + { + coolantAvailable = 1; + coolantFlowScale = 1.0f; + } + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[cool] '" << GetName() + << "' cooling toggled -> " << coolantAvailable << endl << flush; + } +} + +//########################################################################### +//############################# HeatWatcher ############################# +//########################################################################### + +Derivation + HeatWatcher::ClassDerivations( + MechSubsystem::ClassDerivations, + "HeatWatcher" + ); + +HeatWatcher::SharedData + HeatWatcher::DefaultData( + HeatWatcher::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +HeatWatcher::HeatWatcher( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + MechSubsystem( + owner, + subsystem_ID, + (MechSubsystem::SubsystemResource *)subsystem_resource, + shared_data + ), + watchedLink(), + heatAlarm(3) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + degradationTemperature = subsystem_resource->degradationTemperature; + failureTemperature = subsystem_resource->failureTemperature; + watchedSubsystem = subsystem_resource->watchedSubsystem; + + // + // The master instance runs WatchSimulation per-frame; the install is + // deferred with that (staged) method. + // + Check_Fpu(); +} + +HeatWatcher::~HeatWatcher() +{ +} + +Logical + HeatWatcher::TestClass(Mech &) +{ + return True; +} + +Logical + HeatWatcher::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +void + HeatWatcher::ResetToInitialState(Logical /*powered*/) +{ + Check(this); + heatAlarm.SetLevel(0); +} + +// +//############################################################################# +// DeathReset -- respawn restore for the watcher family (AmmoBin chains it). +//############################################################################# +// +void + HeatWatcher::DeathReset(Logical full_reset) +{ + Check(this); + MechSubsystem::DeathReset(full_reset); + ResetToInitialState(True); +} + +// +// Per-frame: resolve the watched subsystem, read its temperature, drive the +// 3-level alarm. Not yet reconstructed. +// +void + HeatWatcher::WatchSimulation(Scalar) +{ + Fail("HeatWatcher::WatchSimulation -- heat.cpp not yet reconstructed"); +} + +//########################################################################### +//############################## Condenser ############################# +//########################################################################### + +Derivation + Condenser::ClassDerivations( + HeatSink::ClassDerivations, + "Condenser" + ); + +// +// The cockpit condenser-valve button (the binary's Condenser handler table +// has exactly ONE entry: id 4, "MoveValve"). ToggleCooling (id 3) is +// inherited from the HeatSink chain. +// +const Condenser::HandlerEntry + Condenser::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(Condenser, MoveValve) +}; + +Condenser::MessageHandlerSet + Condenser::MessageHandlers( + ELEMENTS(Condenser::MessageHandlerEntries), + Condenser::MessageHandlerEntries, + HeatSink::MessageHandlers + ); + +Condenser::SharedData + Condenser::DefaultData( + Condenser::ClassDerivations, + Condenser::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +Condenser::Condenser( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + HeatSink(owner, subsystem_ID, subsystem_resource, shared_data) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + // + // The authentic ctor (binary @4ae568): the valve opens at 1, the + // refrigeration output rides the inherited massScale slot, and the + // condenser's own coolantFlowScale starts 0 -- the valve recompute + // (RecomputeValves: MoveValve presses, and once at mech spawn) assigns + // each condenser its share of the total valve opening. + // + valveState = 1; + coolantFlowScale = 0.0f; + refrigerationFactor = subsystem_resource->refrigerationFactor; + massScale = refrigerationFactor; + + // + // Condenser number from the segment-name DIGIT suffix ("Condenser1" -> 1; + // the letter form -0x40 is the GENERATOR convention). + // + const char *name = GetName(); + condenserNumber = name[strlen(name) - 1] - '0'; + + if (getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[cond] '" << GetName() + << "' refrigFactor=" << refrigerationFactor + << " #" << condenserNumber << endl << flush; + } + + // + // Install the per-frame refrigeration Performance. + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Condenser::RefrigerationSimulation); + } + + Check_Fpu(); +} + +Condenser::~Condenser() +{ +} + +Logical + Condenser::TestClass(Mech &) +{ + return True; +} + +Logical + Condenser::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// RefrigerationSimulation -- the condenser's per-frame step (binary @4ae4d8): +// recompute the refrigeration output as (1 - own structural damage) * +// refrigerationFactor, clamped >= 1, into the inherited massScale slot -- an +// undamaged condenser behaves refrigerationFactor-times "hotter" in the +// conduction exchange, actively pumping heat toward the central bank and +// chilling itself below ambient -- then run the base HeatSink step. +//############################################################################# +// +void + Condenser::RefrigerationSimulation(Scalar time_slice) +{ + Check(this); + + massScale = (1.0f - GetSubsystemDamageLevel()) * refrigerationFactor; + if (massScale < 1.0f) + { + massScale = 1.0f; + } + + HeatSink::HeatSinkSimulation(time_slice); +} + +// +//############################################################################# +// MoveValveMessageHandler -- the cockpit condenser-valve button (binary +// @4ae464, id 4): novice-locked, press-only; cycles THIS condenser's valve +// 1 -> 5 -> 50 -> 0 -> 1, then recomputes every condenser's flow share. +//############################################################################# +// +void + Condenser::MoveValveMessageHandler(ReceiverDataMessageOf *message) +{ + Check(this); + Check(message); + + if (NoviceLockout()) + { + return; + } + if (message->dataContents <= 0) // press only + { + return; + } + + switch (valveState) + { + case 0: valveState = 1; break; + case 1: valveState = 5; break; + case 5: valveState = 50; break; + case 50: valveState = 0; break; + default: return; // unknown -> no change, no recompute + } + + RecomputeValves((Entity *)owner); +} + +// +//############################################################################# +// RecomputeValves -- assign every condenser its coolant flow share +// (valve / sum-of-valves) after a valve move (binary @0049f788). All valves +// at the spawn default (1) yield equal shares. (The condenser-alarm change +// pulse joins with the gauge wave.) +//############################################################################# +// +void + Condenser::RecomputeValves(Entity *owner_mech) +{ + Check(owner_mech); + + int count = owner_mech->GetSubsystemCount(); + int total = 0; + int i; + + for (i = 2; i < count; ++i) + { + Subsystem *s = owner_mech->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) + { + total += ((Condenser *)s)->valveState; + } + } + + for (i = 2; i < count; ++i) + { + Subsystem *s = owner_mech->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) + { + Condenser *condenser = (Condenser *)s; + condenser->coolantFlowScale = (total > 0) + ? ((Scalar)condenser->valveState / (Scalar)total) + : 0.0f; + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[valve] '" << condenser->GetName() + << "' valve=" << condenser->valveState + << " flow=" << condenser->coolantFlowScale << endl; + } + } + } + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << flush; + } +} + +//########################################################################### +//######################### AggregateHeatSink ########################### +//########################################################################### + +Derivation + AggregateHeatSink::ClassDerivations( + HeatSink::ClassDerivations, + "HeatSinkBank" + ); + +AggregateHeatSink::SharedData + AggregateHeatSink::DefaultData( + AggregateHeatSink::ClassDerivations, + HeatSink::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// The heat-sink bank (binary ctor @4ae8d0): the aggregate count scales the +// conductance (0.1 x count, byte-verified), the ambient setpoint defaults +// 300 K (the mission's [mission] temperature overwrites it in the PlayerLink +// pass -- that override joins the Mech-PlayerLink wave), and a master runs +// the RADIATOR Performance instead of the base heat step. +//############################################################################# +// +AggregateHeatSink::AggregateHeatSink( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + HeatSink(owner, subsystem_ID, subsystem_resource, shared_data) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + heatSinkCount = subsystem_resource->heatSinkCount; + ambientTemperature = 300.0f; + + thermalConductance = 0.1f * (Scalar)heatSinkCount * thermalConductance; + + if (getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[bank] '" << GetName() + << "' heatSinkCount=" << heatSinkCount + << " conductance=" << thermalConductance << endl << flush; + } + + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&AggregateHeatSink::RadiatorSimulation); + } + + Check_Fpu(); +} + +AggregateHeatSink::~AggregateHeatSink() +{ +} + +Logical + AggregateHeatSink::TestClass(Mech &) +{ + return True; +} + +Logical + AggregateHeatSink::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// RadiatorSimulation -- the bank's per-frame step (binary @4ae73c), replacing +// the base HeatSinkSimulation: under the heat-model gate, absorb + recompute +// the load, then the AMBIENT RADIATOR -- relax the bank toward the setpoint +// target (300 - (300 - ambient) x 3) with rate k = conductance x (1 - own +// damage) x (coolant/capacity) x flowScale / mass. This is the system's ONLY +// heat exit. Tail: top the bank's coolant up from the attached store via the +// DrawCoolant virtual (the reservoir-attach routing joins that wave; the base +// supplies 0 until then, a harmless no-op). +//############################################################################# +// +void + AggregateHeatSink::RadiatorSimulation(Scalar time_slice) +{ + Check(this); + + if (HeatModelActive()) + { + heatEnergy += pendingHeat; + currentTemperature = heatEnergy / thermalMass; + UpdateHeatLoad(); + pendingHeat = 0.0f; + + Scalar target = 300.0f - (300.0f - ambientTemperature) * 3.0f; + Scalar decay = 1.0f - (Scalar)exp( + (double)(-(time_slice * thermalConductance + * (1.0f - GetSubsystemDamageLevel()) + * (coolantLevel / thermalCapacity) * coolantFlowScale) + / thermalMass) + ); + Scalar shed = -((currentTemperature * massScale - target) + * thermalMass * decay); + pendingHeat += shed; + + if (getenv("BT_HEAT_LOG")) + { + static Scalar bankAccum = 0.0f; + bankAccum += time_slice; + if (bankAccum >= 5.0f) + { + bankAccum = 0.0f; + DEBUG_STREAM << "[heat-t] " << GetName() << " (bank)" + << " T=" << currentTemperature + << " shed=" << shed + << " cool=" << coolantLevel << "/" << thermalCapacity + << " load=" << heatLoad << endl << flush; + } + } + } + + // + // Coolant top-up from the attached store. + // + Scalar deficit = thermalCapacity - coolantLevel; + if (deficit > 1.0e-4f) + { + coolantLevel += DrawCoolant(deficit); + } +} diff --git a/restoration/source410/BT/HEAT.HPP b/restoration/source410/BT/HEAT.HPP index 48e7bf14..85fcbcd8 100644 --- a/restoration/source410/BT/HEAT.HPP +++ b/restoration/source410/BT/HEAT.HPP @@ -1,495 +1,508 @@ -//===========================================================================// -// File: heat.hpp // -// Project: BattleTech // -// Contents: Implementation details for heatable subsystems // -//---------------------------------------------------------------------------// -// Date Who Modification // -// -------- --- ---------------------------------------------------------- // -// // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(HEAT_HPP) -# define HEAT_HPP - -# if !defined(MECHSUB_HPP) -# include -# endif - -# if !defined(AVERAGE_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - -//########################################################################### -//################## HeatableSubsystem Model Resource ################### -//########################################################################### - - struct HeatableSubsystem__SubsystemResource: - public MechSubsystem::SubsystemResource - { - Scalar startingTemperature; - Scalar degradationTemperature; - Scalar failureTemperature; - Scalar thermalConductance; - Scalar thermalMass; - }; - -//########################################################################### -//######################## HeatableSubsystem ############################ -//########################################################################### - - class HeatableSubsystem: - public MechSubsystem - { - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test Class Support - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef HeatableSubsystem__SubsystemResource SubsystemResource; - - HeatableSubsystem( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - - ~HeatableSubsystem(); - - static int - CreateStreamedSubsystem( - NotationFile *model_file, - const char *model_name, - const char *subsystem_name, - SubsystemResource *subsystem_resource, - NotationFile *subsystem_file, - const ResourceDirectories *directories, - int passes = 1 - ); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - protected: - Scalar currentTemperature; - Scalar heatLoad; - Scalar degradationTemperature; - Scalar failureTemperature; - }; - -//########################################################################### -//########################## SubsystemConnection ######################## -//########################################################################### -// -// A slot linking a heat sink to its master / linked heat sink. -// - class SubsystemConnection - { - public: - SubsystemConnection() { linked = NULL; } - void Add(Subsystem *s) { linked = s; } - Subsystem* Resolve() const { return linked; } - void Clear() { linked = NULL; } - protected: - Subsystem *linked; - int reserved[2]; - }; - -//########################################################################### -//####################### HeatSink Model Resource ####################### -//########################################################################### - - struct HeatSink__SubsystemResource: - public HeatableSubsystem__SubsystemResource - { - // - // WIRE-VERIFIED (raw-stream dump): the roster index of the sink this - // one conducts its heat into -- the weapons/equipment link to the - // Condenser bank (slots 4-9), the Condensers to the central HeatSink. - // This was THE missing ancestry int that shifted every descendant - // resource block +1 (PoweredSubsystem voltageSourceIndex, the MechWeapon - // block, ...). - // - int linkedSinkIndex; - }; - -//########################################################################### -//############################## HeatSink ############################### -//########################################################################### - -//########################################################################### -//################### HeatWatcher Model Resource ####################### -//########################################################################### - - struct HeatWatcher__SubsystemResource: - public MechSubsystem__SubsystemResource - { - int watchedSubsystem; - Scalar degradationTemperature; - Scalar failureTemperature; - }; - -//########################################################################### -//############################# HeatWatcher ############################# -//########################################################################### -// -// Watches another subsystem's temperature and drives a 3-level alarm. A -// sibling branch to HeatableSubsystem (derives straight from MechSubsystem); -// PowerWatcher and the Torso/HUD/Gyroscope leaves descend from it. -// - class HeatWatcher: - public MechSubsystem - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(Logical powered); - void - WatchSimulation(Scalar time_slice); - - public: - typedef HeatWatcher__SubsystemResource SubsystemResource; - - HeatWatcher( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - ~HeatWatcher(); - - protected: - SubsystemConnection watchedLink; - int watchedSubsystem; - Scalar degradationTemperature; - Scalar failureTemperature; - AlarmIndicator heatAlarm; - }; - - class HeatSink: - public HeatableSubsystem - { - friend class Condenser; - friend class Reservoir; - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test Class Support - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(Logical powered); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Messaging Support. The cockpit cooling toggle rides the first - // subsystem-family message slot (id 3 = Receiver::NextMessageID); the - // per-class id-4 slot is claimed by each subclass (Condenser MoveValve, - // Reservoir InjectCoolant, ...). - // - public: - enum { - ToggleCoolingMessageID = Receiver::NextMessageID, // 3 - NextMessageID // 4 - }; - - static const HandlerEntry MessageHandlerEntries[]; - static MessageHandlerSet MessageHandlers; - - void - ToggleCoolingMessageHandler(ReceiverDataMessageOf *message); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Heat state (carried in heatAlarm, 3 levels). The thresholds are the - // authored degradation/failure temperatures. - // - public: - enum { - NormalHeat = 0, - DegradationHeat, - FailureHeat, - HeatStateCount - }; - - unsigned - GetHeatState() { Check(this); return heatAlarm.GetLevel(); } - Scalar - CurrentTemperatureOf() { Check(this); return currentTemperature; } - Scalar - StartingTemperatureOf() { Check(this); return startingTemperature; } - void - AddPendingHeat(Scalar heat) - { Check(this); pendingHeat += heat; } - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Simulation Support - // - public: - typedef void - (HeatSink::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - - // - // The heat-model master switch (binary FUN_004ad7d4): the owning - // BTPlayer's heatModelOn experience flag, reached through the mech's - // playerLink. ON only for veteran / expert; a NULL player reads ON. - // - Logical - HeatModelActive(); - - void - HeatSinkSimulation(Scalar time_slice); - void - UpdateHeatLoad(); - void - ConductHeat(Scalar time_slice); - Scalar - ComputeHeatFlow(HeatSink *other, Scalar time_slice); - void - UpdateCoolant(Scalar time_slice); - void - BalanceCoolant(Scalar time_slice); - virtual Scalar - DrawCoolant(Scalar requested); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef HeatSink__SubsystemResource SubsystemResource; - - HeatSink( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - - ~HeatSink(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - protected: - Scalar coolantEfficiency; - Scalar thermalCapacity; - Scalar coolantLevel; - Scalar coolantDraw; - int coolantAvailable; - int coolantActive; - Scalar startingTemperature; - Scalar thermalConductance; - Scalar filterDecay; - Scalar thermalMass; - Scalar heatEnergy; - Scalar coolantFlowScale; - Scalar massScale; - Scalar pendingHeat; - Scalar radiatedHeat; - SubsystemConnection linkedSinks; - AlarmIndicator heatAlarm; - AverageOf heatFilter; - }; - -//########################################################################### -//#################### Condenser Model Resource ######################## -//########################################################################### - - struct Condenser__SubsystemResource: - public HeatSink__SubsystemResource - { - Scalar refrigerationFactor; - }; - -//########################################################################### -//############################## Condenser ############################# -//########################################################################### -// -// A HeatSink with active refrigeration (a coolant loop's condenser). -// - class Condenser: - public HeatSink - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Messaging Support: the cockpit condenser-valve button (binary handler - // table @0x50E52C, exactly one entry -- id 4, "MoveValve"). Each press - // cycles this condenser's valve 1 -> 5 -> 50 -> 0 -> 1 and recomputes every - // condenser's coolant flow share (valve / sum-of-valves). - // - public: - enum { - MoveValveMessageID = HeatSink::NextMessageID, // 4 - NextMessageID - }; - - static const HandlerEntry MessageHandlerEntries[]; - static MessageHandlerSet MessageHandlers; - - void - MoveValveMessageHandler(ReceiverDataMessageOf *message); - - // - // Recompute every condenser's coolantFlowScale as its share of the - // total valve opening (called by MoveValve, and once at mech spawn so - // the initial all-1 valves yield equal shares). - // - static void - RecomputeValves(Entity *owner_mech); - - public: - typedef Condenser__SubsystemResource SubsystemResource; - - Condenser( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - ~Condenser(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Per-frame refrigeration (binary @4ae4d8): the condenser actively pumps - // heat toward the central bank -- massScale is recomputed each frame as - // (1 - own damage) * refrigerationFactor, clamped >= 1, so an undamaged - // condenser behaves refrigerationFactor-times "hotter" in the conduction - // exchange and chills itself below ambient (which is what cools the - // weapons equalizing against it). Damage degrades it toward a plain sink. - // - public: - typedef void - (Condenser::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - void - RefrigerationSimulation(Scalar time_slice); - - protected: - int valveState; - int condenserNumber; - Scalar refrigerationFactor; - }; - -//########################################################################### -//##################### AggregateHeatSink Resource ###################### -//########################################################################### - - struct AggregateHeatSink__SubsystemResource: - public HeatSink__SubsystemResource - { - int heatSinkCount; - }; - -//########################################################################### -//######################### AggregateHeatSink ########################### -//########################################################################### -// -// The mech's heat-sink BANK (binary classID 0x0BBE -- the value our VDATA -// enum names HeatSinkClassID; there is no streamed plain-HeatSink class). -// Holds the aggregate heat-sink count and the ambient-temperature setpoint, -// and runs the AMBIENT RADIATOR -- the only place heat ever LEAVES the mech. -// - class AggregateHeatSink: - public HeatSink - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - - public: - typedef AggregateHeatSink__SubsystemResource SubsystemResource; - - AggregateHeatSink( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - ~AggregateHeatSink(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // The radiator (binary @4ae73c) -- replaces the base HeatSinkSimulation on - // the bank: absorb, then relax toward the ambient setpoint (the heat EXIT), - // then top the bank's coolant up from the attached store. - // - public: - typedef void - (AggregateHeatSink::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - void - RadiatorSimulation(Scalar time_slice); - - int - GetHeatSinkCount() const { Check(this); return heatSinkCount; } - - protected: - int heatSinkCount; - Scalar ambientTemperature; - }; - -#endif +//===========================================================================// +// File: heat.hpp // +// Project: BattleTech // +// Contents: Implementation details for heatable subsystems // +//---------------------------------------------------------------------------// +// Date Who Modification // +// -------- --- ---------------------------------------------------------- // +// // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(HEAT_HPP) +# define HEAT_HPP + +# if !defined(MECHSUB_HPP) +# include +# endif + +# if !defined(AVERAGE_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + +//########################################################################### +//################## HeatableSubsystem Model Resource ################### +//########################################################################### + + struct HeatableSubsystem__SubsystemResource: + public MechSubsystem::SubsystemResource + { + Scalar startingTemperature; + Scalar degradationTemperature; + Scalar failureTemperature; + Scalar thermalConductance; + Scalar thermalMass; + }; + +//########################################################################### +//######################## HeatableSubsystem ############################ +//########################################################################### + + class HeatableSubsystem: + public MechSubsystem + { + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test Class Support + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef HeatableSubsystem__SubsystemResource SubsystemResource; + + HeatableSubsystem( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + + ~HeatableSubsystem(); + + static int + CreateStreamedSubsystem( + NotationFile *model_file, + const char *model_name, + const char *subsystem_name, + SubsystemResource *subsystem_resource, + NotationFile *subsystem_file, + const ResourceDirectories *directories, + int passes = 1 + ); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + protected: + Scalar currentTemperature; + Scalar heatLoad; + Scalar degradationTemperature; + Scalar failureTemperature; + }; + +//########################################################################### +//########################## SubsystemConnection ######################## +//########################################################################### +// +// A slot linking a heat sink to its master / linked heat sink. +// + class SubsystemConnection + { + public: + SubsystemConnection() { linked = NULL; } + void Add(Subsystem *s) { linked = s; } + Subsystem* Resolve() const { return linked; } + void Clear() { linked = NULL; } + protected: + Subsystem *linked; + int reserved[2]; + }; + +//########################################################################### +//####################### HeatSink Model Resource ####################### +//########################################################################### + + struct HeatSink__SubsystemResource: + public HeatableSubsystem__SubsystemResource + { + // + // WIRE-VERIFIED (raw-stream dump): the roster index of the sink this + // one conducts its heat into -- the weapons/equipment link to the + // Condenser bank (slots 4-9), the Condensers to the central HeatSink. + // This was THE missing ancestry int that shifted every descendant + // resource block +1 (PoweredSubsystem voltageSourceIndex, the MechWeapon + // block, ...). + // + int linkedSinkIndex; + }; + +//########################################################################### +//############################## HeatSink ############################### +//########################################################################### + +//########################################################################### +//################### HeatWatcher Model Resource ####################### +//########################################################################### + + struct HeatWatcher__SubsystemResource: + public MechSubsystem__SubsystemResource + { + int watchedSubsystem; + Scalar degradationTemperature; + Scalar failureTemperature; + }; + +//########################################################################### +//############################# HeatWatcher ############################# +//########################################################################### +// +// Watches another subsystem's temperature and drives a 3-level alarm. A +// sibling branch to HeatableSubsystem (derives straight from MechSubsystem); +// PowerWatcher and the Torso/HUD/Gyroscope leaves descend from it. +// + class HeatWatcher: + public MechSubsystem + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(Logical powered); + + // + // Respawn restore: base heal + the thermal state back to spawn + // (temperature/coolant via ResetToInitialState). + // + virtual void + DeathReset(Logical full_reset); + void + WatchSimulation(Scalar time_slice); + + public: + typedef HeatWatcher__SubsystemResource SubsystemResource; + + HeatWatcher( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + ~HeatWatcher(); + + protected: + SubsystemConnection watchedLink; + int watchedSubsystem; + Scalar degradationTemperature; + Scalar failureTemperature; + AlarmIndicator heatAlarm; + }; + + class HeatSink: + public HeatableSubsystem + { + friend class Condenser; + friend class Reservoir; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test Class Support + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(Logical powered); + + // + // Respawn restore: base heal + the thermal state back to spawn. + // + virtual void + DeathReset(Logical full_reset); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Messaging Support. The cockpit cooling toggle rides the first + // subsystem-family message slot (id 3 = Receiver::NextMessageID); the + // per-class id-4 slot is claimed by each subclass (Condenser MoveValve, + // Reservoir InjectCoolant, ...). + // + public: + enum { + ToggleCoolingMessageID = Receiver::NextMessageID, // 3 + NextMessageID // 4 + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + ToggleCoolingMessageHandler(ReceiverDataMessageOf *message); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Heat state (carried in heatAlarm, 3 levels). The thresholds are the + // authored degradation/failure temperatures. + // + public: + enum { + NormalHeat = 0, + DegradationHeat, + FailureHeat, + HeatStateCount + }; + + unsigned + GetHeatState() { Check(this); return heatAlarm.GetLevel(); } + Scalar + CurrentTemperatureOf() { Check(this); return currentTemperature; } + Scalar + StartingTemperatureOf() { Check(this); return startingTemperature; } + void + AddPendingHeat(Scalar heat) + { Check(this); pendingHeat += heat; } + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Simulation Support + // + public: + typedef void + (HeatSink::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + // + // The heat-model master switch (binary FUN_004ad7d4): the owning + // BTPlayer's heatModelOn experience flag, reached through the mech's + // playerLink. ON only for veteran / expert; a NULL player reads ON. + // + Logical + HeatModelActive(); + + void + HeatSinkSimulation(Scalar time_slice); + void + UpdateHeatLoad(); + void + ConductHeat(Scalar time_slice); + Scalar + ComputeHeatFlow(HeatSink *other, Scalar time_slice); + void + UpdateCoolant(Scalar time_slice); + void + BalanceCoolant(Scalar time_slice); + virtual Scalar + DrawCoolant(Scalar requested); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef HeatSink__SubsystemResource SubsystemResource; + + HeatSink( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + + ~HeatSink(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + protected: + Scalar coolantEfficiency; + Scalar thermalCapacity; + Scalar coolantLevel; + Scalar coolantDraw; + int coolantAvailable; + int coolantActive; + Scalar startingTemperature; + Scalar thermalConductance; + Scalar filterDecay; + Scalar thermalMass; + Scalar heatEnergy; + Scalar coolantFlowScale; + Scalar massScale; + Scalar pendingHeat; + Scalar radiatedHeat; + SubsystemConnection linkedSinks; + AlarmIndicator heatAlarm; + AverageOf heatFilter; + }; + +//########################################################################### +//#################### Condenser Model Resource ######################## +//########################################################################### + + struct Condenser__SubsystemResource: + public HeatSink__SubsystemResource + { + Scalar refrigerationFactor; + }; + +//########################################################################### +//############################## Condenser ############################# +//########################################################################### +// +// A HeatSink with active refrigeration (a coolant loop's condenser). +// + class Condenser: + public HeatSink + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Messaging Support: the cockpit condenser-valve button (binary handler + // table @0x50E52C, exactly one entry -- id 4, "MoveValve"). Each press + // cycles this condenser's valve 1 -> 5 -> 50 -> 0 -> 1 and recomputes every + // condenser's coolant flow share (valve / sum-of-valves). + // + public: + enum { + MoveValveMessageID = HeatSink::NextMessageID, // 4 + NextMessageID + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + MoveValveMessageHandler(ReceiverDataMessageOf *message); + + // + // Recompute every condenser's coolantFlowScale as its share of the + // total valve opening (called by MoveValve, and once at mech spawn so + // the initial all-1 valves yield equal shares). + // + static void + RecomputeValves(Entity *owner_mech); + + public: + typedef Condenser__SubsystemResource SubsystemResource; + + Condenser( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + ~Condenser(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame refrigeration (binary @4ae4d8): the condenser actively pumps + // heat toward the central bank -- massScale is recomputed each frame as + // (1 - own damage) * refrigerationFactor, clamped >= 1, so an undamaged + // condenser behaves refrigerationFactor-times "hotter" in the conduction + // exchange and chills itself below ambient (which is what cools the + // weapons equalizing against it). Damage degrades it toward a plain sink. + // + public: + typedef void + (Condenser::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + void + RefrigerationSimulation(Scalar time_slice); + + protected: + int valveState; + int condenserNumber; + Scalar refrigerationFactor; + }; + +//########################################################################### +//##################### AggregateHeatSink Resource ###################### +//########################################################################### + + struct AggregateHeatSink__SubsystemResource: + public HeatSink__SubsystemResource + { + int heatSinkCount; + }; + +//########################################################################### +//######################### AggregateHeatSink ########################### +//########################################################################### +// +// The mech's heat-sink BANK (binary classID 0x0BBE -- the value our VDATA +// enum names HeatSinkClassID; there is no streamed plain-HeatSink class). +// Holds the aggregate heat-sink count and the ambient-temperature setpoint, +// and runs the AMBIENT RADIATOR -- the only place heat ever LEAVES the mech. +// + class AggregateHeatSink: + public HeatSink + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + public: + typedef AggregateHeatSink__SubsystemResource SubsystemResource; + + AggregateHeatSink( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + ~AggregateHeatSink(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // The radiator (binary @4ae73c) -- replaces the base HeatSinkSimulation on + // the bank: absorb, then relax toward the ambient setpoint (the heat EXIT), + // then top the bank's coolant up from the attached store. + // + public: + typedef void + (AggregateHeatSink::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + void + RadiatorSimulation(Scalar time_slice); + + int + GetHeatSinkCount() const { Check(this); return heatSinkCount; } + + protected: + int heatSinkCount; + Scalar ambientTemperature; + }; + +#endif diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 03ba7034..ca2de873 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -54,6 +54,7 @@ #include // EntitySegment -- the skeleton segment table #include // Mech::DamageZone -- the hull zone fill (Pass 3) #include // DamageLookupTable -- the cylinder hit table +#include // Player::VehicleDeadMessage -- the death notification // //############################################################################# @@ -189,6 +190,7 @@ Mech::Mech( targetEntity = NULL; lastInflictingID = EntityID::Null; damageLookupTable = NULL; + deathTransitionDone = 0; // // Look-view angles: defaults until the GameModel read below overrides them @@ -687,6 +689,80 @@ void subsystemArray[0] = subsystem; } +// +//############################################################################# +// Reset -- the respawn heal-and-move (binary @0049fb74). REUSES the same +// entity (the sever-and-recreate respawn was the source of the 1995 port's +// "two mechs / camera inside / on-fire respawn" glitch family): +// 1. reposition at the drop zone (origin + transform + update base) +// 2. kill all motion (a respawn is a TELEPORT, no dead-reckon lerp back) +// 3. clear the death latch (the alarm trigger + the once-per-death flag) +// 4. heal every hull zone (structure 0, intact skin, not burning) -- +// the crit-allotment accountant (damagePercentageUsed) PERSISTS by +// design: nothing in the recovered binary resets it across lives +// (BT411-observed; spent crit budgets stay spent) +// 5. sweep the roster through DeathReset (heat/power/ammo/charge restore) +// 6. revalidate for the sim (PreRun). +// The ForceUpdate(0x1f) re-broadcast + warp/alarm effects join the render / +// cockpit waves. +//############################################################################# +// +void + Mech::Reset(const Origin &origin, Logical full_reset) +{ + Check(this); + + localOrigin = origin; + localToWorld = origin; + updateOrigin = origin; + + worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); + localVelocity = Motion::Identity; + updateVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f); + updateVelocity.angularMotion = Vector3D(0.0f, 0.0f, 0.0f); + currentBodySpeed = 0.0f; + bodyTargetSpeed = 0.0f; + + statusAlarm.SetLevel(0); + deathTransitionDone = 0; + + { + for (int dz = 0; dz < damageZoneCount; ++dz) + { + if (damageZones[dz] != NULL) + { + ::DamageZone *zone = (::DamageZone *)damageZones[dz]; + zone->damageLevel = 0.0f; + zone->SetGraphicState(0); + zone->SetDamageZoneState(0); + } + } + } + + { + for (int ss = 0; ss < subsystemCount; ++ss) + { + if (subsystemArray[ss] != NULL) + { + subsystemArray[ss]->DeathReset(full_reset); + } + } + } + + SetPreRunFlag(); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[reset] mech " << GetEntityID() + << " HEALED + placed at (" + << origin.linearPosition.x << "," + << origin.linearPosition.y << "," + << origin.linearPosition.z << ")" << endl << flush; + } + + Check_Fpu(); +} + // //############################################################################# // CurrentTorsoTwist -- the live torso twist for the cylinder table's @@ -886,6 +962,71 @@ void { Check(this); + // + //----------------------------------------------------------------------- + // DEATH (binary UpdateDeathState @mech4, the once-per-death transition): + // a destroyed mech FREEZES -- no drive, no eyepoint, no dev harness (the + // weapon hard gates silence the guns state-side). The one-shot: sweep + // the roster through DeathShutdown(1) and dispatch the VehicleDead + // death notification (deathCount = -1, the ctor default) to the owning + // player -- only the owner master has a live playerLink; an unowned + // wreck (the dev enemy) just settles. + //----------------------------------------------------------------------- + // + if (IsMechDestroyed()) + { + if (!deathTransitionDone) + { + deathTransitionDone = 1; + + // + // A wreck is STILL: kill the broadcastable motion so replicant + // wrecks don't dead-reckon away at the last drive vector. + // + worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); + updateVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f); + updateVelocity.angularMotion = Vector3D(0.0f, 0.0f, 0.0f); + currentBodySpeed = 0.0f; + bodyTargetSpeed = 0.0f; + + for (int ds = 0; ds < subsystemCount; ++ds) + { + if (subsystemArray[ds] != NULL) + { + subsystemArray[ds]->DeathShutdown(1); + } + } + + // + // The death notification is MASTER-only: our + // InitializePlayerLink binds replicant mechs to replicant + // players too, and a replicant dispatch would run a second, + // non-authoritative death cycle on every remote pod. + // + Player *pilot = (GetInstance() != Entity::ReplicantInstance) + ? GetPlayerLink() : NULL; + if (pilot != NULL) + { + Player::VehicleDeadMessage + dead( + Player::VehicleDeadMessageID, + sizeof(Player::VehicleDeadMessage) + ); + pilot->Dispatch(&dead); + } + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[death] mech " << GetEntityID() + << " WRECKED (pilot " + << ((pilot != NULL) ? "notified" : "none") + << ")" << endl << flush; + } + } + Check_Fpu(); + return; + } + // //----------------------------------------------------------------------- // Read the control-mapper locomotion demands. The mapper lives at roster diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index 20473b7a..218445ae 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -224,6 +224,15 @@ void Simulate(Scalar time_slice); + // + // The respawn heal-and-move (binary @0049fb74): reposition the SAME + // entity at the drop-zone origin, kill all motion, clear the death + // latch, heal every hull zone and sweep the roster through + // DeathReset. Shared by the initial drop-in and the respawn. + // + void + Reset(const Origin &origin, Logical full_reset); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Resource creation // @@ -446,6 +455,8 @@ Entity *targetEntity; EntityID lastInflictingID; // binary mech+0x43c -- last // attacker (zone LOD router) + int deathTransitionDone; // the once-per-death latch + // (binary movementMode 2||9) DamageLookupTable *damageLookupTable; // binary mech+0x444 -- the // cylinder hit-location table @@ -462,7 +473,7 @@ // mech2/mech3/mech4 simulation. Reserved until that path is // reconstructed with named fields. // - int reservedState[198]; + int reservedState[197]; }; #endif diff --git a/restoration/source410/BT/MECH.NOTES.md b/restoration/source410/BT/MECH.NOTES.md index af9021f6..f614bd4f 100644 --- a/restoration/source410/BT/MECH.NOTES.md +++ b/restoration/source410/BT/MECH.NOTES.md @@ -393,3 +393,37 @@ Found while verifying the novice-mode heat lockout end-to-end. - `friend class Mech__DamageZone` (BT411 mech.hpp:301 — authentic): the per-zone damage code walks the roster / segment table / statusAlarm. - `IsMechDestroyed()` = statusAlarm (the @0x714 body-graphic alarm) >= 9. + +## 5.3.24 — the death -> respawn cycle (workflow-researched + adversarially reviewed) + +- Once-per-death transition at the top of Mech::Simulate (binary + UpdateDeathState): a destroyed mech FREEZES (early return -- no drive, + eyepoint, or harness), zeroes broadcastable motion (replicant wrecks must + not dead-reckon away), sweeps the roster through DeathShutdown(1), and -- + MASTER-only (our InitializePlayerLink binds replicants too, unlike the + binary) -- dispatches Player::VehicleDeadMessage (deathCount = -1, the + ctor default) to the player link. An unowned wreck (dev enemy) settles + silently. deathTransitionDone = the binary's movementMode 2||9 latch + (gait wave replaces it). +- Mech::Reset(origin, full) (binary @0049fb74): REUSES the same entity -- + reposition + motion kill + latch clear + hull-zone heal + the roster + DeathReset sweep + SetPreRunFlag. The crit-allotment accountant + (damagePercentageUsed) PERSISTS by design (BT411-observed: nothing in the + recovered binary resets it). ForceUpdate(0x1f)/warp/alarm = render+cockpit + waves. Deferred: in-flight Seekers keep the raw target pointer across a + respawn (a long-range round can chase the healed mech to the drop zone -- + matches the recovered seeker's shape; a life-check joins the missile wave). +- Weapon hard gates gained the dead-owner term (emitter @4baab9 third + clause): wrecks fall silent state-side. Dead-TARGET beam cut = render wave. +- ADVERSARIAL REVIEW CATCHES (3-lens workflow, all fixed): AmmoBin restock + read the FREED padBuffer through MechSubsystem::resource (use-after-free; + now initialAmmoCount -- **resource is a LANDMINE: never deref post-ctor**); + MechWeapon::DeathReset now chains the FULL powered/thermal restore (died- + hot mechs no longer respawn at FailureHeat); Sensor chains the base heal; + Generator preserves currentTapCount across reset (consumers stay tapped); + PowerWatcher got its own DeathReset (non-virtual ResetToInitialState + binds statically); cooling toggle + modeAlarm restored. +- VERIFIED (killfight, BT_FORCE_ZONE=10): kill -> [vdead] +5s post -> engine + drop-zone hunt -> AssignDropZone -> Reply(deathCount) -> [reset] at a REAL + drop zone -> [respawn]; 0 shots while dead, 134 after respawn, 12 SRM + fires post-respawn (restock proof); enemy wreck stays silent; zero faults. diff --git a/restoration/source410/BT/MECHSUB.CPP b/restoration/source410/BT/MECHSUB.CPP index 4927e263..5b4f05c0 100644 --- a/restoration/source410/BT/MECHSUB.CPP +++ b/restoration/source410/BT/MECHSUB.CPP @@ -1,310 +1,338 @@ -//===========================================================================// -// File: mechsub.cpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: Implementation details for the MechSubsystem base // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(MECHSUB_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -#if !defined(DAMAGE_HPP) -# include -#endif - -#if !defined(BTPLAYER_HPP) -# include -#endif - -#if !defined(BTMSSN_HPP) -# include -#endif - -// -//############################################################################# -// Shared data support -- MechSubsystem adds no message handlers or attributes -// that are boot-critical, so it reuses the base Subsystem sets (which are the -// inherited Simulation sets). -//############################################################################# -// -Derivation - MechSubsystem::ClassDerivations( - Subsystem::ClassDerivations, - "MechSubsystem" - ); - -MechSubsystem::SharedData - MechSubsystem::DefaultData( - MechSubsystem::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -// Light constructor (name + classID). For a subsystem created without a -// streamed resource; builds a trivial (armour-free) damage zone named by the -// base. -//############################################################################# -// -MechSubsystem::MechSubsystem( - Mech *owner, - int subsystem_ID, - const char *subsystem_name, - RegisteredClass::ClassID class_id, - SharedData &shared_data -): - Subsystem(owner, subsystem_ID, subsystem_name, class_id, shared_data) -{ - this->owner = owner; - vitalSubsystem = False; - alarmModel = ResourceDescription::NullResourceID; - criticalReference = 0.0f; - collisionCriticalHitWeight = 0.0f; - printSimulationState = False; - configureActivePress = -1; - resource = NULL; - statusAlarm.SetLevel(0); - - // - // The base Subsystem ctor leaves damageZone NULL; give this subsystem a - // trivial (armour-free) zone. - // - damageZone = new DamageZone(owner, 0); - Register_Object(damageZone); -} - -// -//############################################################################# -// Resource constructor. Chains the base Subsystem resource ctor and seeds the -// mech-specific fields from the subsystem resource. -// -// The 4.10 damage model is per-damage-TYPE: the damage zone owns -// defaultArmorPoints + damageScale[5] and streams them itself (a -// Mech__DamageZone built from the model's damage-zone stream). Wiring that -// stream through from the resource is deferred (see MECHSUB.NOTES.md); until -// then the subsystem gets a trivial armour-free zone so it constructs and takes -// damage with default (unscaled) behaviour. -//############################################################################# -// -MechSubsystem::MechSubsystem( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - Subsystem(owner, subsystem_ID, subsystem_resource, shared_data) -{ - Check_Pointer(subsystem_resource); - - this->owner = owner; - resource = subsystem_resource; - vitalSubsystem = (subsystem_resource->vitalSubsystemIndex == 1); - alarmModel = subsystem_resource->alarmModel; - printSimulationState = subsystem_resource->printSimulationState; - criticalReference = subsystem_resource->criticalReference; - collisionCriticalHitWeight = subsystem_resource->collisionCriticalHitWeight; - configureActivePress = -1; - statusAlarm.SetLevel(0); - - // - // The subsystem's PRIVATE damage zone (binary @4abf28: `new - // DamageZone(this, 0)` -- owned by the SUBSYSTEM, index 0, NOT an entry - // in the entity's hull-zone array). The resource authors its armor the - // same way the hull stream does -- structureReference points + - // armorByFacing[5] per-type values -- normalized with the same rule as - // Mech::DamageZone (scale = 1/(raw x points); already-normalized cells - // pass through). This is what makes critical hits MEASURABLE - // (ApplyDamageAndMeasure) and the crit-allotment push meaningful. - // - damageZone = new DamageZone(this, 0); - Register_Object(damageZone); - damageZone->damageZoneName = GetName(); - if (subsystem_resource->structureReference > 0.0f) - { - damageZone->defaultArmorPoints = subsystem_resource->structureReference; - for (int tt = 0; tt < Damage::DamageTypeCount; ++tt) - { - Scalar raw = subsystem_resource->armorByFacing[tt]; - Scalar even = 1.0f / subsystem_resource->structureReference; - Scalar diff = raw - even; - if (diff < 0.0f) - { - diff = -diff; - } - if (diff > 1.0e-4f && raw > 0.0f) - { - damageZone->damageScale[tt] = - 1.0f / (raw * subsystem_resource->structureReference); - } - else - { - damageZone->damageScale[tt] = raw; - } - } - } -} - -// -//############################################################################# -//############################################################################# -// -MechSubsystem::~MechSubsystem() -{ - if (damageZone) - { - Unregister_Object(damageZone); - delete damageZone; - damageZone = NULL; - } -} - -// -//############################################################################# -//############################################################################# -// -Logical - MechSubsystem::TestClass(Mech &) -{ - return True; -} - -Logical - MechSubsystem::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// Damage support -- the 4.10 damage zone tracks damageLevel in [0,1] -// (0 = intact, 1 = destroyed). -//############################################################################# -// -Scalar - MechSubsystem::GetSubsystemDamageLevel() const -{ - Check(this); - return damageZone ? damageZone->damageLevel : 0.0f; -} - -void - MechSubsystem::SetSubsystemDamageLevel(Scalar level) -{ - Check(this); - if (damageZone) - { - damageZone->damageLevel = level; - } -} - -void - MechSubsystem::ForceCriticalFailure() -{ - Check(this); - statusAlarm.SetLevel(MechSubsystem::Destroyed); - // - // DestroyedState is the hard-failure gate the weapon simulations poll - // (`GetSimulationState() == 1`): a destroyed weapon drops its charge / - // pins the unavailable alarm from the next frame on. - // - SetSimulationState(DestroyedState); - if (damageZone) - { - damageZone->SetDamageZoneState(DamageZone::BurningState); - } -} - -// -//############################################################################# -// ApplyDamageAndMeasure (binary @4ac07c): apply through the virtual -// TakeDamage and report the zone-level delta -- the critical accountant -// (Mech::DamageZone::CriticalHit) charges the delta against the entry's -// damagePercentage allotment. -//############################################################################# -// -Scalar - MechSubsystem::ApplyDamageAndMeasure(Damage &damage) -{ - Check(this); - Scalar before = (damageZone != NULL) ? damageZone->damageLevel : 0.0f; - TakeDamage(damage); - Scalar after = (damageZone != NULL) ? damageZone->damageLevel : 0.0f; - return after - before; -} - -// -//############################################################################# -// TakeDamage Forward to the damage zone (per-type scaling happens there). -//############################################################################# -// -void - MechSubsystem::TakeDamage(Damage &damage) -{ - Check(this); - Check_Pointer(&damage); - if (damageZone) - { - damageZone->TakeDamage(damage); - } -} - -// -//############################################################################# -// CreateStreamedSubsystem Model-load-time construction of the subsystem -// resource + its damage-zone stream. Not yet reconstructed (see -// MECHSUB.NOTES.md -- the 4.10 per-type damage-zone stream format). -//############################################################################# -// -int - MechSubsystem::CreateStreamedSubsystem( - NotationFile *, - const char *, - const char *, - SubsystemResource *, - NotationFile *, - const ResourceDirectories *, - int - ) -{ - Fail("MechSubsystem::CreateStreamedSubsystem -- mechsub.cpp not yet reconstructed"); - return 0; -} - -// -//############################################################################# -// NoviceLockout -- the novice-experience lockout predicate (binary @4ac9c8): -// owner mech -> Entity::playerLink -> the BTPlayer's experience level == 0. -// Locks the advanced cockpit controls (condenser valves, coolant flush, -// cooling toggles). An unlinked mech reads UNLOCKED (dev-permissive). -//############################################################################# -// -Logical - MechSubsystem::NoviceLockout() -{ - Check(this); - - if (owner == NULL) - { - return False; - } - BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); - if (player == NULL) - { - return False; - } - return (player->GetExperienceLevel() == BTMission::NoviceMode) - ? True : False; -} +//===========================================================================// +// File: mechsub.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Implementation details for the MechSubsystem base // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(MECHSUB_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(DAMAGE_HPP) +# include +#endif + +#if !defined(BTPLAYER_HPP) +# include +#endif + +#if !defined(BTMSSN_HPP) +# include +#endif + +// +//############################################################################# +// Shared data support -- MechSubsystem adds no message handlers or attributes +// that are boot-critical, so it reuses the base Subsystem sets (which are the +// inherited Simulation sets). +//############################################################################# +// +Derivation + MechSubsystem::ClassDerivations( + Subsystem::ClassDerivations, + "MechSubsystem" + ); + +MechSubsystem::SharedData + MechSubsystem::DefaultData( + MechSubsystem::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// Light constructor (name + classID). For a subsystem created without a +// streamed resource; builds a trivial (armour-free) damage zone named by the +// base. +//############################################################################# +// +MechSubsystem::MechSubsystem( + Mech *owner, + int subsystem_ID, + const char *subsystem_name, + RegisteredClass::ClassID class_id, + SharedData &shared_data +): + Subsystem(owner, subsystem_ID, subsystem_name, class_id, shared_data) +{ + this->owner = owner; + vitalSubsystem = False; + alarmModel = ResourceDescription::NullResourceID; + criticalReference = 0.0f; + collisionCriticalHitWeight = 0.0f; + printSimulationState = False; + configureActivePress = -1; + resource = NULL; + statusAlarm.SetLevel(0); + + // + // The base Subsystem ctor leaves damageZone NULL; give this subsystem a + // trivial (armour-free) zone. + // + damageZone = new DamageZone(owner, 0); + Register_Object(damageZone); +} + +// +//############################################################################# +// Resource constructor. Chains the base Subsystem resource ctor and seeds the +// mech-specific fields from the subsystem resource. +// +// The 4.10 damage model is per-damage-TYPE: the damage zone owns +// defaultArmorPoints + damageScale[5] and streams them itself (a +// Mech__DamageZone built from the model's damage-zone stream). Wiring that +// stream through from the resource is deferred (see MECHSUB.NOTES.md); until +// then the subsystem gets a trivial armour-free zone so it constructs and takes +// damage with default (unscaled) behaviour. +//############################################################################# +// +MechSubsystem::MechSubsystem( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + Subsystem(owner, subsystem_ID, subsystem_resource, shared_data) +{ + Check_Pointer(subsystem_resource); + + this->owner = owner; + resource = subsystem_resource; + vitalSubsystem = (subsystem_resource->vitalSubsystemIndex == 1); + alarmModel = subsystem_resource->alarmModel; + printSimulationState = subsystem_resource->printSimulationState; + criticalReference = subsystem_resource->criticalReference; + collisionCriticalHitWeight = subsystem_resource->collisionCriticalHitWeight; + configureActivePress = -1; + statusAlarm.SetLevel(0); + + // + // The subsystem's PRIVATE damage zone (binary @4abf28: `new + // DamageZone(this, 0)` -- owned by the SUBSYSTEM, index 0, NOT an entry + // in the entity's hull-zone array). The resource authors its armor the + // same way the hull stream does -- structureReference points + + // armorByFacing[5] per-type values -- normalized with the same rule as + // Mech::DamageZone (scale = 1/(raw x points); already-normalized cells + // pass through). This is what makes critical hits MEASURABLE + // (ApplyDamageAndMeasure) and the crit-allotment push meaningful. + // + damageZone = new DamageZone(this, 0); + Register_Object(damageZone); + damageZone->damageZoneName = GetName(); + if (subsystem_resource->structureReference > 0.0f) + { + damageZone->defaultArmorPoints = subsystem_resource->structureReference; + for (int tt = 0; tt < Damage::DamageTypeCount; ++tt) + { + Scalar raw = subsystem_resource->armorByFacing[tt]; + Scalar even = 1.0f / subsystem_resource->structureReference; + Scalar diff = raw - even; + if (diff < 0.0f) + { + diff = -diff; + } + if (diff > 1.0e-4f && raw > 0.0f) + { + damageZone->damageScale[tt] = + 1.0f / (raw * subsystem_resource->structureReference); + } + else + { + damageZone->damageScale[tt] = raw; + } + } + } +} + +// +//############################################################################# +//############################################################################# +// +MechSubsystem::~MechSubsystem() +{ + if (damageZone) + { + Unregister_Object(damageZone); + delete damageZone; + damageZone = NULL; + } +} + +// +//############################################################################# +//############################################################################# +// +Logical + MechSubsystem::TestClass(Mech &) +{ + return True; +} + +Logical + MechSubsystem::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// Damage support -- the 4.10 damage zone tracks damageLevel in [0,1] +// (0 = intact, 1 = destroyed). +//############################################################################# +// +Scalar + MechSubsystem::GetSubsystemDamageLevel() const +{ + Check(this); + return damageZone ? damageZone->damageLevel : 0.0f; +} + +void + MechSubsystem::SetSubsystemDamageLevel(Scalar level) +{ + Check(this); + if (damageZone) + { + damageZone->damageLevel = level; + } +} + +void + MechSubsystem::ForceCriticalFailure() +{ + Check(this); + // + // Binary @4ac8xx family: the status alarm's DESTROYED display level is 1 + // (the TechStatusType enum's Destroyed=0 is the display CATEGORY, not + // the alarm level -- BT411 disasm: "+0x2C: 1 = Destroyed"). + // + statusAlarm.SetLevel(1); + // + // DestroyedState is the hard-failure gate the weapon simulations poll + // (`GetSimulationState() == 1`): a destroyed weapon drops its charge / + // pins the unavailable alarm from the next frame on. + // + SetSimulationState(DestroyedState); + if (damageZone) + { + damageZone->SetDamageZoneState(DamageZone::BurningState); + } +} + +// +//############################################################################# +// DeathReset -- the respawn restore (the engine base virtual pair's reset +// half; Mech::Reset @0049fb74 loops every subsystem through it). The base: +// heal the private zone, clear the Destroyed display level and -- the +// load-bearing part -- clear DestroyedState so the weapon hard gates +// (GetSimulationState() == 1) release. +//############################################################################# +// +void + MechSubsystem::DeathReset(Logical /*full_reset*/) +{ + Check(this); + + if (damageZone != NULL) + { + damageZone->damageLevel = 0.0f; + damageZone->SetDamageZoneState(0); + } + statusAlarm.SetLevel(0); + SetSimulationState(Simulation::DefaultState); +} + +// +//############################################################################# +// ApplyDamageAndMeasure (binary @4ac07c): apply through the virtual +// TakeDamage and report the zone-level delta -- the critical accountant +// (Mech::DamageZone::CriticalHit) charges the delta against the entry's +// damagePercentage allotment. +//############################################################################# +// +Scalar + MechSubsystem::ApplyDamageAndMeasure(Damage &damage) +{ + Check(this); + Scalar before = (damageZone != NULL) ? damageZone->damageLevel : 0.0f; + TakeDamage(damage); + Scalar after = (damageZone != NULL) ? damageZone->damageLevel : 0.0f; + return after - before; +} + +// +//############################################################################# +// TakeDamage Forward to the damage zone (per-type scaling happens there). +//############################################################################# +// +void + MechSubsystem::TakeDamage(Damage &damage) +{ + Check(this); + Check_Pointer(&damage); + if (damageZone) + { + damageZone->TakeDamage(damage); + } +} + +// +//############################################################################# +// CreateStreamedSubsystem Model-load-time construction of the subsystem +// resource + its damage-zone stream. Not yet reconstructed (see +// MECHSUB.NOTES.md -- the 4.10 per-type damage-zone stream format). +//############################################################################# +// +int + MechSubsystem::CreateStreamedSubsystem( + NotationFile *, + const char *, + const char *, + SubsystemResource *, + NotationFile *, + const ResourceDirectories *, + int + ) +{ + Fail("MechSubsystem::CreateStreamedSubsystem -- mechsub.cpp not yet reconstructed"); + return 0; +} + +// +//############################################################################# +// NoviceLockout -- the novice-experience lockout predicate (binary @4ac9c8): +// owner mech -> Entity::playerLink -> the BTPlayer's experience level == 0. +// Locks the advanced cockpit controls (condenser valves, coolant flush, +// cooling toggles). An unlinked mech reads UNLOCKED (dev-permissive). +//############################################################################# +// +Logical + MechSubsystem::NoviceLockout() +{ + Check(this); + + if (owner == NULL) + { + return False; + } + BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); + if (player == NULL) + { + return False; + } + return (player->GetExperienceLevel() == BTMission::NoviceMode) + ? True : False; +} diff --git a/restoration/source410/BT/MECHSUB.HPP b/restoration/source410/BT/MECHSUB.HPP index 260dc440..00877a11 100644 --- a/restoration/source410/BT/MECHSUB.HPP +++ b/restoration/source410/BT/MECHSUB.HPP @@ -1,192 +1,202 @@ -//===========================================================================// -// File: mechsub.hpp // -// Project: BattleTech // -// Contents: Implementation details for mech subsystems // -//---------------------------------------------------------------------------// -// Date Who Modification // -// -------- --- ---------------------------------------------------------- // -// // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(MECHSUB_HPP) -# define MECHSUB_HPP - -# if !defined(SUBSYSTM_HPP) -# include -# endif - -# if !defined(ALARM_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - class Damage; - -//########################################################################### -//#################### MechSubsystem Model Resource ##################### -//########################################################################### - - struct MechSubsystem__SubsystemResource: - public Subsystem::SubsystemResource - { - Scalar armorByFacing[5]; - Scalar structureReference; - int vitalSubsystemIndex; - char videoObjectName[128]; - ResourceDescription::ResourceID - alarmModel; - int alarmModelReserved[2]; - int printSimulationState; - Scalar collisionCriticalHitWeight; - Scalar criticalReference; - }; - -//########################################################################### -//########################## MechSubsystem ############################## -//########################################################################### - - class MechSubsystem: - public Subsystem - { - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Status model - // - public: - enum TechStatusType { - Destroyed = 0, - Damaged = 1, - CoolantLeaking = 2, - Overheating = 3, - AmmoBurning = 4, - Jammed = 5, - BadPower = 6, - TechStatusTypeCount - }; - - // - // The subsystem SIMULATION states (binary PrintState @4ac8c0 string - // pool: "Destroyed" @0050df7f, "Exploding" @0050df8c). DestroyedState - // (1) is the hard-failure gate every weapon simulation checks - // (`GetSimulationState() == 1` -- emitter @4baab9, ballistic @4bbd36). - // - enum { - DestroyedState = Simulation::DefaultState + 1, - ExplodingState, - MechSubsystemStateCount - }; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test Class Support - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef MechSubsystem__SubsystemResource SubsystemResource; - - MechSubsystem( - Mech *owner, - int subsystem_ID, - const char *subsystem_name, - RegisteredClass::ClassID class_id, - SharedData &shared_data = DefaultData - ); - - MechSubsystem( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - - ~MechSubsystem(); - - static int - CreateStreamedSubsystem( - NotationFile *model_file, - const char *model_name, - const char *subsystem_name, - SubsystemResource *subsystem_resource, - NotationFile *subsystem_file, - const ResourceDirectories *directories, - int passes = 1 - ); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Damage support - // - public: - virtual void - TakeDamage(Damage &damage); - - Scalar - GetSubsystemDamageLevel() const; - void - SetSubsystemDamageLevel(Scalar level); - void - ForceCriticalFailure(); - - // - // Critical-hit application (binary @4ac07c): run the damage through - // TakeDamage and return how much the subsystem's own zone level - // actually moved (the crit accountant charges it against the entry's - // damagePercentage allotment). - // - Scalar - ApplyDamageAndMeasure(Damage &damage); - - Logical - IsVitalSubsystem() const { Check(this); return vitalSubsystem; } - - // - // The NOVICE-experience lockout predicate (binary @4ac9c8): a novice - // pilot's advanced cockpit controls (condenser valves, coolant flush, - // cooling toggles) are locked out. Owner mech -> playerLink -> - // experience level == novice. Unlinked mechs read UNLOCKED. - // - Logical - NoviceLockout(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - protected: - Mech - *owner; - AlarmIndicator - statusAlarm; - Logical - vitalSubsystem; - ResourceDescription::ResourceID - alarmModel; - Scalar - criticalReference; - Scalar - collisionCriticalHitWeight; - Logical - printSimulationState; - int - configureActivePress; - SubsystemResource - *resource; - }; - -#endif +//===========================================================================// +// File: mechsub.hpp // +// Project: BattleTech // +// Contents: Implementation details for mech subsystems // +//---------------------------------------------------------------------------// +// Date Who Modification // +// -------- --- ---------------------------------------------------------- // +// // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(MECHSUB_HPP) +# define MECHSUB_HPP + +# if !defined(SUBSYSTM_HPP) +# include +# endif + +# if !defined(ALARM_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + class Damage; + +//########################################################################### +//#################### MechSubsystem Model Resource ##################### +//########################################################################### + + struct MechSubsystem__SubsystemResource: + public Subsystem::SubsystemResource + { + Scalar armorByFacing[5]; + Scalar structureReference; + int vitalSubsystemIndex; + char videoObjectName[128]; + ResourceDescription::ResourceID + alarmModel; + int alarmModelReserved[2]; + int printSimulationState; + Scalar collisionCriticalHitWeight; + Scalar criticalReference; + }; + +//########################################################################### +//########################## MechSubsystem ############################## +//########################################################################### + + class MechSubsystem: + public Subsystem + { + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Status model + // + public: + enum TechStatusType { + Destroyed = 0, + Damaged = 1, + CoolantLeaking = 2, + Overheating = 3, + AmmoBurning = 4, + Jammed = 5, + BadPower = 6, + TechStatusTypeCount + }; + + // + // The subsystem SIMULATION states (binary PrintState @4ac8c0 string + // pool: "Destroyed" @0050df7f, "Exploding" @0050df8c). DestroyedState + // (1) is the hard-failure gate every weapon simulation checks + // (`GetSimulationState() == 1` -- emitter @4baab9, ballistic @4bbd36). + // + enum { + DestroyedState = Simulation::DefaultState + 1, + ExplodingState, + MechSubsystemStateCount + }; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test Class Support + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef MechSubsystem__SubsystemResource SubsystemResource; + + MechSubsystem( + Mech *owner, + int subsystem_ID, + const char *subsystem_name, + RegisteredClass::ClassID class_id, + SharedData &shared_data = DefaultData + ); + + MechSubsystem( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + + ~MechSubsystem(); + + static int + CreateStreamedSubsystem( + NotationFile *model_file, + const char *model_name, + const char *subsystem_name, + SubsystemResource *subsystem_resource, + NotationFile *subsystem_file, + const ResourceDirectories *directories, + int passes = 1 + ); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Damage support + // + public: + virtual void + TakeDamage(Damage &damage); + + Scalar + GetSubsystemDamageLevel() const; + void + SetSubsystemDamageLevel(Scalar level); + void + ForceCriticalFailure(); + + // + // Critical-hit application (binary @4ac07c): run the damage through + // TakeDamage and return how much the subsystem's own zone level + // actually moved (the crit accountant charges it against the entry's + // damagePercentage allotment). + // + Scalar + ApplyDamageAndMeasure(Damage &damage); + + Logical + IsVitalSubsystem() const { Check(this); return vitalSubsystem; } + + // + // The respawn restore (engine base virtual, SUBSYSTM.HPP:184 -- the + // pair with DeathShutdown). Mech::Reset sweeps every roster slot; + // the base heals the subsystem's private zone and clears the + // Destroyed simulation state (the weapon hard gate). full_reset is + // the binary's reset_command (every recovered override ignores it). + // + virtual void + DeathReset(Logical full_reset); + + // + // The NOVICE-experience lockout predicate (binary @4ac9c8): a novice + // pilot's advanced cockpit controls (condenser valves, coolant flush, + // cooling toggles) are locked out. Owner mech -> playerLink -> + // experience level == novice. Unlinked mechs read UNLOCKED. + // + Logical + NoviceLockout(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + protected: + Mech + *owner; + AlarmIndicator + statusAlarm; + Logical + vitalSubsystem; + ResourceDescription::ResourceID + alarmModel; + Scalar + criticalReference; + Scalar + collisionCriticalHitWeight; + Logical + printSimulationState; + int + configureActivePress; + SubsystemResource + *resource; + }; + +#endif diff --git a/restoration/source410/BT/MECHWEAP.CPP b/restoration/source410/BT/MECHWEAP.CPP index 2c9dd623..dc6d0699 100644 --- a/restoration/source410/BT/MECHWEAP.CPP +++ b/restoration/source410/BT/MECHWEAP.CPP @@ -203,6 +203,34 @@ MechWeapon::MechWeapon( Check_Fpu(); } +// +//############################################################################# +// DeathReset -- the weapon half of the respawn restore. +//############################################################################# +// +void + MechWeapon::DeathReset(Logical full_reset) +{ + Check(this); + + // + // The FULL powered-chain restore (base heal + thermal + electrical + // re-init): a mech that died hot must NOT respawn with guns still at + // FailureHeat (the hard gate + the jam roll read the carried + // temperature). The electrical FSM re-enters Starting and lifts to + // Ready on the next sim frame (startTimer holds startTime). + // + PoweredSubsystem::DeathReset(full_reset); + + fireImpulse = 0.0f; + previousFireImpulse = 0.0f; + rechargeLevel = 0.0f; + recoil = 0.0f; + rangeToTarget = 0.0f; + targetWithinRange = False; + weaponAlarm.SetLevel(LoadingState); +} + // //############################################################################# // The weapon-group config buttons (ids 9/10). STAGED: the binary bodies diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index 9cdd2d65..203e0fdf 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -1,263 +1,271 @@ -//===========================================================================// -// File: mechweap.hpp // -// Project: BattleTech // -// Contents: Implementation details for mech weapons // -//---------------------------------------------------------------------------// -// Date Who Modification // -// -------- --- ---------------------------------------------------------- // -// // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(MECHWEAP_HPP) -# define MECHWEAP_HPP - -# if !defined(POWERSUB_HPP) -# include -# endif - -# if !defined(DAMAGE_HPP) -# include -# endif - -# if !defined(ALARM_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - -//########################################################################### -//###################### MechWeapon Model Resource ###################### -//########################################################################### - - // - // WIRE-VERIFIED layout (raw-stream dump vs the BT411 verified overlay): - // eleven fields; the pip tail is pipPosition(int) + pipColor(3 floats) + - // pipExtendedRange(int). bhk1 PPC reads: recharge 5.0s, range 900, - // damage 12, type 4, heatCost 11, pipColor (0,0,1). - // - struct MechWeapon__SubsystemResource: - public PoweredSubsystem::SubsystemResource - { - Scalar rechargeRate; - Scalar weaponRange; - ResourceDescription::ResourceID - explosionModelFile; - Scalar damageAmount; - int damageType; - Scalar heatCostToFire; - int pipPosition; - Scalar pipColor[3]; - int pipExtendedRange; - }; - -//########################################################################### -//############################ MechWeapon ############################### -//########################################################################### - - class MechWeapon: - public PoweredSubsystem - { - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - static const HandlerEntry MessageHandlerEntries[]; - static MessageHandlerSet MessageHandlers; - - // - // The weapon-group config buttons (binary ids 9/10, chained after - // the PoweredSubsystem generator panel 4-8). - // - enum { - ConfigureMappablesMessageID = PoweredSubsystem::NextMessageID, // 9 - ChooseButtonMessageID, // 10 - NextMessageID // 11 - }; - - void - ConfigureMappablesMessageHandler(ReceiverDataMessageOf *message); - void - ChooseButtonMessageHandler(ReceiverDataMessageOf *message); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Attribute Support. The real IDs are PINNED to the 1995 binary numbering - // (table @0x511890, ids 0x12..0x1C): the streamed per-mech control mappings - // reference them by NUMBER -- TriggerState (0x13) is the fire-button binding. - // Our parent chain publishes nothing past Subsystem::NextAttributeID (2) - // yet, so pads bridge the gap 2..0x11 (they shrink to the authentic - // 0x0F..0x11 once the mechsub/heat/powersub attribute waves publish -- - // SENSOR.HPP pins PoweredSubsystem::NextAttributeID at 0x0F). The pads are - // LOAD-BEARING: AttributeIndexSet::Build leaves unlisted gap slots as - // uninitialized garbage, so every ID up to the max must be covered. - // - public: - enum { - MechWeaponPadFirstAttributeID = Subsystem::NextAttributeID, // 2 - PercentDoneAttributeID = 0x12, - TriggerStateAttributeID, // 0x13 -- the streamed fire-button binding - DistanceToTargetAttributeID, // 0x14 - TargetWithinRangeAttributeID, // 0x15 - WeaponRangeAttributeID, // 0x16 - PipPositionAttributeID, // 0x17 - PipColorAttributeID, // 0x18 - PipExtendedRangeAttributeID, // 0x19 - EstimatedReadyTimeAttributeID, // 0x1A - RearFiringAttributeID, // 0x1B - WeaponStateAttributeID, // 0x1C (binary table end) - NextAttributeID // 0x1D (the Emitter family starts here) - }; - - static const IndexEntry AttributePointers[]; - static AttributeIndexSet AttributeIndex; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test Class Support - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef MechWeapon__SubsystemResource SubsystemResource; - - MechWeapon( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - - ~MechWeapon(); - - static int - CreateStreamedSubsystem( - ResourceFile *resource_file, - NotationFile *model_file, - const char *model_name, - const char *subsystem_name, - SubsystemResource *subsystem_resource, - NotationFile *subsystem_file, - const ResourceDirectories *directories, - int passes = 1 - ); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Weapon interface - // - public: - virtual void - FireWeapon(); - - void - SendDamage( - Entity *target, - Damage &damage - ); - - // - // View-fire gating (the look-state commit re-arms these): a weapon - // mounted on a back gun port (rearFiring) fires only into the LOOK-BACK - // view; the rest fire only in the forward view. - // - Logical - IsRearFiring() const { Check(this); return rearFiring; } - Logical - GetViewFireEnable() const { Check(this); return viewFireEnable; } - void - SetViewFireEnable(Logical enable){ Check(this); viewFireEnable = enable; } - - // - // Fire state machine. The weapon state is carried in the weapon alarm - // level: 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the - // trigger-during-load blip. CheckFireEdge is the rising-edge detector - // on fireImpulse (binary @004b9608): TriggerState carries the raw - // ControlsButton INT bit-copied by the streamed direct mapping, so the - // compare works on the BIT PATTERNS as signed ints (sign-correct for - // the button ints AND genuine floats), reproducing the binary's x87 - // semantics. ComputeOutputVoltage (@004b9c9c) is the recharge-dial - // writer. - // - enum { - FiringState = 0, - DryTriggerState = 1, - LoadedState = 2, - LoadingState = 3, - TriggerDuringLoadState = 4, - JammedState = 5, - TriggerDuringJamState = 6, - NoAmmoState = 7, - WeaponStateCount = 8 - }; - - unsigned - GetWeaponState() { Check(this); return weaponAlarm.GetLevel(); } - Logical - CheckFireEdge(); - virtual void - ComputeOutputVoltage(); // vtable slot 17; the Emitter overrides - // with its charge-voltage form - - // - // Targeting: the owner mech's target slot gates the Loaded->Firing - // transition (no target = the denial blip, no shot); UpdateTargeting - // (binary @004b9bdc) refreshes rangeToTarget / targetWithinRange from - // the target's position each frame. - // - Logical - HasActiveTarget(); - Logical - UpdateTargeting(); - - // - // The muzzle world position (binary @004b9948): the MOUNT segment's - // world frame -- rounds and beams leave FROM THE GUN, following the - // torso twist. Falls back to the mech origin when the mount - // segment doesn't resolve. - // - void - GetMuzzlePoint(Point3D &point); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - protected: - Scalar rechargeRate; - Scalar weaponRange; - Scalar damageAmount; - int damageType; - Scalar heatCostToFire; - ResourceDescription::ResourceID - explosionResourceID; // binary @0x3e4 -- the projectile / - // explosion GameModel this weapon spawns - int targetWithinRange; - Damage damageData; - Logical rearFiring; - Logical viewFireEnable; - // - // Fire-machine state (binary @0x31C/0x320/0x324/0x328/0x330/0x350/ - // 0x3A4/0x3E8): the trigger sample pair, the recharge dial, targeting - // ranges, and the weapon-state alarm. - // - Scalar fireImpulse; - Scalar previousFireImpulse; - Scalar rechargeLevel; - Scalar rangeToTarget; - Scalar effectiveRange; - int estimatedReadyTime; - Scalar recoil; - AlarmIndicator weaponAlarm; - }; - -#endif +//===========================================================================// +// File: mechweap.hpp // +// Project: BattleTech // +// Contents: Implementation details for mech weapons // +//---------------------------------------------------------------------------// +// Date Who Modification // +// -------- --- ---------------------------------------------------------- // +// // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(MECHWEAP_HPP) +# define MECHWEAP_HPP + +# if !defined(POWERSUB_HPP) +# include +# endif + +# if !defined(DAMAGE_HPP) +# include +# endif + +# if !defined(ALARM_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + +//########################################################################### +//###################### MechWeapon Model Resource ###################### +//########################################################################### + + // + // WIRE-VERIFIED layout (raw-stream dump vs the BT411 verified overlay): + // eleven fields; the pip tail is pipPosition(int) + pipColor(3 floats) + + // pipExtendedRange(int). bhk1 PPC reads: recharge 5.0s, range 900, + // damage 12, type 4, heatCost 11, pipColor (0,0,1). + // + struct MechWeapon__SubsystemResource: + public PoweredSubsystem::SubsystemResource + { + Scalar rechargeRate; + Scalar weaponRange; + ResourceDescription::ResourceID + explosionModelFile; + Scalar damageAmount; + int damageType; + Scalar heatCostToFire; + int pipPosition; + Scalar pipColor[3]; + int pipExtendedRange; + }; + +//########################################################################### +//############################ MechWeapon ############################### +//########################################################################### + + class MechWeapon: + public PoweredSubsystem + { + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + // + // The weapon-group config buttons (binary ids 9/10, chained after + // the PoweredSubsystem generator panel 4-8). + // + enum { + ConfigureMappablesMessageID = PoweredSubsystem::NextMessageID, // 9 + ChooseButtonMessageID, // 10 + NextMessageID // 11 + }; + + void + ConfigureMappablesMessageHandler(ReceiverDataMessageOf *message); + void + ChooseButtonMessageHandler(ReceiverDataMessageOf *message); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Attribute Support. The real IDs are PINNED to the 1995 binary numbering + // (table @0x511890, ids 0x12..0x1C): the streamed per-mech control mappings + // reference them by NUMBER -- TriggerState (0x13) is the fire-button binding. + // Our parent chain publishes nothing past Subsystem::NextAttributeID (2) + // yet, so pads bridge the gap 2..0x11 (they shrink to the authentic + // 0x0F..0x11 once the mechsub/heat/powersub attribute waves publish -- + // SENSOR.HPP pins PoweredSubsystem::NextAttributeID at 0x0F). The pads are + // LOAD-BEARING: AttributeIndexSet::Build leaves unlisted gap slots as + // uninitialized garbage, so every ID up to the max must be covered. + // + public: + enum { + MechWeaponPadFirstAttributeID = Subsystem::NextAttributeID, // 2 + PercentDoneAttributeID = 0x12, + TriggerStateAttributeID, // 0x13 -- the streamed fire-button binding + DistanceToTargetAttributeID, // 0x14 + TargetWithinRangeAttributeID, // 0x15 + WeaponRangeAttributeID, // 0x16 + PipPositionAttributeID, // 0x17 + PipColorAttributeID, // 0x18 + PipExtendedRangeAttributeID, // 0x19 + EstimatedReadyTimeAttributeID, // 0x1A + RearFiringAttributeID, // 0x1B + WeaponStateAttributeID, // 0x1C (binary table end) + NextAttributeID // 0x1D (the Emitter family starts here) + }; + + static const IndexEntry AttributePointers[]; + static AttributeIndexSet AttributeIndex; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test Class Support + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef MechWeapon__SubsystemResource SubsystemResource; + + MechWeapon( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + + ~MechWeapon(); + + static int + CreateStreamedSubsystem( + ResourceFile *resource_file, + NotationFile *model_file, + const char *model_name, + const char *subsystem_name, + SubsystemResource *subsystem_resource, + NotationFile *subsystem_file, + const ResourceDirectories *directories, + int passes = 1 + ); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Weapon interface + // + public: + virtual void + FireWeapon(); + + void + SendDamage( + Entity *target, + Damage &damage + ); + + // + // View-fire gating (the look-state commit re-arms these): a weapon + // mounted on a back gun port (rearFiring) fires only into the LOOK-BACK + // view; the rest fire only in the forward view. + // + Logical + IsRearFiring() const { Check(this); return rearFiring; } + Logical + GetViewFireEnable() const { Check(this); return viewFireEnable; } + void + SetViewFireEnable(Logical enable){ Check(this); viewFireEnable = enable; } + + // + // Fire state machine. The weapon state is carried in the weapon alarm + // level: 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the + // trigger-during-load blip. CheckFireEdge is the rising-edge detector + // on fireImpulse (binary @004b9608): TriggerState carries the raw + // ControlsButton INT bit-copied by the streamed direct mapping, so the + // compare works on the BIT PATTERNS as signed ints (sign-correct for + // the button ints AND genuine floats), reproducing the binary's x87 + // semantics. ComputeOutputVoltage (@004b9c9c) is the recharge-dial + // writer. + // + enum { + FiringState = 0, + DryTriggerState = 1, + LoadedState = 2, + LoadingState = 3, + TriggerDuringLoadState = 4, + JammedState = 5, + TriggerDuringJamState = 6, + NoAmmoState = 7, + WeaponStateCount = 8 + }; + + unsigned + GetWeaponState() { Check(this); return weaponAlarm.GetLevel(); } + Logical + CheckFireEdge(); + virtual void + ComputeOutputVoltage(); // vtable slot 17; the Emitter overrides + // with its charge-voltage form + + // + // Targeting: the owner mech's target slot gates the Loaded->Firing + // transition (no target = the denial blip, no shot); UpdateTargeting + // (binary @004b9bdc) refreshes rangeToTarget / targetWithinRange from + // the target's position each frame. + // + Logical + HasActiveTarget(); + Logical + UpdateTargeting(); + + // + // Respawn restore: base heal + the fire machine back to a fresh + // Loading cycle (charge/recoil/trigger cleared; jams and the NoAmmo + // roach-motel release -- the bins restock in their own DeathReset). + // + virtual void + DeathReset(Logical full_reset); + + // + // The muzzle world position (binary @004b9948): the MOUNT segment's + // world frame -- rounds and beams leave FROM THE GUN, following the + // torso twist. Falls back to the mech origin when the mount + // segment doesn't resolve. + // + void + GetMuzzlePoint(Point3D &point); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + protected: + Scalar rechargeRate; + Scalar weaponRange; + Scalar damageAmount; + int damageType; + Scalar heatCostToFire; + ResourceDescription::ResourceID + explosionResourceID; // binary @0x3e4 -- the projectile / + // explosion GameModel this weapon spawns + int targetWithinRange; + Damage damageData; + Logical rearFiring; + Logical viewFireEnable; + // + // Fire-machine state (binary @0x31C/0x320/0x324/0x328/0x330/0x350/ + // 0x3A4/0x3E8): the trigger sample pair, the recharge dial, targeting + // ranges, and the weapon-state alarm. + // + Scalar fireImpulse; + Scalar previousFireImpulse; + Scalar rechargeLevel; + Scalar rangeToTarget; + Scalar effectiveRange; + int estimatedReadyTime; + Scalar recoil; + AlarmIndicator weaponAlarm; + }; + +#endif diff --git a/restoration/source410/BT/MISLANCH.CPP b/restoration/source410/BT/MISLANCH.CPP index 238da3a3..6d2ab606 100644 --- a/restoration/source410/BT/MISLANCH.CPP +++ b/restoration/source410/BT/MISLANCH.CPP @@ -1,450 +1,455 @@ -//===========================================================================// -// File: mislanch.cpp // -// Project: BattleTech Brick: Mech weapons // -// Contents: MissileLauncher -- a projectile weapon that fires missile salvos // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(MISLANCH_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -#if !defined(MISSILE_HPP) -# include -#endif - -#if !defined(APP_HPP) -# include -#endif - -#if !defined(HOSTMGR_HPP) -# include -#endif - -// -// STAGED launch speed (the binary derives it from the authored -// MuzzleVelocity vector -- the muzzle wave). -// -static const Scalar kMissileLaunchSpeed = 150.0f; // u/s - -Derivation - MissileLauncher::ClassDerivations( - ProjectileWeapon::ClassDerivations, - "MissileLauncher" - ); - -MissileLauncher::SharedData - MissileLauncher::DefaultData( - MissileLauncher::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -MissileLauncher::MissileLauncher( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &default_data -): - ProjectileWeapon(owner, subsystem_ID, subsystem_resource, default_data) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - missileCount = subsystem_resource->missileCount; - - // - // A salvo delivers missileCount missiles; split the per-shot damage across - // the salvo. (The per-missile launch mechanics live in FireWeapon, staged.) - // - if (missileCount > 0) - { - damageData.burstCount = missileCount; - damageData.damageAmount = damageData.damageAmount / (Scalar)missileCount; - } - - Check_Fpu(); -} - -MissileLauncher::~MissileLauncher() -{ -} - -Logical - MissileLauncher::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// Damage / death. -//############################################################################# -// -void - MissileLauncher::TakeDamage(Damage &damage) -{ - Check(this); - ProjectileWeapon::TakeDamage(damage); -} - -void - MissileLauncher::DeathReset(Logical /*full_reset*/) -{ - Check(this); -} - -// -// The missile-salvo discharge. Not yet reconstructed. -// -void - MissileLauncher::FireWeapon() -{ - Check(this); - - // - // PARTIAL (binary @004bcc60): the authentic body launches missileCount - // Missile entities toward the owner's target (each carrying the salvo-split - // damageData) and dumps the firing heat. The Missile entity spawn needs - // the entity-spawn + targeting waves; the view/target gate, ammo pull and - // recoil all live in the CALLER (ProjectileWeaponSimulation's Loaded case). - // - // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 - // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the - // PPC's +632K); dump it raw, under the heat-model experience gate (novice / - // standard fire generates no heat -- authentic). (The EMITTER's authored - // value is small and its 1e7 comes from the energy algebra.) - if (HeatModelActive()) - { - AddPendingHeat(heatCostToFire); - } - - // - // The salvo launch (binary @004bcc60): ONE cluster Missile entity per - // trigger, spawned through the Mover make machinery -- the MakeMessage's - // resourceID is this weapon's projectile GameModel (explosionResourceID - // @0x3e4), so the Mover base streams the authored mass/drag. The round - // carries the salvo-split cluster damage record (burstCount = - // missileCount rides into the gyro bounce + splash) and homes via its - // Seeker; the proximity fuse delivers the damage ONCE (the arcade - // economy, BT411 task #62). Muzzle = the mech origin STAGED (the - // authored MuzzleVelocity arc through the MOUNT segment frame is the - // muzzle wave). If the projectile model resource is absent the launch - // falls back to the 5.3.16 instant-hit delivery. - // - if (targetWithinRange && owner != NULL - && owner->GetTargetEntity() != NULL - && getenv("BT_MISSILE_INSTANT") != NULL) - { - // - // DEV opt-out (BT_MISSILE_INSTANT=1): the 5.3.18a instant-hit - // cluster message, kept for A/B damage-economy checks. The flying - // Missile below is the DEFAULT since 5.3.19 (teardown closed: the - // Projectile ctor NULLs the inherited zone slots). - // - SendDamage(owner->GetTargetEntity(), damageData); - } - else if (targetWithinRange && owner != NULL - && owner->GetTargetEntity() != NULL) - { - // - // The MakeMessage's resourceID must be a resource FAMILY id -- the - // Mover base ctor runs its own SearchList(resourceID, GameModel), - // and SearchList CRASHES (engine @0x414938) when the id isn't in - // the top-level directory (member resources resolve only through - // their family head). The streamed explosionModelFile is a small - // INDEX (SRM6 reads 17) into a model list -- decoding it into the - // real projectile family is the projectile-model brick. Until - // then the round spawns on the OWNER's family (its GameModel - // member provably streams every boot -- the mech model params); - // our staged flight integration reads none of the Mover mass/drag. - // - { - // - // DEV one-shot (BT_MODEL_DUMP=1): raw-dump the mech's ModelList - // stream -- the decode bench for the explosionModelFile INDEX - // (SRM6 reads 17). The owner's make resourceID IS the - // ModelList record (CreatePlayerVehicle passes - // mech_res->resourceID). - // - { - static int dumped = 0; - if (!dumped && getenv("BT_MODEL_DUMP")) - { - dumped = 1; - ResourceDescription *ml = - application->GetResourceFile()-> - FindResourceDescription(owner->GetResourceID()); - if (ml != NULL) - { - ml->Lock(); - unsigned char *raw = - (unsigned char *)ml->resourceAddress; - int size = ml->resourceSize; - DEBUG_STREAM << "[mdl] list res id=" << ml->resourceID - << " type=" << ml->resourceType - << " size=" << size - << " count=" << *(int *)raw << endl; - int limit = (size < 640) ? size : 640; - for (int bb = 0; bb < limit; bb += 16) - { - DEBUG_STREAM << "[mdl] +" << bb << ":"; - int cc; - for (cc = bb; cc < bb + 16 && cc < limit; ++cc) - { - DEBUG_STREAM << " " << (unsigned)raw[cc]; - } - DEBUG_STREAM << " "; - for (cc = bb; cc < bb + 16 && cc < limit; ++cc) - { - char ch = (raw[cc] >= 32 && raw[cc] < 127) - ? (char)raw[cc] : '.'; - DEBUG_STREAM << ch; - } - DEBUG_STREAM << endl; - } - // - // Identify the members + the streamed - // explosionModelFile value by name/type. - // - int nn = *(int *)raw; - if (nn > 0 && nn < 40) - { - int *ids = (int *)(raw + 4); - for (int ee = 0; ee < nn; ++ee) - { - ResourceDescription *mm = - application->GetResourceFile()-> - FindResourceDescription(ids[ee]); - DEBUG_STREAM << "[mdl] member[" << ee - << "] id=" << ids[ee]; - if (mm != NULL) - { - DEBUG_STREAM << " type=" << mm->resourceType - << " name=" << mm->resourceName; - } - DEBUG_STREAM << endl; - } - } - { - ResourceDescription *ex = - application->GetResourceFile()-> - FindResourceDescription( - explosionResourceID); - DEBUG_STREAM << "[mdl] explosionModelFile=" - << explosionResourceID; - if (ex != NULL) - { - DEBUG_STREAM << " -> type=" << ex->resourceType - << " name=" << ex->resourceName - << " size=" << ex->resourceSize; - } - else - { - DEBUG_STREAM << " -> NOT IN DIRECTORY"; - } - DEBUG_STREAM << endl; - - // - // Walk the explosion family's OWN members. - // - if (ex != NULL && ex->resourceType == - ResourceDescription::ModelListResourceType) - { - ex->Lock(); - int *xr = (int *)ex->resourceAddress; - int xn = xr[0]; - DEBUG_STREAM << "[mdl] '" << ex->resourceName - << "' members=" << xn << endl; - for (int xx = 0; xx < xn && xx < 16; ++xx) - { - ResourceDescription *xm = - application->GetResourceFile()-> - FindResourceDescription(xr[1 + xx]); - DEBUG_STREAM << "[mdl] [" << xx - << "] id=" << xr[1 + xx]; - if (xm != NULL) - { - DEBUG_STREAM - << " type=" << xm->resourceType - << " name=" << xm->resourceName - << " size=" << xm->resourceSize; - } - DEBUG_STREAM << endl; - - // - // The GameModel member: dump as floats - // (the missile model record -- lifetime - // @+0x44, thrust @+0x48, mode @+0x50). - // - if (xm != NULL && xm->resourceType == - ResourceDescription::GameModelResourceType) - { - xm->Lock(); - float *fv = (float *)xm->resourceAddress; - int nf = xm->resourceSize / 4; - for (int ff = 0; ff < nf; ++ff) - { - DEBUG_STREAM << "[mdl] +" - << (ff * 4) - << " f=" << fv[ff] - << " i=" << ((int *)fv)[ff] - << endl; - } - xm->Unlock(); - } - } - ex->Unlock(); - } - } - DEBUG_STREAM << flush; - ml->Unlock(); - } - } - } - - // - // Resolve the round's resource FAMILY. The streamed - // explosionModelFile IS a family id after all (SRM6 = 17 = - // 'mslhit': VideoModel + AudioStreamList + an 8-byte GameModel - // {int 498, maxTimeOfFlight 5.0}). Guard: only use it when the - // family really carries a GameModel MEMBER -- the Mover base - // ctor SearchLists for one and CRASHES on a missing member - // (the walk runs off the family list). Fallback = the owner's - // family (GameModel provably present). - // - ResourceDescription::ResourceID round_family = - owner->GetResourceID(); - if (explosionResourceID != ResourceDescription::NullResourceID) - { - ResourceDescription *family = - application->GetResourceFile()-> - FindResourceDescription(explosionResourceID); - if (family != NULL - && family->resourceType - == ResourceDescription::ModelListResourceType) - { - family->Lock(); - int *members = (int *)family->resourceAddress; - int member_count = members[0]; - Logical family_complete = True; - Logical has_game_model = False; - for (int mi = 0; mi < member_count; ++mi) - { - ResourceDescription *member = - application->GetResourceFile()-> - FindResourceDescription(members[1 + mi]); - if (member == NULL) - { - // - // A member id absent from the directory makes - // the engine SearchList walk CRASH -- this - // family is unusable for the Mover make. - // - family_complete = False; - } - else if (member->resourceType - == ResourceDescription::GameModelResourceType) - { - has_game_model = True; - } - } - if (family_complete && has_game_model) - { - round_family = explosionResourceID; - // - // Keep the family RESIDENT (no Unlock): dropping a - // first-touch family back to lockCount 0 left the - // engine's re-Lock inside the Mover ctor reading a - // dead list (SearchList returned NULL on members we - // just resolved). The binary's launcher holds its - // projectile model locked for the mission anyway. - // - } - else - { - family->Unlock(); - } - } - } - - HostManager *host_manager = application->GetHostManager(); - Check(host_manager); - - // - // The round leaves FROM THE GUN: the mount segment's world - // frame (GetMuzzlePoint @004b9948 -- follows the torso twist). - // The rotation stays the mech's (the missile aims by velocity). - // - Origin muzzle = owner->localOrigin; - GetMuzzlePoint(muzzle.linearPosition); - // - // NoCollisionVolumeFlag is LOAD-BEARING: collision volumes are - // the Mover DEFAULT (IsCollisionVolume = the flag NOT set), and - // the ctor then demands a BoxedSolidStream member -- which a - // projectile family doesn't carry (SearchList NULL -> - // Lock() page fault). The round's collision is the world - // query / proximity fuse, not a boxed solid. - // - Mover::MakeMessage - spawn_round( - Entity::MakeMessageID, - sizeof(Mover::MakeMessage), - EntityID(host_manager->GetLocalHostID()), - (Entity::ClassID)MissileClassID, - EntityID::Null, - round_family, - Entity::DefaultFlags | Mover::NoCollisionVolumeFlag, - muzzle, - Motion::Identity, - Motion::Identity - ); - Missile *round = new Missile(&spawn_round); - Register_Object(round); - round->Launch( - owner, - owner->GetTargetEntity(), - damageData, - kMissileLaunchSpeed - ); - } - } - - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' FIRED (salvo of " << missileCount - << ", recharge=" << rechargeRate - << "s heat+=" << heatCostToFire << ")" << endl << flush; - } -} - -// -// Model-load-time construction. Not yet reconstructed. -// -Logical - MissileLauncher::CreateStreamedSubsystem( - ResourceFile *, - NotationFile *, - const char *, - const char *, - SubsystemResource *, - NotationFile *, - const ResourceDirectories *, - int - ) -{ - Fail("MissileLauncher::CreateStreamedSubsystem -- mislanch.cpp not yet reconstructed"); - return False; -} +//===========================================================================// +// File: mislanch.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: MissileLauncher -- a projectile weapon that fires missile salvos // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(MISLANCH_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(MISSILE_HPP) +# include +#endif + +#if !defined(APP_HPP) +# include +#endif + +#if !defined(HOSTMGR_HPP) +# include +#endif + +// +// STAGED launch speed (the binary derives it from the authored +// MuzzleVelocity vector -- the muzzle wave). +// +static const Scalar kMissileLaunchSpeed = 150.0f; // u/s + +Derivation + MissileLauncher::ClassDerivations( + ProjectileWeapon::ClassDerivations, + "MissileLauncher" + ); + +MissileLauncher::SharedData + MissileLauncher::DefaultData( + MissileLauncher::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +MissileLauncher::MissileLauncher( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &default_data +): + ProjectileWeapon(owner, subsystem_ID, subsystem_resource, default_data) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + missileCount = subsystem_resource->missileCount; + + // + // A salvo delivers missileCount missiles; split the per-shot damage across + // the salvo. (The per-missile launch mechanics live in FireWeapon, staged.) + // + if (missileCount > 0) + { + damageData.burstCount = missileCount; + damageData.damageAmount = damageData.damageAmount / (Scalar)missileCount; + } + + Check_Fpu(); +} + +MissileLauncher::~MissileLauncher() +{ +} + +Logical + MissileLauncher::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// Damage / death. +//############################################################################# +// +void + MissileLauncher::TakeDamage(Damage &damage) +{ + Check(this); + ProjectileWeapon::TakeDamage(damage); +} + +void + MissileLauncher::DeathReset(Logical full_reset) +{ + Check(this); + // + // The weapon respawn restore (the bin restocks in its own DeathReset; + // the fresh Loading cycle releases the NoAmmo roach-motel). + // + MechWeapon::DeathReset(full_reset); +} + +// +// The missile-salvo discharge. Not yet reconstructed. +// +void + MissileLauncher::FireWeapon() +{ + Check(this); + + // + // PARTIAL (binary @004bcc60): the authentic body launches missileCount + // Missile entities toward the owner's target (each carrying the salvo-split + // damageData) and dumps the firing heat. The Missile entity spawn needs + // the entity-spawn + targeting waves; the view/target gate, ammo pull and + // recoil all live in the CALLER (ProjectileWeaponSimulation's Loaded case). + // + // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 + // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the + // PPC's +632K); dump it raw, under the heat-model experience gate (novice / + // standard fire generates no heat -- authentic). (The EMITTER's authored + // value is small and its 1e7 comes from the energy algebra.) + if (HeatModelActive()) + { + AddPendingHeat(heatCostToFire); + } + + // + // The salvo launch (binary @004bcc60): ONE cluster Missile entity per + // trigger, spawned through the Mover make machinery -- the MakeMessage's + // resourceID is this weapon's projectile GameModel (explosionResourceID + // @0x3e4), so the Mover base streams the authored mass/drag. The round + // carries the salvo-split cluster damage record (burstCount = + // missileCount rides into the gyro bounce + splash) and homes via its + // Seeker; the proximity fuse delivers the damage ONCE (the arcade + // economy, BT411 task #62). Muzzle = the mech origin STAGED (the + // authored MuzzleVelocity arc through the MOUNT segment frame is the + // muzzle wave). If the projectile model resource is absent the launch + // falls back to the 5.3.16 instant-hit delivery. + // + if (targetWithinRange && owner != NULL + && owner->GetTargetEntity() != NULL + && getenv("BT_MISSILE_INSTANT") != NULL) + { + // + // DEV opt-out (BT_MISSILE_INSTANT=1): the 5.3.18a instant-hit + // cluster message, kept for A/B damage-economy checks. The flying + // Missile below is the DEFAULT since 5.3.19 (teardown closed: the + // Projectile ctor NULLs the inherited zone slots). + // + SendDamage(owner->GetTargetEntity(), damageData); + } + else if (targetWithinRange && owner != NULL + && owner->GetTargetEntity() != NULL) + { + // + // The MakeMessage's resourceID must be a resource FAMILY id -- the + // Mover base ctor runs its own SearchList(resourceID, GameModel), + // and SearchList CRASHES (engine @0x414938) when the id isn't in + // the top-level directory (member resources resolve only through + // their family head). The streamed explosionModelFile is a small + // INDEX (SRM6 reads 17) into a model list -- decoding it into the + // real projectile family is the projectile-model brick. Until + // then the round spawns on the OWNER's family (its GameModel + // member provably streams every boot -- the mech model params); + // our staged flight integration reads none of the Mover mass/drag. + // + { + // + // DEV one-shot (BT_MODEL_DUMP=1): raw-dump the mech's ModelList + // stream -- the decode bench for the explosionModelFile INDEX + // (SRM6 reads 17). The owner's make resourceID IS the + // ModelList record (CreatePlayerVehicle passes + // mech_res->resourceID). + // + { + static int dumped = 0; + if (!dumped && getenv("BT_MODEL_DUMP")) + { + dumped = 1; + ResourceDescription *ml = + application->GetResourceFile()-> + FindResourceDescription(owner->GetResourceID()); + if (ml != NULL) + { + ml->Lock(); + unsigned char *raw = + (unsigned char *)ml->resourceAddress; + int size = ml->resourceSize; + DEBUG_STREAM << "[mdl] list res id=" << ml->resourceID + << " type=" << ml->resourceType + << " size=" << size + << " count=" << *(int *)raw << endl; + int limit = (size < 640) ? size : 640; + for (int bb = 0; bb < limit; bb += 16) + { + DEBUG_STREAM << "[mdl] +" << bb << ":"; + int cc; + for (cc = bb; cc < bb + 16 && cc < limit; ++cc) + { + DEBUG_STREAM << " " << (unsigned)raw[cc]; + } + DEBUG_STREAM << " "; + for (cc = bb; cc < bb + 16 && cc < limit; ++cc) + { + char ch = (raw[cc] >= 32 && raw[cc] < 127) + ? (char)raw[cc] : '.'; + DEBUG_STREAM << ch; + } + DEBUG_STREAM << endl; + } + // + // Identify the members + the streamed + // explosionModelFile value by name/type. + // + int nn = *(int *)raw; + if (nn > 0 && nn < 40) + { + int *ids = (int *)(raw + 4); + for (int ee = 0; ee < nn; ++ee) + { + ResourceDescription *mm = + application->GetResourceFile()-> + FindResourceDescription(ids[ee]); + DEBUG_STREAM << "[mdl] member[" << ee + << "] id=" << ids[ee]; + if (mm != NULL) + { + DEBUG_STREAM << " type=" << mm->resourceType + << " name=" << mm->resourceName; + } + DEBUG_STREAM << endl; + } + } + { + ResourceDescription *ex = + application->GetResourceFile()-> + FindResourceDescription( + explosionResourceID); + DEBUG_STREAM << "[mdl] explosionModelFile=" + << explosionResourceID; + if (ex != NULL) + { + DEBUG_STREAM << " -> type=" << ex->resourceType + << " name=" << ex->resourceName + << " size=" << ex->resourceSize; + } + else + { + DEBUG_STREAM << " -> NOT IN DIRECTORY"; + } + DEBUG_STREAM << endl; + + // + // Walk the explosion family's OWN members. + // + if (ex != NULL && ex->resourceType == + ResourceDescription::ModelListResourceType) + { + ex->Lock(); + int *xr = (int *)ex->resourceAddress; + int xn = xr[0]; + DEBUG_STREAM << "[mdl] '" << ex->resourceName + << "' members=" << xn << endl; + for (int xx = 0; xx < xn && xx < 16; ++xx) + { + ResourceDescription *xm = + application->GetResourceFile()-> + FindResourceDescription(xr[1 + xx]); + DEBUG_STREAM << "[mdl] [" << xx + << "] id=" << xr[1 + xx]; + if (xm != NULL) + { + DEBUG_STREAM + << " type=" << xm->resourceType + << " name=" << xm->resourceName + << " size=" << xm->resourceSize; + } + DEBUG_STREAM << endl; + + // + // The GameModel member: dump as floats + // (the missile model record -- lifetime + // @+0x44, thrust @+0x48, mode @+0x50). + // + if (xm != NULL && xm->resourceType == + ResourceDescription::GameModelResourceType) + { + xm->Lock(); + float *fv = (float *)xm->resourceAddress; + int nf = xm->resourceSize / 4; + for (int ff = 0; ff < nf; ++ff) + { + DEBUG_STREAM << "[mdl] +" + << (ff * 4) + << " f=" << fv[ff] + << " i=" << ((int *)fv)[ff] + << endl; + } + xm->Unlock(); + } + } + ex->Unlock(); + } + } + DEBUG_STREAM << flush; + ml->Unlock(); + } + } + } + + // + // Resolve the round's resource FAMILY. The streamed + // explosionModelFile IS a family id after all (SRM6 = 17 = + // 'mslhit': VideoModel + AudioStreamList + an 8-byte GameModel + // {int 498, maxTimeOfFlight 5.0}). Guard: only use it when the + // family really carries a GameModel MEMBER -- the Mover base + // ctor SearchLists for one and CRASHES on a missing member + // (the walk runs off the family list). Fallback = the owner's + // family (GameModel provably present). + // + ResourceDescription::ResourceID round_family = + owner->GetResourceID(); + if (explosionResourceID != ResourceDescription::NullResourceID) + { + ResourceDescription *family = + application->GetResourceFile()-> + FindResourceDescription(explosionResourceID); + if (family != NULL + && family->resourceType + == ResourceDescription::ModelListResourceType) + { + family->Lock(); + int *members = (int *)family->resourceAddress; + int member_count = members[0]; + Logical family_complete = True; + Logical has_game_model = False; + for (int mi = 0; mi < member_count; ++mi) + { + ResourceDescription *member = + application->GetResourceFile()-> + FindResourceDescription(members[1 + mi]); + if (member == NULL) + { + // + // A member id absent from the directory makes + // the engine SearchList walk CRASH -- this + // family is unusable for the Mover make. + // + family_complete = False; + } + else if (member->resourceType + == ResourceDescription::GameModelResourceType) + { + has_game_model = True; + } + } + if (family_complete && has_game_model) + { + round_family = explosionResourceID; + // + // Keep the family RESIDENT (no Unlock): dropping a + // first-touch family back to lockCount 0 left the + // engine's re-Lock inside the Mover ctor reading a + // dead list (SearchList returned NULL on members we + // just resolved). The binary's launcher holds its + // projectile model locked for the mission anyway. + // + } + else + { + family->Unlock(); + } + } + } + + HostManager *host_manager = application->GetHostManager(); + Check(host_manager); + + // + // The round leaves FROM THE GUN: the mount segment's world + // frame (GetMuzzlePoint @004b9948 -- follows the torso twist). + // The rotation stays the mech's (the missile aims by velocity). + // + Origin muzzle = owner->localOrigin; + GetMuzzlePoint(muzzle.linearPosition); + // + // NoCollisionVolumeFlag is LOAD-BEARING: collision volumes are + // the Mover DEFAULT (IsCollisionVolume = the flag NOT set), and + // the ctor then demands a BoxedSolidStream member -- which a + // projectile family doesn't carry (SearchList NULL -> + // Lock() page fault). The round's collision is the world + // query / proximity fuse, not a boxed solid. + // + Mover::MakeMessage + spawn_round( + Entity::MakeMessageID, + sizeof(Mover::MakeMessage), + EntityID(host_manager->GetLocalHostID()), + (Entity::ClassID)MissileClassID, + EntityID::Null, + round_family, + Entity::DefaultFlags | Mover::NoCollisionVolumeFlag, + muzzle, + Motion::Identity, + Motion::Identity + ); + Missile *round = new Missile(&spawn_round); + Register_Object(round); + round->Launch( + owner, + owner->GetTargetEntity(), + damageData, + kMissileLaunchSpeed + ); + } + } + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' FIRED (salvo of " << missileCount + << ", recharge=" << rechargeRate + << "s heat+=" << heatCostToFire << ")" << endl << flush; + } +} + +// +// Model-load-time construction. Not yet reconstructed. +// +Logical + MissileLauncher::CreateStreamedSubsystem( + ResourceFile *, + NotationFile *, + const char *, + const char *, + SubsystemResource *, + NotationFile *, + const ResourceDirectories *, + int + ) +{ + Fail("MissileLauncher::CreateStreamedSubsystem -- mislanch.cpp not yet reconstructed"); + return False; +} diff --git a/restoration/source410/BT/POWERSUB.CPP b/restoration/source410/BT/POWERSUB.CPP index 3975f7cb..19b0b47b 100644 --- a/restoration/source410/BT/POWERSUB.CPP +++ b/restoration/source410/BT/POWERSUB.CPP @@ -1,734 +1,778 @@ -//===========================================================================// -// File: powersub.cpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: PoweredSubsystem -- a HeatSink drawing electrical power // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(POWERSUB_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -// -//############################################################################# -// Shared data support -//############################################################################# -// -Derivation - PoweredSubsystem::ClassDerivations( - HeatSink::ClassDerivations, - "PoweredSubsystem" - ); - -// -//############################################################################# -// The cockpit generator buttons (binary handler table @0x50F4EC, ids 4-8), -// chained onto the HeatSink set (id 3 ToggleCooling) so every powered -// subsystem's dispatch reaches the Eng-page Coolant button too. Static-init -// order is safe under the authentic .MAK lib order (heat precedes powersub). -//############################################################################# -// -const PoweredSubsystem::HandlerEntry - PoweredSubsystem::MessageHandlerEntries[] = -{ - MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorA), // id 4 @004b099c - MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorB), // id 5 @004b09e4 - MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorC), // id 6 @004b0a2c - MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorD), // id 7 @004b0a74 - MESSAGE_ENTRY(PoweredSubsystem, ToggleGeneratorMode) // id 8 @004b0abc -}; - -PoweredSubsystem::MessageHandlerSet - PoweredSubsystem::MessageHandlers( - ELEMENTS(PoweredSubsystem::MessageHandlerEntries), - PoweredSubsystem::MessageHandlerEntries, - HeatSink::MessageHandlers - ); - -PoweredSubsystem::SharedData - PoweredSubsystem::DefaultData( - PoweredSubsystem::ClassDerivations, - PoweredSubsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -// A HeatSink that draws electrical power from a generator (binary ctor -// @004b0f74). Resolves the "VoltageSource" roster index to the powering -// generator, attaches the tap, and primes the electrical state machine. The -// voltageSourceIndex indexes the owner mech's SUBSYSTEM ROSTER (the same -// index space the AmmoBin link uses) -- the roster slots ahead of this -// subsystem are already constructed by the segment walk, and the shipped -// stream orders the generators first. -//############################################################################# -// -PoweredSubsystem::PoweredSubsystem( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), - voltageSource(), - electricalStateAlarm(5), - modeAlarm(3) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - inputVoltage = 0.0f; - outputVoltage = 0.0f; - ratedVoltage = 0.0f; - - thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient; - startTime = subsystem_resource->startTime; - startTimer = startTime; - voltageScale = 1.0f; - - // - // Resolve the voltage source from the roster and attach the tap. - // - Subsystem *source = NULL; - if (subsystem_resource->voltageSourceIndex >= 0 - && subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount()) - { - source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex); - } - if (source != NULL) - { - AttachToVoltageSource(source); - } - - if (getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[power] '" << GetName() - << "' srcIdx=" << subsystem_resource->voltageSourceIndex << " -> "; - if (source != NULL) - { - DEBUG_STREAM << source->GetName(); - } - else - { - DEBUG_STREAM << ""; - } - DEBUG_STREAM << " startTime=" << startTime << endl << flush; - } - - electricalStateAlarm.SetLevel(Ready); - modeAlarm.SetLevel(Connected); - - // - // A master (non-replicant) instance runs the per-frame electrical - // simulation. Derived subsystems (the weapons, Sensor, ...) override with - // their own Performance in their ctors, each of which chains this step. - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation); - } - - Check_Fpu(); -} - -// -//############################################################################# -//############################################################################# -// -PoweredSubsystem::~PoweredSubsystem() -{ -} - -// -//############################################################################# -//############################################################################# -// -void - PoweredSubsystem::ResetToInitialState(Logical powered) -{ - Check(this); - HeatSink::ResetToInitialState(powered); - inputVoltage = 0.0f; - outputVoltage = 0.0f; - electricalStateAlarm.SetLevel(0); - modeAlarm.SetLevel(0); -} - -// -//############################################################################# -//############################################################################# -// -Logical - PoweredSubsystem::TestClass(Mech &) -{ - return True; -} - -Logical - PoweredSubsystem::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// AttachToVoltageSource Link this subsystem to its powering generator -// (binary @004b0dd8): take a tap on the generator (-1 when every tap is -// taken) and hold the live connection. -//############################################################################# -// -int - PoweredSubsystem::AttachToVoltageSource(Subsystem *source) -{ - Check(this); - Check(source); - - Generator *generator = (Generator *)source; - if (generator->TapVoltageSource() != 0) - { - return -1; - } - voltageSource.Add(source); - inputVoltage = generator->MeasuredVoltage(); - return 0; -} - -// -//############################################################################# -// DetachFromVoltageSource (binary @004b0e30): release the tap on the current -// source and clear the connection. -//############################################################################# -// -void - PoweredSubsystem::DetachFromVoltageSource() -{ - Check(this); - - Generator *source = (Generator *)voltageSource.Resolve(); - if (source != NULL) - { - source->UntapVoltageSource(); - voltageSource.Clear(); - } -} - -// -//############################################################################# -// FindGeneratorByNumber (binary @004b0b18): walk the owner's roster for the -// Generator whose authored generatorNumber matches (1=A .. 4=D). -//############################################################################# -// -Subsystem* - PoweredSubsystem::FindGeneratorByNumber(int generator_number) -{ - Check(this); - - for (int slot = 2; slot < owner->GetSubsystemCount(); ++slot) - { - Subsystem *sub = owner->GetSubsystem(slot); - if (sub != NULL - && sub->IsDerivedFrom(Generator::ClassDerivations) - && ((Generator *)sub)->GetGeneratorNumber() == generator_number) - { - return sub; - } - } - return NULL; -} - -// -//############################################################################# -// SelectGenerator -- the manual re-tap: release the current source, tap -// generator N, drop the connect mode back to Connected (a manual selection -// ends any auto-hunt). The binary dereferences the find unguarded (authored -// mechs always carry A-D); we skip loud instead. -//############################################################################# -// -void - PoweredSubsystem::SelectGenerator(int generator_number) -{ - Check(this); - - Subsystem *generator = FindGeneratorByNumber(generator_number); - if (generator == NULL) - { - if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[gensel] '" << GetName() - << "' -> generator " << generator_number - << " NOT FOUND" << endl << flush; - } - return; - } - - DetachFromVoltageSource(); - int tap = AttachToVoltageSource(generator); - modeAlarm.SetLevel(Connected); - - if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[gensel] '" << GetName() - << "' -> '" << generator->GetName() << "'" - << ((tap >= 0) ? " (tapped)" : " (REFUSED: no spare tap)") - << endl << flush; - } -} - -// -//############################################################################# -// The cockpit button handlers (ids 4-8). Press-only (dataContents > 0), -// novice-locked -- a novice cockpit's generator panel is inert. -//############################################################################# -// -void - PoweredSubsystem::SelectGeneratorAMessageHandler( - ReceiverDataMessageOf *message) -{ - Check(this); - if (!NoviceLockout() && message->dataContents > 0) - { - SelectGenerator(1); - } -} - -void - PoweredSubsystem::SelectGeneratorBMessageHandler( - ReceiverDataMessageOf *message) -{ - Check(this); - if (!NoviceLockout() && message->dataContents > 0) - { - SelectGenerator(2); - } -} - -void - PoweredSubsystem::SelectGeneratorCMessageHandler( - ReceiverDataMessageOf *message) -{ - Check(this); - if (!NoviceLockout() && message->dataContents > 0) - { - SelectGenerator(3); - } -} - -void - PoweredSubsystem::SelectGeneratorDMessageHandler( - ReceiverDataMessageOf *message) -{ - Check(this); - if (!NoviceLockout() && message->dataContents > 0) - { - SelectGenerator(4); - } -} - -// -//############################################################################# -// ToggleGeneratorMode (id 8, binary @004b0abc): cycle Manual -> AutoConnect -// -> (detach +) Manual. The auto-hunt itself (a shorted/dead source makes -// the subsystem walk for a live generator) lives in -// PoweredSubsystemSimulation -- a later brick. -//############################################################################# -// -void - PoweredSubsystem::ToggleGeneratorModeMessageHandler( - ReceiverDataMessageOf *message) -{ - Check(this); - - if (NoviceLockout() || message->dataContents <= 0) - { - return; - } - if ((unsigned)modeAlarm.GetLevel() < (unsigned)AutoConnect) - { - modeAlarm.SetLevel(AutoConnect); - } - else if (modeAlarm.GetLevel() == AutoConnect) - { - DetachFromVoltageSource(); - modeAlarm.SetLevel(ManualConnect); - } - if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[gensel] '" << GetName() - << "' mode -> " << modeAlarm.GetLevel() << endl << flush; - } -} - -// -//############################################################################# -// ChargeTimeScale -- the heat/firepower feedback (binary @004b0d50): -// rise = max(0, sourceTemperature - sourceStartingTemperature) -// scale = max(voltageScale, -// (thermalResistivityCoefficient * rise + 1) * voltageScale) -// A hot generator stretches the exponential charge constant, so recharging -// slows exactly when the electrical plant is cooking. -//############################################################################# -// -Scalar - PoweredSubsystem::ChargeTimeScale() -{ - Check(this); - - Generator *source = (Generator *)voltageSource.Resolve(); - if (source == NULL) - { - return voltageScale; - } - - Scalar rise = source->CurrentTemperatureOf() - - source->StartingTemperatureOf(); - if (rise < 0.0f) - { - rise = 0.0f; - } - - Scalar stretched = - (thermalResistivityCoefficient * rise + 1.0f) * voltageScale; - return (stretched > voltageScale) ? stretched : voltageScale; -} - -// -//############################################################################# -// ForceShortRecovery (binary @004b11bc): on a short event, drive the powering -// generator to Shorted and clear its output; GeneratorSimulation then runs -// the short-recovery timer back to Ready. Both @4ac9c8 calls in the binary -// are the not-novice experience predicate (this part and its source share -// the same mech, hence the same player) -- novice cockpits never see -// electrical shorts. -//############################################################################# -// -void - PoweredSubsystem::ForceShortRecovery() -{ - Check(this); - - if (NoviceLockout()) - { - return; - } - - Generator *source = (Generator *)voltageSource.Resolve(); - if (source != NULL) - { - source->ForceShort(); - if (getenv("BT_POWER_LOG") || getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[short] '" << source->GetName() - << "' SHORTED by special damage" << endl << flush; - } - } -} - -// -//############################################################################# -// PoweredSubsystemSimulation -- the per-frame electrical step (binary -// @004b0bd0). Runs the HeatSink thermal step, then advances the electrical -// state machine from the state of the powering generator. -// -// PARTIAL: the AutoConnect replacement-generator hunt (modeAlarm AutoConnect + -// the status-flag gate) joins with the damage wave. -//############################################################################# -// -void - PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice) -{ - Check(this); - - HeatSink::HeatSinkSimulation(time_slice); - - Generator *source = (Generator *)voltageSource.Resolve(); - if (source == NULL) - { - electricalStateAlarm.SetLevel(NoVoltage); - } - else - { - if (source->GeneratorStateOf() == Generator::GeneratorShorted) - { - electricalStateAlarm.SetLevel(Shorted); - } - if (source->GeneratorStateOf() == Generator::GeneratorStarting - || source->GeneratorStateOf() == Generator::GeneratorFailed) - { - electricalStateAlarm.SetLevel(GeneratorOff); - } - } - - switch (electricalStateAlarm.GetLevel()) - { - case Starting: - startTimer += time_slice; - if (startTime <= startTimer) - { - electricalStateAlarm.SetLevel(Ready); - } - break; - - case NoVoltage: - if (source != NULL) - { - electricalStateAlarm.SetLevel(Starting); - startTimer = 0.0f; - } - break; - - case Shorted: - case GeneratorOff: - if (source != NULL - && source->GeneratorStateOf() == Generator::GeneratorReady) - { - electricalStateAlarm.SetLevel(Starting); - startTimer = 0.0f; - } - break; - } - - if (source != NULL) - { - inputVoltage = source->MeasuredVoltage(); - } - - Check_Fpu(); -} - -//########################################################################### -//############################## Generator ############################# -//########################################################################### - -// -//############################################################################# -// Shared data support -//############################################################################# -// -Derivation - Generator::ClassDerivations( - HeatSink::ClassDerivations, - "Generator" - ); - -Generator::SharedData - Generator::DefaultData( - Generator::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -// The generator -- the voltage source loads tap. -//############################################################################# -// -Generator::Generator( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), - stateAlarm(5) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - ratedVoltage = subsystem_resource->ratedVoltage; - outputVoltage = ratedVoltage; - maxTapCount = subsystem_resource->maxTapCount; - currentTapCount = 0; - percentVoltageAvailable = 1.0f; - startTime = subsystem_resource->startTime; - startTimer = startTime; - stateAlarm.SetLevel(GeneratorReady); - generatorOn = 1; - shortRecoveryTime = subsystem_resource->shortRecoveryTime; - shortTimer = shortRecoveryTime; - - // - // Generator number from the last character of the segment name - // ('A' -> 1, 'B' -> 2, ...). - // - const char *name = GetName(); - generatorNumber = name[strlen(name) - 1] - 0x40; - - // - // Install the generator's per-frame electrical Performance. - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&Generator::GeneratorSimulation); - } - - Check_Fpu(); -} - -// -//############################################################################# -//############################################################################# -// -Generator::~Generator() -{ -} - -// -//############################################################################# -//############################################################################# -// -Logical - Generator::TestClass(Mech &) -{ - return True; -} - -Logical - Generator::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -//############################################################################# -// -void - Generator::ResetToInitialState() -{ - Check(this); - HeatSink::ResetToInitialState(True); - outputVoltage = ratedVoltage; - currentTapCount = 0; - percentVoltageAvailable = 1.0f; - startTimer = startTime; - shortTimer = shortRecoveryTime; - generatorOn = 1; - stateAlarm.SetLevel(GeneratorReady); -} - -// -//############################################################################# -// GeneratorSimulation -- the generator's per-frame step (PARTIAL). Runs the -// HeatSink thermal step and the start/short-recovery timers. The authentic -// load model (output voltage sag under tap load / I^2R self-heat feeding the -// charge integration) joins with the electrical-charge wave -// (TrackSeekVoltage). A healthy generator holds GeneratorReady at its rated -// voltage. -//############################################################################# -// -void - Generator::GeneratorSimulation(Scalar time_slice) -{ - Check(this); - - HeatSink::HeatSinkSimulation(time_slice); - - switch (stateAlarm.GetLevel()) - { - case GeneratorStarting: - startTimer += time_slice; - if (startTime <= startTimer) - { - stateAlarm.SetLevel(GeneratorReady); - outputVoltage = ratedVoltage; - } - break; - - case GeneratorShorted: - shortTimer -= time_slice; - if (shortTimer <= 0.0f) - { - shortTimer = shortRecoveryTime; - stateAlarm.SetLevel(GeneratorStarting); - startTimer = 0.0f; - } - break; - - default: - break; - } - - Check_Fpu(); -} - -//########################################################################### -//############################ PowerWatcher ############################ -//########################################################################### - -Derivation - PowerWatcher::ClassDerivations( - HeatWatcher::ClassDerivations, - "PowerWatcher" - ); - -PowerWatcher::SharedData - PowerWatcher::DefaultData( - PowerWatcher::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -PowerWatcher::PowerWatcher( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - HeatWatcher(owner, subsystem_ID, subsystem_resource, shared_data), - watchdogAlarm(5) -{ - Check(owner); - Check_Pointer(subsystem_resource); - // - // minVoltage is a scaled fraction of the watched supply; the exact scale - // constant is a tuning value (stored 1:1 here until located). - // - minVoltage = subsystem_resource->minVoltagePercent; - Check_Fpu(); -} - -PowerWatcher::~PowerWatcher() -{ -} - -Logical - PowerWatcher::TestClass(Mech &) -{ - return True; -} - -Logical - PowerWatcher::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -void - PowerWatcher::ResetToInitialState(Logical powered) -{ - Check(this); - HeatWatcher::ResetToInitialState(powered); - watchdogAlarm.SetLevel(0); -} - -// -// Per-frame supply-voltage watchdog. Not yet reconstructed. -// -void - PowerWatcher::Simulation(Scalar) -{ - Fail("PowerWatcher::Simulation -- powersub.cpp not yet reconstructed"); -} +//===========================================================================// +// File: powersub.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: PoweredSubsystem -- a HeatSink drawing electrical power // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(POWERSUB_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +// +//############################################################################# +// Shared data support +//############################################################################# +// +Derivation + PoweredSubsystem::ClassDerivations( + HeatSink::ClassDerivations, + "PoweredSubsystem" + ); + +// +//############################################################################# +// The cockpit generator buttons (binary handler table @0x50F4EC, ids 4-8), +// chained onto the HeatSink set (id 3 ToggleCooling) so every powered +// subsystem's dispatch reaches the Eng-page Coolant button too. Static-init +// order is safe under the authentic .MAK lib order (heat precedes powersub). +//############################################################################# +// +const PoweredSubsystem::HandlerEntry + PoweredSubsystem::MessageHandlerEntries[] = +{ + MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorA), // id 4 @004b099c + MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorB), // id 5 @004b09e4 + MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorC), // id 6 @004b0a2c + MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorD), // id 7 @004b0a74 + MESSAGE_ENTRY(PoweredSubsystem, ToggleGeneratorMode) // id 8 @004b0abc +}; + +PoweredSubsystem::MessageHandlerSet + PoweredSubsystem::MessageHandlers( + ELEMENTS(PoweredSubsystem::MessageHandlerEntries), + PoweredSubsystem::MessageHandlerEntries, + HeatSink::MessageHandlers + ); + +PoweredSubsystem::SharedData + PoweredSubsystem::DefaultData( + PoweredSubsystem::ClassDerivations, + PoweredSubsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// A HeatSink that draws electrical power from a generator (binary ctor +// @004b0f74). Resolves the "VoltageSource" roster index to the powering +// generator, attaches the tap, and primes the electrical state machine. The +// voltageSourceIndex indexes the owner mech's SUBSYSTEM ROSTER (the same +// index space the AmmoBin link uses) -- the roster slots ahead of this +// subsystem are already constructed by the segment walk, and the shipped +// stream orders the generators first. +//############################################################################# +// +PoweredSubsystem::PoweredSubsystem( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), + voltageSource(), + electricalStateAlarm(5), + modeAlarm(3) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + inputVoltage = 0.0f; + outputVoltage = 0.0f; + ratedVoltage = 0.0f; + + thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient; + startTime = subsystem_resource->startTime; + startTimer = startTime; + voltageScale = 1.0f; + + // + // Resolve the voltage source from the roster and attach the tap. + // + Subsystem *source = NULL; + if (subsystem_resource->voltageSourceIndex >= 0 + && subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount()) + { + source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex); + } + if (source != NULL) + { + AttachToVoltageSource(source); + } + + if (getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[power] '" << GetName() + << "' srcIdx=" << subsystem_resource->voltageSourceIndex << " -> "; + if (source != NULL) + { + DEBUG_STREAM << source->GetName(); + } + else + { + DEBUG_STREAM << ""; + } + DEBUG_STREAM << " startTime=" << startTime << endl << flush; + } + + electricalStateAlarm.SetLevel(Ready); + modeAlarm.SetLevel(Connected); + + // + // A master (non-replicant) instance runs the per-frame electrical + // simulation. Derived subsystems (the weapons, Sensor, ...) override with + // their own Performance in their ctors, each of which chains this step. + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation); + } + + Check_Fpu(); +} + +// +//############################################################################# +//############################################################################# +// +PoweredSubsystem::~PoweredSubsystem() +{ +} + +// +//############################################################################# +//############################################################################# +// +void + PoweredSubsystem::ResetToInitialState(Logical powered) +{ + Check(this); + HeatSink::ResetToInitialState(powered); + inputVoltage = 0.0f; + outputVoltage = 0.0f; + electricalStateAlarm.SetLevel(0); + modeAlarm.SetLevel(0); +} + +// +//############################################################################# +//############################################################################# +// +Logical + PoweredSubsystem::TestClass(Mech &) +{ + return True; +} + +Logical + PoweredSubsystem::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// AttachToVoltageSource Link this subsystem to its powering generator +// (binary @004b0dd8): take a tap on the generator (-1 when every tap is +// taken) and hold the live connection. +//############################################################################# +// +int + PoweredSubsystem::AttachToVoltageSource(Subsystem *source) +{ + Check(this); + Check(source); + + Generator *generator = (Generator *)source; + if (generator->TapVoltageSource() != 0) + { + return -1; + } + voltageSource.Add(source); + inputVoltage = generator->MeasuredVoltage(); + return 0; +} + +// +//############################################################################# +// DetachFromVoltageSource (binary @004b0e30): release the tap on the current +// source and clear the connection. +//############################################################################# +// +void + PoweredSubsystem::DetachFromVoltageSource() +{ + Check(this); + + Generator *source = (Generator *)voltageSource.Resolve(); + if (source != NULL) + { + source->UntapVoltageSource(); + voltageSource.Clear(); + } +} + +// +//############################################################################# +// FindGeneratorByNumber (binary @004b0b18): walk the owner's roster for the +// Generator whose authored generatorNumber matches (1=A .. 4=D). +//############################################################################# +// +Subsystem* + PoweredSubsystem::FindGeneratorByNumber(int generator_number) +{ + Check(this); + + for (int slot = 2; slot < owner->GetSubsystemCount(); ++slot) + { + Subsystem *sub = owner->GetSubsystem(slot); + if (sub != NULL + && sub->IsDerivedFrom(Generator::ClassDerivations) + && ((Generator *)sub)->GetGeneratorNumber() == generator_number) + { + return sub; + } + } + return NULL; +} + +// +//############################################################################# +// SelectGenerator -- the manual re-tap: release the current source, tap +// generator N, drop the connect mode back to Connected (a manual selection +// ends any auto-hunt). The binary dereferences the find unguarded (authored +// mechs always carry A-D); we skip loud instead. +//############################################################################# +// +void + PoweredSubsystem::SelectGenerator(int generator_number) +{ + Check(this); + + Subsystem *generator = FindGeneratorByNumber(generator_number); + if (generator == NULL) + { + if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[gensel] '" << GetName() + << "' -> generator " << generator_number + << " NOT FOUND" << endl << flush; + } + return; + } + + DetachFromVoltageSource(); + int tap = AttachToVoltageSource(generator); + modeAlarm.SetLevel(Connected); + + if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[gensel] '" << GetName() + << "' -> '" << generator->GetName() << "'" + << ((tap >= 0) ? " (tapped)" : " (REFUSED: no spare tap)") + << endl << flush; + } +} + +// +//############################################################################# +// The cockpit button handlers (ids 4-8). Press-only (dataContents > 0), +// novice-locked -- a novice cockpit's generator panel is inert. +//############################################################################# +// +void + PoweredSubsystem::SelectGeneratorAMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + if (!NoviceLockout() && message->dataContents > 0) + { + SelectGenerator(1); + } +} + +void + PoweredSubsystem::SelectGeneratorBMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + if (!NoviceLockout() && message->dataContents > 0) + { + SelectGenerator(2); + } +} + +void + PoweredSubsystem::SelectGeneratorCMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + if (!NoviceLockout() && message->dataContents > 0) + { + SelectGenerator(3); + } +} + +void + PoweredSubsystem::SelectGeneratorDMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + if (!NoviceLockout() && message->dataContents > 0) + { + SelectGenerator(4); + } +} + +// +//############################################################################# +// ToggleGeneratorMode (id 8, binary @004b0abc): cycle Manual -> AutoConnect +// -> (detach +) Manual. The auto-hunt itself (a shorted/dead source makes +// the subsystem walk for a live generator) lives in +// PoweredSubsystemSimulation -- a later brick. +//############################################################################# +// +void + PoweredSubsystem::ToggleGeneratorModeMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + + if (NoviceLockout() || message->dataContents <= 0) + { + return; + } + if ((unsigned)modeAlarm.GetLevel() < (unsigned)AutoConnect) + { + modeAlarm.SetLevel(AutoConnect); + } + else if (modeAlarm.GetLevel() == AutoConnect) + { + DetachFromVoltageSource(); + modeAlarm.SetLevel(ManualConnect); + } + if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[gensel] '" << GetName() + << "' mode -> " << modeAlarm.GetLevel() << endl << flush; + } +} + +// +//############################################################################# +// ChargeTimeScale -- the heat/firepower feedback (binary @004b0d50): +// rise = max(0, sourceTemperature - sourceStartingTemperature) +// scale = max(voltageScale, +// (thermalResistivityCoefficient * rise + 1) * voltageScale) +// A hot generator stretches the exponential charge constant, so recharging +// slows exactly when the electrical plant is cooking. +//############################################################################# +// +Scalar + PoweredSubsystem::ChargeTimeScale() +{ + Check(this); + + Generator *source = (Generator *)voltageSource.Resolve(); + if (source == NULL) + { + return voltageScale; + } + + Scalar rise = source->CurrentTemperatureOf() + - source->StartingTemperatureOf(); + if (rise < 0.0f) + { + rise = 0.0f; + } + + Scalar stretched = + (thermalResistivityCoefficient * rise + 1.0f) * voltageScale; + return (stretched > voltageScale) ? stretched : voltageScale; +} + +// +//############################################################################# +// DeathReset -- respawn restore for the powered chain and the generators. +//############################################################################# +// +void + PoweredSubsystem::DeathReset(Logical full_reset) +{ + Check(this); + MechSubsystem::DeathReset(full_reset); + ResetToInitialState(True); + // + // ResetToInitialState drops the connect-mode indicator to Manual; the + // respawned subsystem keeps its live tap, so the mode is Connected + // (the ctor's spawn state). + // + modeAlarm.SetLevel(Connected); +} + +void + Generator::DeathReset(Logical full_reset) +{ + Check(this); + MechSubsystem::DeathReset(full_reset); + // + // PRESERVE the tap accounting across the reset: consumers keep their + // voltageSource links through respawn (nothing detaches), so zeroing + // currentTapCount here would desync the maxTapCount invariant and let + // post-respawn SelectGenerator presses oversubscribe the generator + // (caught by the 5.3.24 adversarial review). + // + int live_taps = currentTapCount; + ResetToInitialState(); + currentTapCount = live_taps; +} + +// +//############################################################################# +// ForceShortRecovery (binary @004b11bc): on a short event, drive the powering +// generator to Shorted and clear its output; GeneratorSimulation then runs +// the short-recovery timer back to Ready. Both @4ac9c8 calls in the binary +// are the not-novice experience predicate (this part and its source share +// the same mech, hence the same player) -- novice cockpits never see +// electrical shorts. +//############################################################################# +// +void + PoweredSubsystem::ForceShortRecovery() +{ + Check(this); + + if (NoviceLockout()) + { + return; + } + + Generator *source = (Generator *)voltageSource.Resolve(); + if (source != NULL) + { + source->ForceShort(); + if (getenv("BT_POWER_LOG") || getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[short] '" << source->GetName() + << "' SHORTED by special damage" << endl << flush; + } + } +} + +// +//############################################################################# +// PoweredSubsystemSimulation -- the per-frame electrical step (binary +// @004b0bd0). Runs the HeatSink thermal step, then advances the electrical +// state machine from the state of the powering generator. +// +// PARTIAL: the AutoConnect replacement-generator hunt (modeAlarm AutoConnect + +// the status-flag gate) joins with the damage wave. +//############################################################################# +// +void + PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice) +{ + Check(this); + + HeatSink::HeatSinkSimulation(time_slice); + + Generator *source = (Generator *)voltageSource.Resolve(); + if (source == NULL) + { + electricalStateAlarm.SetLevel(NoVoltage); + } + else + { + if (source->GeneratorStateOf() == Generator::GeneratorShorted) + { + electricalStateAlarm.SetLevel(Shorted); + } + if (source->GeneratorStateOf() == Generator::GeneratorStarting + || source->GeneratorStateOf() == Generator::GeneratorFailed) + { + electricalStateAlarm.SetLevel(GeneratorOff); + } + } + + switch (electricalStateAlarm.GetLevel()) + { + case Starting: + startTimer += time_slice; + if (startTime <= startTimer) + { + electricalStateAlarm.SetLevel(Ready); + } + break; + + case NoVoltage: + if (source != NULL) + { + electricalStateAlarm.SetLevel(Starting); + startTimer = 0.0f; + } + break; + + case Shorted: + case GeneratorOff: + if (source != NULL + && source->GeneratorStateOf() == Generator::GeneratorReady) + { + electricalStateAlarm.SetLevel(Starting); + startTimer = 0.0f; + } + break; + } + + if (source != NULL) + { + inputVoltage = source->MeasuredVoltage(); + } + + Check_Fpu(); +} + +//########################################################################### +//############################## Generator ############################# +//########################################################################### + +// +//############################################################################# +// Shared data support +//############################################################################# +// +Derivation + Generator::ClassDerivations( + HeatSink::ClassDerivations, + "Generator" + ); + +Generator::SharedData + Generator::DefaultData( + Generator::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// The generator -- the voltage source loads tap. +//############################################################################# +// +Generator::Generator( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), + stateAlarm(5) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + ratedVoltage = subsystem_resource->ratedVoltage; + outputVoltage = ratedVoltage; + maxTapCount = subsystem_resource->maxTapCount; + currentTapCount = 0; + percentVoltageAvailable = 1.0f; + startTime = subsystem_resource->startTime; + startTimer = startTime; + stateAlarm.SetLevel(GeneratorReady); + generatorOn = 1; + shortRecoveryTime = subsystem_resource->shortRecoveryTime; + shortTimer = shortRecoveryTime; + + // + // Generator number from the last character of the segment name + // ('A' -> 1, 'B' -> 2, ...). + // + const char *name = GetName(); + generatorNumber = name[strlen(name) - 1] - 0x40; + + // + // Install the generator's per-frame electrical Performance. + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Generator::GeneratorSimulation); + } + + Check_Fpu(); +} + +// +//############################################################################# +//############################################################################# +// +Generator::~Generator() +{ +} + +// +//############################################################################# +//############################################################################# +// +Logical + Generator::TestClass(Mech &) +{ + return True; +} + +Logical + Generator::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +//############################################################################# +// +void + Generator::ResetToInitialState() +{ + Check(this); + HeatSink::ResetToInitialState(True); + outputVoltage = ratedVoltage; + currentTapCount = 0; + percentVoltageAvailable = 1.0f; + startTimer = startTime; + shortTimer = shortRecoveryTime; + generatorOn = 1; + stateAlarm.SetLevel(GeneratorReady); +} + +// +//############################################################################# +// GeneratorSimulation -- the generator's per-frame step (PARTIAL). Runs the +// HeatSink thermal step and the start/short-recovery timers. The authentic +// load model (output voltage sag under tap load / I^2R self-heat feeding the +// charge integration) joins with the electrical-charge wave +// (TrackSeekVoltage). A healthy generator holds GeneratorReady at its rated +// voltage. +//############################################################################# +// +void + Generator::GeneratorSimulation(Scalar time_slice) +{ + Check(this); + + HeatSink::HeatSinkSimulation(time_slice); + + switch (stateAlarm.GetLevel()) + { + case GeneratorStarting: + startTimer += time_slice; + if (startTime <= startTimer) + { + stateAlarm.SetLevel(GeneratorReady); + outputVoltage = ratedVoltage; + } + break; + + case GeneratorShorted: + shortTimer -= time_slice; + if (shortTimer <= 0.0f) + { + shortTimer = shortRecoveryTime; + stateAlarm.SetLevel(GeneratorStarting); + startTimer = 0.0f; + } + break; + + default: + break; + } + + Check_Fpu(); +} + +//########################################################################### +//############################ PowerWatcher ############################ +//########################################################################### + +Derivation + PowerWatcher::ClassDerivations( + HeatWatcher::ClassDerivations, + "PowerWatcher" + ); + +PowerWatcher::SharedData + PowerWatcher::DefaultData( + PowerWatcher::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +PowerWatcher::PowerWatcher( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + HeatWatcher(owner, subsystem_ID, subsystem_resource, shared_data), + watchdogAlarm(5) +{ + Check(owner); + Check_Pointer(subsystem_resource); + // + // minVoltage is a scaled fraction of the watched supply; the exact scale + // constant is a tuning value (stored 1:1 here until located). + // + minVoltage = subsystem_resource->minVoltagePercent; + Check_Fpu(); +} + +PowerWatcher::~PowerWatcher() +{ +} + +Logical + PowerWatcher::TestClass(Mech &) +{ + return True; +} + +Logical + PowerWatcher::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +void + PowerWatcher::ResetToInitialState(Logical powered) +{ + Check(this); + HeatWatcher::ResetToInitialState(powered); + watchdogAlarm.SetLevel(0); +} + +void + PowerWatcher::DeathReset(Logical full_reset) +{ + Check(this); + MechSubsystem::DeathReset(full_reset); + ResetToInitialState(True); +} + +// +// Per-frame supply-voltage watchdog. Not yet reconstructed. +// +void + PowerWatcher::Simulation(Scalar) +{ + Fail("PowerWatcher::Simulation -- powersub.cpp not yet reconstructed"); +} diff --git a/restoration/source410/BT/POWERSUB.HPP b/restoration/source410/BT/POWERSUB.HPP index b652a86f..0b25c8a5 100644 --- a/restoration/source410/BT/POWERSUB.HPP +++ b/restoration/source410/BT/POWERSUB.HPP @@ -1,407 +1,424 @@ -//===========================================================================// -// File: powersub.hpp // -// Project: BattleTech // -// Contents: Implementation details for powered subsystems // -//---------------------------------------------------------------------------// -// Date Who Modification // -// -------- --- ---------------------------------------------------------- // -// // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(POWERSUB_HPP) -# define POWERSUB_HPP - -# if !defined(HEAT_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - -//########################################################################### -//################### PoweredSubsystem Model Resource ################### -//########################################################################### - - struct PoweredSubsystem__SubsystemResource: - public HeatSink::SubsystemResource - { - int voltageSourceIndex; - Scalar thermalResistivityCoefficient; - int auxScreenNumber; - int auxScreenPlacement; - char auxScreenLabel[64]; - char engScreenLabel[64]; - Scalar startTime; - }; - -//########################################################################### -//######################### PoweredSubsystem ############################ -//########################################################################### - - class PoweredSubsystem: - public HeatSink - { - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test Class Support - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(Logical powered); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Electrical state machine (carried in electricalStateAlarm, 5 levels) and - // the connection-mode indicator (modeAlarm, 3 levels). - // - public: - enum ElectricalState { - Starting = 0, // powering up; startTimer counts toward startTime - NoVoltage = 1, // voltage source missing / unresolvable - Shorted = 2, // source generator shorted - GeneratorOff = 3, // source generator off / not ready - Ready = 4 // powered and operating - }; - - enum ConnectMode { - ManualConnect = 0, - Connected = 1, - AutoConnect = 2 - }; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Power support - // - public: - // - // The cockpit generator-select buttons (binary handler table - // @0x50F4EC, ids 4-8, chained onto the HeatSink set so id 3 - // ToggleCooling stays reachable through every powered subsystem). - // - enum { - SelectGeneratorAMessageID = HeatSink::NextMessageID, // 4 - SelectGeneratorBMessageID, // 5 - SelectGeneratorCMessageID, // 6 - SelectGeneratorDMessageID, // 7 - ToggleGeneratorModeMessageID, // 8 - NextMessageID // 9 - }; - - static const HandlerEntry MessageHandlerEntries[]; - static MessageHandlerSet MessageHandlers; - - void - SelectGeneratorAMessageHandler(ReceiverDataMessageOf *message); - void - SelectGeneratorBMessageHandler(ReceiverDataMessageOf *message); - void - SelectGeneratorCMessageHandler(ReceiverDataMessageOf *message); - void - SelectGeneratorDMessageHandler(ReceiverDataMessageOf *message); - void - ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf *message); - - // - // The manual generator re-tap (binary @004b0b18/@004b0dd8 family): - // find generator N in the owner's roster, release the current tap, - // tap the new source, drop the connect mode to Connected. - // - void - SelectGenerator(int generator_number); - Subsystem* - FindGeneratorByNumber(int generator_number); - void - DetachFromVoltageSource(); - - Subsystem* - ResolveVoltageSource() { return voltageSource.Resolve(); } - int - AttachToVoltageSource(Subsystem *source); - unsigned - GetVoltageState() { Check(this); return electricalStateAlarm.GetLevel(); } - - // - // The charge time-scale (binary @004b0d50) -- the heat/firepower - // feedback: the base voltageScale (the ctor-calibrated exponential - // charge constant) stretched by the powering generator's temperature - // rise above its start point. - // - Scalar - ChargeTimeScale(); - - // - // Short event (binary @004b11bc): drive the powering generator into - // the Shorted state and zero its output -- the electrical FSM then - // runs its short-recovery cycle. Gated on the not-novice experience - // predicate (both hops share the mech's player). Called by the - // special-damage path (a type-4 Energy hit on a zone with attached - // generator criticals). - // - void - ForceShortRecovery(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Per-frame simulation (heat step + the electrical state machine). - // - public: - typedef void - (PoweredSubsystem::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - - void - PoweredSubsystemSimulation(Scalar time_slice); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef PoweredSubsystem__SubsystemResource SubsystemResource; - - PoweredSubsystem( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - - ~PoweredSubsystem(); - - static int - CreateStreamedSubsystem( - NotationFile *model_file, - const char *model_name, - const char *subsystem_name, - SubsystemResource *subsystem_resource, - NotationFile *subsystem_file, - const ResourceDirectories *directories, - int passes = 1 - ); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - protected: - Scalar inputVoltage; - Scalar outputVoltage; - Scalar ratedVoltage; - SubsystemConnection voltageSource; - AlarmIndicator electricalStateAlarm; - AlarmIndicator modeAlarm; - Scalar thermalResistivityCoefficient; - Scalar startTime; - Scalar startTimer; - Scalar voltageScale; - }; - -//########################################################################### -//################### PowerWatcher Model Resource ###################### -//########################################################################### - - struct PowerWatcher__SubsystemResource: - public HeatWatcher::SubsystemResource - { - Scalar minVoltagePercent; - }; - -//########################################################################### -//############################ PowerWatcher ############################ -//########################################################################### -// -// A HeatWatcher that also watches its subject's supply voltage. Base of the -// Torso / HUD / Gyroscope cockpit subsystems. -// - class PowerWatcher: - public HeatWatcher - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(Logical powered); - void - Simulation(Scalar time_slice); - - public: - typedef PowerWatcher__SubsystemResource SubsystemResource; - - PowerWatcher( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - ~PowerWatcher(); - - protected: - Scalar minVoltage; - AlarmIndicator watchdogAlarm; - }; - -//########################################################################### -//#################### Generator Model Resource ######################## -//########################################################################### - - struct Generator__SubsystemResource: - public HeatSink::SubsystemResource - { - Scalar ratedVoltage; - int maxTapCount; - Scalar startTime; - Scalar shortRecoveryTime; - }; - -//########################################################################### -//############################## Generator ############################# -//########################################################################### -// -// The voltage SOURCE a PoweredSubsystem attaches to (currentTapCount tracks -// attached loads). A HeatSink that produces power. -// - class Generator: - public HeatSink - { - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Shared Data Support - // - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - enum { - GeneratorOff = 0, - GeneratorStarting, - GeneratorReady, - GeneratorShorted, - GeneratorFailed, - GeneratorStateCount - }; - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Test / reset - // - public: - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Power interface - // - public: - Scalar MeasuredVoltage() { Check(this); return outputVoltage; } - Scalar RatedVoltageOf() { Check(this); return ratedVoltage; } - int GetGeneratorNumber(){ Check(this); return generatorNumber; } - unsigned - GeneratorStateOf() { Check(this); return stateAlarm.GetLevel(); } - - // - // The short event landing ON this generator (the tail of binary - // @004b11bc): Shorted state + output killed; GeneratorSimulation - // runs the short-recovery timer back to Ready. Callers gate on the - // novice experience predicate. - // - void - ForceShort() - { - Check(this); - stateAlarm.SetLevel(GeneratorShorted); - outputVoltage = 0.0f; - } - - // - // Tap the generator for one load (a PoweredSubsystem attaching): -1 when - // every tap is taken, else 0. - // - int - TapVoltageSource() - { - Check(this); - if (currentTapCount >= maxTapCount) - { - return -1; - } - ++currentTapCount; - return 0; - } - void - UntapVoltageSource() - { - Check(this); - if (currentTapCount > 0) - { - --currentTapCount; - } - } - - typedef void - (Generator::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - - void - GeneratorSimulation(Scalar time_slice); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Construction and Destruction - // - public: - typedef Generator__SubsystemResource SubsystemResource; - - Generator( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - - ~Generator(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Local Data - // - public: - Scalar percentVoltageAvailable; - int generatorOn; - Scalar ratedVoltage; - Scalar outputVoltage; - int generatorNumber; - int maxTapCount; - int currentTapCount; - Scalar startTime; - Scalar startTimer; - Scalar shortRecoveryTime; - Scalar shortTimer; - AlarmIndicator stateAlarm; - }; - -#endif +//===========================================================================// +// File: powersub.hpp // +// Project: BattleTech // +// Contents: Implementation details for powered subsystems // +//---------------------------------------------------------------------------// +// Date Who Modification // +// -------- --- ---------------------------------------------------------- // +// // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(POWERSUB_HPP) +# define POWERSUB_HPP + +# if !defined(HEAT_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + +//########################################################################### +//################### PoweredSubsystem Model Resource ################### +//########################################################################### + + struct PoweredSubsystem__SubsystemResource: + public HeatSink::SubsystemResource + { + int voltageSourceIndex; + Scalar thermalResistivityCoefficient; + int auxScreenNumber; + int auxScreenPlacement; + char auxScreenLabel[64]; + char engScreenLabel[64]; + Scalar startTime; + }; + +//########################################################################### +//######################### PoweredSubsystem ############################ +//########################################################################### + + class PoweredSubsystem: + public HeatSink + { + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test Class Support + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(Logical powered); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Electrical state machine (carried in electricalStateAlarm, 5 levels) and + // the connection-mode indicator (modeAlarm, 3 levels). + // + public: + enum ElectricalState { + Starting = 0, // powering up; startTimer counts toward startTime + NoVoltage = 1, // voltage source missing / unresolvable + Shorted = 2, // source generator shorted + GeneratorOff = 3, // source generator off / not ready + Ready = 4 // powered and operating + }; + + enum ConnectMode { + ManualConnect = 0, + Connected = 1, + AutoConnect = 2 + }; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Power support + // + public: + // + // The cockpit generator-select buttons (binary handler table + // @0x50F4EC, ids 4-8, chained onto the HeatSink set so id 3 + // ToggleCooling stays reachable through every powered subsystem). + // + enum { + SelectGeneratorAMessageID = HeatSink::NextMessageID, // 4 + SelectGeneratorBMessageID, // 5 + SelectGeneratorCMessageID, // 6 + SelectGeneratorDMessageID, // 7 + ToggleGeneratorModeMessageID, // 8 + NextMessageID // 9 + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + SelectGeneratorAMessageHandler(ReceiverDataMessageOf *message); + void + SelectGeneratorBMessageHandler(ReceiverDataMessageOf *message); + void + SelectGeneratorCMessageHandler(ReceiverDataMessageOf *message); + void + SelectGeneratorDMessageHandler(ReceiverDataMessageOf *message); + void + ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf *message); + + // + // The manual generator re-tap (binary @004b0b18/@004b0dd8 family): + // find generator N in the owner's roster, release the current tap, + // tap the new source, drop the connect mode to Connected. + // + void + SelectGenerator(int generator_number); + Subsystem* + FindGeneratorByNumber(int generator_number); + void + DetachFromVoltageSource(); + + // + // Respawn restore: thermal + electrical re-init (the FSM restarts + // from GeneratorOff/Starting exactly like spawn). + // + virtual void + DeathReset(Logical full_reset); + + Subsystem* + ResolveVoltageSource() { return voltageSource.Resolve(); } + int + AttachToVoltageSource(Subsystem *source); + unsigned + GetVoltageState() { Check(this); return electricalStateAlarm.GetLevel(); } + + // + // The charge time-scale (binary @004b0d50) -- the heat/firepower + // feedback: the base voltageScale (the ctor-calibrated exponential + // charge constant) stretched by the powering generator's temperature + // rise above its start point. + // + Scalar + ChargeTimeScale(); + + // + // Short event (binary @004b11bc): drive the powering generator into + // the Shorted state and zero its output -- the electrical FSM then + // runs its short-recovery cycle. Gated on the not-novice experience + // predicate (both hops share the mech's player). Called by the + // special-damage path (a type-4 Energy hit on a zone with attached + // generator criticals). + // + void + ForceShortRecovery(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame simulation (heat step + the electrical state machine). + // + public: + typedef void + (PoweredSubsystem::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + void + PoweredSubsystemSimulation(Scalar time_slice); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef PoweredSubsystem__SubsystemResource SubsystemResource; + + PoweredSubsystem( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + + ~PoweredSubsystem(); + + static int + CreateStreamedSubsystem( + NotationFile *model_file, + const char *model_name, + const char *subsystem_name, + SubsystemResource *subsystem_resource, + NotationFile *subsystem_file, + const ResourceDirectories *directories, + int passes = 1 + ); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + protected: + Scalar inputVoltage; + Scalar outputVoltage; + Scalar ratedVoltage; + SubsystemConnection voltageSource; + AlarmIndicator electricalStateAlarm; + AlarmIndicator modeAlarm; + Scalar thermalResistivityCoefficient; + Scalar startTime; + Scalar startTimer; + Scalar voltageScale; + }; + +//########################################################################### +//################### PowerWatcher Model Resource ###################### +//########################################################################### + + struct PowerWatcher__SubsystemResource: + public HeatWatcher::SubsystemResource + { + Scalar minVoltagePercent; + }; + +//########################################################################### +//############################ PowerWatcher ############################ +//########################################################################### +// +// A HeatWatcher that also watches its subject's supply voltage. Base of the +// Torso / HUD / Gyroscope cockpit subsystems. +// + class PowerWatcher: + public HeatWatcher + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(Logical powered); + // + // Respawn restore: base heal + the watchdog re-baseline (the + // HeatWatcher::DeathReset it would otherwise inherit binds the + // NON-virtual ResetToInitialState statically and skips this one). + // + virtual void + DeathReset(Logical full_reset); + void + Simulation(Scalar time_slice); + + public: + typedef PowerWatcher__SubsystemResource SubsystemResource; + + PowerWatcher( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + ~PowerWatcher(); + + protected: + Scalar minVoltage; + AlarmIndicator watchdogAlarm; + }; + +//########################################################################### +//#################### Generator Model Resource ######################## +//########################################################################### + + struct Generator__SubsystemResource: + public HeatSink::SubsystemResource + { + Scalar ratedVoltage; + int maxTapCount; + Scalar startTime; + Scalar shortRecoveryTime; + }; + +//########################################################################### +//############################## Generator ############################# +//########################################################################### +// +// The voltage SOURCE a PoweredSubsystem attaches to (currentTapCount tracks +// attached loads). A HeatSink that produces power. +// + class Generator: + public HeatSink + { + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Shared Data Support + // + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + enum { + GeneratorOff = 0, + GeneratorStarting, + GeneratorReady, + GeneratorShorted, + GeneratorFailed, + GeneratorStateCount + }; + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Test / reset + // + public: + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Power interface + // + public: + Scalar MeasuredVoltage() { Check(this); return outputVoltage; } + Scalar RatedVoltageOf() { Check(this); return ratedVoltage; } + int GetGeneratorNumber(){ Check(this); return generatorNumber; } + unsigned + GeneratorStateOf() { Check(this); return stateAlarm.GetLevel(); } + + // + // The short event landing ON this generator (the tail of binary + // @004b11bc): Shorted state + output killed; GeneratorSimulation + // runs the short-recovery timer back to Ready. Callers gate on the + // novice experience predicate. + // + void + ForceShort() + { + Check(this); + stateAlarm.SetLevel(GeneratorShorted); + outputVoltage = 0.0f; + } + + // + // Tap the generator for one load (a PoweredSubsystem attaching): -1 when + // every tap is taken, else 0. + // + int + TapVoltageSource() + { + Check(this); + if (currentTapCount >= maxTapCount) + { + return -1; + } + ++currentTapCount; + return 0; + } + virtual void + DeathReset(Logical full_reset); + + void + UntapVoltageSource() + { + Check(this); + if (currentTapCount > 0) + { + --currentTapCount; + } + } + + typedef void + (Generator::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + void + GeneratorSimulation(Scalar time_slice); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Construction and Destruction + // + public: + typedef Generator__SubsystemResource SubsystemResource; + + Generator( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + + ~Generator(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data + // + public: + Scalar percentVoltageAvailable; + int generatorOn; + Scalar ratedVoltage; + Scalar outputVoltage; + int generatorNumber; + int maxTapCount; + int currentTapCount; + Scalar startTime; + Scalar startTimer; + Scalar shortRecoveryTime; + Scalar shortTimer; + AlarmIndicator stateAlarm; + }; + +#endif diff --git a/restoration/source410/BT/PROJWEAP.CPP b/restoration/source410/BT/PROJWEAP.CPP index dacd6cc8..85a8f754 100644 --- a/restoration/source410/BT/PROJWEAP.CPP +++ b/restoration/source410/BT/PROJWEAP.CPP @@ -1,438 +1,439 @@ -//===========================================================================// -// File: projweap.cpp // -// Project: BattleTech Brick: Mech weapons // -// Contents: ProjectileWeapon -- ballistic (ammo-fed) weapon base // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(PROJWEAP_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -#if !defined(AMMOBIN_HPP) -# include -#endif - -#if !defined(RANDOM_HPP) -# include -#endif - -#if !defined(BTPLAYER_HPP) -# include -#endif - -Derivation - ProjectileWeapon::ClassDerivations( - MechWeapon::ClassDerivations, - "ProjectileWeapon" - ); - -ProjectileWeapon::SharedData - ProjectileWeapon::DefaultData( - ProjectileWeapon::ClassDerivations, - MechWeapon::MessageHandlers, - MechWeapon::AttributeIndex, - Subsystem::StateCount - ); - -ProjectileWeapon::ProjectileWeapon( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data), - ammoBinLink() -{ - Check(owner); - Check_Pointer(subsystem_resource); - - tracerInterval = subsystem_resource->tracerInterval; - minTimeOfFlight = subsystem_resource->minTimeOfFlight; - minJamChance = subsystem_resource->minJamChance; - minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; - - tracerCounter = 0; - ejectState = 0; - totalTimeToEject = 0.0f; - timeToEject = 0.0f; - percentOfEject = 0.0f; - tracerModelHandle = 0; - leadPosition = Point3D(0.0f, 0.0f, 0.0f); - launchVelocity = Vector3D(0.0f, 0.0f, 0.0f); - tracerOrigin = Vector3D(0.0f, 0.0f, 0.0f); - - // - // Link the AmmoBin subsystem the resource references by roster index (the - // binary ctor resolves it from the owner's subsystem table and connects the - // embedded link @0x43C). Shipped content always links a bin; log when the - // data doesn't resolve one. - // - { - Subsystem *roster_bin = NULL; - if (subsystem_resource->ammoBinIndex >= 0 - && subsystem_resource->ammoBinIndex < owner->GetSubsystemCount()) - { - roster_bin = owner->GetSubsystem(subsystem_resource->ammoBinIndex); - } - if (roster_bin != NULL) - { - ammoBinLink.Add(roster_bin); - } - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[proj] '" << GetName() - << "' ammoBinIndex=" << subsystem_resource->ammoBinIndex - << " -> "; - if (roster_bin != NULL) - { - DEBUG_STREAM << roster_bin->GetName() - << " (rounds=" << ((AmmoBin *)roster_bin)->GetAmmoCount() << ")"; - } - else - { - DEBUG_STREAM << ""; - } - DEBUG_STREAM << endl << flush; - } - } - - // - // Install the ballistic fire state machine (a replicant copy is driven by - // console updates instead). - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); - } - - Check_Fpu(); -} - -ProjectileWeapon::~ProjectileWeapon() -{ -} - -Logical - ProjectileWeapon::TestClass(Mech &) -{ - return True; -} - -Logical - ProjectileWeapon::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// FireWeapon The ballistic discharge (PARTIAL -- binary @004bc104). The -// authentic body is heat + projectile/tracer spawn ONLY: the view/target gate, -// the ammo pull and the recoil set all live in the CALLER (the Loaded case of -// ProjectileWeaponSimulation) -- early-returning any gate from here while the -// caller cycles the alarm anyway is exactly the 1995 "denied shot fakes a full -// firing cycle" defect class. The projectile spawn (Missile entities / -// tracer) needs the entity-spawn + targeting waves; MissileLauncher overrides -// this for the salvo launch. -//############################################################################# -// -void - ProjectileWeapon::FireWeapon() -{ - Check(this); - - // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 - // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the - // PPC's +632K); dump it raw, under the heat-model experience gate (novice / - // standard fire generates no heat -- authentic). (The EMITTER's authored - // value is small and its 1e7 comes from the energy algebra -- see - // EMITTER.CPP.) - if (HeatModelActive()) - { - AddPendingHeat(heatCostToFire); - } - - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' FIRED (ballistic, recharge=" << rechargeRate - << "s heat+=" << heatCostToFire << ")" - << endl << flush; - } -} - -// -//############################################################################# -// LiveFireEnabled -- the "sim live" novice lockout: owner mech -> -// Entity::playerLink -> BTPlayer::simLive, 0 only for NOVICE experience. A -// NULL player link reads LIVE (the unlinked dev mech / target dummy). -//############################################################################# -// -Logical - ProjectileWeapon::LiveFireEnabled() -{ - Check(this); - - if (owner == NULL) - { - return True; - } - BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); - if (player == NULL) - { - return True; - } - return player->IsSimLive(); -} - -// -//############################################################################# -// CheckForJam -- the heat-scaled jam roll (binary @4bbfcc), AUTHENTIC form: -// novice never jams (LiveFireEnabled); a cold heat model never jams (heatLoad -// stays 0 with the model off -- UpdateHeatLoad only runs under the experience -// gate); otherwise -// p = 0.41 * currentTemperature / failureTemperature, -// clamped to [minJamChance, 1.0], rolled against the uniform random. Note the -// floor: even a cool launcher at veteran+ carries the authored minimum jam -// chance per shot (SRM6: 5%) -- authentic pod behavior. A jammed launcher -// clears only on ResetToInitialState (mission reset). -//############################################################################# -// -Logical - ProjectileWeapon::CheckForJam() -{ - Check(this); - - if (!LiveFireEnabled()) - { - return False; - } - if (heatLoad <= 0.0f) - { - return False; - } - - Scalar p = (0.41f * CurrentTemperatureOf()) / failureTemperature; - if (minJamChance <= p) - { - if (p > 1.0f) - { - p = 1.0f; - } - } - else - { - p = minJamChance; - } - - return (p > (Scalar)Random) ? True : False; -} - -// -//############################################################################# -// ProjectileWeaponSimulation The ballistic per-frame fire state machine -// (binary @004bbd04, FULLY RECOVERED in the BT411 RE). The weapon state is -// carried in the weapon alarm: 0/1/4/6 transient audio blips, 2 Loaded, -// 3 Loading, 5 Jammed, 7 unavailable/NoAmmo (the roach-motel: nothing in the -// machine ever leaves it -- only a mission reset does). -// -// PARTIAL: the leading PoweredSubsystemSimulation electrical step, the -// destroyed / FailureHeat / mech-disabled hard gate (gate 1), the magazine -// eject, the electrical-Ready recoil gate, the heat-scaled jam roll and the -// HasActiveTarget half of the fire gate are deferred with the power / heat / -// damage / targeting waves. What runs is authentic in shape: the single -// trigger-edge sample, the dry-bin gate (gate 2), the Loaded ammo-pull -> -// Firing -> Loading cycle with the denial blip, the Loading recoil bleed + -// recharge dial, and the NoAmmo dry-fire blip. -// -// DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded -// (same as the emitter hook) -- every armed weapon auto-fires at its authored -// cadence until its bin runs dry. -//############################################################################# -// -void - ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice) -{ - Check(this); - - // - // The PoweredSubsystem step first (binary @4bbd12): the HeatSink thermal - // absorb/conduct + the electrical state machine. - // - PoweredSubsystem::PoweredSubsystemSimulation(time_slice); - - { - static int forceFire = -1; - if (forceFire < 0) - { - forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; - } - if (forceFire) - { - fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; - } - } - - // - // The trigger edge is sampled ONCE, before the gates. - // - Logical trigger = CheckFireEdge(); - - targetWithinRange = UpdateTargeting(); - - // - // Gate 1 (@4bbd36): weapon DESTROYED or its own sink at FailureHeat -> pin - // full recoil + the unavailable alarm. No return -- the frame continues - // into the state machine (whose NoAmmo case re-asserts). (The owning-mech- - // disabled half joins with the damage wave.) - // - if (GetSimulationState() == 1 || GetHeatState() == FailureHeat) - { - if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' -> UNAVAILABLE (gate1: heatState=" << GetHeatState() - << " T=" << CurrentTemperatureOf() << ")" << endl << flush; - } - recoil = rechargeRate; - weaponAlarm.SetLevel(NoAmmoState); - } - - // - // Gate 2: the linked AmmoBin. Empty (or missing) -> pin unavailable EVERY - // frame while dry. - // - AmmoBin *bin = (AmmoBin *)ammoBinLink.Resolve(); - if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoEmptyState) - { - if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' -> NoAmmo (bin dry)" << endl << flush; - } - weaponAlarm.SetLevel(NoAmmoState); - } - - switch (GetWeaponState()) - { - case LoadedState: - if (trigger) - { - // - // The authentic fire gate (@4bbee6): armed in the current look - // view AND a target under the reticle. - // - if (viewFireEnable && HasActiveTarget()) - { - // - // The ammo pull happens HERE, in the caller -- a failed pull - // just stays Loaded, silently. - // - if (bin != NULL && bin->FeedAmmo() != 0) - { - weaponAlarm.SetLevel(FiringState); - ForceUpdate(); - FireWeapon(); - if (bin->GetAmmoState() == AmmoBin::AmmoEmptyState) - { - weaponAlarm.SetLevel(NoAmmoState); - } - else if (CheckForJam()) - { - weaponAlarm.SetLevel(JammedState); - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' JAMMED (T=" << CurrentTemperatureOf() - << ")" << endl << flush; - } - } - else - { - weaponAlarm.SetLevel(LoadingState); - } - ForceUpdate(); - recoil = rechargeRate; - } - } - else - { - // - // THE DENIAL BLIP: a shot denied by the look-view gate does - // SetLevel(4); SetLevel(2) -- a one-frame audio blip, the pip - // stays Loaded and NO ammo is pulled. - // - weaponAlarm.SetLevel(TriggerDuringLoadState); - weaponAlarm.SetLevel(LoadedState); - } - } - break; - - case LoadingState: - if (trigger) - { - weaponAlarm.SetLevel(TriggerDuringLoadState); - weaponAlarm.SetLevel(LoadingState); - } - // - // Recoil bleeds ONLY while the electrical supply is Ready (binary - // @4bbdf5/@4bbe04); at zero (and a round chambered) -> Loaded. - // - if (GetVoltageState() == Ready) - { - recoil -= time_slice; - if (recoil < 0.0f) - { - recoil = 0.0f; - if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoLoadedState) - { - weaponAlarm.SetLevel(LoadedState); - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' LOADED (rounds=" << bin->GetAmmoCount() - << " T=" << CurrentTemperatureOf() << ")" - << endl << flush; - } - } - } - } - ComputeOutputVoltage(); - break; - - case JammedState: - if (trigger) - { - weaponAlarm.SetLevel(TriggerDuringJamState); - weaponAlarm.SetLevel(JammedState); - } - recoil = rechargeRate; - break; - - case NoAmmoState: - if (trigger) - { - weaponAlarm.SetLevel(DryTriggerState); // the dry-fire blip - } - weaponAlarm.SetLevel(NoAmmoState); // re-assert (the roach-motel) - recoil = rechargeRate; - ComputeOutputVoltage(); // dial pinned at 0 - break; - - default: // 0/1/4/6: transient audio-blip states, no case body - break; - } - - Check_Fpu(); -} +//===========================================================================// +// File: projweap.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: ProjectileWeapon -- ballistic (ammo-fed) weapon base // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(PROJWEAP_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(AMMOBIN_HPP) +# include +#endif + +#if !defined(RANDOM_HPP) +# include +#endif + +#if !defined(BTPLAYER_HPP) +# include +#endif + +Derivation + ProjectileWeapon::ClassDerivations( + MechWeapon::ClassDerivations, + "ProjectileWeapon" + ); + +ProjectileWeapon::SharedData + ProjectileWeapon::DefaultData( + ProjectileWeapon::ClassDerivations, + MechWeapon::MessageHandlers, + MechWeapon::AttributeIndex, + Subsystem::StateCount + ); + +ProjectileWeapon::ProjectileWeapon( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data), + ammoBinLink() +{ + Check(owner); + Check_Pointer(subsystem_resource); + + tracerInterval = subsystem_resource->tracerInterval; + minTimeOfFlight = subsystem_resource->minTimeOfFlight; + minJamChance = subsystem_resource->minJamChance; + minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; + + tracerCounter = 0; + ejectState = 0; + totalTimeToEject = 0.0f; + timeToEject = 0.0f; + percentOfEject = 0.0f; + tracerModelHandle = 0; + leadPosition = Point3D(0.0f, 0.0f, 0.0f); + launchVelocity = Vector3D(0.0f, 0.0f, 0.0f); + tracerOrigin = Vector3D(0.0f, 0.0f, 0.0f); + + // + // Link the AmmoBin subsystem the resource references by roster index (the + // binary ctor resolves it from the owner's subsystem table and connects the + // embedded link @0x43C). Shipped content always links a bin; log when the + // data doesn't resolve one. + // + { + Subsystem *roster_bin = NULL; + if (subsystem_resource->ammoBinIndex >= 0 + && subsystem_resource->ammoBinIndex < owner->GetSubsystemCount()) + { + roster_bin = owner->GetSubsystem(subsystem_resource->ammoBinIndex); + } + if (roster_bin != NULL) + { + ammoBinLink.Add(roster_bin); + } + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[proj] '" << GetName() + << "' ammoBinIndex=" << subsystem_resource->ammoBinIndex + << " -> "; + if (roster_bin != NULL) + { + DEBUG_STREAM << roster_bin->GetName() + << " (rounds=" << ((AmmoBin *)roster_bin)->GetAmmoCount() << ")"; + } + else + { + DEBUG_STREAM << ""; + } + DEBUG_STREAM << endl << flush; + } + } + + // + // Install the ballistic fire state machine (a replicant copy is driven by + // console updates instead). + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); + } + + Check_Fpu(); +} + +ProjectileWeapon::~ProjectileWeapon() +{ +} + +Logical + ProjectileWeapon::TestClass(Mech &) +{ + return True; +} + +Logical + ProjectileWeapon::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// FireWeapon The ballistic discharge (PARTIAL -- binary @004bc104). The +// authentic body is heat + projectile/tracer spawn ONLY: the view/target gate, +// the ammo pull and the recoil set all live in the CALLER (the Loaded case of +// ProjectileWeaponSimulation) -- early-returning any gate from here while the +// caller cycles the alarm anyway is exactly the 1995 "denied shot fakes a full +// firing cycle" defect class. The projectile spawn (Missile entities / +// tracer) needs the entity-spawn + targeting waves; MissileLauncher overrides +// this for the salvo launch. +//############################################################################# +// +void + ProjectileWeapon::FireWeapon() +{ + Check(this); + + // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 + // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the + // PPC's +632K); dump it raw, under the heat-model experience gate (novice / + // standard fire generates no heat -- authentic). (The EMITTER's authored + // value is small and its 1e7 comes from the energy algebra -- see + // EMITTER.CPP.) + if (HeatModelActive()) + { + AddPendingHeat(heatCostToFire); + } + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' FIRED (ballistic, recharge=" << rechargeRate + << "s heat+=" << heatCostToFire << ")" + << endl << flush; + } +} + +// +//############################################################################# +// LiveFireEnabled -- the "sim live" novice lockout: owner mech -> +// Entity::playerLink -> BTPlayer::simLive, 0 only for NOVICE experience. A +// NULL player link reads LIVE (the unlinked dev mech / target dummy). +//############################################################################# +// +Logical + ProjectileWeapon::LiveFireEnabled() +{ + Check(this); + + if (owner == NULL) + { + return True; + } + BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); + if (player == NULL) + { + return True; + } + return player->IsSimLive(); +} + +// +//############################################################################# +// CheckForJam -- the heat-scaled jam roll (binary @4bbfcc), AUTHENTIC form: +// novice never jams (LiveFireEnabled); a cold heat model never jams (heatLoad +// stays 0 with the model off -- UpdateHeatLoad only runs under the experience +// gate); otherwise +// p = 0.41 * currentTemperature / failureTemperature, +// clamped to [minJamChance, 1.0], rolled against the uniform random. Note the +// floor: even a cool launcher at veteran+ carries the authored minimum jam +// chance per shot (SRM6: 5%) -- authentic pod behavior. A jammed launcher +// clears only on ResetToInitialState (mission reset). +//############################################################################# +// +Logical + ProjectileWeapon::CheckForJam() +{ + Check(this); + + if (!LiveFireEnabled()) + { + return False; + } + if (heatLoad <= 0.0f) + { + return False; + } + + Scalar p = (0.41f * CurrentTemperatureOf()) / failureTemperature; + if (minJamChance <= p) + { + if (p > 1.0f) + { + p = 1.0f; + } + } + else + { + p = minJamChance; + } + + return (p > (Scalar)Random) ? True : False; +} + +// +//############################################################################# +// ProjectileWeaponSimulation The ballistic per-frame fire state machine +// (binary @004bbd04, FULLY RECOVERED in the BT411 RE). The weapon state is +// carried in the weapon alarm: 0/1/4/6 transient audio blips, 2 Loaded, +// 3 Loading, 5 Jammed, 7 unavailable/NoAmmo (the roach-motel: nothing in the +// machine ever leaves it -- only a mission reset does). +// +// PARTIAL: the leading PoweredSubsystemSimulation electrical step, the +// destroyed / FailureHeat / mech-disabled hard gate (gate 1), the magazine +// eject, the electrical-Ready recoil gate, the heat-scaled jam roll and the +// HasActiveTarget half of the fire gate are deferred with the power / heat / +// damage / targeting waves. What runs is authentic in shape: the single +// trigger-edge sample, the dry-bin gate (gate 2), the Loaded ammo-pull -> +// Firing -> Loading cycle with the denial blip, the Loading recoil bleed + +// recharge dial, and the NoAmmo dry-fire blip. +// +// DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded +// (same as the emitter hook) -- every armed weapon auto-fires at its authored +// cadence until its bin runs dry. +//############################################################################# +// +void + ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice) +{ + Check(this); + + // + // The PoweredSubsystem step first (binary @4bbd12): the HeatSink thermal + // absorb/conduct + the electrical state machine. + // + PoweredSubsystem::PoweredSubsystemSimulation(time_slice); + + { + static int forceFire = -1; + if (forceFire < 0) + { + forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; + } + if (forceFire) + { + fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; + } + } + + // + // The trigger edge is sampled ONCE, before the gates. + // + Logical trigger = CheckFireEdge(); + + targetWithinRange = UpdateTargeting(); + + // + // Gate 1 (@4bbd36): weapon DESTROYED or its own sink at FailureHeat -> pin + // full recoil + the unavailable alarm. No return -- the frame continues + // into the state machine (whose NoAmmo case re-asserts). (The owning-mech- + // disabled half joins with the damage wave.) + // + if (GetSimulationState() == 1 || GetHeatState() == FailureHeat + || (owner != NULL && owner->IsMechDestroyed())) + { + if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' -> UNAVAILABLE (gate1: heatState=" << GetHeatState() + << " T=" << CurrentTemperatureOf() << ")" << endl << flush; + } + recoil = rechargeRate; + weaponAlarm.SetLevel(NoAmmoState); + } + + // + // Gate 2: the linked AmmoBin. Empty (or missing) -> pin unavailable EVERY + // frame while dry. + // + AmmoBin *bin = (AmmoBin *)ammoBinLink.Resolve(); + if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoEmptyState) + { + if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' -> NoAmmo (bin dry)" << endl << flush; + } + weaponAlarm.SetLevel(NoAmmoState); + } + + switch (GetWeaponState()) + { + case LoadedState: + if (trigger) + { + // + // The authentic fire gate (@4bbee6): armed in the current look + // view AND a target under the reticle. + // + if (viewFireEnable && HasActiveTarget()) + { + // + // The ammo pull happens HERE, in the caller -- a failed pull + // just stays Loaded, silently. + // + if (bin != NULL && bin->FeedAmmo() != 0) + { + weaponAlarm.SetLevel(FiringState); + ForceUpdate(); + FireWeapon(); + if (bin->GetAmmoState() == AmmoBin::AmmoEmptyState) + { + weaponAlarm.SetLevel(NoAmmoState); + } + else if (CheckForJam()) + { + weaponAlarm.SetLevel(JammedState); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' JAMMED (T=" << CurrentTemperatureOf() + << ")" << endl << flush; + } + } + else + { + weaponAlarm.SetLevel(LoadingState); + } + ForceUpdate(); + recoil = rechargeRate; + } + } + else + { + // + // THE DENIAL BLIP: a shot denied by the look-view gate does + // SetLevel(4); SetLevel(2) -- a one-frame audio blip, the pip + // stays Loaded and NO ammo is pulled. + // + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadedState); + } + } + break; + + case LoadingState: + if (trigger) + { + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadingState); + } + // + // Recoil bleeds ONLY while the electrical supply is Ready (binary + // @4bbdf5/@4bbe04); at zero (and a round chambered) -> Loaded. + // + if (GetVoltageState() == Ready) + { + recoil -= time_slice; + if (recoil < 0.0f) + { + recoil = 0.0f; + if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoLoadedState) + { + weaponAlarm.SetLevel(LoadedState); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' LOADED (rounds=" << bin->GetAmmoCount() + << " T=" << CurrentTemperatureOf() << ")" + << endl << flush; + } + } + } + } + ComputeOutputVoltage(); + break; + + case JammedState: + if (trigger) + { + weaponAlarm.SetLevel(TriggerDuringJamState); + weaponAlarm.SetLevel(JammedState); + } + recoil = rechargeRate; + break; + + case NoAmmoState: + if (trigger) + { + weaponAlarm.SetLevel(DryTriggerState); // the dry-fire blip + } + weaponAlarm.SetLevel(NoAmmoState); // re-assert (the roach-motel) + recoil = rechargeRate; + ComputeOutputVoltage(); // dial pinned at 0 + break; + + default: // 0/1/4/6: transient audio-blip states, no case body + break; + } + + Check_Fpu(); +} diff --git a/restoration/source410/BT/SENSOR.CPP b/restoration/source410/BT/SENSOR.CPP index cc5be8f5..e1e94f68 100644 --- a/restoration/source410/BT/SENSOR.CPP +++ b/restoration/source410/BT/SENSOR.CPP @@ -1,227 +1,232 @@ -//===========================================================================// -// File: sensor.cpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: Sensor -- the radar / targeting subsystem // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(SENSOR_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -// -//############################################################################# -// Shared data support -//############################################################################# -// -Derivation - Sensor::ClassDerivations( - PoweredSubsystem::ClassDerivations, - "Sensor" - ); - -Sensor::SharedData - Sensor::DefaultData( - Sensor::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -// -//############################################################################# -// The radar / targeting subsystem. A non-replicant (master) instance runs the -// per-frame SensorSimulation. -//############################################################################# -// -Sensor::Sensor( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource -): - PoweredSubsystem(owner, subsystem_ID, subsystem_resource, DefaultData) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - radarPercent = 1.0f; // RadarBaseline - selfTest = False; - badVoltage = False; - - // - // Install the per-frame performance unless the owner is a replicant copy - // (which is driven by console updates instead). - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&Sensor::SensorSimulation); - } - - Check_Fpu(); -} - -// -//############################################################################# -//############################################################################# -// -Sensor::~Sensor() -{ -} - -// -//############################################################################# -//############################################################################# -// -Logical - Sensor::TestClass(Mech &) -{ - return True; -} - -Logical - Sensor::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -//############################################################################# -// -void - Sensor::ResetToInitialState() -{ - Check(this); - - radarPercent = 1.0f; // RadarBaseline - selfTest = False; - badVoltage = False; - - PoweredSubsystem::ResetToInitialState(True); -} - -// -//############################################################################# -// Per-frame radar / targeting update. Not yet reconstructed -- fires only once -// the mech is ticking. -//############################################################################# -// -void - Sensor::SensorSimulation(Scalar time_slice) -{ - Check(this); - - // - // The authentic per-frame sensor update (binary @004b1c4c): the - // PoweredSubsystem step first, then radarPercent = baseline - structural - // damage, gated by the electrical Ready state and the heat state. - // (Still deferred: the novice-mode HeatModelOff gate -- the player - // experience switch -- joins with the player-link accessor wave.) - // - PoweredSubsystemSimulation(time_slice); - - radarPercent = 1.0f - GetSubsystemDamageLevel(); // RadarBaseline - zoneDamage - if (radarPercent < 0.0f) - { - radarPercent = 0.0f; - } - selfTest = True; - - if (GetVoltageState() == Ready) - { - badVoltage = False; - } - else - { - badVoltage = True; - radarPercent = 0.0f; - } - - switch (GetHeatState()) - { - case NormalHeat: - selfTest = True; - break; - - case DegradationHeat: - radarPercent *= 0.5f; // HeatDegradationScale - selfTest = True; - break; - - case FailureHeat: - radarPercent = 0.0f; - selfTest = False; - break; - - default: - break; - } - - { - // - // One-shot confirmation that the engine's per-frame entity/roster tick - // (Entity::PerformAndWatch) has engaged -- i.e. the mission reached - // RunningMission and the mech is being simulated each frame. - // - static int firstTick = 0; - if (!firstTick && getenv("BT_MECH_LOG")) - { - firstTick = 1; - DEBUG_STREAM << "[tick] roster live (first Sensor frame), radarPercent=" - << radarPercent << " voltState=" << GetVoltageState() - << endl << flush; - } - } - - Check_Fpu(); -} - -// -//############################################################################# -// Damage / death. -//############################################################################# -// -void - Sensor::TakeDamage(Damage &damage) -{ - Check(this); - PoweredSubsystem::TakeDamage(damage); -} - -void - Sensor::DeathReset(Logical /*full_recovery*/) -{ - Check(this); - ResetToInitialState(); -} - -// -//############################################################################# -// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed -// (see MECHSUB.NOTES.md -- the 4.10 subsystem stream format). -//############################################################################# -// -int - Sensor::CreateStreamedSubsystem( - NotationFile *, - const char *, - const char *, - SubsystemResource *, - NotationFile *, - const ResourceDirectories *, - int - ) -{ - Fail("Sensor::CreateStreamedSubsystem -- sensor.cpp not yet reconstructed"); - return 0; -} +//===========================================================================// +// File: sensor.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Sensor -- the radar / targeting subsystem // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(SENSOR_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +// +//############################################################################# +// Shared data support +//############################################################################# +// +Derivation + Sensor::ClassDerivations( + PoweredSubsystem::ClassDerivations, + "Sensor" + ); + +Sensor::SharedData + Sensor::DefaultData( + Sensor::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// The radar / targeting subsystem. A non-replicant (master) instance runs the +// per-frame SensorSimulation. +//############################################################################# +// +Sensor::Sensor( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource +): + PoweredSubsystem(owner, subsystem_ID, subsystem_resource, DefaultData) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + radarPercent = 1.0f; // RadarBaseline + selfTest = False; + badVoltage = False; + + // + // Install the per-frame performance unless the owner is a replicant copy + // (which is driven by console updates instead). + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Sensor::SensorSimulation); + } + + Check_Fpu(); +} + +// +//############################################################################# +//############################################################################# +// +Sensor::~Sensor() +{ +} + +// +//############################################################################# +//############################################################################# +// +Logical + Sensor::TestClass(Mech &) +{ + return True; +} + +Logical + Sensor::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +//############################################################################# +// +void + Sensor::ResetToInitialState() +{ + Check(this); + + radarPercent = 1.0f; // RadarBaseline + selfTest = False; + badVoltage = False; + + PoweredSubsystem::ResetToInitialState(True); +} + +// +//############################################################################# +// Per-frame radar / targeting update. Not yet reconstructed -- fires only once +// the mech is ticking. +//############################################################################# +// +void + Sensor::SensorSimulation(Scalar time_slice) +{ + Check(this); + + // + // The authentic per-frame sensor update (binary @004b1c4c): the + // PoweredSubsystem step first, then radarPercent = baseline - structural + // damage, gated by the electrical Ready state and the heat state. + // (Still deferred: the novice-mode HeatModelOff gate -- the player + // experience switch -- joins with the player-link accessor wave.) + // + PoweredSubsystemSimulation(time_slice); + + radarPercent = 1.0f - GetSubsystemDamageLevel(); // RadarBaseline - zoneDamage + if (radarPercent < 0.0f) + { + radarPercent = 0.0f; + } + selfTest = True; + + if (GetVoltageState() == Ready) + { + badVoltage = False; + } + else + { + badVoltage = True; + radarPercent = 0.0f; + } + + switch (GetHeatState()) + { + case NormalHeat: + selfTest = True; + break; + + case DegradationHeat: + radarPercent *= 0.5f; // HeatDegradationScale + selfTest = True; + break; + + case FailureHeat: + radarPercent = 0.0f; + selfTest = False; + break; + + default: + break; + } + + { + // + // One-shot confirmation that the engine's per-frame entity/roster tick + // (Entity::PerformAndWatch) has engaged -- i.e. the mission reached + // RunningMission and the mech is being simulated each frame. + // + static int firstTick = 0; + if (!firstTick && getenv("BT_MECH_LOG")) + { + firstTick = 1; + DEBUG_STREAM << "[tick] roster live (first Sensor frame), radarPercent=" + << radarPercent << " voltState=" << GetVoltageState() + << endl << flush; + } + } + + Check_Fpu(); +} + +// +//############################################################################# +// Damage / death. +//############################################################################# +// +void + Sensor::TakeDamage(Damage &damage) +{ + Check(this); + PoweredSubsystem::TakeDamage(damage); +} + +void + Sensor::DeathReset(Logical full_recovery) +{ + Check(this); + // + // The base heal FIRST (private zone + DestroyedState + status level -- + // a crit-killed radar must come back), then the sensor re-baseline. + // + MechSubsystem::DeathReset(full_recovery); + ResetToInitialState(); +} + +// +//############################################################################# +// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed +// (see MECHSUB.NOTES.md -- the 4.10 subsystem stream format). +//############################################################################# +// +int + Sensor::CreateStreamedSubsystem( + NotationFile *, + const char *, + const char *, + SubsystemResource *, + NotationFile *, + const ResourceDirectories *, + int + ) +{ + Fail("Sensor::CreateStreamedSubsystem -- sensor.cpp not yet reconstructed"); + return 0; +}