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:
Nik Afiq 2026-07-05 16:02:02 +09:00
parent ebce31156b
commit 398b936c64
17 changed files with 476 additions and 133 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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
View 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
View File

@ -0,0 +1 @@
"""Python-first Blue Archive daily-automation package."""

16
ba_auto/config.py Normal file
View 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
View 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
View 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
View 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.

View 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.

View File

@ -0,0 +1 @@
"""Task modules dispatched by ba_daily.py."""

6
ba_auto/tasks/cafe.py Normal file
View 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
View 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.")

View File

@ -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" exit 1
VENV_PYTHON="$HOME/.venvs/ba-auto-daily/bin/python3" fi
DETECT_SPARKLE="$HOME/ba_scripts/detect_and_click.py"
CAFE_MAX_CLICKS_PER_ROOM=15
get_window() { exec "$VENV_PYTHON" "$SCRIPT_DIR/ba_daily.py" "$@"
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

34
ba_daily.py Normal file
View 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
View File

@ -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:
``` ```

View 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

View File

@ -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]"