From 0d6ed40db9784b792ee96ffee0a876b0f342fdc5 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sun, 9 Aug 2026 16:19:36 -0500 Subject: [PATCH] #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) Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC --- context/reconstruction-gotchas.md | 31 ++++++++++++++ context/subsystems.md | 23 ++++++---- game/reconstructed/heat.cpp | 70 ++++++++++++++++++++++--------- game/reconstructed/mech4.cpp | 41 ++++++++++++++++++ game/reconstructed/myomers.cpp | 25 +++++++++++ 5 files changed, 163 insertions(+), 27 deletions(-) diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index bc74979..0552956 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -966,3 +966,34 @@ Rules: `[perf-first]` (one-shot per mech: entity ID + instance at its FIRST performance). Anonymous per-frame receipts are useless in a 2-node log — **name the mech.** + +## 30. A finite-difference cache the binary re-seeds at Reset — transcribe the WHOLE seed list (#137, 2026-08-09) + +The binary `Mech::Reset @0049fb74` opens with more than the obvious origin writes: its SECOND +instruction is `FUN_00408440(mech+0x58c, param_2)` — re-seeding the **previous-position memory** +of the AccelerationLastFrame ring feed (+0x81c/0x824/0x828/0x82c) to the new origin. The port +reconstructed the ring itself faithfully (ctor `part_012.c:9836`, derivative `:15169`) but its +Reset never got that one line. Result: the first post-respawn sample computed +`|newPos − prevPos| / dt` = **teleport distance / dt ≈ 1e5** into the velocity ring; the +ring-mean derivative turned it into an acceleration spike (with an opposite-sign ECHO ~15 frames +later as the sample rotated out of the mean); the myomer heat integrator's +`termAccel = (1−accEff)·|v|·|a|·m·dt` turned THAT into a ~3e9 one-tick heat deposit; and the +freshly-reset myomers snapped from 77 to ~9000 against failT=2000 — the #137 respawn freeze. + +Rules: +(a) **When the binary's Reset writes a cell you don't recognize, that write IS the spec.** The + +0x58c re-seed looked like bookkeeping and was silently dropped; it was the only thing + standing between a teleport and a position-derivative spike. Transcribe the whole zero/seed + list, then map each cell — never the recognizable subset. +(b) **Any prev-value cell backing a finite difference must be re-seeded at every discontinuity** + (teleport, warp, respawn). If you add such a cache port-side, grep the binary's reset for its + analog before assuming none exists. +(c) **Derived state hides stale backing state.** `currentTemperature` sampled AT the reset read + 77 (clean) because RTIS wrote it — while the freeze arrived one frame later through + `heatEnergy += pendingHeat` from a live producer. Probing the derived cell at the reset + instant proves nothing about the producers; trace the WINDOW after, per producer. +(d) The diagnosis chain that worked, for reuse: roster-wide state snapshot (at-death / at-reset / + post-reset) → eliminate conduction by trapping flows with full operands → trap the remaining + producer's deposits with operands → cross-reference the operand SHAPE (pure local −z, + magnitude = distance/dt, echo at ring-length) against the writers. Each trap eliminated a + theory the previous data had made plausible; three plausible theories died on operands. diff --git a/context/subsystems.md b/context/subsystems.md index c2b4441..e70c527 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -319,10 +319,19 @@ heat-per-SECOND at any frame rate. A literal transcription would add the full t frame, so at 170 fps it would inject ~6× the heat the pod ever did. `BT_MYO_HZ` overrides the reference rate for bracketing. -**Consequence for #137:** the heat model is not miscalibrated. Myomers running away past -`failT=2000` at sustained top speed is the authentic governor — heat is **quadratic in speed** -(`work = m·v²·0.5`), so v≈50 on open ground after a respawn is ~9× the input of v≈10-18 in a -fight. It is self-limiting: effectiveness hits 0, the mech stops, speed falls, it cools. What -players read as "respawned with the heat bar maxed" is that acceleration to top speed, not a -failed reset ([[combat-damage]] · the reset itself is verified: `T == startingTemperature` at -every reset). Whether the cliff is too punishing is a DESIGN call, not a fidelity defect. +**Consequence for #137 — CORRECTED 2026-08-09, the paragraph that stood here was wrong.** The +"players read 'respawned with heat maxed' as acceleration to top speed" claim did not survive +the data: the deposits were e9-scale within 30 frames of the reset, physically impossible from +motion input (~6.5e5/frame ceiling). The actual cause was a **dropped binary re-seed**: the +binary Reset's second instruction (`FUN_00408440(mech+0x58c, origin)`) re-seeds the +previous-position memory of the AccelerationLastFrame ring feed (+0x81c..+0x82c); the port +reconstructed the ring but not the re-seed, so the first post-respawn sample computed +TELEPORT-DISTANCE/dt (~1e5) into the velocity ring, the ring-mean derivative spiked +`AccelerationLastFrame`, and `termAccel = (1-accEff)·|v|·|a|·m·dt` deposited ~3e9 into +`pendingHeat` in one tick → myomers snapped from 77 to ~9000 (failT 2000) → speedEffect 0 → +frozen until cooled. Fixed by restoring the re-seed (`accelPrevPos = origin.linearPosition` in +Mech::Reset). The ~8% field rate was the |v| factor: 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. The calibration facts above (constants byte-exact, dt-normalised +kinetic term) all STAND; the in-life governor (running hot at sustained top speed derates the +myomers) is authentic and remains. diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index d497a15..6bd2cac 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index a1ccf7b..6667676 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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 diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 3a13ddf..2662323 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -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"))