#154 settled: the generator-out warning was authored on the WRONG EDGE in the shipped 1995 content -- BTL4.RES keys it (trigState=4, inverse=1, Start) = fire on LEAVING GeneratorOut, so a thermal trip was SILENT and the klaxon played at recovery/respawn (live-benched both ways; decode triple-verified: T0 read order, binary @0043e7d4/@0043eb40 semantics, independent byte parse; 144-row census = the game's ONLY inverse-Start fault trigger). DELIBERATE DEVIATION (user-approved, the port's only content override): AudioStateTrigger ctor flips it to enter-4 at stream time -- warning at the TRIP, silent at recovery + the respawn 4->0 edge; BT_GEN_WARN_SHIPPED=1 restores shipped. Threshold audit closes AUTHENTIC: recovery at heatAlarm NormalHeat = T<=1000 vs trip at 2000 = exactly the manual p23 half-cooled rule, and the heat bar's red fill maps to it to the tick. Era-witness reconciliation: BT 4.10 shipped NO speech samples (full AUDIO1/2.RES census) -- the remembered voice at trip is Firestorm's. Rig: BT_GEN_HEAT injection + BT_AUDIO_EDGE receipts, gen_edge_bench.sh PASS (4 fixups, trip FIRED, recovery 0). KB: decomp-reference generator section + no-speech note; manual p23 also answered #133 (X = crit-destroyed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-11 07:39:42 -05:00
co-authored by Claude Fable 5
parent e2ab17665d
commit 08fc87ec6e
7 changed files with 208 additions and 5 deletions
+40
View File
@@ -475,6 +475,46 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
recomputes output via `(1 damage) × rated` and zeroes it. stateAlarm 4's recomputes output via `(1 damage) × rated` and zeroes it. stateAlarm 4's
producer = the THERMAL BREAKER in GeneratorSimulation itself; a 2026-08-03 producer = the THERMAL BREAKER in GeneratorSimulation itself; a 2026-08-03
destruction→state-4 bridge was removed as unfounded. destruction→state-4 bridge was removed as unfounded.
- **Generator trip/recovery + the warning edge (#154 audit, 2026-08-11, all [T1] unless
noted).** Trip: heatAlarm==FailureHeat(2) → stateAlarm 4, i.e. T > FailureTemperature;
recovery gate (case-4 body @4b1f7c) = heatAlarm back to **NormalHeat(0)**, i.e.
T ≤ DegradationTemperature — then 4→0 Starting → StartTime spin-up → 0→2 Ready.
Authored generator temps (raw BTL4.RES): start=77, degradation=**1000**,
failure=**2000** on all six vehicles — recovery at exactly HALF the trip line;
manual p23 prints the half-way rule verbatim, and the heat bar's colored fill
(VertTwoPartBar @4c48fc) exists exactly while T > 1000, so "red gone" == the
recovery gate to the tick. NO hysteresis in the heatAlarm ladder (@4ad924).
**The warning edge is authored backwards in the shipped content**: BTL4.RES
streams exactly TWO AudioStateTriggers per generator on GeneratorState —
(trigState=1 Idle, inv=0, Start) and (trigState=4 GeneratorOut, **inv=1**, Start,
= fire on LEAVING 4). No enter-4 trigger exists (RES-wide census: 144/144 rows,
triple-verified decode + live [audioedge] capture). Consequences, ALL live-benched
(`scratchpad/night15/gen_edge_bench.sh`, receipts BT_AUDIO_EDGE/[audioedge] +
BT_GEN_HEAT injection): thermal trip 2→4 SILENT; natural recovery 4→0 FIRES the
warning; respawn-with-tripped-gen RTIS (4,0) FIRES it (ghost warning during
respawn); crit-death fires enter-Idle one frame late; 4→1 (cooled while switched
off) double-fires. The generators' inv+Start is the ONLY inverse-Start fault
trigger in the game (census) — a 1995 authoring slip [T4 intent]. Port machinery
is faithful end-to-end. **DELIBERATE DEVIATION shipped 2026-08-11 (user-approved):**
the AudioStateTrigger ctor (AUDWTHR.cpp) flips the GeneratorState (trigState=4,
inv=1) record to inv=0 at stream time — warning now fires at the TRIP, silent at
recovery/respawn; `BT_GEN_WARN_SHIPPED=1` restores shipped behavior. This is the
port's ONLY authored-content override (precedent: the self-hit score deviation).
**Why the era witnesses remember a VOICE at trip: BT 4.10 shipped NO speech
samples at all** [T1 — full sample census of content/AUDIO/*.wav (the cracked
AUDIO1/2.RES set): warnings are klaxon zones only; the exe has no "generator
out"/.WAV/.VOC strings]. The spoken "Warning, generator out" they recall is
FIRESTORM's speech system (Tesla II) — memory bleed from the later game. The
manual's "spoken MechTech warnings" (p14) describe the klaxon zones
aspirationally.
Voice-steal note: night-15 field logs show up to 932 channel steals/session — even
fired Starts can be eaten at combat peak (L4AUDRND weight-steal).
Diag hooks: BT_GEN_HEAT=<units/s> (+_DELAY/_SECS) injects into viewpoint
GeneratorA (thermalMass=726000 raw units — rate ~1.5e8 for ~18-25 s trips it);
BT_AUDIO_EDGE=1 prints every state-edge trigger delivery FIRED/no-fire.
Related corrections: AmmoState 3 = EJECTING (an earlier lane's "cook-off enter-3"
label was wrong — cook-off warning rides FireCountdownStarted matchers); manual
p23 also answers #133: the MFD X = generator destroyed by CRIT, not overheat.
- **THE CONDITION FLAGS ARE ROUTINE, NOT ALARMS — field-verified 2026-08-08 [T2]:** a - **THE CONDITION FLAGS ARE ROUTINE, NOT ALARMS — field-verified 2026-08-08 [T2]:** a
`[techstat] <sub> condition <n> SET` is an *operating* flag ("this component is above `[techstat] <sub> condition <n> SET` is an *operating* flag ("this component is above
nominal / browned out RIGHT NOW"), not a fault report. Census over a full Steam match nominal / browned out RIGHT NOW"), not a fault report. Census over a full Steam match
+4 -1
View File
@@ -102,7 +102,10 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
(OwnerBaseSpeed reads the real base instead of the 1.0 stub). (OwnerBaseSpeed reads the real base instead of the 1.0 stub).
5. **Electrical sourcing — the dial's REAL stake is TORSO TWIST.** A gear's voltage must be 5. **Electrical sourcing — the dial's REAL stake is TORSO TWIST.** A gear's voltage must be
<= the generator's measured output (`(1 generator damage) × rated`, thermal breaker on <= the generator's measured output (`(1 generator damage) × rated`, thermal breaker on
FailureHeat; there is NO load model — demand never pulls the bus down), or the myomers leave FailureHeat with recovery gated on NormalHeat — trip at T>2000, re-arm at T≤1000, exactly
the manual p23 "cooled down half way" rule; full trip/recovery/warning-edge audit in
[[decomp-reference]] §Generator trip/recovery (#154); there is NO load model — demand
never pulls the bus down), or the myomers leave
Ready. The Torso is a PowerWatcher on the myomers: `TorsoSimulation` ZEROES the twist rate Ready. The Torso is a PowerWatcher on the myomers: `TorsoSimulation` ZEROES the twist rate
while the watched myomers are not Ready (halves it at DegradationHeat) — so an over-high gear while the watched myomers are not Ready (halves it at DegradationHeat) — so an over-high gear
on a damaged generator freezes the pilot's AIM until they downshift. On a healthy mech, on a damaged generator freezes the pilot's AIM until they downshift. On a healthy mech,
+41 -4
View File
@@ -1,4 +1,5 @@
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include "munga.h" #include "munga.h"
#pragma hdrstop #pragma hdrstop
@@ -978,6 +979,31 @@ AudioStateTrigger::AudioStateTrigger(
MemoryStream_Read(stream, &excludeTransition); MemoryStream_Read(stream, &excludeTransition);
MemoryStream_Read(stream, &excludeState); MemoryStream_Read(stream, &excludeState);
// #154 DELIBERATE DEVIATION from shipped 1995 content (the ONLY authored-
// content override in the port -- revert with BT_GEN_WARN_SHIPPED=1).
// BTL4.RES authors the generator-out warning as (trigState=4, inverse=1,
// Start) = fire on LEAVING GeneratorOut -- so a thermal trip was SILENT and
// the klaxon played at RECOVERY/respawn instead (live-benched,
// gen_edge_bench.sh; bytes @0x2bf13, decode triple-verified). Every other
// fault alarm in the game is authored enter-state, inverse is otherwise
// only a Stop terminator (144-row census), and the manual (p14/p23) lists
// this as a loss-time warning: a one-bit 1995 authoring slip. Flip it to
// enter-4 so the warning speaks at the TRIP, stays silent at recovery and
// at the respawn reset's 4->0 edge. (Testers' "voice at trip" memories are
// Firestorm's speech system -- BT 4.10 shipped no speech samples at all.)
{
extern char g_lastBoundAttrName[64];
if (triggerState == 4 && inverseTrigger != 0
&& strcmp(g_lastBoundAttrName, "GeneratorState") == 0
&& getenv("BT_GEN_WARN_SHIPPED") == 0)
{
inverseTrigger = 0;
if (getenv("BT_ATTRBIND_LOG"))
DEBUG_STREAM << "[statecfg] #154 fixup: GeneratorState leave-4"
" Start -> enter-4 Start\n" << std::flush;
}
}
if (getenv("BT_ATTRBIND_LOG")) { static int s_stc=0; if (s_stc++<120) if (getenv("BT_ATTRBIND_LOG")) { static int s_stc=0; if (s_stc++<120)
DEBUG_STREAM << "[statecfg] attrPtr=" << (void*)attributePointer DEBUG_STREAM << "[statecfg] attrPtr=" << (void*)attributePointer
<< " comp=" << (void*)audioComponentSocket.GetCurrent() << " comp=" << (void*)audioComponentSocket.GetCurrent()
@@ -1076,10 +1102,21 @@ void
// //
// Check for the trigger state // Check for the trigger state
// //
if ( const int bt_fires =
(!inverseTrigger && new_state == triggerState) || ((!inverseTrigger && new_state == triggerState) ||
(inverseTrigger && old_state == triggerState) (inverseTrigger && old_state == triggerState));
) // #154 diag (BT_AUDIO_EDGE=1): EVERY state-edge delivery to EVERY authored
// trigger, fired or not -- state edges are rare, so this is low-volume.
// Correlate with the [gen] TRIPPED/RESTARTING lines to observe which edge
// actually starts a warning sequence.
if (getenv("BT_AUDIO_EDGE"))
DEBUG_STREAM << "[audioedge] old=" << old_state << " new=" << new_state
<< " trig=" << triggerState << " inv=" << (int)inverseTrigger
<< " ctl=" << (int)controlID << "/" << controlValue
<< " comp=" << (void*)audioComponentSocket.GetCurrent()
<< (bt_fires ? " FIRED" : " no-fire")
<< "\n" << std::flush;
if (bt_fires)
{ {
Check(&audioComponentSocket); Check(&audioComponentSocket);
Check(audioComponentSocket.GetCurrent()); Check(audioComponentSocket.GetCurrent());
+13
View File
@@ -96,6 +96,19 @@ AttributeWatcher::AttributeWatcher(
attributePointer = simulation->GetAttributePointer(attribute_name); attributePointer = simulation->GetAttributePointer(attribute_name);
// The bound attribute's NAME, visible to derived ctors (the stream layout
// puts the trigger fields AFTER the base bind, so a derived ctor that needs
// to know WHICH attribute it just bound -- the #154 GeneratorState fixup --
// reads it from here). Valid only during construction.
{
extern char g_lastBoundAttrName[64];
const char *an = (const char *)attribute_name;
int i = 0;
for (; an != 0 && an[i] != '\0' && i < 63; ++i)
g_lastBoundAttrName[i] = an[i];
g_lastBoundAttrName[i] = '\0';
}
if (getenv("BT_ATTRBIND_LOG")) if (getenv("BT_ATTRBIND_LOG"))
{ {
extern int g_curAudioWatcherClass; extern int g_curAudioWatcherClass;
+1
View File
@@ -23,6 +23,7 @@ ALuint *g_buffers;
int g_numBuffers; int g_numBuffers;
const char *g_bufferNames[512]; // bufferInd -> sample file (diag: the live-playing dump) const char *g_bufferNames[512]; // bufferInd -> sample file (diag: the live-playing dump)
int g_curAudioWatcherClass = 0; int g_curAudioWatcherClass = 0;
char g_lastBoundAttrName[64] = {0}; // AttributeWatcher bind name, for derived-ctor fixups (#154)
// //
// Minimal canonical-PCM WAV reader. The repo's libsndfile-1.dll is a STUB // Minimal canonical-PCM WAV reader. The repo's libsndfile-1.dll is a STUB
+43
View File
@@ -1295,6 +1295,49 @@ void
void void
Generator::GeneratorSimulation(Scalar time_slice) Generator::GeneratorSimulation(Scalar time_slice)
{ {
// DEV: BT_GEN_HEAT=<kJ/s> pumps heat into the VIEWPOINT mech's Generator A
// for a bounded window (BT_GEN_HEAT_DELAY=<s> lead-in, default 10;
// BT_GEN_HEAT_SECS=<s> injection length, default 20) -- the deterministic
// thermal-trip bench for #154: drive T past FailureTemperature, stop, let
// it cool through DegradationTemperature, and correlate the [gen]
// TRIPPED/RESTARTING receipts against the [audioedge] receipts to observe
// which stateAlarm edge actually starts the warning sequence.
{
static const char *s_ghEnv = getenv("BT_GEN_HEAT");
if (s_ghEnv != 0)
{
static int s_ghSaid = 0;
if (s_ghSaid < 8)
{
++s_ghSaid;
DEBUG_STREAM << "[genheat] gate: gen#" << generatorNumber
<< " owner=" << (void *)owner
<< " viewpoint=" << (void *)application->GetViewpointEntity()
<< " T=" << currentTemperature
<< " mass=" << thermalMass << "\n" << std::flush;
}
}
if (s_ghEnv != 0 && generatorNumber == 1
&& (Entity *)owner == application->GetViewpointEntity())
{
static float s_ghClock = 0.0f;
static float s_ghDelay = -1.0f, s_ghSecs = -1.0f;
if (s_ghDelay < 0.0f)
{
const char *d = getenv("BT_GEN_HEAT_DELAY");
const char *s = getenv("BT_GEN_HEAT_SECS");
s_ghDelay = (d != 0 && *d != '\0') ? (float)atof(d) : 10.0f;
s_ghSecs = (s != 0 && *s != '\0') ? (float)atof(s) : 20.0f;
}
s_ghClock += time_slice;
if (s_ghClock >= s_ghDelay && s_ghClock < s_ghDelay + s_ghSecs)
{
heatEnergy += (Scalar)atof(s_ghEnv) * time_slice;
currentTemperature = heatEnergy / thermalMass;
}
}
}
HeatSink::HeatSinkSimulation(time_slice); // FUN_004ad924 HeatSink::HeatSinkSimulation(time_slice); // FUN_004ad924
if (simulationState == 1) // this[0x10] @0x40 if (simulationState == 1) // this[0x10] @0x40
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# #154: OBSERVE (not infer) which generator stateAlarm edge starts the warning
# sequence. BT_GEN_HEAT drives the viewpoint mech's Generator A over
# FailureTemperature (trip), then the injection window closes and it cools
# through DegradationTemperature (recovery). Receipts:
# [gen] GeneratorA TRIPPED/RESTARTING (BT_HEAT_LOG, powersub.cpp)
# [audioedge] old= new= trig= inv= ... FIRED/no-fire (BT_AUDIO_EDGE, AUDWTHR.cpp)
# [statecfg] authored trigger table + the #154 fixup line (BT_ATTRBIND_LOG)
# SHIPPED content: trip (2->4) silent, recovery (4->0) fires (captured 2026-08-11).
# WITH the #154 deviation (default): trip FIRES the enter-4 Start, recovery and
# the respawn 4->0 edge are silent. PASS = trip-edge fires>=1, old=4 fires==0.
# (BT_GEN_WARN_SHIPPED=1 restores shipped behavior; bench asserts the deviation.)
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
rm -f genedge.log
bt_expert_egg MP.EGG GEN.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" GEN.EGG
( export BT_GEN_HEAT=150000000 BT_GEN_HEAT_DELAY=15 BT_GEN_HEAT_SECS=18
export BT_HEAT_LOG=1 BT_AUDIO_EDGE=1 BT_ATTRBIND_LOG=1
bt_launch genedge.log GEN.EGG 0x03 -egg GEN.EGG )
sleep 300
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
python - <<'PY'
import io, re
txt = io.open("genedge.log", encoding="latin-1", errors="replace").read()
lines = txt.splitlines()
trip = [i for i,l in enumerate(lines) if "TRIPPED" in l and "GeneratorA" in l]
rest = [i for i,l in enumerate(lines) if "RESTARTING" in l and "GeneratorA" in l]
print("=== #154 LIVE EDGE OBSERVATION ===")
print("GeneratorA TRIPPED lines :", len(trip))
print("GeneratorA RESTARTING lines:", len(rest))
def edges_near(idx, span=30):
lo, hi = max(0, idx-span), min(len(lines), idx+span)
return [l for l in lines[lo:hi] if "[audioedge]" in l]
if trip:
print("--- [audioedge] around FIRST TRIP (line %d) ---" % trip[0])
for l in edges_near(trip[0]): print(" ", l.strip()[:120])
if rest:
print("--- [audioedge] around FIRST RESTART (line %d) ---" % rest[0])
for l in edges_near(rest[0]): print(" ", l.strip()[:120])
# the authored generator trigger table
cfg = [l for l in lines if "statecfg" in l and ("GeneratorState" in l or "Generator" in l)]
print("--- [statecfg] generator rows (%d) ---" % len(cfg))
for l in cfg[:10]: print(" ", l.strip()[:140])
# verdict material: any FIRED edge with new=4 (trip-edge start)?
fired = [l for l in lines if "[audioedge]" in l and "FIRED" in l]
trip_fired = [l for l in fired if re.search(r"old=\d+ new=4 ", l)]
rec_fired = [l for l in fired if re.search(r"old=4 new=\d+ ", l)]
print("FIRED with new=4 (trip) :", len(trip_fired))
print("FIRED with old=4 (recovery):", len(rec_fired))
fixup = sum(1 for l in lines if "#154 fixup" in l)
print("fixup receipts :", fixup)
if not trip:
print("VERDICT: INCONCLUSIVE -- no trip occurred (raise BT_GEN_HEAT)")
elif not rest:
print("VERDICT: PARTIAL -- tripped but no recovery in window (lengthen run / check cooling)")
elif fixup >= 1 and len(trip_fired) >= 1 and len(rec_fired) == 0:
print("VERDICT: PASS -- #154 deviation live: warning at TRIP, silent at recovery")
else:
print("VERDICT: FAIL -- trip-edge fires=%d (want >=1), recovery-edge fires=%d (want 0), fixups=%d"
% (len(trip_fired), len(rec_fired), fixup))
PY