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>
357 lines
9.5 KiB
C++
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();
|
|
}
|