From 9dd7d48b4112661578610b6a741e11a017af78bb Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sat, 1 Aug 2026 13:14:55 -0500 Subject: [PATCH] #96: instrument the myomer heat model -- per-mech profile CONFIRMED, overheat cause LOCATED Two player claims, both now answered from the decomp + measurement rather than inference. THE EQUATION (FUN_004b8d18, constants read from the image: _DAT_004b8ee4=0.5, _DAT_004b8ee8=0.0 (the fabs), _DAT_004b8eec=1.0): heat += ratio^2 * (1+damageLevel) * [ (1-accEff)*|vy|*m*g*dt climb POWER + (1-velEff)*(0.5*m*|v|^2) kinetic ENERGY -- NO dt + (1-accEff)*|v|*|a|*m*dt ] accel POWER Our implementation already reproduces this verbatim, dt-less term included. CLAIM 1 -- "each mech has a unique heating profile". TRUE, and working, but NOT by the mechanism the player described. Measured across thr1/own1/mad1/vul1: * the myomer record is IDENTICAL on every chassis (velEff 0.995, accEff 0.8, gears 3000/5000/7000/9999, rec 2, degradeT 1000, failT 2000, thermalMass 250000) * the heat-family COUNT is identical too -- 6 Condensers, 1 HeatSinkBank, 1 Reservoir, 4 Generators on all four * every cooling parameter is byte-identical Thor vs Owens (condenser conductance 315000 / mass 420000, bank 231000 / 1.39e6, reservoir 190000 / 3.42e6) So there is NO authored per-chassis cooling variation. The profile emerges from the equation instead: heat ~ m*v^2, and light mechs are faster. Measured at seek 4, flat out: thr1 mass 70000 |v| 11.34 kinetic/tick 49026 own1 mass 35000 |v| 17.22 kinetic/tick 57350 The Owens is HALF the mass and generates 17% MORE drive heat, because v^2 beats m. That reproduces the player's OUTCOME (a Thor sustains seek 4, a light chicken-walker cannot) via speed, not heatsink count. CLAIM 2 -- "it runs too hot". The kinetic term carries NO dt: it adds an ENERGY every TICK, so its contribution per SECOND scales with the tick rate. Measured dt here is ~0.017 (~59Hz) and variable. The other two terms are power terms and are rate-independent. On flat ground the climb term is additionally dead -- gravity reads 0 (the carried "environment gravity unwired" open), so hills do not heat at all right now. NOT yet established: the 1995 tick rate the dt-less term was calibrated against. Until that is pinned the OVERHEAT FACTOR is unquantified -- flagged, not guessed. Adds three diagnostics, all under BT_MYO_LOG: [myoheat] now splits climb/kinetic/accel + dt + the kinetic share [myoparm] one line per myomer: efficiencies, gears, thermal thresholds [hsparm] one line per heat subsystem: conductance + thermal mass and three benches (myoheat/myoparm/myocmp) that produced the tables above. Co-Authored-By: Claude Opus 5 (1M context) --- game/reconstructed/heat.cpp | 13 +++++++++++ game/reconstructed/myomers.cpp | 40 +++++++++++++++++++++++++++++----- scratchpad/night8/myocmp.sh | 19 ++++++++++++++++ scratchpad/night8/myoheat.sh | 29 ++++++++++++++++++++++++ scratchpad/night8/myoparm.sh | 20 +++++++++++++++++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 scratchpad/night8/myocmp.sh create mode 100644 scratchpad/night8/myoheat.sh create mode 100644 scratchpad/night8/myoparm.sh diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 9446f27..6ac3032 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -546,6 +546,19 @@ HeatSink::HeatSink( pendingHeat = 0.0f; + // #96 -- "each mech has a unique heating profile". The heat-family COUNT is + // identical on every chassis (6 condensers, 1 bank, 1 reservoir), so if + // per-mech cooling variation exists it must live in these authored values. + // Dump them so that is a measured fact rather than an assumption. + if (getenv("BT_MYO_LOG")) + DEBUG_STREAM << "[hsparm] " << (GetName() ? GetName() : "?") + << " startT=" << startingTemperature + << " degradeT=" << degradationTemperature + << " failT=" << failureTemperature + << " conductance=" << thermalConductance + << " thermalMass=" << thermalMass + << "\n" << std::flush; + // // A "master" heat sink (segment flagged 0x100, not a sub-/damaged copy) // drives the per-frame thermal simulation. diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 659b32a..a981fc8 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -229,6 +229,25 @@ Myomers::Myomers( recommendedSeekVoltageIndex = resource->seekVoltageRecommendedIndex; // @0x324 res+0x1AC currentSeekVoltageIndex = recommendedSeekVoltageIndex; // @0x320 minSeekVoltageIndex = 0; // @0x328 + + // #96 -- "each mech has a unique heating profile". Dump what THIS mech's + // myomer record actually streamed, so per-chassis variation is a measured + // fact rather than an assumption. One line per myomer built. + if (getenv("BT_MYO_LOG")) + { + DEBUG_STREAM << "[myoparm] " << (GetName() ? GetName() : "?") + << " velEff=" << velocityEfficiency + << " accEff=" << accelerationEfficiency + << " gears=["; + for (int g = 0; g <= maxSeekVoltageIndex && g < 5; ++g) + DEBUG_STREAM << (g ? "," : "") << seekVoltage[g]; + DEBUG_STREAM << "] rec=" << recommendedSeekVoltageIndex + << " max=" << maxSeekVoltageIndex + << " degradeT=" << degradationTemperature + << " failT=" << failureTemperature + << " thermalMass=" << thermalMass + << "\n" << std::flush; + } } @@ -726,20 +745,31 @@ void Myomers::MyomersDriveHeat(Scalar time_slice) // accTerm = m |v| |a| dt (acceleration power) Scalar work = mass * (velMag * velMag) * 0.5f; // dot(v,v)*0.5 (_DAT_004b8ee4) - pendingHeat /* @0x1C8 */ += - ratio * ratio * damageGain * - ( velComplement * vy * mass * gravity * time_slice - + workComplement * work - + velComplement * velMag * accMag * mass * time_slice ); + // The three terms, kept separate for diagnosis (#96 "myomers run too hot"): + // two are POWER (x dt), one is an ENERGY added per TICK. + Scalar termClimb = velComplement * vy * mass * gravity * time_slice; + Scalar termKinetic = workComplement * work; // <-- no dt, per the binary + Scalar termAccel = velComplement * velMag * accMag * mass * time_slice; + Scalar gain = ratio * ratio * damageGain; + + pendingHeat /* @0x1C8 */ += gain * (termClimb + termKinetic + termAccel); if (getenv("BT_MYO_LOG")) { static int s_n = 0; if ((s_n++ % 120) == 0) + { + Scalar sum = termClimb + termKinetic + termAccel; DEBUG_STREAM << "[myoheat] v=" << velMag << " a=" << accMag << " m=" << mass << " g=" << gravity << " ratio=" << ratio << " dmgGain=" << damageGain + << " dt=" << time_slice + << " | climb=" << (gain * termClimb) + << " kinetic=" << (gain * termKinetic) + << " accel=" << (gain * termAccel) + << " kineticShare=" << (sum != 0.0f ? (termKinetic / sum) : 0.0f) << " pending=" << pendingHeat << "\n" << std::flush; + } } } diff --git a/scratchpad/night8/myocmp.sh b/scratchpad/night8/myocmp.sh new file mode 100644 index 0000000..ca3ff61 --- /dev/null +++ b/scratchpad/night8/myocmp.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# #96: compare myomer heat GENERATION between chassis at seek 4, flat out. +# Cooling capacity is identical across chassis (measured), and heat ~ m*v^2, so +# a light fast mech may out-heat a heavy slow one despite half the mass. +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +V="$1" +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +sed "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=$V/" MP.EGG > MYOC.EGG +LOG=myocmp_$V.log +rm -f "$LOG" +export BT_MYO_LOG=1 BT_FORCE_SEEK=3 +export BT_GOTO="1500 1500" +bt_launch "$LOG" MYOC.EGG 0x03 +sleep 100 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 diff --git a/scratchpad/night8/myoheat.sh b/scratchpad/night8/myoheat.sh new file mode 100644 index 0000000..caa4590 --- /dev/null +++ b/scratchpad/night8/myoheat.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# #96: WHY do the myomers run too hot? Split the heat into its three terms. +# +# The binary's integrator (FUN_004b8d18, constants 0.5 / 0.0 / 1.0 verified): +# heat += ratio^2 * (1+damage) * [ (1-effA)*|vy|*m*g*dt climb POWER +# + (1-effV)*(0.5*m*|v|^2) kinetic ENERGY, NO dt +# + (1-effA)*|v|*|a|*m*dt ] accel POWER +# Two terms are power (x dt); the kinetic one is an ENERGY added PER TICK, so its +# contribution per SECOND scales with the tick rate. On flat ground at steady +# speed vy~0 and |a|~0, which should leave the dt-less term carrying nearly all +# of the heat. BT_MYO_LOG now prints the split. +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 +sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > MYO.EGG +LOG=myoheat_${1:-seek4}.log +rm -f "$LOG" +# Run flat out in the top gear, straight line, no combat. +export BT_MYO_LOG=1 BT_DRIVE_LOG=1 BT_FORCE_SEEK=${2:-3} +export BT_SPAWN_ENEMY=1 # a SECOND chassis -> one run dumps two myomer records +export BT_GOTO="1500 1500" +bt_launch "$LOG" MYO.EGG 0x03 +sleep 90 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +echo "=== heat term split ===" +grep -oE "\[myoheat\][^\n]*" "$LOG" | tail -6 diff --git a/scratchpad/night8/myoparm.sh b/scratchpad/night8/myoparm.sh new file mode 100644 index 0000000..42b8c0c --- /dev/null +++ b/scratchpad/night8/myoparm.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# #96 Q1: "each mech has a unique heating profile" -- is the myomer record +# per-chassis, or shared? Build one mech per run and dump what it streamed. +# Oracle's example is the pair thr1 (Thor, sustains seek 4) vs own1 (Owens, +# a light chicken-walker that cannot). +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +V="$1" +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +# swap the FIRST pilot's vehicle -- that is the one solo flies +sed "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=$V/" MP.EGG > MYOP.EGG +LOG=myoparm_$V.log +rm -f "$LOG" +export BT_MYO_LOG=1 BT_ROSTER=1 +bt_launch "$LOG" MYOP.EGG 0x03 +sleep 70 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2