From 189fcede5b169d80a92bea1229a07aae14374efa Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 16:07:26 +0000 Subject: [PATCH] re: bounded scan fixes the freeze; mission-over branch confirmed on the oracle Bounding the pointer scan to 0xBC000000-0xBD000000 (with a full-sweep fallback) drops find_mission from a ~371MB walk to 0.7s. The run then went 694s with the probe attached and NO freeze, against 3-of-3 frozen inside ~4 minutes with the unbounded version. n=1, but the first probe-attached run to survive. State encoding pinned to three points: 1 = not yet deployed, 2 = active, 4 = destroyed. ADN111 caught going 2 -> 4 at 433s while the active count fell 36 -> 27. The phase ended at 694.9s WITHOUT the ordinal advancing, and every field matches the branch read statically from sub_82260710: [phase+300]=2 (last-phase flag), [mission+20]=0 (mission-over state), [phase+196]=1 (finished), [mission+40]=1 (unchanged). The static state machine is confirmed on the live oracle for the mission-over half. But this was a LOSS, not a clear: GAME OVER on screen, escort at 35.7%, pilot DEAD at 676s, and two of the three objective squadrons still at state 2. So the 'destroy all three clears phase 1' prediction remains untested. What is established is that the else-branch is the only route to phase 2 and needs [phase+300] != 2 when the phase ends. Five attempts, still no phase advance observed -- the obstacle is now keeping the escort alive, not the freeze or the instrument. --- docs/re/data/phase-watch-s02-full.txt | 16 +++++++ docs/re/script-runtime-probe.md | 64 +++++++++++++++++++++++++++ tools/re-capture/squadron_state.py | 36 ++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 docs/re/data/phase-watch-s02-full.txt diff --git a/docs/re/data/phase-watch-s02-full.txt b/docs/re/data/phase-watch-s02-full.txt new file mode 100644 index 00000000..4d1b13c2 --- /dev/null +++ b/docs/re/data/phase-watch-s02-full.txt @@ -0,0 +1,16 @@ +file base 0xAB840010 ScriptMission 0xBC79C960 + [ 0.7s] phase=1 finished=0 active= 24 ADN110:1 ADN111:1 ADN112:1 + [ 19.9s] phase=1 finished=0 active= 27 ADN110:1 ADN111:1 ADN112:1 + [ 54.9s] phase=1 finished=0 active= 30 ADN110:1 ADN111:1 ADN112:1 + [ 113.8s] phase=1 finished=0 active= 33 ADN110:2 ADN111:2 ADN112:2 + [ 161.0s] phase=1 finished=0 active= 35 ADN110:2 ADN111:2 ADN112:2 + [ 191.0s] phase=1 finished=0 active= 36 ADN110:2 ADN111:2 ADN112:2 + [ 220.1s] phase=1 finished=0 active= 34 ADN110:2 ADN111:2 ADN112:2 + [ 245.1s] phase=1 finished=0 active= 33 ADN110:2 ADN111:2 ADN112:2 + [ 255.2s] phase=1 finished=0 active= 32 ADN110:2 ADN111:2 ADN112:2 + [ 303.3s] phase=1 finished=0 active= 31 ADN110:2 ADN111:2 ADN112:2 + [ 433.0s] phase=1 finished=0 active= 30 ADN110:2 ADN111:4 ADN112:2 + [ 438.0s] phase=1 finished=0 active= 29 ADN110:2 ADN111:4 ADN112:2 + [ 616.7s] phase=1 finished=0 active= 28 ADN110:2 ADN111:4 ADN112:2 + [ 631.7s] phase=1 finished=0 active= 27 ADN110:2 ADN111:4 ADN112:2 + [ 694.9s] phase=1 finished=1 active= 27 ADN110:2 ADN111:4 ADN112:2 diff --git a/docs/re/script-runtime-probe.md b/docs/re/script-runtime-probe.md index fac166f2..acf74fe8 100644 --- a/docs/re/script-runtime-probe.md +++ b/docs/re/script-runtime-probe.md @@ -117,3 +117,67 @@ for them to be **destroyed**, which is when `[ScriptMission+40]` should step to `tools/re-capture/phase_watch.py` is the harness: it samples the real counter and the watched squadrons together, witnesses the freeze every 60 s, and prints only on change. + +## ✅ 2026-08-25 — the bounded scan fixes the freeze, and the state machine is confirmed live + +**The sweeps were the cost.** Bounding the pointer scan to `0xBC000000–0xBD000000` +(with a full-sweep fallback) drops `find_mission` from a full ~371 MB walk to +**0.7 s**. The run then went **694 s with the probe attached and no freeze**, +against **3 of 3 frozen inside ~4 minutes** with the unbounded version. n=1, but +it is the first probe-attached run to survive past four minutes. + +Full trace in `data/phase-watch-s02-full.txt`: + +``` +[ 0.7s] phase=1 finished=0 active=24 ADN110:1 ADN111:1 ADN112:1 +[ 113.8s] phase=1 finished=0 active=33 ADN110:2 ADN111:2 ADN112:2 <- arrive +[ 191.0s] phase=1 finished=0 active=36 +[ 433.0s] phase=1 finished=0 active=30 ADN110:2 ADN111:4 ADN112:2 <- ADN111 destroyed +[ 631.7s] phase=1 finished=0 active=27 +[ 694.9s] phase=1 finished=1 active=27 <- phase ends +``` + +### ✅ State 4 = destroyed — a squadron death caught in the act + +`ADN111` goes **2 → 4** at 433 s while the active count falls 36 → 27 over the +same window. Together with the earlier 1 → 2 arrival this pins three points of +the encoding: **1 = not yet deployed, 2 = active, 4 = destroyed**. + +### ✅ The mission-over branch, observed exactly as disassembled + +The phase ended at 694.9 s, but **the ordinal did not advance** — and the reason +is the branch [mission-phase-advance](../mission-phase-advance.md) read out of +`sub_82260710`: + +``` +if ([phase+300] == 2) post 994 ; state = 0 ; MISSION OVER +else state = 5 ; [mission+40] += 1 NEXT PHASE +``` + +Measured at the end of the run: + +| field | value | meaning | +|---|---|---| +| `[phase+300]` | **2** | last-phase flag set (built-in 39) | +| `[mission+20]` | **0** | the mission-over state | +| `[phase+196]` | **1** | phase finished | +| `[mission+40]` | **1** | ordinal unchanged — correct for this branch | + +Every field matches the disassembled branch, on the live oracle. **The static +reading of the phase state machine is confirmed** — for the mission-over half. + +### 🔴 This was a LOSS, not a phase clear + +`screen_id` shows the `GAME OVER` frame, the escort was down to **35.7 %**, and +the pilot logged `DEAD` at 676 s. So a lose path ran built-in 39 +(`MARK_LAST_PHASE`) and then `END_PHASE`, which is why the mission ended instead +of advancing. + +**Two of the three objective squadrons were still alive** (`ADN110` and `ADN112` +at state 2), so this says nothing about whether destroying all three clears +phase 1 — that prediction is **still untested**. What it does establish is that +the `else` branch is the only way to reach phase 2, and it requires +`[phase+300] != 2` at the moment the phase ends. + +**Still not observed: a phase ADVANCE.** Five attempts. The obstacle is no longer +the freeze or the instrument — it is keeping the escort alive long enough to win. diff --git a/tools/re-capture/squadron_state.py b/tools/re-capture/squadron_state.py index 4cecbfd3..2e1c5401 100644 --- a/tools/re-capture/squadron_state.py +++ b/tools/re-capture/squadron_state.py @@ -34,6 +34,11 @@ sys.path.insert(0, __file__.rsplit('/', 1)[0]) import gmem import isl +# Bound for the pointer scan; observed ScriptMission addresses: 0xBC7A2A20 (x3), +# 0xBC79C960. Widened generously either side -- a miss falls back to the full +# sweep rather than failing. +PTR_WINDOW = (0xBC000000, 0xBD000000) + HDR_LEN = 20 # version, +4, code offset, symtab1, symtab2 -- distinctive @@ -42,9 +47,25 @@ def _fd_extents(f, size): return gmem.extents(f.fileno(), size) -def _find(f, size, needle): +def _find(f, size, needle, window=None): + """Search allocated extents for `needle`. + + `window` is an optional (lo_va, hi_va) guest-address bound. The full sweep + covers ~371 MB and is the leading suspect for the in-mission freeze + (mission-freeze-resume-spin.md); every ScriptMission observed so far has sat + in 0xBC79xxxx-0xBC7Axxxx, so bounding the POINTER scan cuts it by ~10x while + still finding the object. The header scan stays unbounded -- the .ssb has + been loaded at two different addresses across runs, so it cannot be bounded + on this evidence. + """ out = [] for start, end in _fd_extents(f, size): + if window is not None: + vs = gmem.va_to_off(window[0]), gmem.va_to_off(window[1]) + if vs[0] is not None and vs[1] is not None: + if end <= vs[0] or start >= vs[1]: + continue + start = max(start, vs[0]); end = min(end, vs[1]) f.seek(start) remaining, base, prev = end - start, start, b'' while remaining > 0: @@ -84,6 +105,19 @@ def find_mission(f, size, ssb): hdr = ssb[:HDR_LEN] code_off = struct.unpack_from('>I', ssb, 0x08)[0] sym1_off = struct.unpack_from('>I', ssb, 0x0C)[0] + for off in _find(f, size, hdr): + for filebase in gmem.off_to_vas(off): + code_base = filebase + code_off + want_44 = filebase + sym1_off + 4 + for poff in _find(f, size, struct.pack('>I', code_base & 0xFFFFFFFF), + window=PTR_WINDOW): + if poff % 4: + continue + for pva in gmem.off_to_vas(poff): + m = pva - 24 + if u32(f, m + 44) == want_44: + return m, filebase + # nothing in the window -- fall back to the full sweep rather than fail for off in _find(f, size, hdr): for filebase in gmem.off_to_vas(off): code_base = filebase + code_off