#!/usr/bin/env python3 """Watch the mission's phase ordinal change, live. The static work (docs/re/mission-phase-advance.md) found two 1/2/3 fields: [ScriptMission+40] the counter itself; incremented at 0x82260A00 [*(0x828F35F8) + 236] a mirror, written by CScriptInterpreter:: ChangePhase (opcode 995) after the fact The mirror is the one reachable without a debugger: 0x828F35F8 is a static singleton pointer in guest memory, so this is two reads through /dev/shm and needs no gdb -- which matters, because booting under gdb costs ~300s. This is the first DIRECT observation of a phase advance. Everything about phases so far is either static (route names, disassembly) or inferred; nobody has watched the number change. **It also witnesses the freeze**, because without that a frozen run produces a perfectly clean-looking negative. Measured 2026-08-25: a run entered flight already frozen -- `screen_id` said `flight`, the pilot logged 4029 samples, and every one carried the same yaw, pitch, target and distance from t=0.0. The phase field read 0 for 1070 s and "no phase advance" was about to be written down as a result. It was a fact about a dead world, exactly as frozen.py warns. So: check `frozen.py` on a cadence and SAY SO in the output. A negative from a frozen run is not a negative. Usage: phase_probe.py [secs] [every_s] """ import sys, time sys.path.insert(0, __file__.rsplit('/', 1)[0]) import gmem SINGLETON_PTR = 0x828F35F8 # static pointer to the mission-manager singleton PHASE_OFF = 236 # the mirror ChangePhase writes _FD = None def _fd(): """gmem exposes va_to_off/mem_path but no reader, so open the image once.""" global _FD if _FD is None: _FD = open(gmem.mem_path(), 'rb', buffering=0) return _FD def u32(va): off = gmem.va_to_off(va) if off is None: return None f = _fd() f.seek(off) b = f.read(4) return int.from_bytes(b, 'big') if len(b) == 4 else None def sample(): base = u32(SINGLETON_PTR) if not base or not (0x10000 <= base < 0xFFFFFFFF): return None, base return u32(base + PHASE_OFF), base if __name__ == '__main__': secs = float(sys.argv[1]) if len(sys.argv) > 1 else 600 every = float(sys.argv[2]) if len(sys.argv) > 2 else 5 import frozen next_check = 0.0 t0 = time.time(); last = object() print('singleton ptr 0x%08X, phase at +%d' % (SINGLETON_PTR, PHASE_OFF), flush=True) while time.time() - t0 < secs: now = time.time() - t0 if now >= next_check: next_check = now + 60 try: # frozen() returns (is_frozen, max_delta) -- a TUPLE, so testing # it directly is always truthy and the witness would fire on # every check. Unpack it. dead, _delta = frozen.frozen(5.0) except Exception: dead = False if dead: print(' [%6.1fs] *** GUEST FROZEN -- every reading from here is ' 'about a dead world, not a fact about the game ***' % now, flush=True) ph, base = sample() if ph != last: print(' [%6.1fs] singleton=%s phase=%s' % ( time.time() - t0, ('0x%08X' % base) if base else base, ph), flush=True) last = ph time.sleep(every)