Migrate to Python-first architecture: implement mailbox and cafe tasks, replace Bash scripts with Python CLI, and establish configuration and driver modules.
This commit is contained in:
parent
ebce31156b
commit
398b936c64
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
graphify-out/cost.json
|
graphify-out/cost.json
|
||||||
graphify-out/cache/
|
graphify-out/cache/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
15
CLAUDE.md
15
CLAUDE.md
@ -421,13 +421,16 @@ When debugging image matching, write debug images to `./scratchpad`.
|
|||||||
|
|
||||||
## Existing features
|
## Existing features
|
||||||
|
|
||||||
Current project state before Python-first refactor:
|
Current project state (Phase 1 skeleton in place, mailbox migrated):
|
||||||
|
|
||||||
- mailbox claim exists
|
- `ba_dailies.sh` is a thin launcher that execs `ba_daily.py`
|
||||||
- cafe affection/income exists
|
- `ba_daily.py` dispatches `mailbox`/`cafe`/default flow to `ba_auto/tasks/`
|
||||||
- cafe uses OpenCV template matching for sparkle detection
|
- `ba_auto/tasks/mailbox.py` is real Python: it clicks with `ba_auto/driver.py` primitives and verifies state with `driver.color_at` (ported from `module/mail.py`'s `rgb_in_range` pattern) before pressing further keys — no legacy bridge
|
||||||
- existing Python helper is `scripts/detect_and_click.py`
|
- `ba_auto/tasks/cafe.py` still bridges to `scripts/ba_dailies_legacy.sh` (the pre-migration click sequence), marked with a TODO — the real logic has not moved into Python yet
|
||||||
- existing Bash entry point is `ba_dailies.sh`
|
- cafe's bridge still shells out to `scripts/detect_and_click.py` for OpenCV sparkle detection, unchanged
|
||||||
|
- `ba_auto/driver.py` has the primitives (`run_command`, `focus_game`, `click`, `keypress`, `screenshot`, `wait`, `color_at`) wired into `mailbox.py`; not yet used by `cafe.py`
|
||||||
|
- `ba_auto/detector.py` and `ba_auto/navigation.py` are placeholders
|
||||||
|
- Live testing found the old fixed mailbox-icon coordinate was marginal and could miss, cascading into an unverified Escape press that triggers Blue Archive's own "exit the game?" confirmation — see `plan.md` Phase 5 for details. This is the concrete reason `mailbox.py` verifies state before acting rather than trusting fixed coordinates blindly.
|
||||||
|
|
||||||
Migration goal:
|
Migration goal:
|
||||||
|
|
||||||
|
|||||||
111
README.md
Normal file
111
README.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# ba-auto-daily
|
||||||
|
|
||||||
|
Personal automation for Blue Archive JP daily tasks (mailbox, cafe, ...), driven by desktop control (`xdotool`/`scrot`) against the real PC/Steam/Proton client.
|
||||||
|
|
||||||
|
## How it's wired together
|
||||||
|
|
||||||
|
Two machines are involved:
|
||||||
|
|
||||||
|
- **`nik-macbookair`** (this Mac) — where the code is edited. The game does not run here and none of this can be tested locally.
|
||||||
|
- **`nik-gpu`** (Linux) — where the actual Blue Archive client runs under Steam/Proton, and where every command below actually executes.
|
||||||
|
|
||||||
|
Call path once deployed to `nik-gpu`:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/ba_dailies.sh [command] <- thin Bash launcher, no game logic
|
||||||
|
|
|
||||||
|
v
|
||||||
|
~/ba_daily.py [command] <- Python CLI, dispatches to a task
|
||||||
|
|
|
||||||
|
v
|
||||||
|
~/ba_auto/tasks/<command>.py <- the actual click/verify logic
|
||||||
|
```
|
||||||
|
|
||||||
|
`ba_dailies.sh` only picks the venv Python and execs `ba_daily.py`. All real logic — clicking, screenshotting, verifying game state — lives in Python under `ba_auto/`.
|
||||||
|
|
||||||
|
Current task status:
|
||||||
|
|
||||||
|
| Command | Implementation |
|
||||||
|
|---|---|
|
||||||
|
| `mailbox` | Real Python (`ba_auto/tasks/mailbox.py`). Verifies the mailbox panel actually opened (via a pixel-color probe, `driver.color_at`) before clicking "claim all" or pressing any further keys. Retries the open-click up to 3 times before giving up safely. |
|
||||||
|
| `cafe` | Still a bridge: `ba_auto/tasks/cafe.py` shells out to `scripts/ba_dailies_legacy.sh cafe`, the original unverified fixed-coordinate click sequence, unchanged. Not yet hardened — see "Known issue" below. |
|
||||||
|
|
||||||
|
## Prerequisites on nik-gpu
|
||||||
|
|
||||||
|
One-time, or after a dependency change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh nik-gpu
|
||||||
|
which xdotool scrot # both must be installed
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` (see below) creates the Python venv and checks these for you.
|
||||||
|
|
||||||
|
## Deploying your changes
|
||||||
|
|
||||||
|
From `nik-macbookair`, in the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -av --delete \
|
||||||
|
--exclude='.git/' --exclude='__pycache__/' --exclude='*.pyc' \
|
||||||
|
--exclude='.claude/settings.local.json' --exclude='graphify-out/' \
|
||||||
|
--exclude='screenshots/' \
|
||||||
|
./ nik-gpu:~/repo/ba-auto-daily/
|
||||||
|
|
||||||
|
ssh nik-gpu "cd ~/repo/ba-auto-daily && ./setup.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` copies the synced files into the fixed runtime paths (`~/ba_dailies.sh`, `~/ba_daily.py`, `~/ba_auto/`, `~/ba_scripts/`, `~/ba_assets/`) and (re)installs the venv. Re-run both commands any time you change code — there is no auto-deploy.
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
The game must already be running on `nik-gpu` (window titled `BlueArchive`). Then, on `nik-gpu`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/ba_dailies.sh # default: mailbox, then cafe
|
||||||
|
~/ba_dailies.sh mailbox # just mailbox
|
||||||
|
~/ba_dailies.sh cafe # just cafe
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also run these remotely without a separate `ssh` login step:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh nik-gpu "~/ba_dailies.sh mailbox"
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code `0` means the script ran to completion — it does **not** by itself guarantee the in-game action succeeded (`mailbox` checks and logs this explicitly; `cafe` currently does not).
|
||||||
|
|
||||||
|
## How to watch/verify it
|
||||||
|
|
||||||
|
- **Easiest: watch it live.** If you have Moonlight (or similar) streaming `nik-gpu`'s desktop, just open that and run the command from another terminal — you'll see the clicks happen in real time.
|
||||||
|
- **No stream handy: pull a screenshot after the fact.**
|
||||||
|
```bash
|
||||||
|
ssh nik-gpu "DISPLAY=:0 XAUTHORITY=/run/user/1000/gdm/Xauthority scrot -o /tmp/check.png"
|
||||||
|
scp nik-gpu:/tmp/check.png .
|
||||||
|
open check.png
|
||||||
|
```
|
||||||
|
- **Read the log output.** Each task prints what it's doing, e.g. `mailbox` prints `panel not detected after click (attempt N/3)` if a click misses, and `nothing to claim` vs `claiming all` depending on what it found.
|
||||||
|
|
||||||
|
## Known issue: don't trust `cafe` unattended yet
|
||||||
|
|
||||||
|
Live-testing `mailbox` surfaced a real bug in the *original*, unverified click sequence (see `plan.md` Phase 5 for the full writeup): a slightly-off icon coordinate caused a missed click, and the fixed sequence blindly kept going — clicking, pressing Enter, pressing Escape — with no idea whether any of it landed. The trailing Escape ended up hitting the bare home screen, which triggers Blue Archive's own **"Exit the game?"** confirmation dialog.
|
||||||
|
|
||||||
|
`mailbox.py` now guards against this (verifies state before acting, retries, aborts safely instead of guessing). **`cafe` still uses the old unverified bridge and has the same failure mode.** If you run `~/ba_dailies.sh cafe` and something looks off, check the screen before pressing anything — if you see an unexpected confirmation dialog, press **Escape/Cancel**, never Enter/OK, until you've confirmed what it's asking.
|
||||||
|
|
||||||
|
## Local checks (nik-macbookair)
|
||||||
|
|
||||||
|
The game can't run here, so this only catches syntax errors, not behavior:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n ba_dailies.sh
|
||||||
|
bash -n scripts/ba_dailies_legacy.sh
|
||||||
|
python3 -m py_compile ba_daily.py ba_auto/*.py ba_auto/tasks/*.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Real verification only happens by actually running against the live game on `nik-gpu`, per "How to watch/verify it" above.
|
||||||
|
|
||||||
|
## More detail
|
||||||
|
|
||||||
|
- `CLAUDE.md` — architecture rules and conventions for this repo.
|
||||||
|
- `plan.md` — feature-by-feature migration status and backlog, including the mailbox bug writeup.
|
||||||
|
- `ba_auto/reference_notes/mapping.md` — maps each feature to its `baas-reference` source.
|
||||||
1
ba_auto/__init__.py
Normal file
1
ba_auto/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Python-first Blue Archive daily-automation package."""
|
||||||
16
ba_auto/config.py
Normal file
16
ba_auto/config.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Central configuration for ba-auto-daily, ported from the old ba_dailies.sh."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
DISPLAY = ":0"
|
||||||
|
XAUTHORITY = "/run/user/1000/gdm/Xauthority"
|
||||||
|
ENV = {**os.environ, "DISPLAY": DISPLAY, "XAUTHORITY": XAUTHORITY}
|
||||||
|
|
||||||
|
WINDOW_NAME = "BlueArchive"
|
||||||
|
|
||||||
|
# TODO: remove once cafe is ported off the legacy Bash bridge (mailbox no longer uses this).
|
||||||
|
LEGACY_SCRIPT = os.path.expanduser("~/ba_scripts/ba_dailies_legacy.sh")
|
||||||
|
|
||||||
|
# Refined from (1726, 60): that coordinate sat on the edge of the icon's
|
||||||
|
# hitbox and intermittently missed during live testing.
|
||||||
|
MAILBOX_ICON = (1732, 50)
|
||||||
|
CLAIM_ALL = (1691, 1128)
|
||||||
3
ba_auto/detector.py
Normal file
3
ba_auto/detector.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Image/color matching helpers (OpenCV-based); not wired up yet."""
|
||||||
|
|
||||||
|
# TODO: migrate scripts/detect_and_click.py sparkle-matching logic here.
|
||||||
52
ba_auto/driver.py
Normal file
52
ba_auto/driver.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""Local PC/Steam/Proton control backend (xdotool/scrot wrappers)."""
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
from ba_auto import config
|
||||||
|
|
||||||
|
PROBE_SHOT_PATH = "/tmp/ba_auto_probe.png"
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(args, **kwargs):
|
||||||
|
kwargs.setdefault("env", config.ENV)
|
||||||
|
kwargs.setdefault("check", True)
|
||||||
|
return subprocess.run(args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def focus_game():
|
||||||
|
result = run_command(
|
||||||
|
["xdotool", "search", "--name", config.WINDOW_NAME],
|
||||||
|
check=False, capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
window_ids = result.stdout.split()
|
||||||
|
if not window_ids:
|
||||||
|
raise RuntimeError("Blue Archive window not found. Is the game running?")
|
||||||
|
run_command(["xdotool", "windowactivate", window_ids[0]])
|
||||||
|
wait(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def click(x, y):
|
||||||
|
run_command(["xdotool", "mousemove", str(x), str(y), "click", "1"])
|
||||||
|
wait(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def keypress(key):
|
||||||
|
run_command(["xdotool", "key", key])
|
||||||
|
wait(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot(path):
|
||||||
|
run_command(["scrot", "-a", "0,0,1920,1200", "-o", path])
|
||||||
|
|
||||||
|
|
||||||
|
def color_at(x, y):
|
||||||
|
screenshot(PROBE_SHOT_PATH)
|
||||||
|
image = cv2.imread(PROBE_SHOT_PATH)
|
||||||
|
b, g, r = image[y, x]
|
||||||
|
return int(r), int(g), int(b)
|
||||||
|
|
||||||
|
|
||||||
|
def wait(seconds):
|
||||||
|
time.sleep(seconds)
|
||||||
3
ba_auto/navigation.py
Normal file
3
ba_auto/navigation.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Shared navigation helpers (home, menu, popups, back/escape); not wired up yet."""
|
||||||
|
|
||||||
|
# TODO: extract common navigation flows here as tasks are ported off Bash.
|
||||||
18
ba_auto/reference_notes/mapping.md
Normal file
18
ba_auto/reference_notes/mapping.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Reference mapping
|
||||||
|
|
||||||
|
Maps each local feature to the corresponding `~/repo/baas-reference/module/...` implementation, driver/backend replacements, and current status. Update this before/while implementing or migrating a feature — see `CLAUDE.md` → "Reference mapping notes" and `plan.md` → "Reference mapping table".
|
||||||
|
|
||||||
|
| Local feature | Reference file | Reference functions/classes | Local file | Backend replacements | Status |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Mailbox | `module/mail.py` | `to_mail`, `implement` | `ba_auto/tasks/mailbox.py` | tap/click via xdotool, screenshot via scrot, `color.rgb_in_range` → `driver.color_at` pixel-probe check | Migrated: real Python, state-verified via color probe (no legacy bridge) |
|
||||||
|
| Cafe | Need to confirm in reference | Need to inspect | `ba_auto/tasks/cafe.py` | template matching via OpenCV, click via xdotool | Bridged to `scripts/ba_dailies_legacy.sh` + `scripts/detect_and_click.py`; Python port pending (plan.md Phase 6) |
|
||||||
|
| Stamina/AP | `module/collect_daily_free_power.py`, `module/collect_daily_task_power.py` | Need to inspect | `ba_auto/tasks/stamina.py` | color checks/clicks via local driver | Not started |
|
||||||
|
| Group/Club AP | `module/group.py` | Need to inspect | `ba_auto/tasks/group.py` | fixed click + state check via local driver | Not started |
|
||||||
|
| Bounty | `module/rewarded_task.py` | Need to inspect | `ba_auto/tasks/bounty.py` | sweep/color/OCR adaptation | Not started |
|
||||||
|
| Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started |
|
||||||
|
| Arena | `module/arena.py` | Need to inspect | `ba_auto/tasks/arena.py` | auto-fight + OCR + local driver | Not started |
|
||||||
|
| Common Shop | `module/shop/common_shop.py`, `module/shop/shop_utils.py` | Need to inspect | `ba_auto/tasks/shop_common.py` | OCR + tab navigation + local clicks | Not started |
|
||||||
|
| Tactical Shop | `module/shop/tactical_challenge_shop.py`, `module/shop/shop_utils.py` | Need to inspect | `ba_auto/tasks/shop_tactical.py` | OCR + tab navigation + local clicks | Not started |
|
||||||
|
| Lesson/Schedule | `module/lesson.py` | Need to inspect | `ba_auto/tasks/lesson.py` | OCR + template/portrait search + local driver | Not started |
|
||||||
|
|
||||||
|
Do not implement a feature without filling at least the relevant row.
|
||||||
1
ba_auto/tasks/__init__.py
Normal file
1
ba_auto/tasks/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Task modules dispatched by ba_daily.py."""
|
||||||
6
ba_auto/tasks/cafe.py
Normal file
6
ba_auto/tasks/cafe.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""Cafe daily task."""
|
||||||
|
|
||||||
|
|
||||||
|
def run(driver, config):
|
||||||
|
# TODO: migrate old Bash cafe logic into this Python task.
|
||||||
|
driver.run_command([config.LEGACY_SCRIPT, "cafe"])
|
||||||
58
ba_auto/tasks/mailbox.py
Normal file
58
ba_auto/tasks/mailbox.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Mailbox daily task. Ported from baas-reference module/mail.py's color-probe pattern."""
|
||||||
|
|
||||||
|
# (500, 10) sits on the mailbox panel's plain header background; the home
|
||||||
|
# screen shows character art there instead, so this tells open vs not-open.
|
||||||
|
HEADER_PROBE = (500, 10)
|
||||||
|
HEADER_OPEN_MIN_CHANNEL = 200
|
||||||
|
|
||||||
|
# "Claim all" renders as flat grey (153,153,153) when there is nothing to claim.
|
||||||
|
CLAIM_ALL_PROBE = (1710, 1128)
|
||||||
|
CLAIM_ALL_DISABLED_RGB = (153, 153, 153)
|
||||||
|
CLAIM_ALL_DISABLED_TOLERANCE = 12
|
||||||
|
|
||||||
|
OPEN_RETRIES = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _is_open(driver):
|
||||||
|
r, g, b = driver.color_at(*HEADER_PROBE)
|
||||||
|
return r > HEADER_OPEN_MIN_CHANNEL and g > HEADER_OPEN_MIN_CHANNEL and b > HEADER_OPEN_MIN_CHANNEL
|
||||||
|
|
||||||
|
|
||||||
|
def _claim_all_disabled(driver):
|
||||||
|
r, g, b = driver.color_at(*CLAIM_ALL_PROBE)
|
||||||
|
tr, tg, tb = CLAIM_ALL_DISABLED_RGB
|
||||||
|
return (
|
||||||
|
abs(r - tr) <= CLAIM_ALL_DISABLED_TOLERANCE
|
||||||
|
and abs(g - tg) <= CLAIM_ALL_DISABLED_TOLERANCE
|
||||||
|
and abs(b - tb) <= CLAIM_ALL_DISABLED_TOLERANCE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(driver, config):
|
||||||
|
driver.focus_game()
|
||||||
|
|
||||||
|
opened = False
|
||||||
|
for attempt in range(1, OPEN_RETRIES + 1):
|
||||||
|
driver.click(*config.MAILBOX_ICON)
|
||||||
|
driver.wait(1.5)
|
||||||
|
if _is_open(driver):
|
||||||
|
opened = True
|
||||||
|
break
|
||||||
|
print(f"[mailbox] panel not detected after click (attempt {attempt}/{OPEN_RETRIES})")
|
||||||
|
|
||||||
|
if not opened:
|
||||||
|
print("[mailbox] could not confirm mailbox is open, aborting without pressing further keys")
|
||||||
|
return
|
||||||
|
|
||||||
|
if _claim_all_disabled(driver):
|
||||||
|
print("[mailbox] nothing to claim")
|
||||||
|
else:
|
||||||
|
print("[mailbox] claiming all")
|
||||||
|
driver.click(*config.CLAIM_ALL)
|
||||||
|
driver.wait(1)
|
||||||
|
driver.keypress("Return")
|
||||||
|
driver.wait(1)
|
||||||
|
|
||||||
|
driver.keypress("Escape")
|
||||||
|
driver.wait(1)
|
||||||
|
print("[mailbox] Done.")
|
||||||
124
ba_dailies.sh
124
ba_dailies.sh
@ -1,121 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
export DISPLAY=:0
|
# Thin launcher only -- see CLAUDE.md "Bash policy". Logic lives in ba_daily.py.
|
||||||
export XAUTHORITY=/run/user/1000/gdm/Xauthority
|
set -euo pipefail
|
||||||
|
|
||||||
WIN_NAME="BlueArchive"
|
VENV_PYTHON="${VENV_PYTHON:-$HOME/.venvs/ba-auto-daily/bin/python3}"
|
||||||
MAILBOX_ICON="1726 60"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
CLAIM_ALL="1691 1128"
|
|
||||||
|
|
||||||
CAFE_ICON="165 1100"
|
if [ ! -f "$SCRIPT_DIR/ba_daily.py" ]; then
|
||||||
CAFE_ROOM_SWITCH="190 160"
|
echo "ERROR: ba_daily.py not found in $SCRIPT_DIR" >&2
|
||||||
CAFE_INCOME="1780 1105"
|
|
||||||
VENV_PYTHON="$HOME/.venvs/ba-auto-daily/bin/python3"
|
|
||||||
DETECT_SPARKLE="$HOME/ba_scripts/detect_and_click.py"
|
|
||||||
CAFE_MAX_CLICKS_PER_ROOM=15
|
|
||||||
|
|
||||||
get_window() {
|
|
||||||
xdotool search --name "$WIN_NAME" | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
click() {
|
|
||||||
local coords="$1"
|
|
||||||
xdotool mousemove $coords click 1
|
|
||||||
sleep 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
press_enter() {
|
|
||||||
xdotool key Return
|
|
||||||
sleep 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
press_esc() {
|
|
||||||
xdotool key Escape
|
|
||||||
sleep 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
focus_game() {
|
|
||||||
local win
|
|
||||||
win=$(get_window)
|
|
||||||
if [ -z "$win" ]; then
|
|
||||||
echo "ERROR: Blue Archive window not found. Is the game running?"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
xdotool windowactivate "$win"
|
|
||||||
sleep 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
do_mailbox() {
|
exec "$VENV_PYTHON" "$SCRIPT_DIR/ba_daily.py" "$@"
|
||||||
echo "[mailbox] Opening mailbox..."
|
|
||||||
click "$MAILBOX_ICON"
|
|
||||||
sleep 1.5
|
|
||||||
echo "[mailbox] Claiming all..."
|
|
||||||
click "$CLAIM_ALL"
|
|
||||||
sleep 1
|
|
||||||
echo "[mailbox] Confirming claim..."
|
|
||||||
press_enter
|
|
||||||
sleep 1
|
|
||||||
echo "[mailbox] Closing mailbox..."
|
|
||||||
press_esc
|
|
||||||
sleep 1
|
|
||||||
echo "[mailbox] Done."
|
|
||||||
}
|
|
||||||
|
|
||||||
do_cafe_room() {
|
|
||||||
local i
|
|
||||||
for ((i = 0; i < CAFE_MAX_CLICKS_PER_ROOM; i++)); do
|
|
||||||
if ! "$VENV_PYTHON" "$DETECT_SPARKLE" | grep -q "^MATCH"; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
press_enter
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
do_cafe() {
|
|
||||||
echo "[cafe] Opening cafe..."
|
|
||||||
click "$CAFE_ICON"
|
|
||||||
sleep 3
|
|
||||||
press_enter
|
|
||||||
echo "[cafe] Room 1: farming affection..."
|
|
||||||
do_cafe_room
|
|
||||||
echo "[cafe] Switching to room 2..."
|
|
||||||
click "$CAFE_ROOM_SWITCH"
|
|
||||||
sleep 3
|
|
||||||
press_enter
|
|
||||||
echo "[cafe] Room 2: farming affection..."
|
|
||||||
do_cafe_room
|
|
||||||
echo "[cafe] Claiming cafe income..."
|
|
||||||
click "$CAFE_INCOME"
|
|
||||||
sleep 2
|
|
||||||
press_enter
|
|
||||||
sleep 2
|
|
||||||
press_enter
|
|
||||||
sleep 2
|
|
||||||
echo "[cafe] Exiting cafe..."
|
|
||||||
press_esc
|
|
||||||
sleep 1.5
|
|
||||||
press_esc
|
|
||||||
sleep 1.5
|
|
||||||
echo "[cafe] Done."
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
mailbox)
|
|
||||||
focus_game
|
|
||||||
do_mailbox
|
|
||||||
;;
|
|
||||||
cafe)
|
|
||||||
focus_game
|
|
||||||
do_cafe
|
|
||||||
;;
|
|
||||||
"")
|
|
||||||
focus_game
|
|
||||||
do_mailbox
|
|
||||||
do_cafe
|
|
||||||
echo "All done."
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown phase: $1"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|||||||
34
ba_daily.py
Normal file
34
ba_daily.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Python CLI entry point: ba_dailies.sh -> ba_daily.py -> ba_auto/tasks/*.py"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ba_auto import config, driver
|
||||||
|
from ba_auto.tasks import cafe, mailbox
|
||||||
|
|
||||||
|
TASKS = {
|
||||||
|
"mailbox": mailbox.run,
|
||||||
|
"cafe": cafe.run,
|
||||||
|
}
|
||||||
|
DEFAULT_ORDER = ["mailbox", "cafe"]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
args = argv[1:]
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
for name in DEFAULT_ORDER:
|
||||||
|
TASKS[name](driver, config)
|
||||||
|
print("All done.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
command = args[0]
|
||||||
|
if command not in TASKS:
|
||||||
|
print(f"Unknown phase: {command}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
TASKS[command](driver, config)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
24
plan.md
24
plan.md
@ -156,11 +156,11 @@ Do not implement a feature without filling at least the relevant row.
|
|||||||
|
|
||||||
| Feature | Current status | Target status |
|
| Feature | Current status | Target status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Mailbox claim | Done in old Bash style | Migrate to `ba_auto/tasks/mailbox.py` |
|
| Mailbox claim | Migrated: `ba_auto/tasks/mailbox.py` uses `driver.color_at` to verify the panel opened before acting (found live-testing bug: a marginal icon coordinate could miss and cascade into pressing Escape on the home screen, which triggers Blue Archive's own exit-game confirmation) | Done |
|
||||||
| Cafe pats + income | Done in old Bash + standalone Python detector style | Migrate to `ba_auto/tasks/cafe.py` and `ba_auto/detector.py` |
|
| Cafe pats + income | Bridged: `ba_auto/tasks/cafe.py` calls `scripts/ba_dailies_legacy.sh cafe` (which still shells out to `scripts/detect_and_click.py`) | Migrate to `ba_auto/tasks/cafe.py` and `ba_auto/detector.py` (Phase 6) |
|
||||||
| Shared driver | Partial/implicit in scripts | Build `ba_auto/driver.py` |
|
| Shared driver | `ba_auto/driver.py` built (`run_command`, `focus_game`, `click`, `keypress`, `screenshot`, `wait`); not yet wired into tasks | Wire into mailbox/cafe as they migrate off the Bash bridge |
|
||||||
| Python CLI | Missing | Build `ba_daily.py` |
|
| Python CLI | Built: `ba_daily.py` dispatches `mailbox`/`cafe`/default flow | Extend as new tasks are added |
|
||||||
| Reference mapping | Missing | Build `ba_auto/reference_notes/mapping.md` |
|
| Reference mapping | Built: `ba_auto/reference_notes/mapping.md` | Fill in reference file/function columns per feature |
|
||||||
| Everything else | Not started | Implement reference-first in Python |
|
| Everything else | Not started | Implement reference-first in Python |
|
||||||
|
|
||||||
## Migration phase
|
## Migration phase
|
||||||
@ -169,6 +169,8 @@ Before adding new game features, migrate the existing working implementation.
|
|||||||
|
|
||||||
### Phase 1: Python skeleton
|
### Phase 1: Python skeleton
|
||||||
|
|
||||||
|
**Status: Done.**
|
||||||
|
|
||||||
Create:
|
Create:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -186,6 +188,8 @@ ba_auto/reference_notes/mapping.md
|
|||||||
|
|
||||||
### Phase 2: Launcher
|
### Phase 2: Launcher
|
||||||
|
|
||||||
|
**Status: Done.**
|
||||||
|
|
||||||
Change `ba_dailies.sh` into a thin launcher:
|
Change `ba_dailies.sh` into a thin launcher:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -206,6 +210,8 @@ Keep compatibility with:
|
|||||||
|
|
||||||
### Phase 3: Driver extraction
|
### Phase 3: Driver extraction
|
||||||
|
|
||||||
|
**Status: Primitives added to `ba_auto/driver.py`, not yet wired into task modules.**
|
||||||
|
|
||||||
Move shell interactions into `ba_auto/driver.py`.
|
Move shell interactions into `ba_auto/driver.py`.
|
||||||
|
|
||||||
Driver primitives should include:
|
Driver primitives should include:
|
||||||
@ -223,6 +229,8 @@ wait_until(...)
|
|||||||
|
|
||||||
### Phase 4: Detector extraction
|
### Phase 4: Detector extraction
|
||||||
|
|
||||||
|
**Status: Not started — `ba_auto/detector.py` is currently a placeholder.**
|
||||||
|
|
||||||
Move `scripts/detect_and_click.py` logic into `ba_auto/detector.py`.
|
Move `scripts/detect_and_click.py` logic into `ba_auto/detector.py`.
|
||||||
|
|
||||||
Detector primitives should include:
|
Detector primitives should include:
|
||||||
@ -240,6 +248,8 @@ The old script may remain as a compatibility wrapper temporarily, but the reusab
|
|||||||
|
|
||||||
### Phase 5: Mailbox migration
|
### Phase 5: Mailbox migration
|
||||||
|
|
||||||
|
**Status: Done.** Live testing surfaced a real bug: the old `MAILBOX_ICON` coordinate `(1726, 60)` sat on the edge of the icon's hitbox and intermittently missed, and the fixed click sequence had no way to notice — it cascaded into pressing Escape on the bare home screen, which triggers Blue Archive's own "exit the game?" confirmation (dismissed safely with Cancel during testing; no game state was lost). The Python port in `ba_auto/tasks/mailbox.py` fixes the coordinate and, following `module/mail.py`'s `rgb_in_range` pattern, verifies the panel actually opened (and whether "claim all" is disabled) via `driver.color_at` before pressing any further keys, with a bounded retry and a safe abort if the panel never appears.
|
||||||
|
|
||||||
Move mailbox logic from Bash to:
|
Move mailbox logic from Bash to:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -250,6 +260,8 @@ The CLI should call it through Python.
|
|||||||
|
|
||||||
### Phase 6: Cafe migration
|
### Phase 6: Cafe migration
|
||||||
|
|
||||||
|
**Status: Not started — `ba_auto/tasks/cafe.py` currently bridges to `scripts/ba_dailies_legacy.sh`.**
|
||||||
|
|
||||||
Move cafe logic from Bash to:
|
Move cafe logic from Bash to:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -268,6 +280,8 @@ During migration, verify:
|
|||||||
|
|
||||||
### Phase 7: setup.sh update
|
### Phase 7: setup.sh update
|
||||||
|
|
||||||
|
**Status: Done — `setup.sh` now deploys `ba_daily.py`, `ba_auto/`, and `scripts/ba_dailies_legacy.sh`.**
|
||||||
|
|
||||||
Update `setup.sh` so it deploys:
|
Update `setup.sh` so it deploys:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
122
scripts/ba_dailies_legacy.sh
Normal file
122
scripts/ba_dailies_legacy.sh
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Legacy pre-migration click sequences; bridged from ba_auto/tasks/*.py until ported to Python (see CLAUDE.md).
|
||||||
|
export DISPLAY=:0
|
||||||
|
export XAUTHORITY=/run/user/1000/gdm/Xauthority
|
||||||
|
|
||||||
|
WIN_NAME="BlueArchive"
|
||||||
|
MAILBOX_ICON="1726 60"
|
||||||
|
CLAIM_ALL="1691 1128"
|
||||||
|
|
||||||
|
CAFE_ICON="165 1100"
|
||||||
|
CAFE_ROOM_SWITCH="190 160"
|
||||||
|
CAFE_INCOME="1780 1105"
|
||||||
|
VENV_PYTHON="$HOME/.venvs/ba-auto-daily/bin/python3"
|
||||||
|
DETECT_SPARKLE="$HOME/ba_scripts/detect_and_click.py"
|
||||||
|
CAFE_MAX_CLICKS_PER_ROOM=15
|
||||||
|
|
||||||
|
get_window() {
|
||||||
|
xdotool search --name "$WIN_NAME" | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
click() {
|
||||||
|
local coords="$1"
|
||||||
|
xdotool mousemove $coords click 1
|
||||||
|
sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
press_enter() {
|
||||||
|
xdotool key Return
|
||||||
|
sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
press_esc() {
|
||||||
|
xdotool key Escape
|
||||||
|
sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
focus_game() {
|
||||||
|
local win
|
||||||
|
win=$(get_window)
|
||||||
|
if [ -z "$win" ]; then
|
||||||
|
echo "ERROR: Blue Archive window not found. Is the game running?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
xdotool windowactivate "$win"
|
||||||
|
sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
do_mailbox() {
|
||||||
|
echo "[mailbox] Opening mailbox..."
|
||||||
|
click "$MAILBOX_ICON"
|
||||||
|
sleep 1.5
|
||||||
|
echo "[mailbox] Claiming all..."
|
||||||
|
click "$CLAIM_ALL"
|
||||||
|
sleep 1
|
||||||
|
echo "[mailbox] Confirming claim..."
|
||||||
|
press_enter
|
||||||
|
sleep 1
|
||||||
|
echo "[mailbox] Closing mailbox..."
|
||||||
|
press_esc
|
||||||
|
sleep 1
|
||||||
|
echo "[mailbox] Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
do_cafe_room() {
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < CAFE_MAX_CLICKS_PER_ROOM; i++)); do
|
||||||
|
if ! "$VENV_PYTHON" "$DETECT_SPARKLE" | grep -q "^MATCH"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
press_enter
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
do_cafe() {
|
||||||
|
echo "[cafe] Opening cafe..."
|
||||||
|
click "$CAFE_ICON"
|
||||||
|
sleep 3
|
||||||
|
press_enter
|
||||||
|
echo "[cafe] Room 1: farming affection..."
|
||||||
|
do_cafe_room
|
||||||
|
echo "[cafe] Switching to room 2..."
|
||||||
|
click "$CAFE_ROOM_SWITCH"
|
||||||
|
sleep 3
|
||||||
|
press_enter
|
||||||
|
echo "[cafe] Room 2: farming affection..."
|
||||||
|
do_cafe_room
|
||||||
|
echo "[cafe] Claiming cafe income..."
|
||||||
|
click "$CAFE_INCOME"
|
||||||
|
sleep 2
|
||||||
|
press_enter
|
||||||
|
sleep 2
|
||||||
|
press_enter
|
||||||
|
sleep 2
|
||||||
|
echo "[cafe] Exiting cafe..."
|
||||||
|
press_esc
|
||||||
|
sleep 1.5
|
||||||
|
press_esc
|
||||||
|
sleep 1.5
|
||||||
|
echo "[cafe] Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
mailbox)
|
||||||
|
focus_game
|
||||||
|
do_mailbox
|
||||||
|
;;
|
||||||
|
cafe)
|
||||||
|
focus_game
|
||||||
|
do_cafe
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
focus_game
|
||||||
|
do_mailbox
|
||||||
|
do_cafe
|
||||||
|
echo "All done."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown phase: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
13
setup.sh
13
setup.sh
@ -5,9 +5,9 @@
|
|||||||
# not from macOS, since it installs into nik-gpu-local paths.
|
# not from macOS, since it installs into nik-gpu-local paths.
|
||||||
#
|
#
|
||||||
# It does NOT touch the game or take any screenshots; it only installs the
|
# It does NOT touch the game or take any screenshots; it only installs the
|
||||||
# venv and copies files to the fixed paths ba_dailies.sh/detect_and_click.py
|
# venv and copies files to the fixed paths ba_dailies.sh/ba_daily.py/
|
||||||
# expect (outside the repo, per the two-machine deploy convention in
|
# detect_and_click.py expect (outside the repo, per the two-machine deploy
|
||||||
# CLAUDE.md).
|
# convention in CLAUDE.md).
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VENV_DIR="$HOME/.venvs/ba-auto-daily"
|
VENV_DIR="$HOME/.venvs/ba-auto-daily"
|
||||||
@ -39,9 +39,16 @@ fi
|
|||||||
echo "== Deploying scripts + assets to fixed paths =="
|
echo "== Deploying scripts + assets to fixed paths =="
|
||||||
mkdir -p "$SCRIPTS_DIR" "$ASSETS_DIR"
|
mkdir -p "$SCRIPTS_DIR" "$ASSETS_DIR"
|
||||||
cp scripts/detect_and_click.py "$SCRIPTS_DIR/detect_and_click.py"
|
cp scripts/detect_and_click.py "$SCRIPTS_DIR/detect_and_click.py"
|
||||||
|
cp scripts/ba_dailies_legacy.sh "$SCRIPTS_DIR/ba_dailies_legacy.sh"
|
||||||
|
chmod +x "$SCRIPTS_DIR/ba_dailies_legacy.sh"
|
||||||
cp assets/cafe_sparkle.png "$ASSETS_DIR/cafe_sparkle.png"
|
cp assets/cafe_sparkle.png "$ASSETS_DIR/cafe_sparkle.png"
|
||||||
|
|
||||||
|
echo "== Deploying Python-first entry point =="
|
||||||
cp ba_dailies.sh "$HOME/ba_dailies.sh"
|
cp ba_dailies.sh "$HOME/ba_dailies.sh"
|
||||||
chmod +x "$HOME/ba_dailies.sh"
|
chmod +x "$HOME/ba_dailies.sh"
|
||||||
|
cp ba_daily.py "$HOME/ba_daily.py"
|
||||||
|
rm -rf "$HOME/ba_auto"
|
||||||
|
cp -r ba_auto "$HOME/ba_auto"
|
||||||
|
|
||||||
echo "== Done =="
|
echo "== Done =="
|
||||||
echo "Run with: ~/ba_dailies.sh [mailbox|cafe]"
|
echo "Run with: ~/ba_dailies.sh [mailbox|cafe]"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user