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>
This commit is contained in:
Cyd
2026-08-12 23:27:09 -05:00
co-authored by Claude Fable 5
parent f760d16558
commit b499c88cb7
5 changed files with 397 additions and 0 deletions
@@ -0,0 +1,89 @@
# pod_render_norio.conf -- pod_render_rec with the RIO serial port OFF.
#
# The emulator log shows a steady stream of serial1 RX OVERRUN errors on
# the RIO pipe, and controls post their events at HighEventPriority
# UNCONDITIONALLY (CONTROLS.HPP:250) -- so a chattering RIO port would
# flood a priority the background pump always serves first, starving the
# priority-0 renderer events the load gate waits on. This conf tests that
# by removing the port entirely. Everything else is identical to the rec
# conf, so a launch here and a hang there isolates the RIO.
#
[sdl]
output=opengl
# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop;
# with the retry patches a rare dropout self-recovers (see gauge_rio.conf).
priority=higher,higher
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief
# emulation-thread stalls (RIO retry recovery) don't audibly chop
rate=44100
blocksize=1024
prebuffer=60
[serial]
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
# few-ms ACK deadline is met; plasma display on COM2 (real pod has both).
# VWE fork namedpipe backend (com0com/realport retired -- COM1/COM2 gone):
# DOSBox = pipe client (retry), vRIO/vPLASMA apps = servers; an unconnected
# pipe behaves as an unplugged cable so the mission still runs. serialnamedpipe.h
serial1=disabled
serial2=namedpipe pipe:vplasma
# live UNBUFFERED game output: DOS char devices are not buffered, so
# redirecting stdout to COM3 lands every line immediately. A normal
# '> file' redirect stays 0 bytes until the process exits, which hides
# all progress on a run that does NOT crash.
serial3=file file:C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
c:
cd \REL410\BT
set VIDEOFORMAT=svga
rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per
rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS
rem driver relies on; skipping it left the cards uninitialized (silent).
rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the
rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is
rem dumped from a real card. diagnose /s kept (passes, sets mixer config).
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO
rem attached; stdout redirected so mission-load progress survives kills.
set BT_JOINTS=1
set L4VIEWEXT=1
set BT_STACK_LOG=1
set BT_MECH_LOG=1
set BT_FORCE_COOKOFF=1
set BT_GAUGE_LOG=1
set BT_FORCE_TURN=0
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s n p
32rtm.exe -x
BTL4REC.EXE -egg testarn.egg > COM3
echo GAME-RC=%errorlevel% >> RC.TXT
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+235
View File
@@ -19,6 +19,14 @@
# 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,
@@ -79,6 +87,19 @@ AmmoBin::AmmoBin(
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();
}
@@ -119,3 +140,217 @@ void
//
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();
}
+20
View File
@@ -44,6 +44,26 @@
static Derivation ClassDerivations;
static SharedData DefaultData;
typedef void
(AmmoBin::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
//
// THE COOK-OFF (binary performance @004bd394, detonation @004bd300
// -> @004ac274). A bin whose GUN has gone into heat failure starts
// a randomised countdown; when it expires the bin is destroyed and
// its remaining rounds go off inside the mech.
//
void
AmmoBinSimulation(Scalar time_slice);
void
CookOff();
static Logical
TestClass(Mech &);
Logical
+47
View File
@@ -705,3 +705,50 @@ its list) first, then land the whole thing in one piece.
Note the shape of the find: two dead systems in series hid a third. The
watcher binding did not just fix an alarm -- it exposed that the mech has
never been able to suffer an ammo explosion.
## 5.3.137 -- THE AMMO COOK-OFF LANDS
The blocking identity from 5.3.136 is settled: **DamageZoneClassID == 78
== 0x4e**, computed from our own VDATA enum, so the collector's filter
selects DAMAGE ZONES and the blast is shared across the mech's zones.
Also CORRECTING my 5.3.136 write-up: I said heat clearing cancels the
countdown. It does not. Re-reading @004bd394, both the arm gate and the
cancel test the SAME cell (+0x1a8, adjacent to the ammo alarm and read as
the alarm having settled on SPENT). Once armed, a cook-off is only called
off by the bin being spent or destroyed -- letting the gun cool does NOT
save you.
Landed: `AmmoBin::AmmoBinSimulation` (a Performance the class never had,
installed master+dynamic gated as the binary's ctor does) plus
`AmmoBin::CookOff`. Our fuse is a millisecond COUNTDOWN where the binary
stores an absolute deadline -- same behaviour, no clock-source dependency.
LIVE (BT_FORCE_COOKOFF=1, pod_render_cookoff.conf):
[cookoff] 'AmmoBinAFC100' ARMED -- 20 rounds aboard
[cookoff] 'AmmoBinLRM15_1' ARMED -- 16 rounds aboard
[cookoff] 'AmmoBinLRM15_2' ARMED -- 16 rounds aboard
[cookoff] 'AmmoBinAFC100' DETONATES: 20 x 25 = 500 over 22 zones
[cookoff] 'AmmoBinLRM15_2' DETONATES: 16 x 50 = 800 over 22 zones
[critfail] 'Searchlight' DESTROYED ... 'ThermalSight' DESTROYED
The randomised fuse spreads the rack (the three bins go off at different
times, not on one frame), and the blast feeds straight into the existing
destruction cascade.
### [T1] THE PER-ROUND CHARGE IS A STAND-IN
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 use the FED WEAPON's authored per-shot damage:
right order of magnitude, physically sensible, but a stand-in to be
replaced by the first pass over the ammo resource block.
**A caught mistake worth keeping:** the first landing used `heatPerRound`
for the charge. That is a HEAT figure of about 3e-9, so the detonation
computed 4.8e-08 of damage and did nothing at all -- a mechanic that
fires, announces itself, and is silently inert. Only printing the
arithmetic in the soak log exposed it. Log the numbers, not just the
event.
+6
View File
@@ -212,6 +212,12 @@
unsigned
GetWeaponState() { Check(this); return weaponAlarm.GetLevel(); }
//
// The authored per-shot damage. Read by the ammo cook-off as its
// [T1] stand-in for the per-round charge (see AMMOBIN.CPP).
//
Scalar
DamageAmountOf() const { Check(this); return damageAmount; }
Logical
CheckFireEdge();
virtual void