Files
TeslaRel410/restoration/source410/BT/AMMOBIN.CPP
T
CydandClaude Fable 5 b499c88cb7 BT410 5.3.137: the ammo cooks off -- a mech can finally be gutted by its own magazine
The blocking identity is settled: DamageZoneClassID == 78 == 0x4e,
computed from our own VDATA enum, so the binary's collector filter
selects DAMAGE ZONES and the blast is shared across the mech's zones.

Landed: AmmoBin::AmmoBinSimulation -- a Performance the class never had,
installed under the same master+dynamic gate the binary's ctor uses --
and AmmoBin::CookOff.  A bin whose fed weapon reaches heat failure
latches a randomised fuse; when it burns down the bin is destroyed and
its remaining rounds go off inside the mech, damage scaled by rounds
remaining and SHARED across the zones, every victim announced.

Correcting my own 5.3.136 write-up: heat clearing does NOT cancel the
countdown.  Both the arm gate and the cancel read the same cell -- the
ammo alarm settled on spent -- so once armed, only spending or destroying
the bin calls it off.  Letting the gun cool does not save you.

Live on the rig: three bins arm, the fuse spread sends them off on
different frames rather than together, 20 rounds x 25 = 500 damage over
22 zones from one bin and 16 x 50 = 800 from another, feeding straight
into the existing destruction cascade -- Searchlight and ThermalSight
destroyed as a consequence.

[T1] recorded: the per-round charge is a stand-in.  The binary reads it
from the bin's streamed explosion record, in the block our cookOffState
reserves and nothing parses yet; we use the fed weapon's authored
per-shot damage until that record is read.

And a mistake worth keeping in the notes: the first attempt sourced the
charge from heatPerRound, a heat figure of about 3e-9, so the detonation
computed 4.8e-08 of damage and did precisely nothing -- a mechanic that
fires, announces itself, and is silently inert.  Only printing the
arithmetic caught it.  Log the numbers, not just the event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 23:27:09 -05:00

357 lines
9.5 KiB
C++

