diff --git a/context/decomp-reference.md b/context/decomp-reference.md index b6f7705..aed5eb0 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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 producer = the THERMAL BREAKER in GeneratorSimulation itself; a 2026-08-03 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= (+_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 `[techstat] condition SET` is an *operating* flag ("this component is above nominal / browned out RIGHT NOW"), not a fault report. Census over a full Steam match diff --git a/context/subsystems.md b/context/subsystems.md index e70c527..dcd22fb 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -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). 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 - 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 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, diff --git a/engine/MUNGA/AUDWTHR.cpp b/engine/MUNGA/AUDWTHR.cpp index ded3eaf..dfc77ae 100644 --- a/engine/MUNGA/AUDWTHR.cpp +++ b/engine/MUNGA/AUDWTHR.cpp @@ -1,4 +1,5 @@ #include +#include #include "munga.h" #pragma hdrstop @@ -978,6 +979,31 @@ AudioStateTrigger::AudioStateTrigger( MemoryStream_Read(stream, &excludeTransition); 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) DEBUG_STREAM << "[statecfg] attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent() @@ -1076,10 +1102,21 @@ void // // Check for the trigger state // - if ( - (!inverseTrigger && new_state == triggerState) || - (inverseTrigger && old_state == triggerState) - ) + const int bt_fires = + ((!inverseTrigger && new_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.GetCurrent()); diff --git a/engine/MUNGA/WATCHER.cpp b/engine/MUNGA/WATCHER.cpp index fd0227d..180dc81 100644 --- a/engine/MUNGA/WATCHER.cpp +++ b/engine/MUNGA/WATCHER.cpp @@ -96,6 +96,19 @@ AttributeWatcher::AttributeWatcher( 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")) { extern int g_curAudioWatcherClass; diff --git a/engine/MUNGA_L4/L4AUDRES.cpp b/engine/MUNGA_L4/L4AUDRES.cpp index bbb1fb9..58a7b66 100644 --- a/engine/MUNGA_L4/L4AUDRES.cpp +++ b/engine/MUNGA_L4/L4AUDRES.cpp @@ -23,6 +23,7 @@ ALuint *g_buffers; int g_numBuffers; const char *g_bufferNames[512]; // bufferInd -> sample file (diag: the live-playing dump) 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 diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 32d78fc..593fda0 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -1295,6 +1295,49 @@ void void Generator::GeneratorSimulation(Scalar time_slice) { + // DEV: BT_GEN_HEAT= pumps heat into the VIEWPOINT mech's Generator A + // for a bounded window (BT_GEN_HEAT_DELAY= lead-in, default 10; + // BT_GEN_HEAT_SECS= 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 if (simulationState == 1) // this[0x10] @0x40 diff --git a/scratchpad/night15/gen_edge_bench.sh b/scratchpad/night15/gen_edge_bench.sh new file mode 100644 index 0000000..2389013 --- /dev/null +++ b/scratchpad/night15/gen_edge_bench.sh @@ -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