#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) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-01 13:14:55 -05:00
co-authored by Claude Opus 5
parent 56f15b569a
commit 9dd7d48b41
5 changed files with 116 additions and 5 deletions
+13
View File
@@ -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.
+35 -5
View File
@@ -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;
}
}
}
+19
View File
@@ -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
+29
View File
@@ -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
+20
View File
@@ -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