//===========================================================================//
// 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 <bt.hpp>
#pragma hdrstop
#if !defined(AMMOBIN_HPP)
# include <ammobin.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp>
#endif
#if !defined(RANDOM_HPP)
# include <random.hpp>
#endif
Derivation
AmmoBin::ClassDerivations(
HeatWatcher::ClassDerivations,
"AmmoBin"
);
const AmmoBin::IndexEntry
AmmoBin::AttributePointers[]=
{
ATTRIBUTE_ENTRY(AmmoBin, AmmoCount, ammoCount),
ATTRIBUTE_ENTRY(AmmoBin, TimeToReady, feedTimer),
ATTRIBUTE_ENTRY(AmmoBin, AmmoClassID, ammoClassID),
ATTRIBUTE_ENTRY(AmmoBin, AmmoState, ammoAlarm),
ATTRIBUTE_ENTRY(AmmoBin, FireCountdownStarted, fireCountdownStarted)
};
AmmoBin::AttributeIndexSet
AmmoBin::AttributeIndex(
ELEMENTS(AmmoBin::AttributePointers),
AmmoBin::AttributePointers,
MechSubsystem::AttributeIndex
);
AmmoBin::SharedData
AmmoBin::DefaultData(
AmmoBin::ClassDerivations,
Subsystem::MessageHandlers,
AmmoBin::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;
fireCountdownStarted = 0; // was: reserved
for (int i = 0; i < 12; ++i)
{
cookOffState[i] = 0;
}
//
// The per-frame cook-off watch, on the master instance only (the
// binary ctor @004bd5c4 installs its performance triple under the
// same MasterInstance + DynamicFlag gate every sibling uses).
//
if (
(owner->simulationFlags & Entity::InstanceMask) == Entity::MasterInstance &&
(owner->simulationFlags & Entity::DynamicFlag) != 0
)
{
SetPerformance(&AmmoBin::AmmoBinSimulation);
}
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;
}
//
//#############################################################################
// AmmoBinSimulation -- binary @004bd394. The bin watches the GUN IT FEEDS
// (the watch link bound in 5.3.132): once that weapon reaches heat FAILURE
// the bin latches a randomised countdown, and when the countdown expires
// the rounds go off inside the mech.
//
// A SPENT bin neither arms nor keeps a countdown running -- the binary's
// gate is a cell adjacent to the ammo alarm, read as the alarm having
// SETTLED on spent (level 2); ours reads the alarm directly, which is the
// same condition in our simpler alarm ([T1] noted in AMMOBIN.NOTES.md).
//#############################################################################
//
void
AmmoBin::AmmoBinSimulation(Scalar time_slice)
{
Check(this);
//
// The mirrored heat watch first -- this is what reads the gun.
//
WatchSimulation(time_slice);
if (GetSimulationState() == 1)
{
ammoAlarm.SetLevel(2);
}
Logical
spent = (ammoAlarm.GetLevel() == 2);
//
// ARM: the fed weapon is cooking and this bin still has rounds.
//
//
// BT_FORCE_COOKOFF=1: treat the fed weapon as cooking, so the whole
// mechanic can be exercised headlessly without first driving a gun
// into real heat failure.
//
Logical
gun_cooking = (heatAlarm.GetLevel() == HeatSink::FailureHeat);
if (getenv("BT_FORCE_COOKOFF") != NULL)
{
gun_cooking = True;
}
if (
gun_cooking &&
!spent &&
fireCountdownStarted == 0
)
{
fireCountdownStarted = 1;
cookOffArmed = 1;
//
// The binary stores an absolute deadline (now + a randomised
// delay); we hold the same delay as a COUNTDOWN in milliseconds,
// which needs no clock source and is frame-rate independent. The
// spread is what stops a whole rack going off on one frame.
//
cookOffTime = 1000 + (int)((Scalar)Random * 4000.0f);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[cookoff] '" << GetName()
<< "' ARMED -- fed weapon in heat failure, "
<< ammoCount << " rounds aboard" << endl << flush;
}
}
//
// RUN or CANCEL.
//
if (fireCountdownStarted != 0)
{
if (spent)
{
fireCountdownStarted = 0;
cookOffArmed = 0;
}
else if (cookOffTime > 0)
{
cookOffTime -= (int)(time_slice * 1000.0f);
}
else
{
fireCountdownStarted = 0;
cookOffArmed = 0;
SetSimulationState(1); // the bin is gone
CookOff();
}
}
if (ammoCount < 1)
{
ammoAlarm.SetLevel(2);
}
Check_Fpu();
}
//
//#############################################################################
// CookOff -- binary @004bd300 -> @004ac274. The stored rounds go off:
//
// * the damage is authored PER ROUND and scaled by the rounds remaining,
// so a full bin guts the mech and a nearly empty one barely coughs;
// * the bin's own private damage zone is pinned destroyed;
// * the blast is SHARED -- divided by the number of damage zones, not
// applied whole to each (DamageZoneClassID == 78 == the binary's 0x4e
// collector filter, computed from our own VDATA enum);
// * every victim is announced with the authentic
// "ammo explosion damaging <name>" (string @0x50df61).
//#############################################################################
//
void
AmmoBin::CookOff()
{
Check(this);
Mech
*mech = (Mech *)GetEntity();
if (mech == NULL || mech->damageZoneCount < 1)
{
ammoCount = 0;
ammoAlarm.SetLevel(2);
return;
}
//
// The charge: rounds x the per-round damage.
//
// [T1] THE PER-ROUND FIGURE IS NOT SOURCED YET. The binary reads it
// from the bin's streamed EXPLOSION RECORD (+0x1f4, the second field
// of the block our `cookOffState[12]` reserves and nothing parses),
// then writes the product back into a copy of that record. Until
// that record is parsed we stand in the FED WEAPON's authored
// per-shot damage -- the right order of magnitude and the physically
// sensible reading, but a stand-in; the first pass over the ammo
// resource block should replace it.
//
// (The first attempt used `heatPerRound`, a HEAT figure of about
// 3e-9, which made the whole detonation a silent no-op -- caught only
// because the soak printed the arithmetic.)
//
Scalar
perRound = 0.0f;
{
MechWeapon
*fed = (MechWeapon *)watchedLink.Resolve();
if (fed != NULL && fed->IsDerivedFrom(MechWeapon::ClassDerivations))
{
perRound = fed->DamageAmountOf();
}
}
Scalar
charge = (Scalar)ammoCount * perRound;
//
// Our own zone goes first, before the blast is shared out.
//
if (damageZone != NULL)
{
((::DamageZone *)damageZone)->damageLevel = 1.0f;
}
Scalar
share = charge / (Scalar)mech->damageZoneCount;
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[cookoff] '" << GetName() << "' DETONATES: "
<< ammoCount << " rounds x " << perRound
<< " = " << charge << " shared over "
<< mech->damageZoneCount << " zones (" << share
<< " each)" << endl << flush;
}
for (int z = 0; z < mech->damageZoneCount; ++z)
{
if (mech->damageZones[z] == NULL)
{
continue;
}
Damage
blast;
blast.damageType = Damage::ExplosiveDamageType;
blast.damageAmount = share;
blast.damageForce = Vector3D(0.0f, 0.0f, 0.0f);
blast.impactPoint = mech->localOrigin.linearPosition;
Entity::TakeDamageMessage
hit(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
mech->GetEntityID(),
z,
blast
);
mech->Dispatch(&hit);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "ammo explosion damaging zone " << z
<< endl << flush;
}
}
ammoCount = 0;
ammoAlarm.SetLevel(2);
Check_Fpu();
}