Files
TeslaRel410/restoration/source410/BT/AMMOBIN.CPP
T
CydandClaude Fable 5 77db3290d0 BT410 5.3.138: our ammo explosion may be more lethal than the original -- the cook-off charge re-marked [T2] after the hunt for its source came up empty
Chasing yesterday's [T1] to retire it produced an answer that must not be
papered over.

Two corrections to my own 5.3.137 note first.  FUN_0041db7c is the
engine's Damage CONSTRUCTOR -- it zeroes the amount, seeds the force,
sets the normal to (0,1,0) -- so the bin's +0x1f0 block is a Damage
struct, not an "explosion record", and our cookOffState[12] is that
struct in disguise.  And the resource read I thought filled it belongs to
the GENERATOR ctor: same dword index, different class.  The per-class
offset trap, twice in two days.

The uncomfortable part: across the whole AmmoBin TU that Damage is only
default-constructed and read.  Nothing writes its amount.  The
constructor leaves it zero, so rounds x 0 == 0 -- the shipped game's
cook-off appears to compute ZERO damage.  It destroys the bin, announces
"ammo explosion damaging" for every zone, and delivers nothing.

Three readings survive: the feature is vestigial in 4.10; a writer lives
outside the TU (a table-registered handler is where to look next); or the
multiplier cell is misidentified.

So the landing is re-marked [T2], a deliberate divergence, not [T1]
staging.  With the fed weapon's damage standing in, ours does 500-800
points across the zones and destroys subsystems where the shipped one
does nothing.  That is us being more lethal than the original, which is
exactly the silent gameplay drift this project exists to avoid.  It stays
ON -- an inert mechanic is indistinguishable from an unfinished one and
would hide the question -- but it is now flagged in the code, in the
notes, and to the operator, and BT_FORCE_COOKOFF remains the only way to
trigger it without genuine heat failure.

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

363 lines
9.8 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.
//
// [T2] DELIBERATE DIVERGENCE -- WE ARE MORE LETHAL THAN THE ORIGINAL.
// The binary multiplies the rounds by the amount held in the bin's own
// Damage struct (+0x1f4; our `cookOffState[12]` IS that struct). But
// across the whole AmmoBin TU that Damage is only default-constructed
// and read -- nothing writes its amount -- so the shipped cook-off
// appears to compute ZERO damage: it destroys the bin, announces every
// zone, and delivers nothing.
//
// Either the feature is vestigial in 4.10, or a writer lives outside
// the TU (a table-registered handler is the place to look), or the
// multiplier cell is misidentified. Until that is settled we stand in
// the FED WEAPON's per-shot damage, which makes the mechanic real --
// an inert mechanic is indistinguishable from an unfinished one and
// would hide the question. See MECH4.NOTES.md 5.3.138.
//
// (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();
}