SYLPH_PREFER=Turret, escort at 100%, 1070s, phase field 0 throughout, 4029 Turret-targeting pilot samples. It looked like a clean negative -- 'hunting the objective squadrons does not advance the phase' -- and it is worthless: frozen.py reports max_pixel_delta=0, and the pilot's first sample at t=0.0 already has spd=0 with the same yaw/pitch/target/d=7186 it still had at 1070s. The mission froze on entry to flight and nothing was ever shot, while screen_id said 'flight' the whole time. I was one step from writing this up as a fact about the game; running the freeze test rather than trusting a plausible log is what caught it. phase_probe.py now calls frozen.py every 60s and prints a GUEST FROZEN banner inline, so a dead-world reading is labelled in the data instead of discovered later. Note frozen.frozen() returns a TUPLE (is_frozen, max_delta) -- testing it directly is always truthy and would have made the witness fire on every check. Verified against the frozen guest. Also recorded: the pilot log is itself a freeze witness -- identical yaw/pitch/target across thousands of samples is a dead world, not patience.
89 lines
3.3 KiB
Python
Executable File
89 lines
3.3 KiB
Python
Executable File
#!/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)
|