Files
TeslaRel410/restoration/source410/BT/PROJWEAP.CPP
T
CydandClaude Fable 5 623c872c92 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 <noreply@anthropic.com>
2026-07-24 16:35:05 -05:00

440 lines
13 KiB
C++

//===========================================================================//
// 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 <bt.hpp>
#pragma hdrstop
#if !defined(PROJWEAP_HPP)
# include <projweap.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(AMMOBIN_HPP)
# include <ammobin.hpp>
#endif
#if !defined(RANDOM_HPP)
# include <random.hpp>
#endif
#if !defined(BTPLAYER_HPP)
# include <btplayer.hpp>
#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 << "<no bin>";
}
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();
}