diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 0a3aea9..d497a15 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -1486,3 +1486,67 @@ void BTReportHeatAtReset(void *mech_v) << "\n" << std::flush; } } + +//===========================================================================// +// BTReportMyomerFreeze -- #137 forensic (BT_HEAT_LOG), the POST-reset trace. +// +// BTReportHeatAtReset samples AT the reset and showed every subsystem at +// T == startingTemperature, which is what made the reset path look innocent and +// got #137 wrongly closed. The field then proved the freeze is real (5 of 61 +// respawns: throttle up, speedDemand pinned at 0) -- so the interesting window +// is the frames immediately AFTER the reset, which nothing was sampling. +// +// Three explanations survive the decomp read and only data separates them: +// (a) RESET DIDN'T TAKE -> temp is high right after the reset +// (b) STALE CACHE -> temp is at start but speedEffect is still 0 +// (HeatSink::RTIS @004ad760 writes only bytes +// 0x114/0x12C/0x130/0x134/0x138/0x158/0x15C -- +// it does NOT touch Myomers::speedEffect @0x31C, +// and Myomers::RTIS @004b8aa4 only chains to the +// PoweredSubsystem one, so the pre-death value +// survives until the myomers next ticks) +// (c) INSTANT RE-HEAT -> temp starts at start and climbs back at once +// +// Prints per myomers: temperature, its own speedEffect, and the mech-level MAX +// the mover actually multiplies by. Freeze == that MAX at 0. +//===========================================================================// +void BTReportMyomerFreeze(void *mech_v, const char *when) +{ + if (mech_v == 0 || getenv("BT_HEAT_LOG") == 0) + return; + Entity *mech = (Entity *)mech_v; + extern Scalar BTMyomersSpeedEffectOf(void *subsystem); + const int count = mech->GetSubsystemCount(); + Scalar best = -1.0f; + for (int i = 0; i < count; ++i) + { + Subsystem *s = mech->GetSubsystem(i); + if (s == 0) + continue; + Scalar f = BTMyomersSpeedEffectOf(s); + if (f < -0.5f) + continue; // not a Myomers + if (f > best) best = f; + Scalar t = -1.0f, tdeg = -1.0f, tfail = -1.0f; + if (s->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations())) + { + HeatableSubsystem *h = (HeatableSubsystem *)s; + t = h->currentTemperature; // @0x114 + tdeg = h->degradationTemperature; // @0x118 + tfail = h->failureTemperature; // @0x11C + } + // The derating curve (@004b8ac0) is exactly: + // temp >= degradation -> falls off; temp >= FAILURE -> 0.0 (frozen). + // So printing the thresholds beside the temperature says immediately + // whether a 0 effectiveness is JUSTIFIED by the temperature (reset did + // not take / re-heated) or is a STALE cache (temp fine, effect still 0). + DEBUG_STREAM << "[myofreeze] " << when << " " << (s->GetName() ? s->GetName() : "?") + << " T=" << t << " deg=" << tdeg << " fail=" << tfail + << " speedEffect=" << f + << (f <= 1.0e-4f && t < tfail ? " <<<< STALE (cold but zero)" : "") + << "\n" << std::flush; + } + if (best >= 0.0f) + DEBUG_STREAM << "[myofreeze] " << when << " CHAIN MAX=" << best + << (best <= 1.0e-4f ? " <<<< FROZEN" : "") << "\n" << std::flush; +} diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 34976ba..a1ccf7b 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -666,6 +666,7 @@ static int gBTPPCKey = 0; static int gBTMissileKey = 0; static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45) int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes) +int gBTMyoTrace = 0; // #137: frames of post-reset myomer tracing left (armed by Mech::Reset) int gBTDisplayCycle = 0; // 'N' edge: cycle the secondary schematic (Gitea #6, mapper consumes) int gBTPresetCycle[3] = {0,0,0}; // J/K/L edges: cycle an upper-MFD preset page (Gitea #9, L4 mapper consumes) // @@ -2379,6 +2380,14 @@ void { extern void BTReportHeatAtReset(void *mech_v); BTReportHeatAtReset((void *)this); + // #137: sample the myomers AT the reset, and arm the POST-reset trace. + // Sampling only here is what got this bug wrongly closed -- every + // subsystem reads T == start at this instant, which looks innocent. + // The freeze shows up in the frames AFTER. + extern void BTReportMyomerFreeze(void *mech_v, const char *when); + extern int gBTMyoTrace; + BTReportMyomerFreeze((void *)this, "at-reset"); + gBTMyoTrace = 240; // ~4 s of post-reset frames } // --- DESKTOP THROTTLE RELEASE (#146) -- PORT LAYER, desktop-only --------- diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index c6bd9de..2337b93 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -1090,6 +1090,20 @@ void // reads the same live factor (dead/overheated myomers cannot squat // or rise; the posture selector tests |factor| <= 1e-4). mech->myomerEffectiveness = drive; + // #137 POST-RESET TRACE. Mech::Reset arms gBTMyoTrace; sample here, + // where the mover's actual multiplier is formed, for a few seconds + // after a respawn. This is the window nothing was watching -- the + // at-reset sample always looks clean. + { + extern int gBTMyoTrace; + extern void BTReportMyomerFreeze(void *mech_v, const char *when); + if (gBTMyoTrace > 0) + { + --gBTMyoTrace; + if ((gBTMyoTrace % 30) == 0) + BTReportMyomerFreeze((void *)mech, "post-reset"); + } + } if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE } diff --git a/scratchpad/night14/myofreeze.sh b/scratchpad/night14/myofreeze.sh new file mode 100644 index 0000000..fba726b --- /dev/null +++ b/scratchpad/night14/myofreeze.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# ========================================================================= +# #137 -- "respawns with the myomers overheated ... initially unable to move +# until it cools off" (Oracle, across several builds). +# +# WHY THE OLD BENCH PROVED NOTHING. heatrespawn.sh sampled AT the reset and +# found every heat-bearing subsystem at T == startingTemperature, so I closed +# #137 as not-a-bug. The field then measured the freeze at 5 of 61 respawns +# (~8%) across THREE machines -- throttle up, speedDemand pinned at 0. A +# bench that never reproduces the failure is not evidence the failure does not +# exist, which is the mistake that closed the ticket. +# +# WHAT THE DECOMP SAYS (re-read 2026-08-09): +# Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28 +# (slot 10 = ResetToInitialState) on each subsystem, then @0049f788. +# Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS +# HeatSink::RTIS @004ad760, which does `param_1[0x45] = param_1[0x4f]` +# i.e. currentTemperature(@0x114) = startingTemperature(@0x13C). +# The freeze itself is the derating curve @004b8ac0: +# temp >= degradation(@0x118) -> falls off; temp >= FAILURE(@0x11C) -> 0.0 +# and 0.0 reaches the mover as the chain MAX -> speedDemand *= 0. +# CRUCIALLY: nothing in that reset chain touches Myomers::speedEffect +# (@0x31C). It keeps its pre-death value until the myomers next ticks. +# +# So three explanations survive, and only data separates them: +# (a) RESET DIDN'T TAKE -> post-reset T is high (>= fail) +# (b) STALE CACHE -> T is at start but speedEffect is still 0 +# ("<<<< STALE (cold but zero)" in the receipt) +# (c) INSTANT RE-HEAT -> T starts at start and climbs back immediately +# +# THE MEASUREMENT. Mech::Reset now arms a ~4 s post-reset trace sampled where +# the mover's multiplier is actually formed: +# [myofreeze] at-reset Myomers T=.. deg=.. fail=.. speedEffect=.. +# [myofreeze] post-reset Myomers T=.. deg=.. fail=.. speedEffect=.. +# [myofreeze] post-reset CHAIN MAX=0 <<<< FROZEN +# +# A drives hard (heat) and self-damages to death repeatedly, so deaths land on +# a HOT mech -- the field composition. Long run: at ~8% we need many respawns. +# ========================================================================= +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 2 +rm -f mf137_a.log mf137_b.log mf137_relay.log +bt_expert_egg MP.EGG MF137.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF137.EGG + +( export BT_DEATH_LOG=1 BT_MP_LOG=1 + bt_launch mf137_b.log MF137.EGG 0x0C -net 1601 ) +sleep 2 +# A: run hot (autodrive) + fire continuously (weapon heat) + die often. +( export BT_AUTODRIVE=0.95 BT_SELF_DAMAGE=7 + export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4 + export BT_HEAT_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 + bt_launch mf137_a.log MF137.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py MF137.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf137_relay.log 2>&1 & +RELAY=$! +sleep 540 +kill $RELAY 2>/dev/null +sleep 3 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3 + +echo "=================== #137 MYOMER FREEZE ===================" +echo -n "respawns: "; grep -ac "Mech::Reset" mf137_a.log +echo -n "post-reset FROZEN samples: "; grep -ac "FROZEN" mf137_a.log +echo -n "STALE (cold but zero) samples: "; grep -ac "STALE (cold but zero)" mf137_a.log +echo +echo "--- any reset where the myomers came back AT or OVER the failure temp? ---" +python - <<'PY' +import re, io +bad = 0 +for ln in io.open(r"C:\git\bt411\content\mf137_a.log", encoding="latin-1", errors="replace"): + m = re.search(r"\[myofreeze\] (\S+)\s+(\S+)\s+T=([-\d.e+]+) deg=([-\d.e+]+) fail=([-\d.e+]+)\s+speedEffect=([-\d.e+]+)", ln) + if not m: + continue + when, name, t, deg, fail, se = m.group(1), m.group(2), float(m.group(3)), float(m.group(4)), float(m.group(5)), float(m.group(6)) + if se <= 1e-4: + bad += 1 + if bad <= 12: + why = "TEMP >= fail (reset did not take / re-heated)" if t >= fail else "STALE CACHE (temp fine, effect 0)" + print(" %-10s %-12s T=%8.1f fail=%8.1f effect=%.4f -> %s" % (when, name, t, fail, se, why)) +print(" zero-effect samples: %d" % bad) +PY +echo +echo "--- the first frozen episode in context ---" +grep -aE "Mech::Reset|myofreeze" mf137_a.log | grep -aB2 -A6 "FROZEN" | head -20