#137 FIXED: restore the binary Reset's +0x58c re-seed -- the respawn freeze was a
teleport-poisoned finite difference, not heat, not the reset, not velocity
The one-line fix is the binary's own SECOND Reset instruction, dropped in
transcription: FUN_00408440(mech+0x58c, param_2) ==
accelPrevPos = origin.linearPosition;
+0x58c is the previous-position memory of the AccelerationLastFrame ring feed
(+0x81c/0x824/0x828/0x82c). The port reconstructed the ring faithfully (ctor
part_012.c:9836, derivative :15169) but Reset never re-seeded its cache, so the
first post-respawn sample computed |newPos - prevPos|/dt = TELEPORT DISTANCE/dt
(~1e5) into the velocity ring; the ring-mean derivative spiked
AccelerationLastFrame (pure forward, with an opposite-sign echo ~15 frames later
as the sample rotated out of the 15-ring); and the myomer integrator @004b8d18
turned it into a one-tick pendingHeat deposit of ~3e9:
termAccel = (1-accEff) * |v| * |a| * m * dt
= 0.2 * 40 * 1.04e5 * 75000 * 0.044 = 2.75e9
snapping the freshly-reset myomers from T=77 to T~9000 against failT=2000 ->
speedEffect 0 -> speedDemand *= 0 -> "respawned unable to move until it cools".
WHY ~8% IN THE FIELD: the deposit needs |v| in the same 1-2 frames, so only
pilots whose throttle was still forward at the respawn -- physical lever /
HOTAS, exactly who reported it -- had gait-republished speed in the spike
frames. Idle-throttle respawns deposit ~nothing. (And frozen-subset-of-
died-hot: died hot == was running hard == lever still forward.)
MEASURED, same abusive bench (0.95 throttle + continuous autofire):
before: deposits up to 3.35e9, every one 3-4 lines after a Mech::Reset;
4-6 of 7 respawns frozen; post-reset myomers T 7700-12100
after: deposits >1e7: ZERO across 7 respawns; frozen: ZERO;
post-reset myomers T 77-178 (vs degradeT=1000)
THEORIES KILLED ON THE WAY, each by operand data, in order:
* stale pendingHeat carryover (bounded to 1 frame, +1.2K -- gates agent)
* slow accumulation during the death window (consumers tick the wreck)
* conduction from a hot neighbour (roster-wide snapshot: ALL partners 77;
flow trap: zero e6 flows into the myomers, ever)
* drag/impulse writers (both trapped: never fired)
* my own earlier "velocity-driven, players accelerate to top speed" close of
the ticket -- arithmetically impossible (input ceiling ~6.5e5/frame ~=
60 deg/s; the observed snap was 1,100-21,000 deg/s) and corrected in
context/subsystems.md, which carried the wrong paragraph.
* the localAcceleration zero-fill (+0x1dc) restored along the way is KEPT --
the binary does it -- but it was NOT the cause; the snapshot is rebuilt
from the position difference one frame later.
Probes kept (all BT_HEAT_LOG-gated): roster-wide [myofreeze] at-death/at-reset/
post-reset (T + heatEnergy + pendingHeat per subsystem), the [heatflow]
conduction trap with full operands, the [myodep] deposit trap with mech
identity + acceleration components. Engine MOVER.cpp traps reverted -- they
proved their negative (drag/impulse innocent) and do not belong in engine
source.
Gotcha #30 records the class: when the binary's Reset writes a cell you don't
recognize, that write IS the spec -- transcribe the whole zero/seed list; any
prev-value cell backing a finite difference must be re-seeded at every
teleport; and derived state (T) sampled at the reset proves nothing about the
backing producers.
The operator called the shape of this two days ago: "maybe the math gets screwy
in respawning while some systems are ticking while values are being reset."
That is precisely what it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
co-authored by
Claude Opus 5
parent
df1b4651b3
commit
0d6ed40db9
+50
-20
@@ -947,6 +947,26 @@ void
|
||||
if (other != 0 && coolantAvailable != 0)
|
||||
{
|
||||
Scalar flow = ComputeHeatFlow(other, time_slice); // FUN_004ad9ec
|
||||
// #137 FLOW TRAP (BT_HEAT_LOG): the myomers gains ~2e9 of energy within
|
||||
// 30 frames of a respawn while every partner reads T=77 -- and the only
|
||||
// writers into its pendingHeat are its (dead-at-spawn) integrator and
|
||||
// THIS line. Any e6-scale single flow is the bug caught in the act;
|
||||
// print the complete operand set so the arithmetic can be re-run by
|
||||
// hand instead of guessed at.
|
||||
if (getenv("BT_HEAT_LOG") != 0 && (flow > 1.0e6f || flow < -1.0e6f))
|
||||
{
|
||||
DEBUG_STREAM << "[heatflow] " << (GetName() ? GetName() : "?")
|
||||
<< " -> " << (other->GetName() ? other->GetName() : "?")
|
||||
<< " flow=" << flow << " dt=" << time_slice
|
||||
<< " | this: T=" << currentTemperature << " E=" << heatEnergy
|
||||
<< " pend=" << pendingHeat << " m=" << thermalMass
|
||||
<< " mScale=" << massScale << " k=" << thermalConductance
|
||||
<< " lvl=" << coolantLevel << " cap=" << thermalCapacity
|
||||
<< " fScale=" << coolantFlowScale
|
||||
<< " | other: T=" << other->currentTemperature
|
||||
<< " E=" << other->heatEnergy << " pend=" << other->pendingHeat
|
||||
<< " m=" << other->thermalMass << "\n" << std::flush;
|
||||
}
|
||||
other->pendingHeat += flow;
|
||||
pendingHeat -= flow;
|
||||
BalanceCoolant(time_slice); // FUN_004ada94
|
||||
@@ -1523,28 +1543,38 @@ void BTReportMyomerFreeze(void *mech_v, const char *when)
|
||||
Subsystem *s = mech->GetSubsystem(i);
|
||||
if (s == 0)
|
||||
continue;
|
||||
Scalar f = BTMyomersSpeedEffectOf(s);
|
||||
if (f < -0.5f)
|
||||
continue; // not a Myomers
|
||||
// ROSTER-WIDE now (2026-08-09): the myomers-only version proved the
|
||||
// myomers is being COOKED FROM OUTSIDE -- +2.3e9 of energy arrives in
|
||||
// <=30 frames while its own integrator reads near-zero v (our Reset
|
||||
// zeroes localVelocity, matching the binary's +0x1c4 zero-fill, so
|
||||
// termKinetic/termAccel are dead at spawn). ConductHeat flow is
|
||||
// bounded by deltaT, so an e9 slug through it demands a NEIGHBOUR at
|
||||
// extreme temperature. Print EVERY heat-bearing subsystem's T plus
|
||||
// the two cells T is actually derived from (heatEnergy @0x158,
|
||||
// pendingHeat @0x1C8) to NAME that neighbour.
|
||||
if (!s->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()))
|
||||
continue;
|
||||
HeatableSubsystem *h = (HeatableSubsystem *)s;
|
||||
// Every Heatable-positive roster member in this game IS a HeatSink
|
||||
// (the Watcher branch -- Torso/HUD/Gyro -- rides HeatWatcher, which is
|
||||
// not HeatableSubsystem-derived), so the downcast for E/pend is safe.
|
||||
// Deliberately NOT IsDerivedFrom(HeatSink): that hand-built chain
|
||||
// returns false for Condenser (the night-13 trap).
|
||||
HeatSink *hs = (HeatSink *)s;
|
||||
Scalar f = BTMyomersSpeedEffectOf(s); // >= 0 only for 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;
|
||||
<< " T=" << h->currentTemperature
|
||||
<< " E=" << hs->heatEnergy
|
||||
<< " pend=" << hs->pendingHeat
|
||||
<< " fail=" << h->failureTemperature;
|
||||
if (f >= 0.0f)
|
||||
{
|
||||
DEBUG_STREAM << " speedEffect=" << f
|
||||
<< ((f <= 1.0e-4f && h->currentTemperature < h->failureTemperature)
|
||||
? " <<<< STALE (cold but zero)" : "");
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
if (best >= 0.0f)
|
||||
DEBUG_STREAM << "[myofreeze] " << when << " CHAIN MAX=" << best
|
||||
|
||||
@@ -2271,6 +2271,39 @@ void
|
||||
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
localVelocity = Motion::Identity;
|
||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
// Binary zero-fills restored (Reset @0049fb74 zero-fills FOUR Motion cells:
|
||||
// +0x1c4/+0x1dc/+0x298/+0x2c8; +0x1dc = localAcceleration by the engine
|
||||
// Mover layout). NOTE: zeroing these did NOT fix #137 on its own -- the
|
||||
// acceleration snapshot is REBUILT from a position finite-difference one
|
||||
// frame later (see accelPrevPos below), so the stale-carryover story first
|
||||
// written here was wrong. The zero-fills stay because the binary does them.
|
||||
localAcceleration = Motion::Identity; // binary +0x1dc zero-fill
|
||||
worldLinearAcceleration = Vector3D(0.0f, 0.0f, 0.0f); // world-space mirror
|
||||
|
||||
// #137 ROOT CAUSE -- the DROPPED RE-SEED. The binary Reset's SECOND
|
||||
// instruction is FUN_00408440(mech+0x58c, param_2): re-seed the Point3D at
|
||||
// +0x58c to the NEW ORIGIN. +0x58c is the previous-position memory of the
|
||||
// AccelerationLastFrame ring feed (+0x81c/0x824/0x828/0x82c -- the F19
|
||||
// block below, port member accelPrevPos). This port reconstructed the
|
||||
// ring (ctor part_012.c:9836-9840, derivative :15169-15195) but its Reset
|
||||
// never got the +0x58c line -- so the first post-respawn sample computed
|
||||
// |newPos - accelPrevPos| / dt = TELEPORT DISTANCE / dt ~ 1e5
|
||||
// into the velocity ring, and the ring-mean derivative turned that into an
|
||||
// AccelerationLastFrame spike of 3e4..2.4e5 (pure forward/-z; with a +z
|
||||
// ECHO ~15 frames later as the garbage sample rotates out of the mean).
|
||||
// The myomer drive-heat integrator (@004b8d18) then computed
|
||||
// termAccel = (1-accEff) * |v| * |a| * mass * dt
|
||||
// with |a|~1e5 while the gait re-published |v|~40 under a still-held
|
||||
// throttle: ONE tick deposited ~2.7e9 into pendingHeat -> heatEnergy,
|
||||
// snapping the freshly-reset myomers from T=77 to T~9000 (failT=2000) ->
|
||||
// speedEffect 0 -> speedDemand *= 0 -> "respawned unable to move until it
|
||||
// cools" (#137). Measured: top deposits 3.35e9/3.29e9/3.05e9, every one
|
||||
// 3-4 log lines after a Mech::Reset, aXYZ pure z, master mech.
|
||||
// Why ~8% in the field: the deposit needs |v| in the SAME 1-2 frames, so
|
||||
// only pilots whose throttle is still forward at the respawn (physical
|
||||
// lever / HOTAS -- exactly who reported it) get the freeze; idle-throttle
|
||||
// respawns read v~0 and deposit nothing.
|
||||
accelPrevPos = origin.linearPosition; // binary +0x58c re-seed
|
||||
ramLastVictim = 0;
|
||||
ramContactLinger = 0.0f;
|
||||
// StopAllEntityEffects (@004d0c14): a respawned mech must not trail its
|
||||
@@ -2598,6 +2631,14 @@ void
|
||||
// (wreck shape: alarms/state settle, ammo
|
||||
// bins do NOT refill the corpse)
|
||||
}
|
||||
// #137 forensic: roster heat state right AFTER the death shutdown sweep --
|
||||
// whatever is still hot here is what the wreck period starts from, and a
|
||||
// member that stays hot through BOTH sweeps is the conduction source that
|
||||
// cooks the fresh myomers after the respawn.
|
||||
{
|
||||
extern void BTReportMyomerFreeze(void *mech_v, const char *when);
|
||||
BTReportMyomerFreeze((void *)this, "at-death");
|
||||
}
|
||||
// Request the DEATH record BEFORE entering the disabled state (the
|
||||
// ForceUpdate filter masks types 2..8 once IsDisabled) -- the binary
|
||||
// death sender is Force(1) + Force(0x40) (@0x4aab2f/@0x4aab3a). The
|
||||
|
||||
@@ -809,6 +809,31 @@ void Myomers::MyomersDriveHeat(Scalar time_slice)
|
||||
Scalar termAccel = velComplement * velMag * accMag * mass * time_slice;
|
||||
Scalar gain = ratio * ratio * damageGain;
|
||||
|
||||
// #137 DEPOSIT TRAP (BT_HEAT_LOG): conduction into the myomers is measured
|
||||
// ZERO post-respawn (the [heatflow] trap), weapons deposit into themselves,
|
||||
// so THIS add is the only writer left that can carry the observed one-shot
|
||||
// slug (9e7..2.3e9, varying per respawn). The [myoheat] receipt is
|
||||
// time-sampled and would miss a 1-2 frame spike; this prints EVERY add
|
||||
// over 1e6 with the full operand set, unconditionally.
|
||||
{
|
||||
Scalar deposit = gain * (termClimb + termKinetic + termAccel);
|
||||
if (getenv("BT_HEAT_LOG") != 0 && (deposit > 1.0e6f || deposit < -1.0e6f))
|
||||
{
|
||||
// WHOSE mech (master vs the peer's replicant shares this log!) and
|
||||
// the acceleration COMPONENTS (pure-y = gravity accumulation on the
|
||||
// wreck; planar = teleport/warp-derived).
|
||||
Mech *om = (Mech *)owner;
|
||||
const Vector3D &av = om->localAcceleration.linearMotion;
|
||||
DEBUG_STREAM << "[myodep] mech=" << om->GetEntityID()
|
||||
<< (om->GetInstance() == Entity::ReplicantInstance ? " REPL" : " mstr")
|
||||
<< " deposit=" << deposit
|
||||
<< " v=" << velMag << " a=" << accMag
|
||||
<< " aXYZ=(" << av.x << "," << av.y << "," << av.z << ")"
|
||||
<< " vy=" << vy << " dt=" << time_slice << " gain=" << gain
|
||||
<< " climb=" << termClimb << " kinetic=" << termKinetic
|
||||
<< " accel=" << termAccel << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
pendingHeat /* @0x1C8 */ += gain * (termClimb + termKinetic + termAccel);
|
||||
|
||||
if (getenv("BT_MYO_LOG"))
|
||||
|
||||
Reference in New Issue
Block a user