diff --git a/emulator/render-bridge/pod_render_cookoff.conf b/emulator/render-bridge/pod_render_cookoff.conf new file mode 100644 index 00000000..fd9855f4 --- /dev/null +++ b/emulator/render-bridge/pod_render_cookoff.conf @@ -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 + diff --git a/restoration/source410/BT/AMMOBIN.CPP b/restoration/source410/BT/AMMOBIN.CPP index a9e63fcf..93f87b0e 100644 --- a/restoration/source410/BT/AMMOBIN.CPP +++ b/restoration/source410/BT/AMMOBIN.CPP @@ -19,6 +19,14 @@ # include #endif +#if !defined(MECHWEAP_HPP) +# include +#endif + +#if !defined(RANDOM_HPP) +# include +#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 " (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(); +} diff --git a/restoration/source410/BT/AMMOBIN.HPP b/restoration/source410/BT/AMMOBIN.HPP index e4132789..7d40c1f9 100644 --- a/restoration/source410/BT/AMMOBIN.HPP +++ b/restoration/source410/BT/AMMOBIN.HPP @@ -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 diff --git a/restoration/source410/BT/MECH4.NOTES.md b/restoration/source410/BT/MECH4.NOTES.md index ef797ead..58008464 100644 --- a/restoration/source410/BT/MECH4.NOTES.md +++ b/restoration/source410/BT/MECH4.NOTES.md @@ -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. diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index 09d744b0..9b5842d9 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -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