Gitea #59: the first death permanently disabled ExecuteWatchers -- simulationFlags |= 0x1 should be ForceUpdate()
Verified the claim in the binary myself with capstone before changing behaviour: 0x4c0155: or word ptr [ebx + 0x18], 1 +0x18 is Simulation::updateModel, a **Word** -- and the 16-bit 'or word ptr' settles it, since simulationFlags is an LWord at +0x28 and would assemble as 'or dword ptr'. So the authentic op is updateModel |= DefaultUpdateModelFlag, which is exactly Simulation::ForceUpdate() (SIMULATE.h:146-147). The old transcription wrote simulationFlags |= 0x1 instead. Bit 0 there is DelayWatchersFlag (SIMULATE.h:170); Simulation::Simulate then skips ExecuteWatchers() forever (SIMULATE.cpp:461), and NOTHING clears it -- the only ClearWatcherDelay() in the tree is in the encore path (UPDATE.cpp:215), which a Player never takes. So every pilot's first death permanently disabled watcher execution on their simulation, and the replication mark the binary intended was never set at all. context/reconstruction-gotchas.md had flagged this exact line as the un-audited sibling of the #12 dirty-bit class, asking for the disasm first; that is now on the record. LIVE VERIFICATION (scratchpad/sim3.py, 3 mechs + BT_MP_FORCE_DMG, 180s): 4 consecutive death/respawn cycles on pod3, every one completing -- death cycle START (death #N) -> RESET at drop zone [COMPLETE] with 'player watchersDelayed=0' after every death, and no crash. Also re-confirms the #57 latch fix under repeated deaths (the interleaved SWALLOWED lines are the authentic dedup of a second notification, each followed by a completed RESET). Leaves a silent regression guard: the death path now logs only if it ever finds the watcher-delay flag set again. Rigs: sim3.py (3-mech combat + force damage), destest.py (2-node designation proof: 18 designations, deathPending clean on all of them). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
This commit is contained in:
co-authored by
Claude Opus 5
parent
2dd4ee3910
commit
2518e43719
@@ -483,7 +483,38 @@ void
|
||||
scenarioRole->GetReturnFromDeath() - 1);
|
||||
}
|
||||
|
||||
simulationFlags |= 0x1; // request a forced update
|
||||
//
|
||||
// MARK FOR REPLICATION (Gitea #59). The binary's instruction here is
|
||||
// `or word ptr [ebx + 0x18], 1` (verified with capstone at 0x4c0155). Offset
|
||||
// +0x18 is `Simulation::updateModel`, a **Word**, and the 16-bit `or word ptr`
|
||||
// confirms it -- `simulationFlags` is an LWord at +0x28 and would assemble as
|
||||
// `or dword ptr`. So the authentic operation is
|
||||
// `updateModel |= DefaultUpdateModelFlag`, which IS exactly
|
||||
// `Simulation::ForceUpdate()` (SIMULATE.h:146-147).
|
||||
//
|
||||
// The old transcription wrote `simulationFlags |= 0x1` instead. Bit 0 of
|
||||
// simulationFlags is **`DelayWatchersFlag`** (SIMULATE.h:170), and
|
||||
// `Simulation::Simulate` skips `ExecuteWatchers()` for good whenever it is set
|
||||
// (SIMULATE.cpp:461). Nothing clears it -- the only `ClearWatcherDelay()` in
|
||||
// the tree is inside the encore path (UPDATE.cpp:215), which a Player never
|
||||
// takes. Net effect: **the first death of any pilot permanently disabled
|
||||
// watcher execution on that player's simulation**, and the replication mark
|
||||
// the binary intended was never set at all.
|
||||
//
|
||||
// (`context/reconstruction-gotchas.md` flagged this exact line as the one
|
||||
// un-audited sibling of the #12 dirty-bit mis-mapping class, asking for the
|
||||
// disasm before changing it. That is the disasm.)
|
||||
//
|
||||
ForceUpdate(); // updateModel |= DefaultUpdateModelFlag
|
||||
// Gitea #59 regression guard. The PLAYER's watcher-delay flag must never be
|
||||
// set by the death path again -- the old `simulationFlags |= 0x1` set it and
|
||||
// nothing in the tree clears it, permanently killing ExecuteWatchers for this
|
||||
// Player. Silent unless the bug returns. Verified live 2026-07-25:
|
||||
// watchersDelayed=0 across 4 consecutive death/respawn cycles.
|
||||
if (AreWatchersDelayed())
|
||||
DEBUG_STREAM << "[respawn] BUG (Gitea #59): the death path left this "
|
||||
"player's watchers DELAYED -- ExecuteWatchers is dead for it now\n"
|
||||
<< std::flush;
|
||||
Set_Alarm_Level((char *)this + 0x2c, 1); // FUN_0041bbd8(this+0x2c, 1)
|
||||
|
||||
//
|
||||
@@ -1647,7 +1678,16 @@ void BTPilotSetObjectiveMech(void *pilot, void *target_vehicle)
|
||||
{
|
||||
if (pilot == 0)
|
||||
return;
|
||||
((BTPlayer *)pilot)->objectiveMech = (Mech *)target_vehicle;
|
||||
BTPlayer *p = (BTPlayer *)pilot;
|
||||
p->objectiveMech = (Mech *)target_vehicle;
|
||||
// PROOF LINE for the 0x284 bug (Gitea #48/#57): the designation must land on
|
||||
// objectiveMech and must leave deathPending ALONE. Before the fix this wrote
|
||||
// a Mech* into deathPending and killed the pilot's respawn for the session.
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] designate: objectiveMech=" << (void *)p->objectiveMech
|
||||
<< " deathPending=" << p->deathPending
|
||||
<< (p->deathPending == 0 ? " (clean)" : " *** LATCH SET -- BUG ***")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
void *BTPilotObjectiveMech(void *pilot)
|
||||
|
||||
Reference in New Issue
Block a user