- cafe.py: room 2 now invites the lowest-affection candidate instead of highest (room 1 unchanged), plus OCR'd invited-student name logging - exit_game.py: alert (AP_ALERT: log tag, picked up by ba_cron_run.sh's Discord alerting) if AP is still unspent above threshold when the game closes - login.py: re-check window_exists() after the stabilization wait to catch a startup window-flicker race that could crash a run - config.py/mapping.md: supporting constants and reference-mapping updates Also rewrites plan.md: retires the phase-by-phase changelog (now fully superseded by CLAUDE.md's own documentation) down to forward-looking backlog items, and adds a performance-improvement plan from a cron-log time analysis (lesson.py's per-cell screenshot redundancy, event_sweep's multi-call design). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
4.1 KiB
Python
96 lines
4.1 KiB
Python
"""Exit the Blue Archive client. No baas-reference counterpart -- see
|
|
ba_auto/reference_notes/mapping.md's "Exit game" row for the full search:
|
|
the reference project runs as a persistent background scheduler and never
|
|
deliberately exits the app between task cycles. This is new, desktop-specific
|
|
convenience logic for this project's different (cron, one-shot-per-preset)
|
|
architecture, meant to run as the last task in a preset.
|
|
|
|
navigation.return_to_home's own docstring already documents the mechanism
|
|
this relies on: an Escape press on the confirmed true home screen (nowhere
|
|
else) raises Blue Archive's own "exit the game?" confirmation dialog. Every
|
|
other task treats that as a hazard to avoid triggering by accident; this is
|
|
the one task that wants it, so it only fires Escape after verifying true
|
|
home first, never blindly.
|
|
|
|
Enter is this project's established confirm/OK key (circle.py/gem_shop.py's
|
|
own reward dismissal). Success is verified against driver.window_exists()
|
|
actually going False, not just against the keypress having been sent,
|
|
matching every other verified-keypress helper in this project.
|
|
|
|
No force-kill fallback if the graceful path doesn't verify -- matches this
|
|
project's "abort cleanly on unknown state rather than guess" convention
|
|
(gem_shop/bounty) rather than reaching for driver.kill_game().
|
|
|
|
Confirmed live (2026-07-18) via a standalone `exit_game` run: the
|
|
is_modal_open dim-probe correctly detected the exit-confirmation dialog
|
|
after Escape, and the game closed cleanly after Enter. Not yet exercised
|
|
as the tail end of a full daily/q4h preset run specifically.
|
|
"""
|
|
|
|
from ba_auto import navigation
|
|
|
|
CONFIRM_RETRIES = 3
|
|
CLOSE_WAIT_ITERATIONS = 15
|
|
CLOSE_WAIT_INTERVAL = 1
|
|
|
|
|
|
def _alert_if_ap_left_unspent(driver, config):
|
|
"""Print a distinctly-tagged, greppable log line if AP is still above
|
|
config.EXIT_AP_ALERT_THRESHOLD right as the game is about to close --
|
|
ba_cron_run.sh greps its own captured run output for the "AP_ALERT:"
|
|
tag and fires an extra Discord alert on it (see plan.md Phase 30).
|
|
Alert-only: never blocks or delays the exit itself.
|
|
|
|
Must run while still confirmed on true home (navigation.current_ap's
|
|
OCR rect is only meaningful there, same precondition already documented
|
|
on that function). An unreadable OCR result (None) is skipped silently,
|
|
matching this project's "don't guess on unreadable OCR" convention
|
|
rather than alerting on a possibly-wrong value.
|
|
"""
|
|
ap = navigation.current_ap(driver)
|
|
if ap is not None and ap > config.EXIT_AP_ALERT_THRESHOLD:
|
|
print(f"[exit_game] AP_ALERT: exiting with {ap} AP unspent (threshold {config.EXIT_AP_ALERT_THRESHOLD})")
|
|
|
|
|
|
def _raise_exit_dialog(driver, config):
|
|
for attempt in range(1, CONFIRM_RETRIES + 1):
|
|
if attempt == CONFIRM_RETRIES and driver.window_exists():
|
|
driver.focus_game()
|
|
driver.keypress("Escape")
|
|
driver.wait(1)
|
|
if navigation.is_modal_open(driver):
|
|
return True
|
|
print(f"[exit_game] exit-confirmation dialog not detected after Escape (attempt {attempt}/{CONFIRM_RETRIES})")
|
|
return False
|
|
|
|
|
|
def _confirm_exit(driver, config):
|
|
for attempt in range(1, CONFIRM_RETRIES + 1):
|
|
driver.keypress("Return")
|
|
driver.wait(1)
|
|
for _ in range(CLOSE_WAIT_ITERATIONS):
|
|
if not driver.window_exists():
|
|
return True
|
|
driver.wait(CLOSE_WAIT_INTERVAL)
|
|
print(f"[exit_game] game window still present after Enter (attempt {attempt}/{CONFIRM_RETRIES})")
|
|
return not driver.window_exists()
|
|
|
|
|
|
def run(driver, config):
|
|
driver.focus_game()
|
|
if not navigation.return_to_home(driver):
|
|
print("[exit_game] could not confirm the true home screen -- aborting without pressing Escape")
|
|
return
|
|
|
|
_alert_if_ap_left_unspent(driver, config)
|
|
|
|
if not _raise_exit_dialog(driver, config):
|
|
print("[exit_game] could not confirm the exit-confirmation dialog opened -- aborting without pressing Enter")
|
|
return
|
|
|
|
if not _confirm_exit(driver, config):
|
|
print("[exit_game] warning: could not confirm the game actually closed")
|
|
return
|
|
|
|
print("[exit_game] game closed.")
|