diff --git a/docs/re/script-runtime-probe.md b/docs/re/script-runtime-probe.md index 7962e18..34f9164 100644 --- a/docs/re/script-runtime-probe.md +++ b/docs/re/script-runtime-probe.md @@ -225,3 +225,58 @@ writable (`tools/re-capture/gpoke.py`), so **set the two surviving squadrons' autopilot is not good enough to win, and a wrong answer is as informative as a right one — if nothing happens, the condition is not what the bytecode reading says. + +## 🔴 2026-08-25 — poking all three squadrons to "destroyed" does NOT end the phase + +The direct test, run instead of a seventh attempt at winning. All three +objective squadrons were live (state 2) when the poke went in. + +``` +ADN110 rec=0xBCA48BC0 +4=0x0000001A +16=2 +ADN111 rec=0xBCA48C60 +4=0x0000001B +16=2 +ADN112 rec=0xBCA48D00 +4=0x0000001C +16=2 +STICK TEST on ADN110 +16: was=2 wrote=4 after2s=4 -> STICKS +poked all 3 + [+ 5s .. +60s] phase=1 finished=0 states={ADN110:4, ADN111:4, ADN112:4} +``` + +**The write sticks — and nothing happens.** Sixty seconds with all three reading +state 4 (the value a naturally-destroyed squadron takes, measured earlier on +ADN111), and `[ScriptPhase+196]` stayed 0 and the ordinal stayed 1. + +**So "phase 1 clears when ADN110/111/112 are destroyed" is not confirmed, and +the simplest form of it is refuted.** The bytecode reading — three `unit_state` +polls then `set_flag(8)` — is solid; what does not follow is that flipping this +field is equivalent to the kill. + +### 🟡 Why it probably did nothing: the poll was not running + +That the poke **persisted for 60 s** is itself the clue. Built-in 69 is +documented as *normalising* `+16` when it polls, so if the condition coroutine +were running its `unit_state` polls, it should have overwritten the value within +a frame. It did not — which points at the condition being evaluated **only when +a trigger fires**, not on every frame. Poking state without firing the trigger +changes a value nobody reads. + +### 🔴 The per-unit record layout is not what the built-in summary says + +Dumping `ADN110`'s record contradicts *"+4 live object (NULL = absent)"*: + +``` ++0 = 2 +12 = 0x42480000 (50.0f) +20 = 9 ++4 = 26 +16 = 4 (state) +128 = 0x3F733333 (0.95f) +``` + +`+4` is **26/27/28 for the three squadrons — small consecutive integers, not +pointers** (an undeployed squadron, `ADN201`, has `+4 = 0` and `+16 = 0`). And +**`+20 = 9` is exactly these squadrons' member count `n`**, which the roster +gives independently — so the record is per-squadron and carries its strength. + +Earlier readings printed `obj=yes` because the probe tested that word for +non-zero, not for pointer-ness. That is a reporting bug in my own tool, and it +made a small index look like a live object. + +**Not settled:** what `+4` indexes (a route or symtab-1 index is the obvious +guess, given the values), and how to make the condition actually re-evaluate. +Firing the trigger — built-in 100 pushes onto `[phase+272]` — is the next thing +to look at. diff --git a/tools/re-capture/poke_squadron.py b/tools/re-capture/poke_squadron.py new file mode 100644 index 0000000..689fef0 --- /dev/null +++ b/tools/re-capture/poke_squadron.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Test the phase-1 clear condition by KILLING squadrons from outside the game. + +Six flown attempts failed to reach a phase advance, because the autopilot cannot +win Stage 02 (script-runtime-probe.md). The condition itself is cheap to test +directly: the script polls `unit_state` on ADN110/111/112, so make those records +read as gone and watch whether `[ScriptPhase+196]` goes to 1 and +`[ScriptMission+40]` steps to 2. + +Two candidate writes, and the FIRST THING THIS DOES IS CHECK WHICH ONE STICKS: + + +16 state -- but built-in 69 "normalises" this field when it polls, so + it may be a cache that the game rewrites within a frame. + +4 live object -- built-ins 69/70/24 all test `[rec+4]` first and take an + early exit when it is NULL, so NULLing it is the more + faithful "this squadron is gone". + +A write that reverts is itself a result: it means the field is derived, not +authoritative, and the poke route is closed. + +Usage: poke_squadron.py [--field 16|4] [--value N] [names...] +""" +import os +import struct +import sys +import time + +sys.path.insert(0, __file__.rsplit('/', 1)[0]) +import gmem +import isl +import squadron_state as S + +def poke32(f, va, value): + off = gmem.va_to_off(va) + f.seek(off) + f.write(struct.pack('>I', value & 0xFFFFFFFF)) + f.flush() + + +def main(): + ssb = isl.load(sys.argv[1]) + sym2 = isl.symbols(ssb, 2) + field = 16 + value = 4 + if '--field' in sys.argv: + field = int(sys.argv[sys.argv.index('--field') + 1], 0) + if '--value' in sys.argv: + value = int(sys.argv[sys.argv.index('--value') + 1], 0) + names = [a for a in sys.argv[2:] if not a.startswith('--') and not a.isdigit()][1:] \ + or ['ADN110', 'ADN111', 'ADN112'] + path = gmem.mem_path() + size = os.path.getsize(path) + idx = {n: i for i, (_t, n) in sym2.items()} + with open(path, 'r+b', buffering=0) as f: + m, fb = S.find_mission(f, size, ssb) + if m is None: + print('ScriptMission not located'); return 1 + ph = S.u32(f, m + 4) + arr = S.u32(f, ph + 324) + base = S.u32(f, arr + 4) + print('ScriptMission 0x%08X ScriptPhase 0x%08X' % (m, ph)) + print('before: phase=%s finished=%s' % (S.u32(f, m + 40), S.u32(f, ph + 196))) + + recs = {} + for n in names: + rec = S.u32(f, base + idx[n] * 4) + recs[n] = rec + print(' %-8s rec=0x%08X +4=0x%08X +16=%s' % ( + n, rec, S.u32(f, rec + 4), S.u32(f, rec + 16))) + + # --- does the write even stick? --- + probe = recs[names[0]] + was = S.u32(f, probe + field) + poke32(f, probe + field, value) + imm = S.u32(f, probe + field) + time.sleep(2.0) + later = S.u32(f, probe + field) + print('STICK TEST on %s +%d: was=%s wrote=%s immediately=%s after2s=%s -> %s' % ( + names[0], field, was, value, imm, later, + 'STICKS' if later == value else 'REVERTED (field is derived)')) + if later != value: + poke32(f, probe + field, was) + return 2 + + for n in names[1:]: + poke32(f, recs[n] + field, value) + print('poked all %d' % len(names)) + for t in range(0, 60, 5): + time.sleep(5) + print(' [+%2ds] phase=%s finished=%s states=%s' % ( + t + 5, S.u32(f, m + 40), S.u32(f, ph + 196), + {n: S.u32(f, recs[n] + 16) for n in names}), flush=True) + return 0 + + +if __name__ == '__main__': + sys.exit(main())