Files
Sylpheed/tools/re-capture/screen_id.py
Sylpheed RE agent 8bea94a47f tools: launch_mission reaches Stage 02 flight unattended again - wait for screens, not seconds
Two separate reasons the scripted route stopped, both measured rather than guessed:

* LOAD -> READY ROOM is not 28 s. Both runs on 2026-08-23 overran it, so the next
  press was eaten by the transition and the run ended up in OPTIONS once and
  BRIEFINGS once. wait_screen.sh now waits for the screen, with an optional
  --tap that clears a dialog the caller cannot know about (a freshly restored
  profile inserts "Auto-Save is active. OK?" here).
* The READY ROOM is DRAWN long before it is USABLE: it comes up with a
  "Preparing to Sortie" spinner and TAKE OFF greyed out. The two states differ by
  1.7 units of blue whole-image, so screen_id.py cannot separate them and should
  not try. take_off_armed.py tests the label instead: 0.0000 bright pixels while
  preparing, 0.1633 once armed, on three captures from two runs. It carries its
  own position check - the always-enabled BRIEFINGS label below reads 0.1027 in
  all three, to four decimals, so if that reference is dark the boxes are off the
  labels and the answer is "unknown", not a confident wrong one.

screen_id.py gains a "readyroom" class from the same measurements; nothing else
reclassifies.

Verified end to end and unattended: boot -> title -> LOAD GAME -> slot 01 ->
READY ROOM -> TAKE OFF -> "IN FLIGHT at 34s", pilot bound and engaging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-23 18:02:12 +00:00

112 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Identify which game screen a screenshot shows, without fixed pixel positions.
The nav scripts used to test named pixels ("648,221 is white"). That works only
while the game image sits at a known place on the root window, and it does not:
xenia's GTK window has a menu bar, so on some displays the image is ~25 px lower
and every constant reads the wrong row. The failure is silent and expensive — a
run sat on a plainly visible MAIN MENU for 300 s reporting "no main menu", and a
later one missed the title screen so the attract movie looped for ten minutes.
So identify screens by WHOLE-IMAGE statistics instead, which no vertical shift
(or scale, or letterbox) can move:
green fraction of pixels that are the game's green UI text/HUD colour
white fraction of near-white pixels
mean per-channel mean
Measured on known-good captures of each screen (1280x720, no menu bar):
title green 0.11% white 7.4% mean (62, 75, 84) blue-ish, bright
main menu green 0.04% white 3.5% mean (25, 38, 70) dark, strongly blue
in flight green 1.2% white 3.4% mean (54, 39, 37) green HUD everywhere
movie green 0% white varies no green at all
Usage: screen_id.py <png> [--json]
Prints one of: title | menu | readyroom | flight | other, plus the features.
"""
import json
import subprocess
import sys
# Downscaled first: the features are area fractions, so 320x180 gives the same
# answer for a fraction of the work (a full-size pure-python pass costs seconds,
# and these oracles are polled once a second).
W, H = 320, 180
def features(path):
raw = subprocess.run(
["convert", path, "-alpha", "off", "-resize", f"{W}x{H}!", "-depth", "8",
"rgb:-"], capture_output=True).stdout
n = len(raw) // 3
if not n:
return None
green = white = 0
sr = sg = sb = 0
for i in range(0, n * 3, 3):
r, g, b = raw[i], raw[i + 1], raw[i + 2]
sr += r; sg += g; sb += b
if g > 130 and g - r > 45 and g - b > 45:
green += 1
if r > 230 and g > 230 and b > 230:
white += 1
return {"green": green / n, "white": white / n,
"r": sr / n, "g": sg / n, "b": sb / n}
def classify(f):
if f is None:
return "none"
# Flight first: the HUD paints far more green than any menu.
# (READY ROOM is checked before the menu rule because it is also dark-ish
# and strongly blue, and would otherwise be swallowed by it.)
if f["green"] > 0.004:
return "flight"
# The READY ROOM is far the brightest blue of any 2D screen: measured
# (12.7, 36.5, 133.5) on two independent captures, byte-identical because
# the screen is static. The next-bluest thing on the load path is the save
# list at (16.7, 41.7, 81.0), and the mission briefing is cyan rather than
# blue at (4.6, 47.7, 54.5), so a floor of b > 110 separates all three with
# room to spare. Worth having as its own class: `launch_mission.sh` used a
# fixed `sleep 28` for this transition and its next press was swallowed,
# which put one run in OPTIONS and the next in BRIEFINGS.
if f["b"] > 110 and f["r"] < 40 and f["b"] - f["g"] > 60:
return "readyroom"
# The main menu is dark and strongly blue. TWO measured signatures, not one:
#
# white 3.5% mean (25, 38, 70) the 2026-07 reference at the top
# white 0.03% mean (13, 26, 59) measured 2026-08-18, twice, on the menu
# reached from the boot title
#
# The second one classified as "other" under the original rule's
# `0.015 < white` floor, which is worse than it sounds: a script that waits
# for "menu" then never sees it reports the navigation as FAILED while the
# menu is plainly on screen. Both variants are dark, strongly blue and
# essentially green-free, so key on that and let the near-white fraction be
# anything below the title's 3.8%.
if f["b"] - f["r"] > 30 and f["r"] < 45 and f["white"] < 0.075:
return "menu"
# The title is brighter, still blue-ish, and carries the green PRESS A text.
if f["green"] > 0.0004 and f["b"] > 60 and f["white"] > 0.03:
return "title"
return "other"
def main():
path = sys.argv[1]
f = features(path)
name = classify(f)
if "--json" in sys.argv:
print(json.dumps({"screen": name, **(f or {})}))
elif f:
print(f"{name} green={f['green']:.4f} white={f['white']:.4f} "
f"mean=({f['r']:.1f},{f['g']:.1f},{f['b']:.1f})")
else:
print(name)
return 0
if __name__ == "__main__":
raise SystemExit(main())