diff --git a/.gemini/settings.json b/.gemini/settings.json index 2c63c08510..1db56c6b93 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,2 +1,15 @@ { -} + "mcpServers": { + "adb-vision": { + "command": "uv", + "args": [ + "run", + "--directory", + "adb_vision", + "server.py", + "--serial", + "127.0.0.1:21513" + ] + } + } +} \ No newline at end of file diff --git a/.githooks/pre-push b/.githooks/pre-push index 2db3ce7915..71c13faf84 100644 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -22,6 +22,23 @@ resolve_python() { } PYTHON_BIN="$(resolve_python)" + +# --- Nested .git directory guard --- +# The only allowed .git locations are the repo root and the upstream_alas submodule. +# Any other nested .git directory means git init or git clone was run inside the repo, +# which causes VS Code to misattribute files, creates phantom untracked changes, and +# risks commits going to the wrong repository entirely. +nested_git="$(find . -maxdepth 4 -name ".git" -not -path "./.git" -not -path "./upstream_alas/.git" 2>/dev/null | head -5)" +if [[ -n "$nested_git" ]]; then + echo "ERROR: Nested .git directory detected. Push blocked." >&2 + echo "Only '.git' (repo root) and 'upstream_alas/.git' (submodule) are allowed." >&2 + echo "Found:" >&2 + echo "$nested_git" | sed 's/^/ /' >&2 + echo "Remove the nested .git directory and try again." >&2 + exit 1 +fi +# --- End nested .git guard --- + PATRICK_CUSTOM_SYNC_MODE=check $PYTHON_BIN agent_orchestrator/sync_patrick_custom.py $PYTHON_BIN agent_orchestrator/sync_entrypoint_docs.py --check diff --git a/.github/prompts/alas-separation.prompt.md b/.github/prompts/alas-separation.prompt.md new file mode 100644 index 0000000000..4f33d2d830 --- /dev/null +++ b/.github/prompts/alas-separation.prompt.md @@ -0,0 +1,170 @@ +--- +name: 'alas-separation' +description: 'Separate agent orchestrator from ALAS-with-Dashboard while preserving state engine' +agent: 'agent' +model: 'Claude Sonnet 4' +tools: ['todos', 'runSubagent', 'codebase', 'askQuestions', 'readFile', 'search', 'editFiles'] +--- + +# ALAS Separation: Orchestrator Independence + +**Goal:** Separate the agent orchestrator from ALAS-with-Dashboard into a clean, standalone architecture while preserving the critical state engine work. + +**Pre-Flight Check:** +- Verify git remotes: `origin` → Coldaine/alas, `upstream` → Zuosizhu/Alas-with-Dashboard +- Confirm MEmu setup: Admin-at-startup already solved, ADB serial 127.0.0.1:21513 + +--- + +## Phase 1: Discovery #codebase #runSubagent + +Spawn parallel subagents to find all existing documentation and plans: + +**Subagent A - Repository Documentation:** +``` +Read and summarize all planning documents in: +- docs/plans/ +- docs/ROADMAP.md +- docs/NORTH_STAR.md +- docs/ARCHITECTURE.md +- docs/state_machine/ + +Report: What separation plans exist? What was already decided? What's missing? +``` + +**Subagent B - Fork Investigation:** +``` +Check for related repos or branches with standalone agent work: +- Search for azurlane-agent references +- Check branch history for separation experiments +- Look for MCP server work outside ALAS imports + +Report: What work exists elsewhere that should be integrated? +``` + +**Subagent C - State Machine Audit:** +``` +Document the current state engine: +- module/ui/page.py (Page.all_pages, 43 pages, 98 transitions) +- module/state_machine.py +- How state is currently exposed via MCP + +Report: What must be preserved? How tightly coupled to ALAS internals? +``` + +**Consolidate findings** into `.kilo-prompt/discovered_state.md` + +--- + +## Phase 2: User Alignment #askQuestions + +Interview the user on these key points: + +1. **Git Remotes:** Confirm origin/upstream configuration is correct +2. **State Engine Priority:** Which state machine features are absolutely critical? +3. **Entry Point:** Start with behavioral catalog OR MCP boundary refactor? +4. **MaaMCP:** Evaluate adopting MaaFramework/MaaMCP as foundation? +5. **Branch Strategy:** Which branch to checkout? (pr/all-changes-no-secret? new feature?) +6. **Timeline:** When to flip from manual pilot (coding agent) to autonomous? + +Record answers in `.kilo-prompt/user_alignment_answers.md` + +--- + +## Phase 3: Implementation Analysis #runSubagent + +Once Phase 2 is complete, spawn parallel subagents: + +**Subagent D - Git/Branch Analysis:** +``` +Analyze current git state: +- List all local and remote branches +- Identify cleanest separation starting point +- Document exact checkout command needed + +DO NOT execute git commands - document only. +``` + +**Subagent E - MCP Tool Dependency Map:** +``` +Analyze agent_orchestrator/alas_mcp_server.py: +- Which tools import ALAS internals? (sys.path hacks) +- Which tools are already standalone? (ADB-only) +- Minimal set needed for basic operation? + +Report: Refactoring order - which tools first? +``` + +**Subagent F - State Engine Extraction Plan:** +``` +Plan extracting the 43-page state machine: +- Can Page.all_pages be exposed via MCP without importing? +- Which transition logic must be replicated standalone? +- API contract between orchestrator and state queries? + +Report: Extraction strategy with minimal ALAS dependency. +``` + +--- + +## Phase 4: Write Implementation Plan #todos + +Create comprehensive plan: +- Location: `.kilo-prompt/implementation_plan.md` +- Include: Branch to checkout, step-by-step order, MCP refactoring sequence, state engine approach, testing strategy + +Add all phases to `#todos` tracker. + +--- + +## Key Principles + +### Dual-Use Constraint (Non-Negotiable) +`gui.py --run PatrickCustom` must work forever. Bot development is additive only. + +### Extract, Don't Wrap +ALAS is reference/spec only. Build standalone tools with no `from module.*` imports. + +### Two-Tier Operation +- **Tier 1 (hot path):** Deterministic tools with strict contract `{success, data, error, observed_state, expected_state}` +- **Tier 2 (recovery only):** VLM vision when deterministic tools fail + +### Single-Harness Principle +Same MCP tool surface for dev (Claude Code manual piloting) and prod (autonomous Gemini). + +### MEmu Environment +- Multiple Instance Manager runs with admin at system startup +- ADB serial: 127.0.0.1:21513 (instance index 1) +- Screenshot: DroidCast (bypasses OpenGL issues) + +### State Engine Preservation +43-page state machine with 98 transitions is valuable extracted knowledge: +- Expose via MCP tools (`alas.get_current_state`, `alas.goto`) +- Do not import ALAS modules in orchestrator +- Document transitions as behavioral catalog + +--- + +## Success Criteria + +- [ ] Phase 1: All documentation discovered and consolidated +- [ ] Phase 2: User interview completed with answers recorded +- [ ] Phase 3: Implementation entry point identified +- [ ] Phase 4: Comprehensive plan written and added to #todos +- [ ] Git remotes verified correct +- [ ] State engine extraction strategy documented +- [ ] MCP tool refactoring sequence determined + +--- + +## Execution Flow + +``` +1. READ docs (parallel subagents) → discovered_state.md +2. INTERVIEW user → user_alignment_answers.md +3. ANALYZE entry point (parallel subagents) → subagent_reports/ +4. WRITE plan → implementation_plan.md +5. TRACK in #todos +``` + +**Begin with Phase 1 when this prompt is executed.** diff --git a/.mcp.json b/.mcp.json index c4538b2954..4eeaa7e20c 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,10 +5,10 @@ "args": [ "run", "--directory", - "agent_orchestrator", - "alas_mcp_server.py", - "--config", - "alas" + "adb_vision", + "server.py", + "--serial", + "127.0.0.1:21513" ], "env": { "PYTHONIOENCODING": "utf-8" diff --git a/CLAUDE.md b/CLAUDE.md index 10d858bd6c..e6639025b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ If any instruction file conflicts with this one, this file wins. - Never modify `upstream_alas/` directly. - Never create additional git repos or submodules inside this repo. +- Never run `git init` or `git clone` inside this repository's subdirectories. The only submodule is `upstream_alas/`. - Treat `alas_wrapped/` as the runnable source of truth for customized ALAS behavior. - Use deterministic tools first; use LLM/vision only for recovery. - Do not commit runtime artifacts or secrets (for example: `alas_wrapped/alas_admin_token`, screenshots, ad-hoc runtime logs). diff --git a/TDD_IMPLEMENTATION_PLAN.md b/TDD_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000000..88dea73c59 --- /dev/null +++ b/TDD_IMPLEMENTATION_PLAN.md @@ -0,0 +1,549 @@ +# TDD-Focused Implementation Plan: Azur Lane Agent + +**Target:** Competent coding agent working in `azurlane-agent` repo +**Core Principle:** Every feature driven by live MEmu integration tests +**Test Philosophy:** Write failing test first → implement → test passes → commit + +--- + +## Current State Assessment + +### What Exists (Leverage These) + +| Component | Location | Status | TDD Value | +|-----------|----------|--------|-----------| +| ADB screenshot/tap/swipe | `agent_orchestrator/alas_mcp_server.py` | Working | Reuse with strict contract wrapper | +| ALAS state machine | `alas_wrapped/module/ui/page.py` | 43 pages, 98 transitions | Extract knowledge, not code | +| MEmu config | `docs/dev/memu_playbook.md` | Documented | Admin-at-startup solved | +| MCP server scaffold | `agent_orchestrator/alas_mcp_server.py` | FastMCP operational | Refactor to standalone | +| Navigation tools | `alas_wrapped/tools/navigation.py` | ALAS-dependent | Reimplement standalone | +| Test patterns | `agent_orchestrator/test_integration_mcp.py` | Mock-based | Convert to live MEmu tests | + +### What Must Be Built (Greenfield) + +1. **Standalone MCP server** (no `sys.path.insert` ALAS imports) +2. **Live MEmu test harness** (real emulator, no mocks) +3. **Master scheduler** (task queue from PatrickCustom.json + dynamic) +4. **Deterministic tool framework** (auto-discoverable, strict contract) +5. **Blueprint system** (manual piloting → deterministic tool generation) +6. **Persistent state cache** (resources, current page, timers) + +--- + +## Phase 0: Foundation (Harness + Raw Primitives) + +**Goal:** MCP harness works end-to-end with real MEmu. 20+ live tests passing. + +### P0-T1: MEmu Launch Test [FIRST TEST] +```python +# tests/live/test_memu_launch.py +def test_memu_launch_and_connect(): + """MEmu can be launched via admin plugin and ADB connects.""" + # Pre-condition: MEmu not running + # Action: launch_memu_via_memuc() + # Assert: ADB serial 127.0.0.1:21513 responds to 'adb devices' +``` + +**Implementation:** +- Create `azurlane_agent/emulator/memuc_cli.py` +- Wrap `memuc.exe` commands (start, stop, is_running) +- Use existing memuc patterns from `docs/dev/memu_playbook.md` + +### P0-T2: Screenshot Test +```python +def test_screenshot_returns_valid_image(): + """take_screenshot() returns PIL Image + base64.""" + # Action: img, b64 = take_screenshot() + # Assert: img.size == (1280, 720), b64 is valid base64 PNG +``` + +**Implementation:** +- `azurlane_agent/adb/screenshot.py` - DroidCast method (bypasses OpenGL issues) +- Returns: `{success: True, data: {image: PIL.Image, base64: str}, observed_state: "screenshot_ok"}` + +### P0-T3: Raw Input Tests +```python +def test_raw_tap_moves_ui(): + """raw_tap(x, y) actually changes the game UI.""" + # Setup: Screenshot before tap + # Action: raw_tap(100, 100) + # Assert: Screenshot after is different +``` + +**Implementation:** +- `azurlane_agent/adb/input.py` - MaaTouch socket (20-40ms, not ADB 200-300ms) +- Returns: `{success, data, error, observed_state: "tap_executed", expected_state}` + +### P0-T4: VLM Integration Test +```python +def test_vlm_with_screenshot(): + """call_vlm_with_screenshot(goal) returns structured reasoning.""" + # Setup: Screenshot of main menu + # Action: result = call_vlm_with_screenshot("What page is this?") + # Assert: result contains "main menu" or "page_main" +``` + +**Implementation:** +- `azurlane_agent/vision/gemini_flash.py` +- Structured output: `{reasoning: str, action: str, confidence: float}` + +### P0-T5: State Cache Test +```python +def test_state_cache_query_update(): + """Persistent state cache can be queried and updated.""" + # Action: update_state("oil", 8420) + # Assert: query_state("oil") == 8420 +``` + +**Implementation:** +- `azurlane_agent/state/cache.py` - JSONL or SQLite +- Fast in-memory with disk persistence + +### P0-T6: Strict Contract Enforcement +```python +def test_all_tools_return_strict_contract(): + """Every tool returns {success, data, error, observed_state, expected_state}.""" + # Test every tool in registry + # Assert: All required keys present +``` + +**Implementation:** +- `azurlane_agent/mcp/tool_contract.py` - Base class/decorator +- Runtime validation of contract + +### Phase 0 Exit Criteria +- [ ] 20+ tests in `tests/live/test_p0_*.py` +- [ ] All tests run against real MEmu (no mocks) +- [ ] `pytest tests/live/` passes 100% +- [ ] Coding agent can manually drive: launch → screenshot → tap → check state + +--- + +## Phase 1: Master Scheduler + Core Loop + +**Goal:** Permanent loop exists. `run_loop()` correctly schedules tasks. + +### P1-T1: Task Loading Test +```python +def test_scheduler_loads_patrick_custom(): + """Scheduler loads tasks from PatrickCustom.json + dynamic entries.""" + # Setup: PatrickCustom.json with commissions due + # Action: tasks = scheduler.load_tasks() + # Assert: "Commission" in tasks, priority > 0 +``` + +**Implementation:** +- `azurlane_agent/scheduler/loader.py` +- Parse PatrickCustom.json format +- Add dynamic task injection API + +### P1-T2: Next Action Decision Test +```python +def test_get_next_action_returns_correct_task(): + """get_next_action() returns correct next task or nothing due.""" + # Setup: Commission due now, Dailies due in 1 hour + # Action: next_task = scheduler.get_next_action() + # Assert: next_task.name == "Commission" +``` + +**Implementation:** +- `azurlane_agent/scheduler/decider.py` +- Time-based priority queue +- Returns: `{task, priority, due_time}` or `None` + +### P1-T3: Core Loop Integration Test +```python +def test_core_loop_three_cycles(): + """Core loop runs 3 cycles, calling deterministic or vision path correctly.""" + # Setup: Mock task that takes 5 seconds + # Action: Run loop for 3 cycles + # Assert: Each cycle calls execute_task() or enter_manual_piloting() +``` + +**Implementation:** +- `azurlane_agent/loop/master_loop.py` +- Pseudo-code: +```python +while running: + task = scheduler.get_next_action() + if task: + success = try_deterministic(task) + if not success: + enter_manual_piloting(task) + update_state_cache() + sleep(poll_interval) +``` + +### P1-T4: State Persistence Test +```python +def test_state_updated_after_every_action(): + """Persistent state is updated after every action.""" + # Setup: Start with oil=0 in cache + # Action: Execute commission collection + # Assert: cache.get("oil") > 0 (updated via OCR) +``` + +**Implementation:** +- State cache updated in loop after each tool call +- Opportunistic OCR of resource values + +### Phase 1 Exit Criteria +- [ ] Scheduler loads real PatrickCustom.json +- [ ] Loop runs 3+ cycles against live MEmu +- [ ] State cache reflects actual game state +- [ ] Coding agent can say "run loop" and it works + +--- + +## Phase 2: Deterministic Tool Framework + First 3 Tools + +**Goal:** First-class deterministic tools. Commission cycle completes without vision. + +### Framework (TDD First) + +**P2-T1: Tool Registration Test** +```python +def test_tool_auto_discovery(): + """Tools in tools/ directory auto-register with MCP.""" + # Setup: Create test_tool.py with @deterministic_tool decorator + # Action: Start MCP server + # Assert: test_tool appears in list_tools() +``` + +**Implementation:** +- `azurlane_agent/tools/registry.py` - Auto-discovery +- `@deterministic_tool` decorator +- Each tool returns strict contract + +### First 3 Tools (Choose Based on Your Needs) + +**Option A (Recommended):** +1. `goto_main_menu()` - Navigate to safe hub +2. `collect_commissions(priority="cube > gem > oil")` - Commission collection +3. `check_resource_balances()` - OCR oil/coins/gems + +**Option B (If commissions complex):** +1. `goto_main_menu()` +2. `check_resource_balances()` +3. `collect_mail()` - Simple mail collection + +**P2-T2: Commission Tool Test** +```python +def test_collect_commissions_deterministic(): + """collect_commissions() completes without vision fallback.""" + # Setup: On main menu, commissions available + # Action: result = collect_commissions() + # Assert: result.success == True + # Assert: result.observed_state == "commissions_collected" + # Assert: cache.get("commission_running") == False +``` + +**Implementation:** +- `azurlane_agent/tools/commissions.py` +- Uses goto → detect commission button → tap → wait → collect all → return +- Element detection: OCR + template matching (not raw coordinates) + +### Phase 2 Exit Criteria +- [ ] Tool framework auto-discovers tools +- [ ] 3 tools have live TDD tests passing +- [ ] Can run: goto_main → check_resources → collect_commissions +- [ ] All observed_state == expected_state assertions pass +- [ ] If tool fails, loop automatically drops to vision mode + +--- + +## Phase 3: Manual Piloting + Blueprint System + +**Goal:** Vision/manual mode works. Successful sessions auto-generate blueprints. + +### P3-T1: Manual Piloting Entry Test +```python +def test_enter_manual_piloting(): + """enter_manual_piloting(goal) starts screenshot → VLM → raw actions.""" + # Setup: Unknown screen state + # Action: enter_manual_piloting("Collect daily rewards") + # Assert: Returns sequence of actions taken +``` + +**Implementation:** +- `azurlane_agent/piloting/manual_mode.py` +- Loop: screenshot → VLM reason → raw_tap/swipe → repeat until goal +- Coding agent (you) approves each action or takes control + +### P3-T2: Blueprint Generation Test +```python +def test_manual_pilot_generates_blueprint(): + """After manual success, blueprint is auto-saved.""" + # Setup: Manually pilot "collect dailies" + # Action: Complete piloting successfully + # Assert: Blueprint file exists in blueprints/ + # Assert: Contains: goal, sequence of {screenshot_hash, action, result} +``` + +**Implementation:** +- `azurlane_agent/blueprint/generator.py` +- Blueprint format: +```json +{ + "goal": "collect_daily_rewards", + "version": "1.0", + "steps": [ + {"screenshot_signature": "hash", "action": "tap(100,200)", "expected_result": "rewards_open"}, + ... + ], + "success_criteria": "rewards_collected" +} +``` + +### P3-T3: Blueprint to Tool Conversion Test +```python +def test_blueprint_converts_to_tool(): + """Blueprint can become deterministic tool stub with one command.""" + # Setup: Blueprint exists for "collect_dailies" + # Action: convert_blueprint_to_tool("collect_dailies") + # Assert: New file tools/collect_dailies.py created + # Assert: Tool auto-registers and appears in list_tools() +``` + +**Implementation:** +- `azurlane_agent/blueprint/converter.py` +- Generates tool scaffold from blueprint +- Developer fills in element detection logic + +### Phase 3 Exit Criteria +- [ ] Manual piloting works: screenshot → VLM → action +- [ ] 3 manual sessions generate 3 blueprint files +- [ ] Blueprint converter creates working tool stubs +- [ ] At least 1 blueprint converted to passing deterministic tool + +--- + +## Phase 4: Error Handling + Ultimate Fallback + +**Goal:** Nothing breaks the bot permanently. MEmu restart is final fallback. + +### P4-T1: Error Detection Test +```python +def test_unexpected_state_triggers_recovery(): + """Any unexpected state triggers screenshot + VLM diagnosis.""" + # Setup: Tool returns observed_state != expected_state + # Action: Loop processes result + # Assert: Recovery mode entered, VLM called +``` + +### P4-T2: Recovery Attempts Test +```python +def test_recovery_attempts_raw_actions(): + """Recovery tries raw actions before giving up.""" + # Setup: Stuck on unknown popup + # Action: enter_recovery_mode() + # Assert: VLM suggests actions, 3 attempts made +``` + +### P4-T3: MEmu Restart Test +```python +def test_memu_restart_ultimate_fallback(): + """Final fallback: restart_memu_via_admin_plugin() returns to clean main.""" + # Setup: All recovery failed + # Action: restart_memu() + # Assert: MEmu restarted, game at main menu +``` + +**Implementation:** +- `azurlane_agent/recovery/handler.py` +- Cascade: Tool fails → VLM diagnosis → raw actions → MEmu restart +- `azurlane_agent/emulator/restart.py` - memuc stop → memuc start → wait → verify + +### Phase 4 Exit Criteria +- [ ] 5 different failure modes tested, all recover or restart +- [ ] Recovery cascade tested live against MEmu +- [ ] MEmu restart returns to clean main menu +- [ ] No infinite loops (max retries enforced) + +--- + +## Phase 5: Full Autonomous Mode + Polish + +**Goal:** One command starts autonomous mode. Same loop runs standalone. + +### P5-T1: Autonomous Start Test +```python +def test_autonomous_mode_starts_with_one_command(): + """kimi --autonomous starts the same loop.""" + # Action: Start autonomous mode + # Assert: Loop runs, scheduler active, tools being called +``` + +### P5-T2: Logging Test +```python +def test_every_action_logged(): + """Every decision, tool call, and blueprint created is logged.""" + # Setup: Run loop for 10 minutes + # Assert: Log file contains all tool calls with timestamps + # Assert: Blueprint generations logged +``` + +### P5-T3: Dashboard Query Test +```python +def test_dashboard_queries_work(): + """Dashboard tools: resources, current page, queue.""" + # Action: query_resources(), query_current_page(), query_task_queue() + # Assert: All return valid data from state cache +``` + +### P5-T4: 24-Hour Stability Test +```python +def test_24_hour_stability(): + """Bot runs 24 hours without human intervention.""" + # This is the final acceptance test + # Run for 24 hours, check logs for: + # - Completed tasks + # - Recovery events + # - No crashes +``` + +### Phase 5 Exit Criteria +- [ ] `kimi --autonomous` starts standalone mode +- [ ] All actions logged with structured format +- [ ] Dashboard queries return live data +- [ ] 24-hour test passes (final gate) + +--- + +## Repository Structure Target + +``` +azurlane_agent/ # New repo or clean folder +├── pyproject.toml # Python 3.10+, no ALAS deps +├── README.md # TDD-first setup guide +├── pytest.ini # Live test markers +├── src/ +│ ├── azurlane_agent/ +│ │ ├── __init__.py +│ │ ├── mcp/ # MCP server (standalone) +│ │ │ ├── server.py # FastMCP entry +│ │ │ ├── tool_contract.py +│ │ │ └── registry.py +│ │ ├── emulator/ # MEmu control +│ │ │ ├── memuc_cli.py +│ │ │ └── restart.py +│ │ ├── adb/ # ADB primitives +│ │ │ ├── screenshot.py # DroidCast +│ │ │ └── input.py # MaaTouch socket +│ │ ├── vision/ # VLM integration +│ │ │ └── gemini_flash.py +│ │ ├── scheduler/ # Task scheduling +│ │ │ ├── loader.py # PatrickCustom.json +│ │ │ ├── decider.py +│ │ │ └── queue.py +│ │ ├── loop/ # Master loop +│ │ │ └── master_loop.py +│ │ ├── state/ # Persistent cache +│ │ │ └── cache.py +│ │ ├── tools/ # Deterministic tools +│ │ │ ├── __init__.py +│ │ │ ├── goto_main.py # Tool #1 +│ │ │ ├── check_resources.py # Tool #2 +│ │ │ └── collect_commissions.py # Tool #3 +│ │ ├── piloting/ # Manual mode +│ │ │ └── manual_mode.py +│ │ ├── blueprint/ # Blueprint system +│ │ │ ├── generator.py +│ │ │ └── converter.py +│ │ └── recovery/ # Error handling +│ │ └── handler.py +│ └── tests/ +│ ├── live/ # LIVE MEmu tests (no mocks) +│ │ ├── test_p0_*.py # Phase 0 tests +│ │ ├── test_p1_*.py # Phase 1 tests +│ │ └── conftest.py # MEmu fixture +│ └── unit/ # Unit tests (can mock) +├── blueprints/ # Generated blueprints +└── logs/ # Runtime logs +``` + +--- + +## Execution Strategy for Coding Agent + +### Immediate First Steps (Phase 0, Test 1) + +1. **Create repo structure** - Empty folders, pyproject.toml +2. **Write first failing test** - `test_memu_launch_and_connect()` +3. **Implement minimal code** - Just enough to pass +4. **Run test against live MEmu** - Must pass +5. **Commit** - "P0-T1: MEmu launch working" + +### Daily Rhythm (Strict TDD) + +``` +Morning: + 1. Pick next test from this plan + 2. Write failing test (red) + 3. Implement until passes (green) + 4. Refactor if needed + 5. Commit + +Evening: + 1. Run full test suite + 2. Fix any regressions + 3. Update plan progress +``` + +### Parallel Work (Where Safe) + +- **P0 tests:** Sequential (foundation needed first) +- **P1 scheduler:** Can start after P0-T1 (just needs MEmu running) +- **Tool implementations:** Parallel once framework exists +- **Blueprint system:** Parallel once manual mode works + +### Live Test Requirements + +- MEmu must be running +- Azur Lane must be installed +- Test account can collect resources (burner acceptable) +- Tests clean up after themselves (return to main menu) + +--- + +## Success Metrics by Phase + +| Phase | Exit Criteria | Time Estimate | +|-------|---------------|---------------| +| 0 | 20+ live tests pass | 3-5 days | +| 1 | Loop runs 3+ cycles | 2-3 days | +| 2 | 3 tools pass, commission cycle works | 5-7 days | +| 3 | 3 blueprints → 1 tool | 3-4 days | +| 4 | 5 failure modes recover | 2-3 days | +| 5 | 24-hour autonomous run | 2-3 days | + +**Total: 4-6 weeks for competent agent working full-time** + +--- + +## Final Notes + +### What to Port from Existing Code + +- **Port:** ADB patterns, memuc CLI knowledge, PatrickCustom.json parsing +- **Rewrite:** Everything with ALAS imports (`from module.*`) +- **Reference:** State machine logic (reimplement, don't import) + +### What NOT to Do + +- ❌ Import anything from `alas_wrapped/` +- ❌ Use mock-based tests for emulator behavior +- ❌ Skip tests because "it's obvious" +- ❌ Build big features before loop exists + +### What to Emphasize + +- ✅ Test first, always +- ✅ Live MEmu, no mocks +- ✅ Strict contract on every tool +- ✅ Small commits, frequent passes +- ✅ Parallel subagents where safe + +--- + +**Ready to start:** Run `pytest tests/live/test_p0_memu_launch.py -v` (will fail) → implement → pass → commit. diff --git a/adb_vision/.gitignore b/adb_vision/.gitignore new file mode 100644 index 0000000000..95ca92ce51 --- /dev/null +++ b/adb_vision/.gitignore @@ -0,0 +1,7 @@ +# Runtime artifacts +mcp_actions.jsonl +mcp_screenshots/ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/adb_vision/GEMINI_SYSTEM_PROMPT.md b/adb_vision/GEMINI_SYSTEM_PROMPT.md new file mode 100644 index 0000000000..43d3ff279e --- /dev/null +++ b/adb_vision/GEMINI_SYSTEM_PROMPT.md @@ -0,0 +1,47 @@ +You are an Azur Lane game automation agent. You control an Android emulator +running Azur Lane (EN) via MCP tools. + +## Available MCP Tools + +- **adb_screenshot(method)** — Capture the current screen. Returns a PNG image. + Use `method="screencap"` initially; if the image looks blank/black, try + `method="droidcast"` or `method="u2"`. +- **adb_tap(x, y)** — Tap a coordinate on screen (1280×720 resolution). +- **adb_swipe(x1, y1, x2, y2, duration_ms)** — Swipe gesture. +- **adb_keyevent(keycode)** — Send key event (4=BACK, 3=HOME). +- **adb_launch_game()** — Launch Azur Lane if not running. +- **adb_get_focus()** — Check which app/activity is in foreground. + +## Workflow + +1. **Always start** by taking a screenshot to see the current state. +2. **Describe** what you see on screen (menus, buttons, dialogs, text). +3. **Decide** what action to take and **explain your reasoning**. +4. **Execute** the action (tap, swipe, etc.). +5. **Take another screenshot** to verify the result. +6. **Repeat** until the goal is achieved. + +## Rules + +- The screen resolution is **1280×720** pixels. +- Always take a screenshot BEFORE and AFTER every action. +- If you see a dialog or popup, dismiss it before proceeding. +- If the game is not running, use `adb_launch_game()` first. +- If a screenshot looks blank/black (solid color), report it — the screenshot + method may need to be changed. +- Never tap blindly — always verify what's on screen first. +- Log your reasoning for every action. + +## Common Azur Lane UI Elements + +- **Main lobby**: Shows your secretary ship, bottom menu bar with buttons + (Battle, Dock, Academy, Shop, etc.) +- **Commission/expedition popups**: Dismiss by tapping outside or the X button. +- **Daily login rewards**: Tap to claim, then tap outside to dismiss. +- **Loading screens**: Wait and take another screenshot. + +## Goal + +The user will tell you what to do. Follow their instructions using the tools +above. If no specific goal is given, take a screenshot and describe what you +see. diff --git a/adb_vision/README.md b/adb_vision/README.md new file mode 100644 index 0000000000..7761d0da1d --- /dev/null +++ b/adb_vision/README.md @@ -0,0 +1,65 @@ +# adb_vision — Clean ADB + VLM Game Automation + +Zero ALAS dependency. Pure async ADB over subprocess + pluggable screenshot backends + Gemini CLI as the vision driver. + +## Quick Start + +```bash +# Run tests (no device needed) +cd adb_vision +uv run pytest test_server.py -v + +# Launch Gemini CLI with game control tools +cd adb_vision +drive.bat + +# Or with an initial prompt +drive.bat "Take a screenshot and describe what you see" + +# Headless single-shot (non-interactive) +gemini --policy adb_vision/GEMINI_SYSTEM_PROMPT.md -p "Take a screenshot and tell me what screen the game is on" +``` + +## Architecture + +``` +adb_vision/ +├── server.py — MCP server (FastMCP, stdio transport) +├── screenshot.py — Pluggable screenshot backends (dispatch + stubs) +├── test_server.py — Unit tests (all mocked, no device needed) +├── conftest.py — pytest path setup +├── drive.bat — Launch Gemini CLI with MCP tools +├── GEMINI_SYSTEM_PROMPT.md — System instructions for Gemini +└── pyproject.toml — Dependencies (fastmcp, pillow, aiofiles) +``` + +## MCP Tools + +| Tool | Description | +|------|-------------| +| `adb_screenshot(method)` | Screenshot via pluggable backend (auto/droidcast/scrcpy/u2/screencap) | +| `adb_tap(x, y)` | Tap coordinate | +| `adb_swipe(x1, y1, x2, y2, duration_ms)` | Swipe gesture | +| `adb_keyevent(keycode)` | Send key event (4=BACK, 3=HOME) | +| `adb_launch_game()` | Launch Azur Lane | +| `adb_get_focus()` | Get foreground app/activity | + +## Screenshot Backends + +The screenshot problem: **`adb shell screencap` returns blank images on MEmu/VirtualBox** because the GPU never populates the Linux framebuffer. + +Three alternative backends are being implemented (see GitHub issues #40-#42): + +1. **DroidCast** (#40) — APK that streams screen over HTTP via SurfaceControl API +2. **scrcpy** (#41) — H.264 stream decoded to single frame +3. **uiautomator2 ATX** (#42) — ATX agent HTTP API screenshot endpoint + +The `method="auto"` default tries each backend in order until one returns a valid (>5KB) image. + +## How It Works + +1. Gemini CLI connects to the MCP server via stdio +2. Gemini calls `adb_screenshot()` to see the screen +3. Gemini analyzes the image and decides what to do +4. Gemini calls `adb_tap()` / `adb_swipe()` to interact +5. Repeat — Gemini IS the bot diff --git a/adb_vision/__init__.py b/adb_vision/__init__.py new file mode 100644 index 0000000000..0df6384143 --- /dev/null +++ b/adb_vision/__init__.py @@ -0,0 +1,4 @@ +"""adb_vision — Clean ADB + VLM-driven game automation. + +No ALAS dependency. Pure async ADB over subprocess + pluggable screenshot backends. +""" diff --git a/adb_vision/conftest.py b/adb_vision/conftest.py new file mode 100644 index 0000000000..0c4ceb8071 --- /dev/null +++ b/adb_vision/conftest.py @@ -0,0 +1,5 @@ +"""pytest configuration — add adb_vision/ to sys.path so local imports work.""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) diff --git a/adb_vision/debug_droidcast.py b/adb_vision/debug_droidcast.py new file mode 100644 index 0000000000..c45ec90005 --- /dev/null +++ b/adb_vision/debug_droidcast.py @@ -0,0 +1,33 @@ +"""Debug DroidCast_raw startup.""" +import asyncio +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +from server import _adb_run, ADB_EXECUTABLE, ADB_SERIAL + + +async def main(): + print(f"Trying to start DroidCast_raw on {ADB_SERIAL}...") + + # Run directly (not backgrounded) to see output + proc = await asyncio.create_subprocess_exec( + ADB_EXECUTABLE, "-s", ADB_SERIAL, + "shell", + "CLASSPATH=/data/local/tmp/DroidCast_raw.apk", + "app_process", "/", "ink.mol.droidcast_raw.Main", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=8.0) + print(f"Exit code: {proc.returncode}") + print(f"Stdout: {stdout.decode(errors='replace')[:1000]}") + print(f"Stderr: {stderr.decode(errors='replace')[:1000]}") + except asyncio.TimeoutError: + print("Process still running after 8s — this is GOOD for a server!") + proc.kill() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/adb_vision/drive.bat b/adb_vision/drive.bat new file mode 100644 index 0000000000..121fe8c908 --- /dev/null +++ b/adb_vision/drive.bat @@ -0,0 +1,10 @@ +@echo off +REM Launch Gemini CLI with the adb-vision MCP server in interactive mode +REM Usage: drive.bat [optional initial prompt] +cd /d "%~dp0.." + +if "%~1"=="" ( + gemini --policy adb_vision/GEMINI_SYSTEM_PROMPT.md +) else ( + gemini --policy adb_vision/GEMINI_SYSTEM_PROMPT.md -i "%*" +) diff --git a/adb_vision/pyproject.toml b/adb_vision/pyproject.toml new file mode 100644 index 0000000000..5252954e6e --- /dev/null +++ b/adb_vision/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "adb-vision" +version = "0.1.0" +description = "Clean ADB + VLM-driven game automation — no ALAS dependency" +requires-python = ">=3.11" +dependencies = [ + "fastmcp>=3.0.0b1", + "pillow>=10.0.0", + "aiofiles>=25.1.0", +] + +[tool.hatch.build.targets.wheel] +only-include = ["server.py", "screenshot.py", "__init__.py"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", +] diff --git a/adb_vision/screenshot.py b/adb_vision/screenshot.py new file mode 100644 index 0000000000..d08c44b16f --- /dev/null +++ b/adb_vision/screenshot.py @@ -0,0 +1,246 @@ +"""Pluggable screenshot backends for adb-vision.""" +from __future__ import annotations + +import asyncio +import base64 +import json +import logging +import os +import shutil +import tempfile +import urllib.error +import urllib.request +from typing import Awaitable, Callable + +log = logging.getLogger(__name__) + +AdbRunFn = Callable[..., Awaitable[bytes]] + +_DROIDCAST_PORT = 53516 +_U2_PORT = 7912 +_DROIDCAST_APK_LOCAL = os.path.join( + os.path.dirname(__file__), + "..", + "alas_wrapped", + "bin", + "DroidCast", + "DroidCast_raw-release-1.0.apk", +) +_DROIDCAST_APK_REMOTE = "/data/local/tmp/DroidCast_raw.apk" +_HTTP_TIMEOUT = 2.5 + + +def _http_get_bytes(url: str, timeout: float = _HTTP_TIMEOUT) -> bytes: + request = urllib.request.Request(url) + try: + with urllib.request.urlopen(request, timeout=timeout) as resp: + if getattr(resp, "status", 200) >= 400: + raise RuntimeError(f"HTTP {resp.status} for {url}") + data = resp.read() + if not data: + raise RuntimeError(f"HTTP response had no body: {url}") + return data + except urllib.error.URLError as exc: + raise RuntimeError(f"HTTP request failed for {url}: {exc}") from exc + + +async def _http_bytes(url: str, timeout: float = _HTTP_TIMEOUT) -> bytes: + return await asyncio.to_thread(_http_get_bytes, url, timeout) + + +def _to_png_bytes(raw: bytes) -> bytes: + if raw.startswith(b"\x89PNG"): + return raw + if not raw: + raise RuntimeError("Empty screenshot payload") + if raw[:1] in (b"{", b"["): + payload = json.loads(raw.decode("utf-8", errors="replace")) + if isinstance(payload, dict): + for key in ("value", "data", "image", "screenshot"): + value = payload.get(key) + if isinstance(value, str): + decoded = base64.b64decode(value) + if decoded.startswith(b"\x89PNG"): + return decoded + raise RuntimeError("Response body is JSON but did not contain PNG bytes") + raise RuntimeError("Response payload is not PNG bytes") + + +def _is_png(data: bytes) -> bool: + return data.startswith(b"\x89PNG") + + +async def _ensure_tcp_forward(adb_run: AdbRunFn, local_port: int, remote_port: int) -> None: + local = f"tcp:{local_port}" + remote = f"tcp:{remote_port}" + try: + await adb_run("forward", "--remove", local, timeout=5.0) + except Exception: + pass + await adb_run("forward", local, remote, timeout=5.0) + + +async def _start_droidcast_server(adb_run: AdbRunFn, _serial: str, _adb_exe: str) -> None: + try: + await adb_run("shell", "pkill", "-f", "droidcast_raw", timeout=5.0) + except Exception: + pass + + if os.path.isfile(_DROIDCAST_APK_LOCAL): + await adb_run("push", _DROIDCAST_APK_LOCAL, _DROIDCAST_APK_REMOTE, timeout=20.0) + else: + log.warning("DroidCast APK not found locally: %s", _DROIDCAST_APK_LOCAL) + + command = ( + f"CLASSPATH={_DROIDCAST_APK_REMOTE} " + "app_process / ink.mol.droidcast_raw.Main >/dev/null 2>&1 &" + ) + await adb_run("shell", "nohup", "sh", "-c", command, timeout=5.0) + + +async def _start_uiautomator_agent(adb_run: AdbRunFn, _serial: str, _adb_exe: str) -> None: + commands = ( + [ + "shell", + "nohup", + "sh", + "-c", + f"/data/local/tmp/atx-agent server --nouia -d --addr 127.0.0.1:{_U2_PORT} >/dev/null 2>&1 &", + ], + [ + "shell", + "nohup", + "sh", + "-c", + f"atx-agent server --nouia -d --addr 127.0.0.1:{_U2_PORT} >/dev/null 2>&1 &", + ], + ) + for cmd in commands: + try: + await adb_run(*cmd, timeout=5.0) + return + except Exception as exc: + log.debug("atx-agent start command failed: %s", exc) + raise RuntimeError("Failed to start atx-agent/uiautomator2 HTTP service") + + +async def take_screenshot( + *, + adb_run: AdbRunFn, + serial: str, + adb_exe: str, + method: str = "auto", +) -> str: + backends = _resolve_backends(method) + last_error: Exception | None = None + for name, capture_fn in backends: + try: + log.debug("trying screenshot backend: %s", name) + b64 = await capture_fn(adb_run=adb_run, serial=serial, adb_exe=adb_exe) + if b64 and len(base64.b64decode(b64)) > 5000: + return b64 + last_error = RuntimeError(f"{name}: image too small, likely blank") + except Exception as exc: + last_error = exc + log.warning("screenshot backend %s failed: %s", name, exc) + raise RuntimeError(f"All screenshot backends failed. Last error: {last_error}") + + +def _resolve_backends(method: str): + all_backends = [ + ("droidcast", _capture_droidcast), + ("scrcpy", _capture_scrcpy), + ("u2", _capture_u2), + ("screencap", _capture_screencap), + ] + if method == "auto": + return all_backends + for name, fn in all_backends: + if name == method: + return [(name, fn)] + raise ValueError(f"Unknown screenshot method: {method}") + + +async def _capture_screencap(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str: + png_data = await adb_run("exec-out", "screencap", "-p", timeout=10.0) + return base64.b64encode(png_data).decode("ascii") + + +async def _capture_droidcast(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str: + await _ensure_tcp_forward(adb_run, _DROIDCAST_PORT, _DROIDCAST_PORT) + last_error: Exception | None = None + for attempt in range(3): + try: + raw = await _http_bytes(f"http://127.0.0.1:{_DROIDCAST_PORT}/preview") + if not _is_png(raw): + raise RuntimeError(f"DroidCast response was not a PNG ({len(raw)} bytes)") + return base64.b64encode(raw).decode("ascii") + except Exception as exc: + last_error = exc + try: + await _start_droidcast_server(adb_run, serial, adb_exe) + await asyncio.sleep(1.0) + except Exception: + if attempt == 0: + continue + break + raise RuntimeError(f"Failed to capture via DroidCast: {last_error}") + + +async def _capture_scrcpy(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str: + scrcpy_exe = shutil.which("scrcpy") + if not scrcpy_exe: + raise RuntimeError("scrcpy not found in PATH; install scrcpy v2.7+") + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + tmp_path = tmp.name + + png_data = b"" + try: + proc = await asyncio.create_subprocess_exec( + scrcpy_exe, + "--serial", + serial, + "screenshot", + tmp_path, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + _, stderr = await asyncio.wait_for(proc.communicate(), timeout=15.0) + except asyncio.TimeoutError: + proc.kill() + raise RuntimeError("scrcpy screenshot timed out after 15 seconds") + if proc.returncode != 0: + raise RuntimeError(f"scrcpy exited with code {proc.returncode}: {stderr.decode()[:300]}") + with open(tmp_path, "rb") as f: + png_data = f.read() + finally: + try: + os.unlink(tmp_path) + except FileNotFoundError: + pass + + if not png_data: + raise RuntimeError("scrcpy screenshot produced empty output") + return base64.b64encode(png_data).decode("ascii") + + +async def _capture_u2(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str: + await _ensure_tcp_forward(adb_run, _U2_PORT, _U2_PORT) + await _start_uiautomator_agent(adb_run, serial, adb_exe) + + last_error: Exception | None = None + endpoints = ("/screenshot", "/screenshot?format=png", "/") + for endpoint in endpoints: + try: + raw = await _http_bytes(f"http://127.0.0.1:{_U2_PORT}{endpoint}", 3.0) + png = _to_png_bytes(raw) + if not _is_png(png): + raise RuntimeError(f"u2 endpoint {endpoint} returned non-PNG response") + return base64.b64encode(png).decode("ascii") + except Exception as exc: + last_error = exc + log.warning("u2 endpoint failed: %s", exc) + raise RuntimeError(f"Failed to capture via uiautomator2: {last_error}") + diff --git a/adb_vision/server.py b/adb_vision/server.py new file mode 100644 index 0000000000..8ee29c5b11 --- /dev/null +++ b/adb_vision/server.py @@ -0,0 +1,310 @@ +"""Clean ADB + VLM MCP server — no ALAS dependency. + +Exposes: + adb_screenshot — pluggable backend (DroidCast / scrcpy / u2 / screencap) + adb_tap — input tap via ADB CLI + adb_swipe — input swipe via ADB CLI + adb_launch_game — am start Azur Lane + adb_get_focus — dumpsys mCurrentFocus + adb_keyevent — input keyevent via ADB CLI + +Every tool call is logged to mcp_actions.jsonl. Screenshots are saved to +mcp_screenshots/. +""" +from __future__ import annotations + +import argparse +import asyncio +import base64 +import json +import os +import re +import shutil +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, Optional + +from screenshot import take_screenshot + +# --------------------------------------------------------------------------- +# FastMCP +# --------------------------------------------------------------------------- +try: + from fastmcp import FastMCP +except ModuleNotFoundError: + raise SystemExit( + "fastmcp is not installed. Run: uv pip install fastmcp>=3.0.0b1" + ) + +mcp = FastMCP("adb-vision", version="0.1.0") + +# --------------------------------------------------------------------------- +# Action log — every MCP tool call appended as a JSONL line. +# --------------------------------------------------------------------------- +_ACTION_LOG_PATH = Path(__file__).parent / "mcp_actions.jsonl" +_SCREENSHOT_DIR = Path(__file__).parent / "mcp_screenshots" +_action_seq = 0 + + +def _action_log( + tool: str, + args: dict, + result_summary: str, + error: str = "", + duration_ms: int = 0, +): + global _action_seq + _action_seq += 1 + record = { + "seq": _action_seq, + "ts": datetime.now(timezone.utc).isoformat(), + "tool": tool, + "args": args, + "result": result_summary, + "error": error, + "duration_ms": duration_ms, + } + try: + _ACTION_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(_ACTION_LOG_PATH, "a", encoding="utf-8") as fh: + fh.write(json.dumps(record, ensure_ascii=True) + "\n") + except Exception: + pass + + +def _save_screenshot_png(data_b64: str, seq: int) -> str: + try: + _SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True) + ts = datetime.now().strftime("%Y%m%dT%H%M%S") + fname = _SCREENSHOT_DIR / f"{seq:05d}_{ts}.png" + fname.write_bytes(base64.b64decode(data_b64)) + return str(fname) + except Exception: + return "" + + +# --------------------------------------------------------------------------- +# ADB helpers +# --------------------------------------------------------------------------- +def _find_adb() -> str: + found = shutil.which("adb") + if found: + return found + candidates = [ + r"D:\Program Files\Microvirt\MEmu\adb.exe", + r"C:\Program Files\Microvirt\MEmu\adb.exe", + r"C:\Program Files (x86)\Microvirt\MEmu\adb.exe", + r"C:\Program Files\Android\platform-tools\adb.exe", + ] + for c in candidates: + if os.path.isfile(c): + return c + return "adb" + + +ADB_EXECUTABLE: str = _find_adb() +ADB_SERIAL: str = "127.0.0.1:21513" + + +async def _adb_run(*args: str, timeout: float = 10.0) -> bytes: + """Run ``adb -s `` as a non-blocking subprocess.""" + proc = await asyncio.create_subprocess_exec( + ADB_EXECUTABLE, + "-s", + ADB_SERIAL, + *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + stdout, stderr = await asyncio.wait_for( + proc.communicate(), timeout=timeout + ) + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + raise TimeoutError( + f"adb timed out after {timeout}s: adb -s {ADB_SERIAL} {' '.join(args)}" + ) + if proc.returncode != 0: + raise RuntimeError( + f"adb {' '.join(args)} failed (exit {proc.returncode}): " + f"{stderr.decode(errors='replace').strip()}" + ) + return stdout + + +# --------------------------------------------------------------------------- +# MCP Tools +# --------------------------------------------------------------------------- + +@mcp.tool() +async def adb_screenshot(method: str = "auto") -> Dict[str, Any]: + """Take a screenshot from the emulator. + + Args: + method: Screenshot backend — "droidcast", "scrcpy", "u2", "screencap", + or "auto" (tries each until one works). + + Returns image content with mimeType image/png and base64 data. + """ + t0 = time.monotonic() + png_b64 = await take_screenshot( + adb_run=_adb_run, + serial=ADB_SERIAL, + adb_exe=ADB_EXECUTABLE, + method=method, + ) + ms = int((time.monotonic() - t0) * 1000) + png_bytes = base64.b64decode(png_b64) + saved = _save_screenshot_png(png_b64, _action_seq + 1) + _action_log( + "adb_screenshot", + {"serial": ADB_SERIAL, "method": method}, + f"png_bytes={len(png_bytes)} saved={saved}", + "", + ms, + ) + return {"content": [{"type": "image", "mimeType": "image/png", "data": png_b64}]} + + +@mcp.tool() +async def adb_tap(x: int, y: int) -> str: + """Tap a coordinate on the emulator. + + Args: + x: X coordinate + y: Y coordinate + """ + t0 = time.monotonic() + await _adb_run("shell", "input", "tap", str(x), str(y), timeout=5.0) + ms = int((time.monotonic() - t0) * 1000) + _action_log("adb_tap", {"x": x, "y": y}, f"tapped {x},{y}", "", ms) + return f"tapped {x},{y}" + + +@mcp.tool() +async def adb_swipe( + x1: int, y1: int, x2: int, y2: int, duration_ms: int = 300 +) -> str: + """Swipe between coordinates on the emulator. + + Args: + x1: Start X + y1: Start Y + x2: End X + y2: End Y + duration_ms: Duration in milliseconds (default 300) + """ + t0 = time.monotonic() + await _adb_run( + "shell", + "input", + "swipe", + str(x1), + str(y1), + str(x2), + str(y2), + str(duration_ms), + timeout=5.0 + duration_ms / 1000.0, + ) + ms = int((time.monotonic() - t0) * 1000) + _action_log( + "adb_swipe", + {"x1": x1, "y1": y1, "x2": x2, "y2": y2, "duration_ms": duration_ms}, + f"swiped {x1},{y1}->{x2},{y2}", + "", + ms, + ) + return f"swiped {x1},{y1}->{x2},{y2}" + + +@mcp.tool() +async def adb_keyevent(keycode: int) -> str: + """Send a key event to the emulator. + + Args: + keycode: Android keycode (e.g. 4=BACK, 3=HOME, 82=MENU) + """ + t0 = time.monotonic() + await _adb_run("shell", "input", "keyevent", str(keycode), timeout=5.0) + ms = int((time.monotonic() - t0) * 1000) + _action_log("adb_keyevent", {"keycode": keycode}, f"keyevent {keycode}", "", ms) + return f"keyevent {keycode}" + + +@mcp.tool() +async def adb_launch_game() -> str: + """Launch Azur Lane (EN) in the foreground.""" + t0 = time.monotonic() + await _adb_run( + "shell", + "am", + "start", + "-a", + "android.intent.action.MAIN", + "-c", + "android.intent.category.LAUNCHER", + "-n", + "com.YoStarEN.AzurLane/com.manjuu.azurlane.PrePermissionActivity", + timeout=10.0, + ) + ms = int((time.monotonic() - t0) * 1000) + _action_log("adb_launch_game", {"serial": ADB_SERIAL}, "launch intent sent", "", ms) + return "Azur Lane launch intent sent" + + +@mcp.tool() +async def adb_get_focus() -> Dict[str, Any]: + """Return the currently focused Android window/package/activity. + + Returns: + {"raw": "", "package": "...", "activity": "..."} + """ + t0 = time.monotonic() + stdout = await _adb_run("shell", "dumpsys", "window", "windows", timeout=8.0) + ms = int((time.monotonic() - t0) * 1000) + raw_text = stdout.decode(errors="replace") + focus_line = "" + for line in raw_text.splitlines(): + if "mCurrentFocus" in line: + focus_line = line.strip() + break + + package: Optional[str] = None + activity: Optional[str] = None + m = re.search(r"(\S+)/(\S+)\}", focus_line) + if m: + package = m.group(1) + activity = m.group(2) + + result = {"raw": focus_line, "package": package, "activity": activity} + _action_log("adb_get_focus", {"serial": ADB_SERIAL}, f"{package}/{activity}", "", ms) + return result + + +# --------------------------------------------------------------------------- +# Entrypoint +# --------------------------------------------------------------------------- +def main(): + global ADB_SERIAL + parser = argparse.ArgumentParser(description="adb-vision MCP server") + parser.add_argument( + "--serial", + default=os.environ.get("ADB_SERIAL", "127.0.0.1:21513"), + help="ADB device serial (default: $ADB_SERIAL or 127.0.0.1:21513)", + ) + parser.add_argument( + "--screenshot-method", + default="auto", + choices=["auto", "droidcast", "scrcpy", "u2", "screencap"], + help="Default screenshot method", + ) + args = parser.parse_args() + ADB_SERIAL = args.serial + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/adb_vision/setup_droidcast.py b/adb_vision/setup_droidcast.py new file mode 100644 index 0000000000..988b5c35b1 --- /dev/null +++ b/adb_vision/setup_droidcast.py @@ -0,0 +1,116 @@ +"""Manual DroidCast setup and test script. + +Run: cd adb_vision && uv run python setup_droidcast.py +""" +import asyncio +import os +import sys +import urllib.request +import urllib.error + +sys.path.insert(0, os.path.dirname(__file__)) +from server import _adb_run, ADB_EXECUTABLE, ADB_SERIAL + +DROIDCAST_APK_LOCAL = os.path.join( + os.path.dirname(__file__), "..", "alas_wrapped", "bin", "DroidCast", + "DroidCast_raw-release-1.0.apk" +) +DROIDCAST_APK_REMOTE = "/data/local/tmp/DroidCast_raw.apk" +DROIDCAST_PORT = 53516 + + +async def setup_and_test(): + print(f"ADB: {ADB_EXECUTABLE}") + print(f"Serial: {ADB_SERIAL}") + print(f"APK: {DROIDCAST_APK_LOCAL} (exists={os.path.isfile(DROIDCAST_APK_LOCAL)})") + + # 1. Check emulator + try: + state = await _adb_run("get-state", timeout=5.0) + print(f"1. Emulator state: {state.decode().strip()}") + except Exception as e: + print(f"1. FAIL: Cannot reach emulator: {e}") + return + + # 2. Push APK + print("2. Pushing DroidCast APK...") + try: + proc = await asyncio.create_subprocess_exec( + ADB_EXECUTABLE, "-s", ADB_SERIAL, + "push", DROIDCAST_APK_LOCAL, DROIDCAST_APK_REMOTE, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15.0) + print(f" Push result: {stdout.decode().strip()} {stderr.decode().strip()}") + except Exception as e: + print(f" Push FAILED: {e}") + return + + # 3. Kill existing DroidCast processes + print("3. Killing old DroidCast processes...") + try: + await _adb_run("shell", "pkill", "-f", "droidcast_raw", timeout=5.0) + print(" Killed existing process") + except Exception: + print(" No existing process (or pkill not available)") + + await asyncio.sleep(1) + + # 4. Start DroidCast_raw in background + print("4. Starting DroidCast_raw server on device...") + try: + proc = await asyncio.create_subprocess_exec( + ADB_EXECUTABLE, "-s", ADB_SERIAL, + "shell", + "nohup", "sh", "-c", + f"CLASSPATH={DROIDCAST_APK_REMOTE} app_process / ink.mol.droidcast_raw.Main", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + ) + # Don't wait for completion — it's a background server + # Give it a moment to start + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=3.0) + print(f" Process exited (unexpected): stdout={stdout[:200]}, stderr={stderr[:200]}") + except asyncio.TimeoutError: + print(" DroidCast_raw process is running (good - it didn't exit)") + except Exception as e: + print(f" Start FAILED: {e}") + return + + await asyncio.sleep(2) + + # 5. Forward port + print(f"5. Forwarding port tcp:{DROIDCAST_PORT}...") + try: + await _adb_run("forward", f"tcp:{DROIDCAST_PORT}", f"tcp:{DROIDCAST_PORT}", timeout=5.0) + print(f" Port {DROIDCAST_PORT} forwarded") + except Exception as e: + print(f" Forward FAILED: {e}") + return + + # 6. Test connectivity + print("6. Testing DroidCast HTTP endpoint...") + for endpoint in ["/", "/preview", "/screenshot"]: + url = f"http://127.0.0.1:{DROIDCAST_PORT}{endpoint}" + try: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=5) as resp: + data = resp.read() + print(f" GET {endpoint}: status=200, {len(data)} bytes, content-type={resp.headers.get('Content-Type', 'unknown')}") + if endpoint == "/preview" and len(data) > 5000: + # Save it! + out_path = os.path.join(os.path.dirname(__file__), "test_droidcast_preview.png") + with open(out_path, "wb") as f: + f.write(data) + print(f" SAVED real screenshot to: {out_path} ({len(data)} bytes)") + print(f" PNG header check: {data[:4] == b'\\x89PNG'}") + except urllib.error.HTTPError as e: + print(f" GET {endpoint}: HTTP {e.code} (expected for / endpoint)") + except Exception as e: + print(f" GET {endpoint}: FAILED: {e}") + + print("\nDone!") + + +if __name__ == "__main__": + asyncio.run(setup_and_test()) diff --git a/adb_vision/test_live.py b/adb_vision/test_live.py new file mode 100644 index 0000000000..d447958447 --- /dev/null +++ b/adb_vision/test_live.py @@ -0,0 +1,216 @@ +"""Live integration tests for adb_vision against a real emulator. + +Prerequisites: + - MEmu emulator running with ADB at 127.0.0.1:21513 + - Azur Lane installed (not necessarily running) + +Run: + cd adb_vision && uv run pytest test_live.py -v -s +""" +from __future__ import annotations + +import asyncio +import base64 +import os +import sys + +import pytest + +# Skip entire module if no live emulator is available +pytestmark = pytest.mark.skipif( + os.environ.get("SKIP_LIVE_TESTS", "0") == "1", + reason="SKIP_LIVE_TESTS=1", +) + +# Add parent dir for imports +sys.path.insert(0, os.path.dirname(__file__)) + +import server # noqa: E402 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +async def _check_emulator_reachable() -> bool: + """Return True if we can talk to the emulator via ADB.""" + try: + await server._adb_run("get-state", timeout=5.0) + return True + except Exception: + return False + + +@pytest.fixture(autouse=True) +def skip_if_no_emulator(): + """Skip tests if the emulator is not reachable.""" + reachable = asyncio.run(_check_emulator_reachable()) + if not reachable: + pytest.skip("Emulator not reachable at " + server.ADB_SERIAL) + + +# --------------------------------------------------------------------------- +# Tests — ADB control tools (no screenshot) +# --------------------------------------------------------------------------- + +class TestAdbControl: + """Tests that verify basic ADB control against the live emulator.""" + + @pytest.mark.asyncio + async def test_get_focus_returns_valid_data(self): + """adb_get_focus should return structured focus data.""" + result = await server.adb_get_focus() + assert "raw" in result + assert "package" in result + assert "activity" in result + # Should have SOME focused window + assert result["raw"], "mCurrentFocus line was empty" + print(f" Focus: {result['package']}/{result['activity']}") + + @pytest.mark.asyncio + async def test_get_focus_game_running(self): + """If Azur Lane is running, we should see it.""" + result = await server.adb_get_focus() + if result["package"] == "com.YoStarEN.AzurLane": + print(" Game is in foreground") + else: + print(f" Game NOT in foreground — got: {result['package']}") + # Not a failure — game might not be running + + @pytest.mark.asyncio + async def test_adb_tap_executes(self): + """adb_tap should complete without error. + + Taps a safe coordinate (center of screen) that won't break anything. + """ + result = await server.adb_tap(640, 360) + assert "640,360" in result + + @pytest.mark.asyncio + async def test_adb_keyevent_back(self): + """adb_keyevent BACK (4) should execute without error.""" + result = await server.adb_keyevent(4) + assert "keyevent 4" in result + + @pytest.mark.asyncio + async def test_adb_swipe_executes(self): + """adb_swipe should complete without error.""" + result = await server.adb_swipe(640, 500, 640, 200, duration_ms=200) + assert "swiped" in result + + @pytest.mark.asyncio + async def test_adb_launch_game(self): + """Launch game should send intent without error.""" + result = await server.adb_launch_game() + assert "launch intent sent" in result + # Give it a moment to process + await asyncio.sleep(2) + focus = await server.adb_get_focus() + print(f" After launch: {focus['package']}/{focus['activity']}") + + +# --------------------------------------------------------------------------- +# Tests — Screenshot +# --------------------------------------------------------------------------- + +class TestScreenshot: + """Tests for screenshot capture from the live emulator.""" + + @pytest.mark.asyncio + async def test_screencap_returns_data(self): + """screencap method returns SOME data (even if potentially blank on MEmu).""" + from screenshot import _capture_screencap + b64 = await _capture_screencap( + adb_run=server._adb_run, serial=server.ADB_SERIAL, adb_exe=server.ADB_EXECUTABLE + ) + raw = base64.b64decode(b64) + print(f" screencap: {len(raw)} bytes") + # On MEmu this may be ~3669 bytes (blank). We just verify it runs. + assert len(raw) > 0 + assert raw[:4] == b"\x89PNG" + + @pytest.mark.asyncio + async def test_screencap_likely_blank_on_memu(self): + """Document: screencap returns tiny blank image on MEmu.""" + from screenshot import _capture_screencap + b64 = await _capture_screencap( + adb_run=server._adb_run, serial=server.ADB_SERIAL, adb_exe=server.ADB_EXECUTABLE + ) + raw = base64.b64decode(b64) + if len(raw) < 5000: + print(f" CONFIRMED: screencap is blank ({len(raw)} bytes) — MEmu/VirtualBox issue") + else: + print(f" screencap returned real image ({len(raw)} bytes)") + + @pytest.mark.asyncio + async def test_droidcast_returns_real_image(self): + """DroidCast backend should return a real non-blank screenshot.""" + from screenshot import _capture_droidcast + try: + b64 = await _capture_droidcast( + adb_run=server._adb_run, serial=server.ADB_SERIAL, adb_exe=server.ADB_EXECUTABLE + ) + raw = base64.b64decode(b64) + print(f" DroidCast: {len(raw)} bytes") + assert len(raw) > 5000, f"DroidCast image too small ({len(raw)} bytes)" + assert raw[:4] == b"\x89PNG" + except RuntimeError as exc: + # Service may not be set up in local environments. + pytest.skip(f"DroidCast backend currently unavailable: {exc}") + except NotImplementedError: + pytest.skip("DroidCast backend not yet implemented") + + @pytest.mark.asyncio + async def test_adb_screenshot_tool_auto(self): + """The adb_screenshot MCP tool with auto method should return an image.""" + try: + result = await server.adb_screenshot(method="auto") + content = result["content"][0] + assert content["type"] == "image" + assert content["mimeType"] == "image/png" + raw = base64.b64decode(content["data"]) + print(f" adb_screenshot(auto): {len(raw)} bytes") + assert len(raw) > 5000, f"Screenshot too small ({len(raw)} bytes)" + except RuntimeError as e: + if "All screenshot backends failed" in str(e): + pytest.skip(f"No working screenshot backend: {e}") + raise + + +# --------------------------------------------------------------------------- +# Tests — End-to-end flow +# --------------------------------------------------------------------------- + +class TestEndToEnd: + """End-to-end: screenshot → tap → screenshot → verify change.""" + + @pytest.mark.asyncio + async def test_tap_changes_state(self): + """Tap should produce a visible change in screenshots.""" + # This test requires a working screenshot backend + try: + shot1 = await server.adb_screenshot(method="auto") + except RuntimeError: + pytest.skip("No working screenshot backend") + + data1 = base64.b64decode(shot1["content"][0]["data"]) + print(f" Before tap: {len(data1)} bytes") + + # Tap center of screen + await server.adb_tap(640, 360) + await asyncio.sleep(1) + + try: + shot2 = await server.adb_screenshot(method="auto") + except RuntimeError: + pytest.skip("Screenshot failed after tap") + + data2 = base64.b64decode(shot2["content"][0]["data"]) + print(f" After tap: {len(data2)} bytes") + + # The images should be DIFFERENT (tap should have done something) + # Note: they could be the same if the tap hit a non-interactive area + if data1 == data2: + print(" WARNING: Images identical — tap may not have changed state") + else: + print(" SUCCESS: Images differ — tap changed the screen") diff --git a/adb_vision/test_server.py b/adb_vision/test_server.py new file mode 100644 index 0000000000..f18037ef17 --- /dev/null +++ b/adb_vision/test_server.py @@ -0,0 +1,415 @@ +"""Unit tests for adb-vision MCP server — no device needed, fully mocked.""" +from __future__ import annotations + +import base64 +from unittest import mock + +import pytest + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _fake_png(width: int = 1280, height: int = 720) -> bytes: + """Generate a valid PNG with random noise that is > 5KB.""" + from PIL import Image + import io + import random + + random.seed(42) + # Random pixel data won't compress as small as solid color + pixels = bytes(random.randint(0, 255) for _ in range(width * height * 3)) + img = Image.frombytes("RGB", (width, height), pixels) + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + +FAKE_PNG_B64 = base64.b64encode(_fake_png()).decode("ascii") +FAKE_PNG_BYTES = base64.b64decode(FAKE_PNG_B64) + +# --------------------------------------------------------------------------- +# Mock adb_run +# --------------------------------------------------------------------------- + +async def _mock_adb_run(*args, timeout=10.0) -> bytes: + """Default mock that returns empty bytes.""" + return b"" + + +# --------------------------------------------------------------------------- +# Tests — screenshot dispatch +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_take_screenshot_screencap_backend(): + """screencap backend returns base64 PNG when the image is large enough.""" + from screenshot import take_screenshot + + async def mock_run(*args, timeout=10.0): + return _fake_png() # raw PNG bytes + + result = await take_screenshot( + adb_run=mock_run, serial="test", adb_exe="adb", method="screencap" + ) + decoded = base64.b64decode(result) + assert len(decoded) > 5000 + assert decoded[:4] == b"\x89PNG" + + +@pytest.mark.asyncio +async def test_take_screenshot_auto_falls_through(): + """auto mode follows explicit order: droidcast -> scrcpy -> u2 -> screencap.""" + from screenshot import take_screenshot + + calls = [] + + async def _fail(*args, **kwargs): + return b"" + + async def fake_droidcast(*, adb_run, serial, adb_exe): + calls.append("droidcast") + raise RuntimeError("droidcast unavailable") + + async def fake_u2(*, adb_run, serial, adb_exe): + calls.append("u2") + return FAKE_PNG_B64 + + async def fake_scrcpy(*, adb_run, serial, adb_exe): + calls.append("scrcpy") + raise RuntimeError("scrcpy unavailable") + + async def fake_screencap(*, adb_run, serial, adb_exe): + calls.append("screencap") + return FAKE_PNG_B64 + + with mock.patch("screenshot._capture_droidcast", side_effect=fake_droidcast), mock.patch( + "screenshot._capture_scrcpy", side_effect=fake_scrcpy + ), mock.patch( + "screenshot._capture_u2", side_effect=fake_u2 + ), mock.patch("screenshot._capture_screencap", side_effect=fake_screencap): + result = await take_screenshot( + adb_run=_fail, serial="test", adb_exe="adb", method="auto" + ) + decoded = base64.b64decode(result) + assert len(decoded) > 5000 + assert calls == ["droidcast", "scrcpy", "u2"] + + +@pytest.mark.asyncio +async def test_take_screenshot_scrcpy_selector(): + """scrcpy selector calls the scrcpy backend directly.""" + from screenshot import take_screenshot + + calls = [] + + async def _fail(*args, **kwargs): + return b"" + + async def fake_scrcpy(*, adb_run, serial, adb_exe): + calls.append("scrcpy") + return FAKE_PNG_B64 + + with mock.patch("screenshot._capture_scrcpy", side_effect=fake_scrcpy), mock.patch( + "screenshot._capture_u2" + ) as mock_u2, mock.patch( + "screenshot._capture_screencap" + ) as mock_sc: + result = await take_screenshot( + adb_run=_fail, serial="test", adb_exe="adb", method="scrcpy" + ) + mock_u2.assert_not_called() + mock_sc.assert_not_called() + decoded = base64.b64decode(result) + assert len(decoded) > 5000 + assert calls == ["scrcpy"] + + +@pytest.mark.asyncio +async def test_take_screenshot_scrcpy_fallback_chain(): + """With auto, scrcpy failure falls through to u2 then screencap.""" + from screenshot import take_screenshot + + calls = [] + + async def _fail(*args, **kwargs): + return b"" + + async def fake_droidcast(*, adb_run, serial, adb_exe): + calls.append("droidcast") + raise RuntimeError("droidcast unavailable") + + async def fake_scrcpy(*, adb_run, serial, adb_exe): + calls.append("scrcpy") + raise RuntimeError("scrcpy unavailable") + + async def fake_u2(*, adb_run, serial, adb_exe): + calls.append("u2") + raise RuntimeError("u2 unavailable") + + async def fake_screencap(*, adb_run, serial, adb_exe): + calls.append("screencap") + return FAKE_PNG_B64 + + with mock.patch("screenshot._capture_droidcast", side_effect=fake_droidcast), mock.patch( + "screenshot._capture_scrcpy", side_effect=fake_scrcpy + ), mock.patch("screenshot._capture_u2", side_effect=fake_u2), mock.patch( + "screenshot._capture_screencap", side_effect=fake_screencap + ): + result = await take_screenshot( + adb_run=_fail, serial="test", adb_exe="adb", method="auto" + ) + decoded = base64.b64decode(result) + assert len(decoded) > 5000 + assert calls == ["droidcast", "scrcpy", "u2", "screencap"] + + +@pytest.mark.asyncio +async def test_take_screenshot_blank_rejected(): + """A suspiciously small image (like MEmu blank screencap) is rejected.""" + from screenshot import take_screenshot + + # 3669-byte blank PNG (smaller than threshold) + tiny_png = b"\x89PNG" + b"\x00" * 3665 + + async def mock_run(*args, timeout=10.0): + return tiny_png + + with pytest.raises(RuntimeError, match="All screenshot backends failed"): + await take_screenshot( + adb_run=mock_run, serial="test", adb_exe="adb", method="screencap" + ) + + +@pytest.mark.asyncio +async def test_take_screenshot_unknown_method(): + from screenshot import take_screenshot + + with pytest.raises(ValueError, match="Unknown screenshot method"): + await take_screenshot( + adb_run=_mock_adb_run, serial="test", adb_exe="adb", method="bogus" + ) + + +# --------------------------------------------------------------------------- +# Tests — DroidCast backend +# --------------------------------------------------------------------------- + +def _make_urlopen_mock(data: bytes, status: int = 200): + """Return a context-manager mock for urllib.request.urlopen.""" + cm = mock.MagicMock() + cm.__enter__ = mock.Mock(return_value=cm) + cm.__exit__ = mock.Mock(return_value=False) + cm.read = mock.Mock(return_value=data) + cm.status = status + return cm + + +@pytest.mark.asyncio +async def test_droidcast_backend_success(): + """DroidCast backend returns base64 PNG when the HTTP server responds.""" + import screenshot as sc + + cm = _make_urlopen_mock(FAKE_PNG_BYTES) + with mock.patch("screenshot.urllib.request.urlopen", return_value=cm): + result = await sc._capture_droidcast(adb_run=_mock_adb_run, serial="test", adb_exe="adb") + + raw = base64.b64decode(result) + assert raw[:4] == b"\x89PNG" + assert len(raw) > 5000 + + +@pytest.mark.asyncio +async def test_droidcast_backend_not_running(): + """DroidCast backend raises RuntimeError when the HTTP server is not running.""" + import urllib.error + import screenshot as sc + + with mock.patch( + "screenshot.urllib.request.urlopen", + side_effect=urllib.error.URLError("Connection refused"), + ): + with pytest.raises(RuntimeError, match="Failed to capture via DroidCast"): + await sc._capture_droidcast(adb_run=_mock_adb_run, serial="test", adb_exe="adb") + + +@pytest.mark.asyncio +async def test_droidcast_backend_bad_header(): + """DroidCast backend raises RuntimeError when response is not PNG.""" + import screenshot as sc + + cm = _make_urlopen_mock(b"JFIF" + b"\x00" * 10000) + with mock.patch("screenshot.urllib.request.urlopen", return_value=cm): + with pytest.raises(RuntimeError, match="Failed to capture via DroidCast"): + await sc._capture_droidcast(adb_run=_mock_adb_run, serial="test", adb_exe="adb") + + +# --------------------------------------------------------------------------- +# Tests — u2 backend +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_u2_backend_success(): + """u2 backend returns base64 PNG when the ATX agent responds.""" + import screenshot as sc + + forward_calls = [] + + async def mock_adb_run(*args, timeout=10.0): + forward_calls.append(args) + return b"" + + cm = _make_urlopen_mock(FAKE_PNG_BYTES) + with mock.patch("screenshot.urllib.request.urlopen", return_value=cm): + result = await sc._capture_u2(adb_run=mock_adb_run, serial="test", adb_exe="adb") + + raw = base64.b64decode(result) + assert len(raw) > 5000 + # Verify port forward was requested + assert any("forward" in str(a) for a in forward_calls[0]) + + +@pytest.mark.asyncio +async def test_u2_backend_not_running(): + """u2 backend raises RuntimeError when the ATX agent is not reachable.""" + import urllib.error + import screenshot as sc + + with mock.patch( + "screenshot.urllib.request.urlopen", + side_effect=urllib.error.URLError("Connection refused"), + ): + with pytest.raises(RuntimeError, match="Failed to capture via uiautomator2"): + await sc._capture_u2(adb_run=_mock_adb_run, serial="test", adb_exe="adb") + + +# --------------------------------------------------------------------------- +# Tests — scrcpy backend +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_scrcpy_backend_not_in_path(): + """scrcpy backend raises RuntimeError when scrcpy is not in PATH.""" + import screenshot as sc + + with mock.patch("screenshot.shutil.which", return_value=None): + with pytest.raises(RuntimeError, match="scrcpy not found in PATH"): + await sc._capture_scrcpy(adb_run=_mock_adb_run, serial="test", adb_exe="adb") + + +@pytest.mark.asyncio +async def test_scrcpy_backend_success(): + """scrcpy backend returns base64 PNG when scrcpy succeeds.""" + import screenshot as sc + + with mock.patch("screenshot.shutil.which", return_value="C:/tools/scrcpy.exe"): + with mock.patch("screenshot.tempfile.NamedTemporaryFile") as mock_tmp: + tmp_file = mock.MagicMock() + tmp_file.__enter__ = mock.Mock(return_value=tmp_file) + tmp_file.__exit__ = mock.Mock(return_value=False) + tmp_file.name = "C:/tmp/fake_screen.png" + mock_tmp.return_value = tmp_file + + # Mock subprocess to exit 0 and write fake PNG to tmp file + mock_proc = mock.AsyncMock() + mock_proc.communicate = mock.AsyncMock(return_value=(b"", b"")) + mock_proc.returncode = 0 + + with mock.patch( + "screenshot.asyncio.create_subprocess_exec", + return_value=mock_proc, + ): + with mock.patch("builtins.open", mock.mock_open(read_data=FAKE_PNG_BYTES)): + with mock.patch("screenshot.os.unlink"): + result = await sc._capture_scrcpy( + adb_run=_mock_adb_run, serial="test", adb_exe="adb" + ) + + raw = base64.b64decode(result) + assert len(raw) > 5000 + + +# --------------------------------------------------------------------------- +# Tests — MCP tools (tap, swipe, keyevent, launch, focus) +# --------------------------------------------------------------------------- + +@pytest.fixture +def patch_adb_run(): + """Patch server._adb_run with a mock that records calls.""" + import server + + calls = [] + + async def mock_run(*args, timeout=10.0): + calls.append(args) + return b"" + + with mock.patch.object(server, "_adb_run", side_effect=mock_run): + yield calls + + +@pytest.mark.asyncio +async def test_adb_tap(patch_adb_run): + import server + + result = await server.adb_tap(100, 200) + assert "100,200" in result + assert ("shell", "input", "tap", "100", "200") in patch_adb_run + + +@pytest.mark.asyncio +async def test_adb_swipe(patch_adb_run): + import server + + result = await server.adb_swipe(10, 20, 300, 400, duration_ms=500) + assert "10,20->300,400" in result + assert ("shell", "input", "swipe", "10", "20", "300", "400", "500") in patch_adb_run + + +@pytest.mark.asyncio +async def test_adb_keyevent(patch_adb_run): + import server + + result = await server.adb_keyevent(4) + assert "keyevent 4" in result + assert ("shell", "input", "keyevent", "4") in patch_adb_run + + +@pytest.mark.asyncio +async def test_adb_launch_game(patch_adb_run): + import server + + result = await server.adb_launch_game() + assert "launch intent sent" in result + assert any("am" in a for a in patch_adb_run[0]) + + +@pytest.mark.asyncio +async def test_adb_get_focus(patch_adb_run): + import server + + # Override to return a realistic dumpsys line + async def focus_run(*args, timeout=10.0): + return b" mCurrentFocus=Window{abc u0 com.YoStarEN.AzurLane/com.manjuu.azurlane.MainActivity}\n" + + with mock.patch.object(server, "_adb_run", side_effect=focus_run): + result = await server.adb_get_focus() + assert result["package"] == "com.YoStarEN.AzurLane" + assert result["activity"] == "com.manjuu.azurlane.MainActivity" + + +@pytest.mark.asyncio +async def test_adb_screenshot_tool(): + """The adb_screenshot MCP tool returns image content.""" + import server + + async def mock_take_screenshot(**kwargs): + return FAKE_PNG_B64 + + with mock.patch("server.take_screenshot", side_effect=mock_take_screenshot): + result = await server.adb_screenshot() + + assert result["content"][0]["type"] == "image" + assert result["content"][0]["mimeType"] == "image/png" + data = result["content"][0]["data"] + assert len(base64.b64decode(data)) > 5000 diff --git a/alas_wrapped/.gitignore b/alas_wrapped/.gitignore index 6ffc6a717e..73ce58daea 100644 --- a/alas_wrapped/.gitignore +++ b/alas_wrapped/.gitignore @@ -8,7 +8,6 @@ config/*.yaml config/*.json config/tmp* !config/template*.json -!config/PatrickCustom.json *.pyw dev_tools/debug_tools .idea diff --git a/alas_wrapped/README.md b/alas_wrapped/README.md index 459e08f229..976a3f83e9 100644 --- a/alas_wrapped/README.md +++ b/alas_wrapped/README.md @@ -1,11 +1,180 @@ -相比于源库增加了仪表盘 +**| [English](README_en.md) | 简体中文 | [日本語](README_jp.md) |** -提供了一个较为实用的仪表盘,感谢@MengNianxiaoyao 作出的美观调整 +# AzurLaneAutoScript -![image](https://github.com/Zuosizhu/Alas-with-Dashboard/assets/60862861/ee2e3e8f-9a19-417e-8e5f-441ecdee1ae6) +#### Discord [![](https://img.shields.io/discord/720789890354249748?logo=discord&logoColor=ffffff&color=4e4c97)](https://discord.gg/AQN6GeJ) QQ群 ![](https://img.shields.io/badge/QQ%20Group-1087735381-4e4c97) +Azur Lane bot with GUI (Supports CN, EN, JP, TW, able to support other servers), designed for 24/7 running scenes, can take over almost all Azur Lane gameplay. Azur Lane, as a mobile game, has entered the late stage of its life cycle. During the period from now to the server down, please reduce the time spent on the Azur Lane and leave everything to Alas. -![image](https://github.com/Zuosizhu/Alas-with-Dashboard/assets/60862861/55f95cb3-5234-45d3-a265-6b5e0ab5fc3e) +Alas is a free open source software, link: https://github.com/LmeSzinc/AzurLaneAutoScript -![image](https://github.com/Zuosizhu/Alas-with-Dashboard/assets/60862861/6033931a-c4ea-4262-853f-f315f076d305) +Alas,一个带GUI的碧蓝航线脚本(支持国服, 国际服, 日服, 台服, 可以支持其他服务器),为 7x24 运行的场景而设计,能接管近乎全部的碧蓝航线玩法。碧蓝航线,作为一个手游,已经进入了生命周期的晚期。从现在到关服的这段时间里,请减少花费在碧蓝航线上的时间,把一切都交给 Alas。 + +Alas 是一款免费开源软件,地址:https://github.com/LmeSzinc/AzurLaneAutoScript + +EN support, thanks **[@whoamikyo](https://github.com/whoamikyo)** and **[@nEEtdo0d](https://github.com/nEEtdo0d)**. + +JP support, thanks **[@ferina8-14](https://github.com/ferina8-14)**, **[@noname94](https://github.com/noname94)** and **[@railzy](https://github.com/railzy)**. + +TW support, thanks **[@Zorachristine](https://github.com/Zorachristine)** , some features might not work. + +GUI development, thanks **[@18870](https://github.com/18870)** , say HURRAY. + +![](https://img.shields.io/github/commit-activity/m/LmeSzinc/AzurLaneAutoScript?color=4e4c97) ![](https://img.shields.io/tokei/lines/github/LmeSzinc/AzurLaneAutoScript?color=4e4c97) ![](https://img.shields.io/github/repo-size/LmeSzinc/AzurLaneAutoScript?color=4e4c97) ![](https://img.shields.io/github/issues-closed/LmeSzinc/AzurLaneAutoScript?color=4e4c97) ![](https://img.shields.io/github/issues-pr-closed/LmeSzinc/AzurLaneAutoScript?color=4e4c97) + +这里是一张GUI预览图: +![gui](https://raw.githubusercontent.com/LmeSzinc/AzurLaneAutoScript/master/doc/README.assets/gui.png) + + + +## 功能 Features + +- **出击**:主线图,活动图,共斗活动,紧急委托刷钻石。 +- **收获**:委托,战术学院,科研,后宅,指挥喵,大舰队,收获,商店购买,开发船坞,每日抽卡,档案密钥。 +- **每日**:每日任务,困难图,演习,潜艇图,活动每日AB图,活动每日SP图,共斗活动每日,作战档案。 +- **大世界**:余烬信标,每月开荒,大世界每日,隐秘海域,短猫相接,深渊海域,塞壬要塞。 + +#### 突出特性: + +- **心情控制**:计算心情防止红脸或者保持经验加成状态。 +- **活动图开荒**:支持在非周回模式下运行,能处理移动距离限制,光之壁,岸防炮,地图解谜,地图迷宫等特殊机制。 +- **无缝收菜**:时间管理大师,计算委托科研等的完成时间,完成后立即收获。 +- **大世界**:一条龙完成,接大世界每日,买空港口商店,做大世界每日,短猫相接,购买明石商店,每27分钟清理隐秘海域,清理深渊海域和塞壬要塞,~~计划作战模式是什么垃圾,感觉不如Alas......好用~~。 +- **大世界月初开荒**:大世界每月重置后,不需要购买作战记录仪(5000油道具)即可开荒。 + + + +## 安装 Installation [![](https://img.shields.io/github/downloads/LmeSzinc/AzurLaneAutoScript/total?color=4e4c97)](https://github.com/LmeSzinc/AzurLaneAutoScript/releases) + +[中文安装教程](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/Installation_cn),包含自动安装教程,使用教程,手动安装教程,远程控制教程。 + +[设备支持文档](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/Emulator_cn),包含模拟器运行、云手机运行以及解锁各种骚方式运行。 + + + +## 正确地使用调度器 + +- **理解 *任务* 和 *调度器* 的概念** + + 在 Alas 中每个任务都是独立运行的,被一个统一的调度器调度,任务执行完成后会自动设置这个任务的下一次运行时间。例如,*科研* 任务执行了一个 4 小时的科研,调度器就会把 *科研* 任务推迟 4 小时,以达到无缝收菜的目的。 + +- **理解 *自动心情控制* 机制** + + Alas 的心情控制以预防为主,不会等到出现红脸弹窗才去解决,这样可以保持心情值在 120 以上,贪到 20% 的经验。例如,当前心情值是 113,放置于后宅二楼(+50/h),未婚(+0/h),Alas 会等到 12 分钟之后,心情值回复到 120 以上再继续出击。而在这个等待的期间,Alas 也会穿插执行其他任务。 + +- **正确地使用调度器** + + 调度器的 **错误使用方法是只开一两个** 任务,手动管理任务或开关 Alas,调度器的 **正确使用方法是启用全部** 你觉得可能有用的任务,让调度器自动调度,把模拟器和 Alas 都最小化到托盘,忘记碧蓝航线这个游戏。 + + + +## 修改游戏设置 + +对照这个表格修改游戏内的设置,~~正常玩过游戏的都这么设置~~。 + +> 对着改的意思是,这是统一的标准,照着给定的内容执行,不要问为什么,不允许有不一样的。 + +主界面 => 右下角:设置 => 左侧边栏:选项 + +| 设置名称 | 值 | +| ----------------------------------- | ---- | +| 帧数设置 | 60帧 | +| 大型作战设置 - 减少TB引导 | 开 | +| 大型作战设置 - 自律时自动提交道具 | 开 | +| 大型作战设置 - 安全海域默认开启自律 | 关 | +| 剧情自动播放 | 开启 | +| 剧情自动播放速度调整 | 特快 | +| 待机模式设置 - 启用待机模式 | 关 | +| 其他设置 - 重复角色获得提示 | 关 | +| 其他设置 - 快速更换二次确认界面 | 关 | +| 其他设置 - 展示结算角色 | 关 | + +大世界 => 右上角:雷达 => 指令模块(order):潜艇支援: +| 设置名称 | 值 | +| -------------------------------------------------------- | ---------------- | +| X 消耗时潜艇出击 |取消勾选| + +主界面 => 右下角:建造 => 左侧边栏: 退役 => 左侧齿轮图标:一键退役设置: + +| 设置名称 | 值 | +| -------------------------------------------------------- | ---------------- | +| 选择优先级1 | R | +| 选择优先级2 | SR | +| 选择优先级3 | N | +| 「拥有」满星的同名舰船时,保留几艘符合退役条件的同名舰船 | 不保留 | +| 「没有」满星的同名舰船时,保留几艘符合退役条件的同名舰船 | 满星所需或不保留 | + +将角色设备的装备外观移除,以免影响图像识别 + +## 如何上报bug How to Report Bugs + +在提问题之前至少花费 5 分钟来思考和准备,才会有人花费他的 5 分钟来帮助你。"XX怎么运行不了","XX卡住了",这样的描述将不会得到回复。 + +- 在提问题前,请先阅读 [常见问题(FAQ)](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/FAQ_en_cn)。 +- 检查 Alas 的更新和最近的 commit,确认使用的是最新版。 +- 上传出错 log,在 `log/error` 目录下,以毫秒时间戳为文件夹名,包含 log.txt 和最近的截图。若不是错误而是非预期的行为,提供在 `log` 目录下当天的 log和至少一张游戏截图。 + + + +## 已知问题 Known Issues + +- **无法处理网络波动**,重连弹窗,跳小黄鸡。 +- **在极低配电脑上运行可能会出现各种问题**,极低配指截图耗时大于1s,一般电脑耗时约0.5s,高配耗时约0.3s。 +- **演习可能SL失败**,演习看的是屏幕上方的血槽,血槽可能被立绘遮挡,因此需要一定时间(默认1s)血量低于一定值(默认40%)才会触发SL。一个血皮后排就有30%左右的血槽,所以有可能在 1s 内被打死。 +- **极少数情况下 ADB 和 uiautomator2 会抽风**,是模拟器的问题,重启模拟器即可。 +- **拖动操作在模拟器卡顿时,会被视为点击** + + + +## Alas 社区准则 Alas Community Guidelines + +见 [#1416](https://github.com/LmeSzinc/AzurLaneAutoScript/issues/1416)。 + + + +## 文档 Documents + +[海图识别 perspective](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/perspective) + +`海图识别` 是一个碧蓝航线脚本的核心,如果只是单纯地使用 `模板匹配 (Template matching)` 来进行索敌,就不可避免地会出现 BOSS被小怪堵住 的情况。 Alas 提供了一个更好的海图识别方法,在 `module.map_detection` 中,你将可以得到完整的海域信息,比如: + +``` +2020-03-10 22:09:03.830 | INFO | A B C D E F G H +2020-03-10 22:09:03.830 | INFO | 1 -- ++ 2E -- -- -- -- -- +2020-03-10 22:09:03.830 | INFO | 2 -- ++ ++ MY -- -- 2E -- +2020-03-10 22:09:03.830 | INFO | 3 == -- FL -- -- -- 2E MY +2020-03-10 22:09:03.830 | INFO | 4 -- == -- -- -- -- ++ ++ +2020-03-10 22:09:03.830 | INFO | 5 -- -- -- 2E -- 2E ++ ++ +``` + +更多文档,请前往 [WIKI](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki)。 + + + +## 参与开发 Join Development + +Alas 仍在活跃开发中,我们会不定期发布未来的工作在 [Issues](https://github.com/LmeSzinc/AzurLaneAutoScript/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) 上并标记为 `help wanted`,欢迎向 Alas 提交 [Pull Requests](https://github.com/LmeSzinc/AzurLaneAutoScript/pulls),我们会认真阅读你的每一行代码的。 + +哦对,别忘了阅读 [开发文档](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/1.-Start)。 + + + +## 相关项目 Relative Repositories + +- [AzurStats](https://azur-stats.lyoko.io/),基于 Alas 实现的碧蓝航线掉落统计平台。 +- [AzurLaneUncensored](https://github.com/LmeSzinc/AzurLaneUncensored),与 Alas 对接的碧蓝航线反和谐。 +- [ALAuto](https://github.com/Egoistically/ALAuto),EN服的碧蓝航线脚本,已不再维护,Alas 模仿了其架构。 +- [ALAuto homg_trans_beta](https://github.com/asd111333/ALAuto/tree/homg_trans_beta),Alas 引入了其中的单应性变换至海图识别模块中。 +- [PyWebIO](https://github.com/pywebio/PyWebIO),Alas 使用的 GUI 库。 +- [MaaAssistantArknights](https://github.com/MaaAssistantArknights/MaaAssistantArknights),明日方舟小助手,全日常一键长草,现已加入Alas豪华午餐 -> [MAA 插件使用教程](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/submodule_maa_cn) +- [FGO-py](https://github.com/hgjazhgj/FGO-py),全自动免配置跨平台开箱即用的Fate/Grand Order助手.启动脚本,上床睡觉,养肝护发,满加成圣诞了解一下? +- [StarRailCopilot](https://github.com/LmeSzinc/StarRailCopilot),星铁速溶茶,崩坏:星穹铁道脚本,基于下一代Alas框架。 + + + +## 联系我们 Contact Us + +- Discord: [https://discord.gg/AQN6GeJ](https://discord.gg/AQN6GeJ) +- QQ 八群:[938081688](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=3h8Gl323WkIt6yGx8Jx5Ht93puZxeA8T&authKey=xPT6kPm7W9jWO2TNzPdohJ27l1njxorwKmkDrbwwYGGA6Oni1xQSJhHsRIJ8w7GZ&noverify=0&group_code=938081688) +- QQ 一群:[1087735381](https://jq.qq.com/?_wv=1027&k=I4NSqX7g) (有开发意向请加一群,入群需要提供你的Github用户名) +- Bilibili 直播间:https://live.bilibili.com/22216705 ,偶尔直播写Alas,~~为了拯救Alas,Lme决定出道成为偶像~~ -![image](https://github.com/Zuosizhu/Alas-with-Dashboard/assets/60862861/6fafb159-2092-4423-9d58-3d6c1262e691) diff --git a/alas_wrapped/alas.py b/alas_wrapped/alas.py index cf897ade87..6268c638a7 100644 --- a/alas_wrapped/alas.py +++ b/alas_wrapped/alas.py @@ -6,41 +6,14 @@ import inflection from cached_property import cached_property -try: - from adbutils.errors import AdbError -except ImportError: - class AdbError(Exception): - pass from module.base.decorator import del_cached_property -from module.base.jsonl import append_jsonl from module.config.config import AzurLaneConfig, TaskEnd from module.config.deep import deep_get, deep_set from module.exception import * from module.logger import logger from module.notify import handle_notify -# Command whitelist for security (frozenset for O(1) lookup) -_ALLOWED_COMMANDS = frozenset([ - 'restart', 'start', 'goto_main', 'research', 'commission', 'tactical', - 'dorm', 'meowfficer', 'guild', 'reward', 'awaken', 'shop_frequent', - 'shop_once', 'shipyard', 'gacha', 'freebies', 'minigame', 'private_quarters', - 'daily', 'hard', 'exercise', 'sos', 'war_archives', 'raid_daily', - 'event_a', 'event_b', 'event_c', 'event_d', 'event_sp', 'maritime_escort', - 'opsi_ash_assist', 'opsi_ash_beacon', 'opsi_explore', 'opsi_shop', - 'opsi_voucher', 'opsi_daily', 'opsi_obscure', 'opsi_month_boss', - 'opsi_abyssal', 'opsi_archive', 'opsi_stronghold', 'opsi_meowfficer_farming', - 'opsi_hazard1_leveling', 'opsi_cross_month', 'main', 'main2', 'main3', - 'event', 'event2', 'raid', 'hospital', 'coalition', 'coalition_sp', - 'c72_mystery_farming', 'c122_medium_leveling', 'c124_large_leveling', - 'gems_farming', 'daemon', 'opsi_daemon', 'event_story', - 'azur_lane_uncensored', 'benchmark', 'game_manager' -]) - -_RUNTIME_ROOT = os.path.dirname(os.path.abspath(__file__)) -_SCHEDULE_STATUS_FILE = os.path.join(_RUNTIME_ROOT, 'log', 'schedule_status.jsonl') -_JSONL_ROTATE_BYTES = 20 * 1024 * 1024 - class AzurLaneAutoScript: stop_event: threading.Event = None @@ -53,13 +26,6 @@ def __init__(self, config_name='alas'): # Failure count of tasks # Key: str, task name, value: int, failure count self.failure_record = {} - # Used by sidecar schedule status logs for correlation. - self.last_task = None - # Count transport failures across consecutive run attempts: - # first failure may be transient; repeated failures force restart path. - self.transport_error_streak = 0 - # Prevents duplicate restart calls when one restart is already queued. - self.restart_dedupe_seconds = 30 @cached_property def config(self): @@ -68,10 +34,10 @@ def config(self): return config except RequestHumanTakeover: logger.critical('Request human takeover') - raise + exit(1) except Exception as e: logger.exception(e) - raise RequestHumanTakeover(str(e)) from e + exit(1) @cached_property def device(self): @@ -81,10 +47,10 @@ def device(self): return device except RequestHumanTakeover: logger.critical('Request human takeover') - raise + exit(1) except Exception as e: logger.exception(e) - raise RequestHumanTakeover(str(e)) from e + exit(1) @cached_property def checker(self): @@ -94,156 +60,52 @@ def checker(self): return checker except Exception as e: logger.exception(e) - raise RequestHumanTakeover(str(e)) from e - - @cached_property - def state_machine(self): - try: - from module.state_machine import StateMachine - from module.ui.ui import UI - ui = UI(config=self.config, device=self.device) - return StateMachine(ui=ui) - except Exception as e: - logger.exception(e) - raise RequestHumanTakeover(str(e)) from e - - def _write_schedule_status(self, next_task): - # Emit scheduler intent and task queues to a separate stream so - # external tooling can inspect current/next task order. - pending = [f.command for f in getattr(self.config, 'pending_task', [])] - waiting = [f.command for f in getattr(self.config, 'waiting_task', [])] - payload = { - 'ts': datetime.utcnow().isoformat(timespec='milliseconds') + 'Z', - 'config': self.config_name, - 'current_task': self.last_task, - 'next_task': next_task, - 'pending': pending, - 'next_waiting': waiting[:10], - 'pending_count': len(pending), - 'waiting_count': len(waiting), - 'source': 'scheduler_loop', - } - append_jsonl( - _SCHEDULE_STATUS_FILE, - payload, - rotate_bytes=_JSONL_ROTATE_BYTES, - error_callback=lambda e: logger.warning( - f'Failed to append JSONL `{_SCHEDULE_STATUS_FILE}`: {e}' - ), - ) - - def _is_restart_pending_soon(self, within_seconds=None): - # Treat an existing upcoming Restart task as already-reported work. - within_seconds = self.restart_dedupe_seconds if within_seconds is None else within_seconds - restart_next_run = deep_get(self.config.data, keys='Restart.Scheduler.NextRun', default=None) - restart_enabled = bool(deep_get(self.config.data, keys='Restart.Scheduler.Enable', default=False)) - if not restart_enabled or not isinstance(restart_next_run, datetime): - return False - return restart_next_run <= datetime.now() + timedelta(seconds=max(int(within_seconds), 0)) - - def _schedule_restart(self, reason, within_seconds=None): - # Centralize all restart scheduling through one dedupe-aware path. - if self._is_restart_pending_soon(within_seconds=within_seconds): - logger.info(f'Task call: Restart (deduped, already pending) reason={reason}') - return False - logger.info(f'Task call: Restart (reason={reason})') - self.config.task_call('Restart') - return True - - def _recover_transport_once(self, reason): - # Use a single reconnect-screenshot probe before declaring transport - # failure terminal and letting restart logic run. - logger.warning(f'Transport failure detected ({reason}), attempting adb_reconnect once') - try: - self.device.adb_reconnect() - self.device.screenshot() - logger.info('Transport recovery succeeded') - return True - except Exception as e: - logger.warning(f'Transport recovery failed: {type(e).__name__}: {e}') - return False + exit(1) def run(self, command, skip_first_screenshot=False): - """ - Args: - command (str): Task name to run. - skip_first_screenshot (bool): - """ - if command not in _ALLOWED_COMMANDS: - logger.error(f'Command "{command}" is not in the whitelist.') - return False - try: if not skip_first_screenshot: self.device.screenshot() self.__getattribute__(command)() - self.transport_error_streak = 0 return True except TaskEnd: - self.transport_error_streak = 0 return True except GameNotRunningError as e: - self.transport_error_streak = 0 logger.warning(e) - self._schedule_restart(reason='game_not_running') + self.config.task_call('Restart') return False except (GameStuckError, GameTooManyClickError) as e: - self.transport_error_streak = 0 logger.error(e) self.save_error_log() logger.warning(f'Game stuck, {self.device.package} will be restarted in 10 seconds') logger.warning('If you are playing by hand, please stop Alas') - self._schedule_restart(reason=f'ui_stuck:{type(e).__name__}') - self.device.sleep(10) - return False - except (AdbError, GameTransportError) as e: - self.transport_error_streak += 1 - logger.error(f'{type(e).__name__}: {e}') - self.save_error_log() - # One-shot recovery: avoid immediate restart churn on one-off ADB blips. - if self.transport_error_streak == 1 and self._recover_transport_once(reason=f'{command}:{type(e).__name__}'): - logger.warning('Transport recovered on first failure, skip immediate restart') - self.device.sleep(2) - return False - logger.warning('Transport failure repeated or unrecoverable, schedule restart') - self._schedule_restart(reason=f'transport_failure:{type(e).__name__}') + self.config.task_call('Restart') self.device.sleep(10) return False except GameBugError as e: - self.transport_error_streak = 0 logger.warning(e) self.save_error_log() logger.warning('An error has occurred in Azur Lane game client, Alas is unable to handle') logger.warning(f'Restarting {self.device.package} to fix it') - self._schedule_restart(reason='game_bug') + self.config.task_call('Restart') self.device.sleep(10) return False except GamePageUnknownError: - self.transport_error_streak = 0 logger.info('Game server may be under maintenance or network may be broken, check server status now') self.checker.check_now() if self.checker.is_available(): - if self.config.Error_RestartOnUnknownPage: - logger.warning('Game page unknown, server is available. Attempting restart.') - self.save_error_log() - self._schedule_restart(reason='unknown_page') - self.device.sleep(10) - return False - else: - logger.critical('Game page unknown') - self.save_error_log() - handle_notify( - self.config.Error_OnePushConfig, - title=f"Alas <{self.config_name}> crashed", - content=f"<{self.config_name}> GamePageUnknownError", - ) - raise RequestHumanTakeover('GamePageUnknownError') + logger.critical('Game page unknown') + self.save_error_log() + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config_name}> crashed", + content=f"<{self.config_name}> GamePageUnknownError", + ) + exit(1) else: - logger.warning('Game server is under maintenance or network is broken, Alas will wait for it') self.checker.wait_until_available() return False except ScriptError as e: - self.transport_error_streak = 0 logger.exception(e) logger.critical('This is likely to be a mistake of developers, but sometimes just random issues') handle_notify( @@ -251,18 +113,16 @@ def run(self, command, skip_first_screenshot=False): title=f"Alas <{self.config_name}> crashed", content=f"<{self.config_name}> ScriptError", ) - raise RequestHumanTakeover(str(e)) from e + exit(1) except RequestHumanTakeover: - self.transport_error_streak = 0 logger.critical('Request human takeover') handle_notify( self.config.Error_OnePushConfig, title=f"Alas <{self.config_name}> crashed", content=f"<{self.config_name}> RequestHumanTakeover", ) - raise + exit(1) except Exception as e: - self.transport_error_streak = 0 logger.exception(e) self.save_error_log() handle_notify( @@ -270,7 +130,8 @@ def run(self, command, skip_first_screenshot=False): title=f"Alas <{self.config_name}> crashed", content=f"<{self.config_name}> Exception occured", ) - raise RequestHumanTakeover(str(e)) from e + exit(1) + def save_error_log(self): """ Save last 60 screenshots in ./log/error/ @@ -589,7 +450,7 @@ def wait_until(self, future): if self.stop_event.is_set(): logger.info("Update event detected") logger.info(f"[{self.config_name}] exited. Reason: Update") - return False + exit(0) time.sleep(5) @@ -623,7 +484,7 @@ def get_next_task(self): del_cached_property(self, 'config') continue if task.command != 'Restart': - self._schedule_restart(reason='close_game_wait') + self.config.task_call('Restart') del_cached_property(self, 'config') continue elif method == 'goto_main': @@ -673,10 +534,9 @@ def loop(self): # So update it once recovered del_cached_property(self, 'config') logger.info('Server or network is recovered. Restart game client') - self._schedule_restart(reason='server_recovered') + self.config.task_call('Restart') # Get task task = self.get_next_task() - self._write_schedule_status(next_task=task) # Init device and change server _ = self.device self.device.config = self.config @@ -695,39 +555,24 @@ def loop(self): success = self.run(inflection.underscore(task)) logger.info(f'Scheduler: End task `{task}`') self.is_first_task = False - self.last_task = task # Check failures - failed = self.failure_record.get(task, 0) + failed = deep_get(self.failure_record, keys=task, default=0) failed = 0 if success else failed + 1 - self.failure_record[task] = failed + deep_set(self.failure_record, keys=task, value=failed) if failed >= 3: logger.critical(f"Task `{task}` failed 3 or more times.") logger.critical("Possible reason #1: You haven't used it correctly. " "Please read the help text of the options.") logger.critical("Possible reason #2: There is a problem with this task. " "Please contact developers or try to fix it yourself.") - if self.config.Error_HandleError: - logger.warning(f"Auto recovery enabled, skip human takeover for task `{task}`") - logger.warning(f"Delay task `{task}` for 10 minutes and schedule `Restart`") - self.failure_record[task] = 0 - self.config.task_delay(task=task, minute=10) - self._schedule_restart(reason=f'task_failed_3x:{task}') - if task == 'Restart': - logger.warning('Restart task failed repeatedly, wait 60 seconds before next scheduler cycle') - # Use time.sleep directly: device.sleep is only a thin - # wrapper today, but when Restart itself has failed the - # device/ADB connection may be in an unknown state. - time.sleep(60) - del_cached_property(self, 'config') - continue logger.critical('Request human takeover') handle_notify( self.config.Error_OnePushConfig, title=f"Alas <{self.config_name}> crashed", content=f"<{self.config_name}> RequestHumanTakeover\nTask `{task}` failed 3 or more times.", ) - raise RequestHumanTakeover(f"Task `{task}` failed 3 or more times.") + exit(1) if success: del_cached_property(self, 'config') diff --git a/alas_wrapped/assets/cn/combat_ui/PAUSE_Ancient.png b/alas_wrapped/assets/cn/combat_ui/PAUSE_Ancient.png new file mode 100644 index 0000000000..2f9c7623ba Binary files /dev/null and b/alas_wrapped/assets/cn/combat_ui/PAUSE_Ancient.png differ diff --git a/alas_wrapped/assets/cn/os_handler/MISSION_OVERVIEW_EMPTY.png b/alas_wrapped/assets/cn/os_handler/MISSION_OVERVIEW_EMPTY.png new file mode 100644 index 0000000000..3769c8611f Binary files /dev/null and b/alas_wrapped/assets/cn/os_handler/MISSION_OVERVIEW_EMPTY.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_OCR_PT.png b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_PT.png new file mode 100644 index 0000000000..69f91e6248 Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_PT.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_EASY.png b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_EASY.png new file mode 100644 index 0000000000..81cc31b969 Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_EASY.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_EX.png b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_EX.png new file mode 100644 index 0000000000..3cfa3e1157 Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_EX.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_HARD.png b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_HARD.png new file mode 100644 index 0000000000..058042521e Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_HARD.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_NORMAL.png b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_NORMAL.png new file mode 100644 index 0000000000..dae85770ac Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_OCR_REMAIN_NORMAL.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_RAID_EASY.png b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_EASY.png new file mode 100644 index 0000000000..f77fe6413b Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_EASY.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_RAID_EX.png b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_EX.png new file mode 100644 index 0000000000..6488b54c7d Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_EX.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_RAID_HARD.png b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_HARD.png new file mode 100644 index 0000000000..e221a3764a Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_HARD.png differ diff --git a/alas_wrapped/assets/cn/raid/CHANGWU_RAID_NORMAL.png b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_NORMAL.png new file mode 100644 index 0000000000..9e50e8a8dc Binary files /dev/null and b/alas_wrapped/assets/cn/raid/CHANGWU_RAID_NORMAL.png differ diff --git a/alas_wrapped/assets/cn/raid/RPG_BACK.png b/alas_wrapped/assets/cn/raid/RPG_BACK.png new file mode 100644 index 0000000000..6c02673af7 Binary files /dev/null and b/alas_wrapped/assets/cn/raid/RPG_BACK.png differ diff --git a/alas_wrapped/assets/cn/raid/RPG_RAID_EX.png b/alas_wrapped/assets/cn/raid/RPG_RAID_EX.png index b16b3f0f00..0c84d4ac2d 100644 Binary files a/alas_wrapped/assets/cn/raid/RPG_RAID_EX.png and b/alas_wrapped/assets/cn/raid/RPG_RAID_EX.png differ diff --git a/alas_wrapped/assets/cn/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png b/alas_wrapped/assets/cn/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png new file mode 100644 index 0000000000..1740e7d757 Binary files /dev/null and b/alas_wrapped/assets/cn/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png differ diff --git a/alas_wrapped/assets/en/os_handler/MISSION_OVERVIEW_EMPTY.png b/alas_wrapped/assets/en/os_handler/MISSION_OVERVIEW_EMPTY.png new file mode 100644 index 0000000000..3769c8611f Binary files /dev/null and b/alas_wrapped/assets/en/os_handler/MISSION_OVERVIEW_EMPTY.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_OCR_PT.png b/alas_wrapped/assets/en/raid/CHANGWU_OCR_PT.png new file mode 100644 index 0000000000..69f91e6248 Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_OCR_PT.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_EASY.png b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_EASY.png new file mode 100644 index 0000000000..81cc31b969 Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_EASY.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_EX.png b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_EX.png new file mode 100644 index 0000000000..3cfa3e1157 Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_EX.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_HARD.png b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_HARD.png new file mode 100644 index 0000000000..058042521e Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_HARD.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_NORMAL.png b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_NORMAL.png new file mode 100644 index 0000000000..dae85770ac Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_OCR_REMAIN_NORMAL.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_RAID_EASY.png b/alas_wrapped/assets/en/raid/CHANGWU_RAID_EASY.png new file mode 100644 index 0000000000..2d92d8c58b Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_RAID_EASY.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_RAID_EX.png b/alas_wrapped/assets/en/raid/CHANGWU_RAID_EX.png new file mode 100644 index 0000000000..6488b54c7d Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_RAID_EX.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_RAID_HARD.png b/alas_wrapped/assets/en/raid/CHANGWU_RAID_HARD.png new file mode 100644 index 0000000000..e04d8b506f Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_RAID_HARD.png differ diff --git a/alas_wrapped/assets/en/raid/CHANGWU_RAID_NORMAL.png b/alas_wrapped/assets/en/raid/CHANGWU_RAID_NORMAL.png new file mode 100644 index 0000000000..2431085cbd Binary files /dev/null and b/alas_wrapped/assets/en/raid/CHANGWU_RAID_NORMAL.png differ diff --git a/alas_wrapped/assets/en/raid/RPG_BACK.png b/alas_wrapped/assets/en/raid/RPG_BACK.png new file mode 100644 index 0000000000..6c02673af7 Binary files /dev/null and b/alas_wrapped/assets/en/raid/RPG_BACK.png differ diff --git a/alas_wrapped/assets/en/raid/RPG_RAID_EX.png b/alas_wrapped/assets/en/raid/RPG_RAID_EX.png index b16b3f0f00..0c84d4ac2d 100644 Binary files a/alas_wrapped/assets/en/raid/RPG_RAID_EX.png and b/alas_wrapped/assets/en/raid/RPG_RAID_EX.png differ diff --git a/alas_wrapped/assets/en/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png b/alas_wrapped/assets/en/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png new file mode 100644 index 0000000000..d19ca6c166 Binary files /dev/null and b/alas_wrapped/assets/en/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png differ diff --git a/alas_wrapped/assets/gui/css/alas-mobile.css b/alas_wrapped/assets/gui/css/alas-mobile.css index eb94121448..9acad6eb77 100644 --- a/alas_wrapped/assets/gui/css/alas-mobile.css +++ b/alas_wrapped/assets/gui/css/alas-mobile.css @@ -35,14 +35,6 @@ grid-template-columns: 1fr auto; grid-template-rows: 1fr auto; } -#pywebio-scope-dashboard { - font-weight: 400; - width: 100%; - display: grid; - grid-auto-flow: row; - grid-template-columns: repeat(auto-fit,minmax(6rem,1fr)); - overflow: hidden; -} #pywebio-scope-_groups { grid-template-columns: 0fr 1fr; @@ -67,26 +59,4 @@ #pywebio-scope-waiting, #pywebio-scope-log { overflow-y: auto; -} - -#output-container .status-point { - margin: 45% 50% 55% 50%; - width: .4rem; - height: .4rem; - border-radius: 50%; -} - -*[style*="--dashboard-limit--"] { - font-size: .8rem; - padding: 0.2rem 0 0 0; -} -*[style*="--dashboard-value--"] { - font-size: 1rem; - margin: 0 0 0 0.4rem !important; -} -*[style*="--dashboard-total--"] { - font-size: 1rem; -} -*[style*="--dashboard-help--"] { - font-size: .6rem; } \ No newline at end of file diff --git a/alas_wrapped/assets/gui/css/alas-pc.css b/alas_wrapped/assets/gui/css/alas-pc.css index 0adfa25911..9240171a0c 100644 --- a/alas_wrapped/assets/gui/css/alas-pc.css +++ b/alas_wrapped/assets/gui/css/alas-pc.css @@ -44,4 +44,4 @@ display: grid; grid-auto-flow: column; grid-template-columns: auto auto; -} +} \ No newline at end of file diff --git a/alas_wrapped/assets/gui/css/alas.css b/alas_wrapped/assets/gui/css/alas.css index c73287919f..94e696480f 100644 --- a/alas_wrapped/assets/gui/css/alas.css +++ b/alas_wrapped/assets/gui/css/alas.css @@ -133,13 +133,6 @@ footer { font-size: 0.875rem; } -.status-point { - margin: 37% 50% 63% 50%; - width: .75rem; - height: .75rem; - border-radius: 50%; -} - input[type="checkbox"] { width: 1.25rem; height: 1.25rem; @@ -406,31 +399,11 @@ pre.rich-traceback-code { justify-content: space-between; } -#pywebio-scope-log-bar { - flex-wrap: wrap; -} - #pywebio-scope-log-bar-btns { display: grid; grid-auto-flow: column; } -#pywebio-scope-log-bar .hr-group { - width: 100%; -} - -#pywebio-scope-dashboard { - font-weight: 400; - width: 100%; - display: grid; - grid-auto-flow: row; - grid-template-columns: repeat(auto-fit,minmax(10rem,1fr)); -} - -#pywebio-scope-dashboard .form-control{ - padding: 0 0 0; - } - #pywebio-scope-log { line-height: 1.2; font-size: 0.85rem; @@ -550,50 +523,4 @@ pre.rich-traceback-code { width: 1.5rem; height: 1.5rem; border: .2em solid currentColor; -} - -/**[style*="--dashboard-value--"] {*/ -/* font-size: 1.3rem;*/ -/* font-weight: 400;*/ -/* margin: 0 0 -0.2rem 0.6rem !important;*/ -/* overflow-wrap: break-word;*/ -/* overflow: visible;*/ -/* border-bottom: 0;*/ -/*}*/ - -*[style*="--dashboard-value--"] { - font-size: 1.2rem; - font-weight: 400; - margin: 0 0 0 0.6rem !important; - font-family: - "Arial", - serif; -} - -*[style*="--dashboard-total--"] { - font-size: 1.2rem; - font-weight: 400; - margin: 0 0 0 0.15rem !important; - font-family: - "Arial", - serif; -} - -*[style*="--dashboard-help--"] { - font-size: .8rem; - margin: 0 0 0 0.6rem !important; - font-family: - "Arial", - serif; -} - -*[style*="--dashboard-limit--"] { - font-weight: 400; - font-size: .9rem; - margin: 0 0 0 0 !important; - vertical-align: text-bottom; - overflow-wrap: normal; - font-family: - "Arial", - serif; -} +} \ No newline at end of file diff --git a/alas_wrapped/assets/gui/css/dark-alas.css b/alas_wrapped/assets/gui/css/dark-alas.css index 7d097c06af..c36fa31b06 100644 --- a/alas_wrapped/assets/gui/css/dark-alas.css +++ b/alas_wrapped/assets/gui/css/dark-alas.css @@ -149,19 +149,7 @@ pre.rich-traceback-code { border-bottom: 1px solid #36393f; } -#pywebio-scope-dashboard input { - background-color: #2f3136 !important; -} - *[style*="--arg-help--"], [id^="pywebio-scope-group_"] > p + p { color: #adb5bd; -} - -*[style*="--dashboard-help--"]{ - color: #adb5bd; -} - -*[style*="--dashboard-limit--"]{ - color: #adb5bd; -} +} \ No newline at end of file diff --git a/alas_wrapped/assets/gui/css/light-alas.css b/alas_wrapped/assets/gui/css/light-alas.css index af57893987..b65067f113 100644 --- a/alas_wrapped/assets/gui/css/light-alas.css +++ b/alas_wrapped/assets/gui/css/light-alas.css @@ -60,10 +60,6 @@ input[type="checkbox"] { accent-color: #7a77bb; } -#pywebio-scope-dashboard input { - background-color: white !important; -} - select { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAAC85JREFUeF7tnUezdVURhh+qmDixnGOVVjmxyj+AOsGBEyeGKrMYMGfBCIYqE2LOWVTMOeeMGRQjZhBMmAOCAXO9sE7V+eC711699z5n791vj3vtu/vp9Zy1bh+43zE4TMAEDiRwjNmYgAkcTMCCeHeYwCEELIi3hwlYEO8BE8gR8AmS4+ZVRQhYkCKNdpk5AhYkx82rihCwIEUa7TJzBCxIjptXFSFgQYo02mXmCFiQHDevKkLAghRptMvMEbAgOW5eVYSABSnSaJeZI2BBcty8qggBC1Kk0S4zR8CC5Lh5VRECFqRIo11mjoAFyXHzqiIELEiRRrvMHAELkuPmVUUIWJAijXaZOQIWJMfNq4oQsCBFGu0ycwQsSI6bVxUhYEGKNNpl5ghYkBw3rypCwIIUabTLzBGwIDluXlWEgAUp0miXmSNgQXLcvKoIAQtSpNEuM0fAguS4eVURAhakSKNdZo6ABclx86oiBCxIkUa7zBwBC5Lj5lVFCFiQIo12mTkCFiTHzauKELAgRRrtMnMELEiOm1cVIWBBijTaZeYIWJAcN68qQsCCFGm0y8wRsCA5bl5VhIAFKdJol5kjYEFy3LyqCAELUqTRLjNHwILkuHlVEQIWpEijXWaOgAXJcfOqIgQsSJFGu8wcAQuS4+ZVRQhYkCKNdpk5AhYkx82rihCwIEUa7TJzBCxIjptXFSFgQYo02mXmCFiQHDevKkLAghRptMvMEZizILcAjgOuA1wIfBy4LFemV62AwLWBGwPHA2cD5+1iP8xVkE8DJ1ytqecCpwGfXEGzXUIfgZOBU4Drbi27GLgH8Jm+R/Vlz1GQC4AbHFLG7YC395Xp7AUTeBtw20Pe/0bAd6eqb26CnA6cGij29oDAOdZN4K2APhAPi0uAm08lydwEOR/QJ0Ik7gAIoGOdBN4C6IMwEvpQPSOS2JszN0H+ClyrowhL0gFrQak9cqis9wG3nKK+uQlyEXD9zkLvCAioYx0E3gzog68nXgI8sGdBNHdugrwLuHX05bfy7gQIrGPZBDJyqOLJPiTnJsgNgU+07z96W21JeonNK/9NbaP3vpXGvDfrXRTNn5sgem+N9LITqjsDAu1YFoE3AvqAy4R+99DvIJPEHAVRoRrtZSdUdwEE3LEMAlk5/tv2yTumLHOugliSKbs+n2e/AdCp3xs7kUMvNWdB9H6ag2cnVCcCaoBjngReD+i0743/tJPjnb0LM/lzF0Q1aeSXnVDdFVAjHPMikJXj300OTTt3EksQxJLsZCvs7Ie8DtDp3hs7l2MJV6xtiJp1ZydUdwPUGMd+CZwF6FTvjX+1k+PdvQuH5i/lBNnUqVFgdkJlSYbulmHrs3L8s/0uunM5lnaCjCHJ3QE1yrFbAq8F9AHVG5JDI//39C4cK39pJ8imbo0GsxMq/U82aphjNwReA+iDqTf+0eR4b+/CMfOXKogYaESYnVBZkjF30cHPyspxRbtW7VWOpV6xttsxRJKTADXQMQ2BV7f/Jbb36ZJD16rJ/vORnhda8gmyqVMjw+yE6p6AGukYl0BWjr83Od4/7uvkn7YGQVS9JcnvgbFXngnodO6Nv7Vr1WzkWMMVa7sJmq9nJ1T3AtRYxzACrwJ0KveG5NC16gO9C6fOX8sJsuGkUWJ2QnVvQA125Ahk5dD/Zi05Ppj7sdOuWpsgomVJpt0zR3v6KwGdwr3xl3atmqUca7tibTdHc/fshOo+gBruiBF4BaDTtzckh06OD/Uu3GX+Gk+QDT9915GdUN0XUOMdhxPIynF5k+PDcwe8ZkHE3pJMtwNfDui07Y3FyLHmK9Z20zRyzE6o7gdoIziOJPAyQKdsb+iPj+ta9ZHehfvKX/sJsuGq0WN2QmVJxpHjz+0X8sXIUeUEGUOS+wP61KweLwX0gdEbkkMnx0d7F+47v8oJsuGsUWR2QvUAQBukamTluLTJ8bElgqsmiHqkkWR2QlVVEv1pT52ivfGndq1apBzVrljbzR0iif4GrDZMlXgxoA+G3pAculbpXwZbbFQ8QTbN0ogyO6F6EKCNs/bIyvHHJof+jOyio7IgapwlOXj7vij5F9P/0K5Vi5ej8hVre1tonp+dUD0Y0EZaW7wQ0CnZG5JD16rV/DuS1U+QzQbQ6DI7oXoIoA21lsjK8fsmx6fWAsInyJGdtCTwAkCnYm/8rl2rViWHBbnmNtAoMzuheihXbbClxvMBnYa9ITl0rdI/3b268BXrmi3VSDM7oXoYoI22tMjK8dsmx6T/Vvk+YVqQo9OvJMnzAJ1+vbF6OXzFOnxL6AvB7ITqZEAbb+7xXECnXm/8pp0cZ/cuXFq+T5DDO6ZRZ3ZCNXdJsnL8uv1Cvno5fILEPs6GSHIKoI04t3gOIIF7Q3LoF/LP9i5car5PkFjnNPrMTqgeDmhDziWycvyqyfG5uRSyi/ewIHHKGoFmJ1RzkeTZgE613vhlu1aVksNXrN5tctX3BFlJHgFog+4rngVI1N6QHLpWfb534RryfYL0d1Ej0eyE6pGANuquIyvHJU2OL+z6hefy8yxIrhNLkuSZgE6v3vhFu1aVlcNXrN4tc2S+vj/ITqgeBWjjTh3PAHRq9Ybk0LXqi70L15bvE2RYRzUqzU6oHg1oA08VWTl+3uT40lQvtqTnWpDh3ZqjJE8HdEr1xs/atcpyNHIWpHcLHT1fo9PshOoxgDb0WHEGoNOpNySHrlVf7l245nwLMl53NULNTqjGkiQrx0/byWE5rrYfLMh4guhJQyQ5FdAGz8bTAInWGz9pcpzTu7BCvgUZv8saqWYnVKcB2ui9cTogwXpDcuhadW7vwir5FmSaTmu0mp1Q9UqSlePidnJYjkP2gAWZRhA9dYgkjwW08f9fPBWQUL0hOXRyfKV3YbV8CzJtxzVqzU6oHgdIgIPiKYBE6o2Lmhxf7V1YMd+CTN/1KSTJyvHjdq2yHMG+W5AgqIFp+l4iO6F6PCAhNvFkQKdLb0gOXavO611YOd+C7K77GsFmJlR6wycAEiMrx4VNjq/trtx1/CQLsts+DpFEf1rnhMTrXtCuVZYjAc+CJKANXKLvKyITqoE/5srlkkPXqq+P8bCKz7Ag++m6RrOHTajGeKsfNTm+McbDqj7Dguyv81NK8sN2rbIcA/trQQYCHLhc32NsT6gGPu7K5ZJD16pvjvGw6s+wIPvfARrZajo1RvygnRyWYwyagAUZCeTAx4whyfebHN8a+C5evkXAgsxnO+gLwSclX0dy6Fr17eR6LzuAgAWZ19bQF4JP7Hyl77WTw3J0goukW5AIpd3m9EiiUe5tgPN3+4p1fpoFmWevI3904VLgpsB35lnCOt7Kgsy3j2cCJx3wepcDx1uO6ZtnQaZnPOQn3AQ4C7gecCxwWftjbrcCrhjyYK+NEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEbAgMU7OKkrAghRtvMuOEfgf9hZk2PNubUAAAAAASUVORK5CYII="); } @@ -155,12 +151,4 @@ pre.rich-traceback-code { *[style*="--arg-help--"], [id^="pywebio-scope-group_"] > p + p { color: #777777; -} - -*[style*="--dashboard-help--"]{ - color: #777777; -} - -*[style*="--dashboard-limit--"]{ - color: #777777; } \ No newline at end of file diff --git a/alas_wrapped/assets/jp/os_handler/MISSION_OVERVIEW_EMPTY.png b/alas_wrapped/assets/jp/os_handler/MISSION_OVERVIEW_EMPTY.png new file mode 100644 index 0000000000..3769c8611f Binary files /dev/null and b/alas_wrapped/assets/jp/os_handler/MISSION_OVERVIEW_EMPTY.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_OCR_PT.png b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_PT.png new file mode 100644 index 0000000000..69f91e6248 Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_PT.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_EASY.png b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_EASY.png new file mode 100644 index 0000000000..81cc31b969 Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_EASY.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_EX.png b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_EX.png new file mode 100644 index 0000000000..3cfa3e1157 Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_EX.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_HARD.png b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_HARD.png new file mode 100644 index 0000000000..058042521e Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_HARD.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_NORMAL.png b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_NORMAL.png new file mode 100644 index 0000000000..dae85770ac Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_OCR_REMAIN_NORMAL.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_RAID_EASY.png b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_EASY.png new file mode 100644 index 0000000000..57b3d424ea Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_EASY.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_RAID_EX.png b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_EX.png new file mode 100644 index 0000000000..6488b54c7d Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_EX.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_RAID_HARD.png b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_HARD.png new file mode 100644 index 0000000000..a3078d7995 Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_HARD.png differ diff --git a/alas_wrapped/assets/jp/raid/CHANGWU_RAID_NORMAL.png b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_NORMAL.png new file mode 100644 index 0000000000..1e12dc2799 Binary files /dev/null and b/alas_wrapped/assets/jp/raid/CHANGWU_RAID_NORMAL.png differ diff --git a/alas_wrapped/assets/jp/raid/RPG_BACK.png b/alas_wrapped/assets/jp/raid/RPG_BACK.png new file mode 100644 index 0000000000..6c02673af7 Binary files /dev/null and b/alas_wrapped/assets/jp/raid/RPG_BACK.png differ diff --git a/alas_wrapped/assets/jp/raid/RPG_RAID_EX.png b/alas_wrapped/assets/jp/raid/RPG_RAID_EX.png index b16b3f0f00..0c84d4ac2d 100644 Binary files a/alas_wrapped/assets/jp/raid/RPG_RAID_EX.png and b/alas_wrapped/assets/jp/raid/RPG_RAID_EX.png differ diff --git a/alas_wrapped/assets/tw/coalition/FASHION_COALITION_CHECK.png b/alas_wrapped/assets/tw/coalition/FASHION_COALITION_CHECK.png new file mode 100644 index 0000000000..962edba756 Binary files /dev/null and b/alas_wrapped/assets/tw/coalition/FASHION_COALITION_CHECK.png differ diff --git a/alas_wrapped/assets/tw/coalition/FASHION_MODE_BATTLE.png b/alas_wrapped/assets/tw/coalition/FASHION_MODE_BATTLE.png new file mode 100644 index 0000000000..595a1927fc Binary files /dev/null and b/alas_wrapped/assets/tw/coalition/FASHION_MODE_BATTLE.png differ diff --git a/alas_wrapped/assets/tw/coalition/FASHION_MODE_STORY.png b/alas_wrapped/assets/tw/coalition/FASHION_MODE_STORY.png new file mode 100644 index 0000000000..e395d15961 Binary files /dev/null and b/alas_wrapped/assets/tw/coalition/FASHION_MODE_STORY.png differ diff --git a/alas_wrapped/assets/tw/coalition/FASHION_SWITCH_MULTI.png b/alas_wrapped/assets/tw/coalition/FASHION_SWITCH_MULTI.png new file mode 100644 index 0000000000..a4c5a1d5d9 Binary files /dev/null and b/alas_wrapped/assets/tw/coalition/FASHION_SWITCH_MULTI.png differ diff --git a/alas_wrapped/assets/tw/coalition/FASHION_SWITCH_SINGLE.png b/alas_wrapped/assets/tw/coalition/FASHION_SWITCH_SINGLE.png new file mode 100644 index 0000000000..c47ed7c864 Binary files /dev/null and b/alas_wrapped/assets/tw/coalition/FASHION_SWITCH_SINGLE.png differ diff --git a/alas_wrapped/assets/tw/os_handler/MISSION_OVERVIEW_EMPTY.png b/alas_wrapped/assets/tw/os_handler/MISSION_OVERVIEW_EMPTY.png new file mode 100644 index 0000000000..3769c8611f Binary files /dev/null and b/alas_wrapped/assets/tw/os_handler/MISSION_OVERVIEW_EMPTY.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_OCR_PT.png b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_PT.png new file mode 100644 index 0000000000..69f91e6248 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_PT.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_EASY.png b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_EASY.png new file mode 100644 index 0000000000..81cc31b969 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_EASY.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_EX.png b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_EX.png new file mode 100644 index 0000000000..3cfa3e1157 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_EX.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_HARD.png b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_HARD.png new file mode 100644 index 0000000000..058042521e Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_HARD.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_NORMAL.png b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_NORMAL.png new file mode 100644 index 0000000000..dae85770ac Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_OCR_REMAIN_NORMAL.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_RAID_EASY.png b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_EASY.png new file mode 100644 index 0000000000..a05ce4ed84 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_EASY.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_RAID_EX.png b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_EX.png new file mode 100644 index 0000000000..6488b54c7d Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_EX.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_RAID_HARD.png b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_HARD.png new file mode 100644 index 0000000000..bb8b6bbd68 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_HARD.png differ diff --git a/alas_wrapped/assets/tw/raid/CHANGWU_RAID_NORMAL.png b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_NORMAL.png new file mode 100644 index 0000000000..9d49039789 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/CHANGWU_RAID_NORMAL.png differ diff --git a/alas_wrapped/assets/tw/raid/RPG_BACK.png b/alas_wrapped/assets/tw/raid/RPG_BACK.png new file mode 100644 index 0000000000..6c02673af7 Binary files /dev/null and b/alas_wrapped/assets/tw/raid/RPG_BACK.png differ diff --git a/alas_wrapped/assets/tw/raid/RPG_RAID_EX.png b/alas_wrapped/assets/tw/raid/RPG_RAID_EX.png index b16b3f0f00..0c84d4ac2d 100644 Binary files a/alas_wrapped/assets/tw/raid/RPG_RAID_EX.png and b/alas_wrapped/assets/tw/raid/RPG_RAID_EX.png differ diff --git a/alas_wrapped/campaign/Readme.md b/alas_wrapped/campaign/Readme.md index e8b9700b37..2c9330b534 100644 --- a/alas_wrapped/campaign/Readme.md +++ b/alas_wrapped/campaign/Readme.md @@ -57,6 +57,7 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20251016 | war archives 20231026 cn | Tempesta and the Fountain of Youth | 飓风与青春之泉 | Tempesta and the Fountain of Youth | テンペスタと若返りの泉 | 飓風與青春之泉 | | 20251106 | war archives 20220915 cn | Violet Tempest Blooming Lycoris | 紫绛槿岚 | Violet Tempest Blooming Lycoris | 赫の涙月 菫の暁風 | 紫絳槿嵐 | | 20251218 | war archives 20221222 cn | Parallel Superimposition | 定向折叠 | Parallel Superimposition | 積重なる事象の幻界 | 定向折疊 | +| 20260129 | war archives 20230223 cn | Revelations of Dust | 湮烬尘墟 | Revelations of Dust | 黙示の遺構 | 湮燼塵墟 | | 20200227 | event 20200227 cn | Northern Overture | 北境序曲 | Northern Overture | 凍絶の北海 | - | | 20200312 | event 20200312 cn | The Solomon Ranger | 复刻斯图尔特的硝烟 | The Solomon Ranger Rerun | 南洋に靡く硝煙(復刻) | - | | 20200326 | event 20200326 cn | Microlayer Medley | 微层混合 | Microlayer Medley | 闇靄払う銀翼 | - | @@ -277,3 +278,7 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20260115 | event 20231221 cn | Light-Chasing Sea of Stars Rerun | 复刻星海逐光 | Light-Chasing Sea of Stars Rerun | 光追う星の海(復刻) | - | | 20260122 | coalition 20260122 | Light & Shadow Fashion Shoot! | 光影风尚-拍摄进行时 | Light & Shadow Fashion Shoot! | 特集写真-撮影進行中 | - | | 20260122 | event 20220526 cn | Pledge of the Radiant Court | - | - | - | 泠誓光庭 | +| 20260129 | coalition 20260122 | Light & Shadow Fashion Shoot! | - | - | - | 光影風尚-拍攝進行時 | +| 20260205 | raid 20240328 | From Zero to Hero | 复刻从零开始的魔王讨伐之旅 | From Zero to Hero Rerun | ゼロから頑張る魔王討伐(復刻) | - | +| 20260212 | raid 20260212 | Spring Auction Adventure | 春宴怀玉香满庭 | Spring Auction Adventure | 新春玉逸品会 | 春宴懷玉香滿庭 | +| 20260226 | event 20260226 cn | Springtide Inn Online | 春满客栈Online | Springtide Inn Online | 春色旅籠Online | - | diff --git a/alas_wrapped/campaign/event_20260226_cn/a1.py b/alas_wrapped/campaign/event_20260226_cn/a1.py new file mode 100644 index 0000000000..1f30ab703d --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/a1.py @@ -0,0 +1,84 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('A1') +MAP.shape = 'I8' +MAP.camera_data = ['E2', 'E6'] +MAP.camera_data_spawn_point = ['E2'] +MAP.map_data = """ + -- -- ME -- ME -- -- -- -- + -- ++ -- -- -- ME ++ ++ -- + -- ++ -- SP -- -- Me ++ -- + -- ME -- -- SP -- -- ME -- + ++ -- Me -- -- -- Me -- -- + -- ME ++ MS -- MS ++ ++ ++ + -- -- ME -- __ -- ME ++ -- + -- -- -- -- MB -- -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1, 'boss': 1}, + {'battle': 4, 'enemy': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_CHAPTER_SWITCH_20241219 = True + STAGE_ENTRANCE = ['half', '20240725'] + MAP_HAS_MODE_SWITCH = True + STAGE_INCREASE_AB = True + MAP_WALK_USE_CURRENT_FLEET = True + MAP_SIREN_HAS_BOSS_ICON_SMALL = True + MAP_SWIPE_MULTIPLY = (1.179, 1.201) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.140, 1.161) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.107, 1.127) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_3(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/a2.py b/alas_wrapped/campaign/event_20260226_cn/a2.py new file mode 100644 index 0000000000..b69f02818e --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/a2.py @@ -0,0 +1,74 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .a1 import Config as ConfigBase + +MAP = CampaignMap('A2') +MAP.shape = 'J7' +MAP.camera_data = ['D2', 'D5', 'E4'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + -- ME -- -- ME ++ ++ -- -- -- + MB -- ME -- -- -- ++ ME ME ++ + ++ -- ++ ++ Me -- Me -- -- ++ + -- -- ++ ++ -- __ -- -- ME -- + -- MS -- -- MS -- -- ME ++ -- + ME -- -- -- -- -- ME -- -- -- + ++ -- SP SP -- ++ ++ ++ -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom-left' + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/a3.py b/alas_wrapped/campaign/event_20260226_cn/a3.py new file mode 100644 index 0000000000..677b7771fa --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/a3.py @@ -0,0 +1,75 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .a1 import Config as ConfigBase + +MAP = CampaignMap('A3') +MAP.shape = 'H8' +MAP.camera_data = ['D4', 'D6', 'E3'] +MAP.camera_data_spawn_point = ['D4'] +MAP.map_data = """ + ++ ++ -- -- ME -- -- ME + ++ ++ MS -- -- -- ME -- + SP -- -- MS -- ++ -- -- + SP -- -- MS -- ++ ME -- + ++ Me __ -- -- -- -- -- + ++ ME -- Me ++ ++ ++ -- + -- -- -- -- Me -- ME -- + MB ++ Me ME -- ME -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ +A8, B8, C8, D8, E8, F8, G8, H8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/b1.py b/alas_wrapped/campaign/event_20260226_cn/b1.py new file mode 100644 index 0000000000..88095243d3 --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/b1.py @@ -0,0 +1,85 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('B1') +MAP.shape = 'I8' +MAP.camera_data = ['D3', 'D6', 'F3', 'F6'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ -- -- -- -- -- -- -- -- + ME -- ME ++ ++ ++ -- -- ++ + -- ME -- MS -- MS -- ME ++ + -- ++ MS -- -- -- Me -- ME + -- Me -- -- SP -- -- __ Me + -- -- -- SP -- -- Me -- -- + ++ ++ Me -- -- ++ -- MB -- + ++ ++ -- ME ME ++ ME -- ME +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2, 'boss': 1}, + {'battle': 5, 'enemy': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_CHAPTER_SWITCH_20241219 = True + STAGE_ENTRANCE = ['half', '20240725'] + MAP_HAS_MODE_SWITCH = True + STAGE_INCREASE_AB = True + MAP_WALK_USE_CURRENT_FLEET = True + MAP_SIREN_HAS_BOSS_ICON_SMALL = True + MAP_SWIPE_MULTIPLY = (1.138, 1.160) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.101, 1.121) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.069, 1.088) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/b2.py b/alas_wrapped/campaign/event_20260226_cn/b2.py new file mode 100644 index 0000000000..78bddb7c5f --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/b2.py @@ -0,0 +1,76 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .b1 import Config as ConfigBase + +MAP = CampaignMap('B2') +MAP.shape = 'J8' +MAP.camera_data = ['D2', 'D6', 'G2', 'G6'] +MAP.camera_data_spawn_point = ['G2'] +MAP.map_data = """ + MB ++ ME ME ++ ++ ++ -- -- -- + -- -- -- -- ++ ME MS -- -- SP + -- ME ++ -- Me -- -- MS -- -- + -- -- Me -- -- __ MS -- -- SP + -- ME -- ++ -- -- -- -- -- -- + -- ++ ME -- Me -- Me ++ ++ ME + -- ME -- ME ++ -- -- ME ++ -- + -- -- -- -- ++ ME -- -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/b3.py b/alas_wrapped/campaign/event_20260226_cn/b3.py new file mode 100644 index 0000000000..c8032c4eee --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/b3.py @@ -0,0 +1,82 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .b1 import Config as ConfigBase + +MAP = CampaignMap('B3') +MAP.shape = 'I10' +MAP.camera_data = ['D4', 'D6', 'D8', 'F4', 'F6', 'F8'] +MAP.camera_data_spawn_point = ['D2'] +MAP.map_data = """ + ++ ++ ++ SP -- SP ++ ++ ++ + -- -- -- -- -- -- -- -- -- + -- Me ++ -- MS -- ++ Me -- + -- Me ++ MS -- MS ++ Me -- + -- -- Me -- __ -- Me -- -- + ME -- -- -- ++ -- -- -- ME + -- ME -- ++ ++ ++ -- ME -- + ME -- -- ME -- ME -- -- ME + ++ ++ -- -- MB -- -- ++ ++ + ++ ++ -- ME -- ME -- ++ ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, \ +A10, B10, C10, D10, E10, F10, G10, H10, I10, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/c1.py b/alas_wrapped/campaign/event_20260226_cn/c1.py new file mode 100644 index 0000000000..485086cd3b --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/c1.py @@ -0,0 +1,84 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('C1') +MAP.shape = 'I8' +MAP.camera_data = ['E2', 'E6'] +MAP.camera_data_spawn_point = ['E2'] +MAP.map_data = """ + -- -- ME -- ME -- -- -- -- + -- ++ -- -- -- ME ++ ++ -- + -- ++ -- SP -- -- Me ++ -- + -- ME -- -- SP -- -- ME -- + ++ -- Me -- -- -- Me -- -- + -- ME ++ MS -- MS ++ ++ ++ + -- -- ME -- __ -- ME ++ -- + -- -- -- -- MB -- -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_CHAPTER_SWITCH_20241219 = True + STAGE_ENTRANCE = ['half', '20240725'] + MAP_HAS_MODE_SWITCH = True + STAGE_INCREASE_AB = True + MAP_WALK_USE_CURRENT_FLEET = True + MAP_SIREN_HAS_BOSS_ICON_SMALL = True + MAP_SWIPE_MULTIPLY = (1.179, 1.201) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.140, 1.161) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.107, 1.127) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/c2.py b/alas_wrapped/campaign/event_20260226_cn/c2.py new file mode 100644 index 0000000000..7879eb1226 --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/c2.py @@ -0,0 +1,74 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .c1 import Config as ConfigBase + +MAP = CampaignMap('C2') +MAP.shape = 'J7' +MAP.camera_data = ['D2', 'D5', 'E4'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + -- ME -- -- ME ++ ++ -- -- -- + MB -- ME -- -- -- ++ ME ME ++ + ++ -- ++ ++ Me -- Me -- -- ++ + -- -- ++ ++ -- __ -- -- ME -- + -- MS -- -- MS -- -- ME ++ -- + ME -- -- -- -- -- ME -- -- -- + ++ -- SP SP -- ++ ++ ++ -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom-left' + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/c3.py b/alas_wrapped/campaign/event_20260226_cn/c3.py new file mode 100644 index 0000000000..ffba353612 --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/c3.py @@ -0,0 +1,76 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .c1 import Config as ConfigBase + +MAP = CampaignMap('C3') +MAP.shape = 'H8' +MAP.camera_data = ['D4', 'D6', 'E3'] +MAP.camera_data_spawn_point = ['D4'] +MAP.map_data = """ + ++ ++ -- -- ME -- -- ME + ++ ++ MS -- -- -- ME -- + SP -- -- MS -- ++ -- -- + SP -- -- MS -- ++ ME -- + ++ Me __ -- -- -- -- -- + ++ ME -- Me ++ ++ ++ -- + -- -- -- -- Me -- ME -- + MB ++ Me ME -- ME -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ +A8, B8, C8, D8, E8, F8, G8, H8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/d1.py b/alas_wrapped/campaign/event_20260226_cn/d1.py new file mode 100644 index 0000000000..14139ff3a0 --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/d1.py @@ -0,0 +1,85 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('D1') +MAP.shape = 'I8' +MAP.camera_data = ['D3', 'D6', 'F3', 'F6'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ -- -- -- -- -- -- -- -- + ME -- ME ++ ++ ++ -- -- ++ + -- ME -- MS -- MS -- ME ++ + -- ++ MS -- -- -- Me -- ME + -- Me -- -- SP -- -- __ Me + -- -- -- SP -- -- Me -- -- + ++ ++ Me -- -- ++ -- MB -- + ++ ++ -- ME ME ++ ME -- ME +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_CHAPTER_SWITCH_20241219 = True + STAGE_ENTRANCE = ['half', '20240725'] + MAP_HAS_MODE_SWITCH = True + STAGE_INCREASE_AB = True + MAP_WALK_USE_CURRENT_FLEET = True + MAP_SIREN_HAS_BOSS_ICON_SMALL = True + MAP_SWIPE_MULTIPLY = (1.138, 1.160) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.101, 1.121) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.069, 1.088) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/d2.py b/alas_wrapped/campaign/event_20260226_cn/d2.py new file mode 100644 index 0000000000..c009cb3b79 --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/d2.py @@ -0,0 +1,85 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .d1 import Config as ConfigBase + +MAP = CampaignMap('D2') +MAP.shape = 'J8' +MAP.camera_data = ['D2', 'D6', 'G2', 'G6'] +MAP.camera_data_spawn_point = ['G2'] +MAP.map_data = """ + MB ++ ME ME ++ ++ ++ -- -- -- + -- -- -- -- ++ ME MS -- -- SP + -- ME ++ -- Me -- -- MS -- -- + -- -- Me -- -- __ MS -- -- SP + -- ME -- ++ -- -- -- -- -- -- + -- ++ ME -- Me -- Me ++ ++ ME + -- ME -- ME ++ -- -- ME ++ -- + -- -- -- -- ++ ME -- -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2, 'siren': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/d3.py b/alas_wrapped/campaign/event_20260226_cn/d3.py new file mode 100644 index 0000000000..cc783a8b7d --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/d3.py @@ -0,0 +1,91 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .d1 import Config as ConfigBase + +MAP = CampaignMap('D3') +MAP.shape = 'I10' +MAP.camera_data = ['D4', 'D6', 'D8', 'F4', 'F6', 'F8'] +MAP.camera_data_spawn_point = ['D2'] +MAP.map_data = """ + ++ ++ ++ SP -- SP ++ ++ ++ + -- -- -- -- -- -- -- -- -- + -- Me ++ -- MS -- ++ Me -- + -- Me ++ MS -- MS ++ Me -- + -- -- Me -- __ -- Me -- -- + ME -- -- -- ++ -- -- -- ME + -- ME -- ++ ++ ++ -- ME -- + ME -- -- ME -- ME -- -- ME + ++ ++ -- -- MB -- -- ++ ++ + ++ ++ -- ME -- ME -- ++ ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2, 'siren': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, \ +A10, B10, C10, D10, E10, F10, G10, H10, I10, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/event_20260226_cn/sp.py b/alas_wrapped/campaign/event_20260226_cn/sp.py new file mode 100644 index 0000000000..0352d024fd --- /dev/null +++ b/alas_wrapped/campaign/event_20260226_cn/sp.py @@ -0,0 +1,96 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('SP') +MAP.shape = 'H7' +MAP.camera_data = ['D5', 'E2', 'E5'] +MAP.camera_data_spawn_point = ['D2'] +MAP.map_data = """ + -- -- -- -- ++ -- ME -- + -- SP SP -- ++ ME -- ME + -- -- -- -- MS -- ME -- + ++ ++ ++ MS __ -- ++ ++ + -- ME -- -- -- MS -- ++ + ME -- ME -- ++ -- MB -- + -- ME -- ++ ++ ME -- ME +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 10, 'siren': 3}, + {'battle': 1}, + {'battle': 2}, + {'battle': 3}, + {'battle': 4}, + {'battle': 5}, + {'battle': 6}, + {'battle': 7, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = [] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + STAR_REQUIRE_1 = 0 + STAR_REQUIRE_2 = 0 + STAR_REQUIRE_3 = 0 + # ===== End of generated config ===== + + MAP_CHAPTER_SWITCH_20241219 = True + STAGE_ENTRANCE = ['half', '20240725'] + MAP_HAS_MODE_SWITCH = False + STAGE_INCREASE_AB = True + MAP_WALK_USE_CURRENT_FLEET = True + MAP_IS_ONE_TIME_STAGE = True + MAP_SIREN_HAS_BOSS_ICON_SMALL = True + MAP_SWIPE_MULTIPLY = (1.162, 1.184) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.124, 1.145) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.091, 1.111) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=2): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_7(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/a1.py b/alas_wrapped/campaign/war_archives_20230223_cn/a1.py new file mode 100644 index 0000000000..d8f320096a --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/a1.py @@ -0,0 +1,78 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('A1') +MAP.shape = 'H8' +MAP.camera_data = ['D2', 'D5', 'E2', 'E5'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ -- -- -- ME -- ME -- + -- MB -- ME ++ ME -- ++ + ME -- -- Me ++ Me -- ME + ME -- -- -- MS -- -- -- + -- ME Me -- -- __ ++ ++ + -- ++ ++ ++ Me -- MS ++ + -- -- Me -- -- -- -- -- + ++ -- -- SP SP -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1, 'boss': 1}, + {'battle': 4, 'enemy': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ +A8, B8, C8, D8, E8, F8, G8, H8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Joffre'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (0.993, 1.011) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.960, 0.978) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.932, 0.949) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_3(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/a2.py b/alas_wrapped/campaign/war_archives_20230223_cn/a2.py new file mode 100644 index 0000000000..82d23e153a --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/a2.py @@ -0,0 +1,76 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .a1 import Config as ConfigBase + +MAP = CampaignMap('A2') +MAP.shape = 'I7' +MAP.camera_data = ['F2', 'F5'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + ++ ++ -- Me -- ME ++ ++ ++ + ++ ++ Me -- -- -- -- MB -- + SP -- MS -- Me ME __ -- ME + -- -- MS ++ ++ ++ ME ME ++ + SP -- MS -- -- -- -- -- ++ + ++ -- -- -- ME Me ME -- -- + ++ -- Me ME -- ++ -- ME -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['LeMars'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.053, 1.073) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.018, 1.037) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.989, 1.006) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/a3.py b/alas_wrapped/campaign/war_archives_20230223_cn/a3.py new file mode 100644 index 0000000000..36747761c1 --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/a3.py @@ -0,0 +1,79 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .a1 import Config as ConfigBase + +MAP = CampaignMap('A3') +MAP.shape = 'I8' +MAP.camera_data = ['D2', 'F2', 'F5'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + ME -- ME -- -- -- -- ME -- + -- ME -- Me -- ME ME -- ++ + -- ++ ME __ -- ++ ++ ME ME + -- ++ MS -- MB ++ ++ -- -- + -- -- -- MS -- Me -- -- ME + -- SP -- -- MS -- -- ME ++ + -- -- SP -- Me -- ME -- ++ + -- -- -- -- ++ ++ ++ -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['LaGalissonniere'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.016, 1.035) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.982, 1.000) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.954, 0.971) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/b1.py b/alas_wrapped/campaign/war_archives_20230223_cn/b1.py new file mode 100644 index 0000000000..0fbf3ccba3 --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/b1.py @@ -0,0 +1,94 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('B1') +MAP.shape = 'I8' +MAP.camera_data = ['D3', 'D6', 'F3', 'F6'] +MAP.camera_data_spawn_point = ['D2'] +MAP.map_data = """ + ++ SP SP -- ++ -- -- ++ ++ + -- -- -- -- ++ ME ME ++ ++ + MS -- MS -- MS -- -- ME -- + -- Me ++ -- -- -- ME -- ME + -- ++ ++ Me __ Me ++ ++ -- + -- ME -- -- -- -- ++ ++ -- + ++ -- ME -- Me -- MB MB -- + ++ -- -- ME ++ -- -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2, 'boss': 1}, + {'battle': 5, 'enemy': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['CL', 'CA'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (80, 255 - 33), + 'width': (0.9, 10), + 'prominence': 10, + 'distance': 35, + } + MAP_ENEMY_GENRE_DETECTION_SCALING = { + 'DD': 1.111, + 'CL': 1.111, + 'CA': 1.111, + 'CV': 1.111, + 'BB': 1.111, + } + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' + MAP_WALK_USE_CURRENT_FLEET = True + MAP_SWIPE_MULTIPLY = (1.195, 1.217) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.156, 1.177) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.122, 1.142) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/b2.py b/alas_wrapped/campaign/war_archives_20230223_cn/b2.py new file mode 100644 index 0000000000..eed77a7215 --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/b2.py @@ -0,0 +1,80 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .b1 import Config as ConfigBase + +MAP = CampaignMap('B2') +MAP.shape = 'J8' +MAP.camera_data = ['D2', 'D6'] +MAP.camera_data_spawn_point = ['F3'] +MAP.map_data = """ + -- -- ++ ++ -- ME -- ME -- -- + -- ME ++ ++ Me -- MS ++ ++ -- + ME -- -- __ -- -- -- -- SP -- + -- ME Me -- ME MS ME -- -- ++ + -- ++ ++ -- ++ -- -- -- SP -- + -- ++ ++ -- Me -- MS ++ ++ -- + -- MB MB -- -- ME -- ++ ++ -- + ME -- -- ME ++ ++ ME -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['CA', 'BB'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.069, 1.089) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.034, 1.053) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.004, 1.022) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/b3.py b/alas_wrapped/campaign/war_archives_20230223_cn/b3.py new file mode 100644 index 0000000000..488f8dd137 --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/b3.py @@ -0,0 +1,83 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .b1 import Config as ConfigBase + +MAP = CampaignMap('B3') +MAP.shape = 'J9' +MAP.camera_data = ['E3', 'E7', 'F3', 'F7'] +MAP.camera_data_spawn_point = ['E7'] +MAP.map_data = """ + -- -- ++ -- -- -- -- ++ -- -- + -- -- -- ME ++ ++ ME -- -- -- + ++ ++ Me -- ++ ++ -- Me ++ ++ + ++ ++ -- -- MB MB -- -- ++ ++ + -- Me -- __ -- -- __ -- Me -- + -- -- ME MS ++ ++ MS ME -- -- + -- -- ME MS -- -- MS ME -- -- + ++ ME -- -- -- -- -- -- ME ++ + ++ -- ME ++ SP SP ++ ME -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, J9, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['CA', 'BB'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (0.969, 0.987) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.937, 0.955) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.910, 0.926) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/c1.py b/alas_wrapped/campaign/war_archives_20230223_cn/c1.py new file mode 100644 index 0000000000..2ea73237eb --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/c1.py @@ -0,0 +1,78 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('C1') +MAP.shape = 'H8' +MAP.camera_data = ['D2', 'D5', 'E2', 'E5'] +MAP.camera_data_spawn_point = ['D6'] +MAP.map_data = """ + ++ -- -- -- ME -- ME -- + -- MB -- ME ++ ME -- ++ + ME -- -- Me ++ Me -- ME + ME -- -- -- MS -- -- -- + -- ME Me -- -- __ ++ ++ + -- ++ ++ ++ Me -- MS ++ + -- -- Me -- -- -- -- -- + ++ -- -- SP SP -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ +A8, B8, C8, D8, E8, F8, G8, H8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['Joffre'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (0.993, 1.011) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.960, 0.978) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.932, 0.949) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/c2.py b/alas_wrapped/campaign/war_archives_20230223_cn/c2.py new file mode 100644 index 0000000000..88d37859fd --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/c2.py @@ -0,0 +1,76 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .c1 import Config as ConfigBase + +MAP = CampaignMap('C2') +MAP.shape = 'I7' +MAP.camera_data = ['F2', 'F5'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + ++ ++ -- Me -- ME ++ ++ ++ + ++ ++ Me -- -- -- -- MB -- + SP -- MS -- Me ME __ -- ME + -- -- MS ++ ++ ++ ME ME ++ + SP -- MS -- -- -- -- -- ++ + ++ -- -- -- ME Me ME -- -- + ++ -- Me ME -- ++ -- ME -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['LeMars'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.053, 1.073) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.018, 1.037) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.989, 1.006) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_4(self): + return self.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/c3.py b/alas_wrapped/campaign/war_archives_20230223_cn/c3.py new file mode 100644 index 0000000000..0cc604f43b --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/c3.py @@ -0,0 +1,80 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .c1 import Config as ConfigBase + +MAP = CampaignMap('C3') +MAP.shape = 'I8' +MAP.camera_data = ['D2', 'F2', 'F5'] +MAP.camera_data_spawn_point = ['D5'] +MAP.map_data = """ + ME -- ME -- -- -- -- ME -- + -- ME -- Me -- ME ME -- ++ + -- ++ ME __ -- ++ ++ ME ME + -- ++ MS -- MB ++ ++ -- -- + -- -- -- MS -- Me -- -- ME + -- SP -- -- MS -- -- ME ++ + -- -- SP -- Me -- ME -- ++ + -- -- -- -- ++ ++ ++ -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['LaGalissonniere'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.016, 1.035) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.982, 1.000) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.954, 0.971) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/d1.py b/alas_wrapped/campaign/war_archives_20230223_cn/d1.py new file mode 100644 index 0000000000..7eb18eab9e --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/d1.py @@ -0,0 +1,94 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('D1') +MAP.shape = 'I8' +MAP.camera_data = ['D3', 'D6', 'F3', 'F6'] +MAP.camera_data_spawn_point = ['D2'] +MAP.map_data = """ + ++ SP SP -- ++ -- -- ++ ++ + -- -- -- -- ++ ME ME ++ ++ + MS -- MS -- MS -- -- ME -- + -- Me ++ -- -- -- ME -- ME + -- ++ ++ Me __ Me ++ ++ -- + -- ME -- -- -- -- ++ ++ -- + ++ -- ME -- Me -- MB MB -- + ++ -- -- ME ++ -- -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, \ + = MAP.flatten() + + +class Config: + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['CA', 'BB'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (80, 255 - 33), + 'width': (0.9, 10), + 'prominence': 10, + 'distance': 35, + } + MAP_ENEMY_GENRE_DETECTION_SCALING = { + 'DD': 1.111, + 'CL': 1.111, + 'CA': 1.111, + 'CV': 1.111, + 'BB': 1.111, + } + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' + MAP_WALK_USE_CURRENT_FLEET = True + MAP_SWIPE_MULTIPLY = (1.195, 1.217) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.156, 1.177) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.122, 1.142) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/d2.py b/alas_wrapped/campaign/war_archives_20230223_cn/d2.py new file mode 100644 index 0000000000..4762ba4e9c --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/d2.py @@ -0,0 +1,89 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .d1 import Config as ConfigBase + +MAP = CampaignMap('D2') +MAP.shape = 'J8' +MAP.camera_data = ['D2', 'D6'] +MAP.camera_data_spawn_point = ['F3'] +MAP.map_data = """ + -- -- ++ ++ -- ME -- ME -- -- + -- ME ++ ++ Me -- MS ++ ++ -- + ME -- -- __ -- -- -- -- SP -- + -- ME Me -- ME MS ME -- -- ++ + -- ++ ++ -- ++ -- -- -- SP -- + -- ++ ++ -- Me -- MS ++ ++ -- + -- MB MB -- -- ME -- ++ ++ -- + ME -- -- ME ++ ++ ME -- -- -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2, 'siren': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['CA', 'BB', 'CV'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (1.069, 1.089) + MAP_SWIPE_MULTIPLY_MINITOUCH = (1.034, 1.053) + MAP_SWIPE_MULTIPLY_MAATOUCH = (1.004, 1.022) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/campaign/war_archives_20230223_cn/d3.py b/alas_wrapped/campaign/war_archives_20230223_cn/d3.py new file mode 100644 index 0000000000..a0a28dfb9e --- /dev/null +++ b/alas_wrapped/campaign/war_archives_20230223_cn/d3.py @@ -0,0 +1,92 @@ +from ..campaign_war_archives.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from .d1 import Config as ConfigBase + +MAP = CampaignMap('D3') +MAP.shape = 'J9' +MAP.camera_data = ['E3', 'E7', 'F3', 'F7'] +MAP.camera_data_spawn_point = ['E7'] +MAP.map_data = """ + -- -- ++ -- -- -- -- ++ -- -- + -- -- -- ME ++ ++ ME -- -- -- + ++ ++ Me -- ++ ++ -- Me ++ ++ + ++ ++ -- -- MB MB -- -- ++ ++ + -- Me -- __ -- -- __ -- Me -- + -- -- ME MS ++ ++ MS ME -- -- + -- -- ME MS -- -- MS ME -- -- + ++ ME -- -- -- -- -- -- ME ++ + ++ -- ME ++ SP SP ++ ME -- ++ +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2, 'siren': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1}, + {'battle': 6, 'boss': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, \ +A9, B9, C9, D9, E9, F9, G9, H9, I9, J9, \ + = MAP.flatten() + + +class Config(ConfigBase): + # ===== Start of generated config ===== + MAP_SIREN_TEMPLATE = ['CA', 'BB', 'CV'] + MOVABLE_ENEMY_TURN = (2,) + MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_MAP_STORY = True + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_MYSTERY = False + # ===== End of generated config ===== + + MAP_SWIPE_MULTIPLY = (0.969, 0.987) + MAP_SWIPE_MULTIPLY_MINITOUCH = (0.937, 0.955) + MAP_SWIPE_MULTIPLY_MAATOUCH = (0.910, 0.926) + + +class Campaign(CampaignBase): + MAP = MAP + ENEMY_FILTER = '1L > 1M > 1E > 1C > 2L > 2M > 2E > 2C > 3L > 3M > 3E > 3C' + + def battle_0(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=1): + return True + + return self.battle_default() + + def battle_5(self): + if self.clear_siren(): + return True + if self.clear_filter_enemy(self.ENEMY_FILTER, preserve=0): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_boss.clear_boss() diff --git a/alas_wrapped/config/PatrickCustom.json b/alas_wrapped/config/PatrickCustom.json index 0cb298b156..bfd54cc4f5 100644 --- a/alas_wrapped/config/PatrickCustom.json +++ b/alas_wrapped/config/PatrickCustom.json @@ -1,72 +1,4 @@ { - "Dashboard": { - "Oil": { - "Value": 15792, - "Limit": 17650, - "Color": "^000000", - "Record": "2026-02-04 21:16:13" - }, - "Coin": { - "Value": 0, - "Limit": 0, - "Color": "^FFAA33", - "Record": "2020-01-01 00:00:00" - }, - "Gem": { - "Value": 0, - "Color": "^FF3333", - "Record": "2020-01-01 00:00:00" - }, - "Pt": { - "Value": 3365, - "Color": "^00BFFF", - "Record": "2026-02-01 21:58:44" - }, - "Cube": { - "Value": 0, - "Color": "^33FFFF", - "Record": "2020-01-01 00:00:00" - }, - "ActionPoint": { - "Value": 43, - "Total": 1223, - "Color": "^0000FF", - "Record": "2026-02-04 22:26:41" - }, - "YellowCoin": { - "Value": 0, - "Color": "^FF8800", - "Record": "2020-01-01 00:00:00" - }, - "PurpleCoin": { - "Value": 0, - "Color": "^7700BB", - "Record": "2020-01-01 00:00:00" - }, - "Core": { - "Value": 0, - "Color": "^AAAAAA", - "Record": "2020-01-01 00:00:00" - }, - "Medal": { - "Value": 0, - "Color": "^FFDD00", - "Record": "2020-01-01 00:00:00" - }, - "Merit": { - "Value": 0, - "Color": "^FFFF00", - "Record": "2020-01-01 00:00:00" - }, - "GuildCoin": { - "Value": 0, - "Color": "^AAAAAA", - "Record": "2020-01-01 00:00:00" - }, - "Storage": { - "Storage": {} - } - }, "Alas": { "Emulator": { "Serial": "127.0.0.1:21513", @@ -86,8 +18,7 @@ "HandleError": true, "SaveError": true, "OnePushConfig": "provider: null", - "ScreenshotLength": 6, - "RestartOnUnknownPage": true + "ScreenshotLength": 6 }, "Optimization": { "ScreenshotInterval": 0.3, @@ -136,7 +67,7 @@ "Restart": { "Scheduler": { "Enable": true, - "NextRun": "2026-02-22 01:00:00", + "NextRun": "2026-03-04 01:00:00", "Command": "Restart", "SuccessInterval": 0, "FailureInterval": 0, @@ -441,7 +372,7 @@ }, "Campaign": { "Name": "D3", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -515,7 +446,7 @@ }, "Campaign": { "Name": "D3", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -593,7 +524,7 @@ }, "Campaign": { "Name": "dynamic", - "Event": "raid_20250116", + "Event": "raid_20260212", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -816,7 +747,7 @@ }, "Campaign": { "Name": "dynamic", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -894,7 +825,7 @@ }, "Campaign": { "Name": "dynamic", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -972,7 +903,7 @@ }, "Campaign": { "Name": "dynamic", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -1050,7 +981,7 @@ }, "Campaign": { "Name": "dynamic", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -1124,7 +1055,7 @@ }, "Campaign": { "Name": "sp", - "Event": "event_20231221_cn", + "Event": "event_20260226_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -1201,7 +1132,7 @@ }, "Campaign": { "Name": "dynamic", - "Event": "raid_20250116", + "Event": "raid_20260212", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, @@ -1285,7 +1216,7 @@ "Commission": { "Scheduler": { "Enable": true, - "NextRun": "2026-02-21 21:08:17", + "NextRun": "2026-03-03 14:02:30", "Command": "Commission", "SuccessInterval": "30-60", "FailureInterval": "30-60", @@ -1302,7 +1233,7 @@ }, "Tactical": { "Scheduler": { - "Enable": true, + "Enable": false, "NextRun": "2026-02-21 20:37:37", "Command": "Tactical", "SuccessInterval": "30-60", @@ -1399,8 +1330,8 @@ }, "Guild": { "Scheduler": { - "Enable": false, - "NextRun": "2026-02-08 19:00:00", + "Enable": true, + "NextRun": "2026-03-03 00:00:00", "Command": "Guild", "SuccessInterval": 30, "FailureInterval": 30, @@ -1530,8 +1461,8 @@ }, "ShopFrequent": { "Scheduler": { - "Enable": false, - "NextRun": "2020-01-01 00:00:00", + "Enable": true, + "NextRun": "2026-03-03 00:00:00", "Command": "ShopFrequent", "SuccessInterval": 30, "FailureInterval": 30, @@ -1550,8 +1481,8 @@ }, "ShopOnce": { "Scheduler": { - "Enable": false, - "NextRun": "2020-01-01 00:00:00", + "Enable": true, + "NextRun": "2026-03-03 00:00:00", "Command": "ShopOnce", "SuccessInterval": 30, "FailureInterval": 30, @@ -1595,8 +1526,8 @@ }, "Shipyard": { "Scheduler": { - "Enable": false, - "NextRun": "2020-01-01 00:00:00", + "Enable": true, + "NextRun": "2026-03-03 00:00:00", "Command": "Shipyard", "SuccessInterval": 30, "FailureInterval": 30, @@ -1964,8 +1895,8 @@ }, "OpsiCrossMonth": { "Scheduler": { - "Enable": true, - "NextRun": "2026-03-01 00:50:00", + "Enable": false, + "NextRun": "2026-04-01 00:50:00", "Command": "OpsiCrossMonth", "SuccessInterval": 0, "FailureInterval": 120, diff --git a/alas_wrapped/config/deploy.template.yaml b/alas_wrapped/config/deploy.template.yaml index 2b2d4635b5..a3b8dbce07 100644 --- a/alas_wrapped/config/deploy.template.yaml +++ b/alas_wrapped/config/deploy.template.yaml @@ -9,9 +9,9 @@ Deploy: # [Other] Use 'master', the stable branch Branch: master # Filepath of git executable `git.exe` - # [UV-managed] Use 'git' (system PATH) # [Easy installer] Use './toolkit/Git/mingw64/bin/git.exe' - GitExecutable: git + # [Other] Use you own git + GitExecutable: ./toolkit/Git/mingw64/bin/git.exe # Set git proxy # [CN user] Use your local http proxy (http://127.0.0.1:{port}) or socks5 proxy (socks5://127.0.0.1:{port}) # [Other] Use null @@ -21,23 +21,21 @@ Deploy: # [Other] Use false to when connected to an untrusted network SSLVerify: true # Update Alas at startup - # [UV-managed] Use false — we manage updates via git, not ALAS - # [Original] Use true - AutoUpdate: false + # [In most cases] Use true + AutoUpdate: true Python: # Filepath of python executable `python.exe` - # [UV-managed] Use './.venv/Scripts/python.exe' # [Easy installer] Use './toolkit/python.exe' - PythonExecutable: ./.venv/Scripts/python.exe + # [Other] Use you own python, and its version should be 3.7.6 64bit + PythonExecutable: ./toolkit/python.exe # URL of pypi mirror # [CN user] Use 'https://mirrors.aliyun.com/pypi/simple' for faster and more stable download # [Other] Use null PypiMirror: null # Install dependencies at startup - # [UV-managed] Use false — UV manages deps outside of ALAS - # [Original] Use true - InstallDependencies: false + # [In most cases] Use true + InstallDependencies: true # Path to requirements.txt # [In most cases] Use 'requirements.txt' # [In AidLux] Use './deploy/AidLux/{version}/requirements.txt', version is default to 0.92 @@ -45,9 +43,9 @@ Deploy: Adb: # Filepath of ADB executable `adb.exe` - # [UV-managed] Use 'adb' (system PATH) or './.venv/Lib/site-packages/adbutils/binaries/adb.exe' # [Easy installer] Use './toolkit/Lib/site-packages/adbutils/binaries/adb.exe' - AdbExecutable: adb + # [Other] Use you own latest ADB, but not the ADB in your emulator + AdbExecutable: ./toolkit/Lib/site-packages/adbutils/binaries/adb.exe # Whether to replace ADB # Chinese emulators (NoxPlayer, LDPlayer, MemuPlayer, MuMuPlayer) use their own ADB, instead of the latest. # Different ADB servers will terminate each other at startup, resulting in disconnection. diff --git a/alas_wrapped/config/template.json b/alas_wrapped/config/template.json index e93d9d3b5a..7ec2175f70 100644 --- a/alas_wrapped/config/template.json +++ b/alas_wrapped/config/template.json @@ -1,72 +1,4 @@ { - "Dashboard": { - "Oil": { - "Value": 0, - "Limit": 0, - "Color": "^000000", - "Record": "2020-01-01 00:00:00" - }, - "Coin": { - "Value": 0, - "Limit": 0, - "Color": "^FFAA33", - "Record": "2020-01-01 00:00:00" - }, - "Gem": { - "Value": 0, - "Color": "^FF3333", - "Record": "2020-01-01 00:00:00" - }, - "Pt": { - "Value": 0, - "Color": "^00BFFF", - "Record": "2020-01-01 00:00:00" - }, - "Cube": { - "Value": 0, - "Color": "^33FFFF", - "Record": "2020-01-01 00:00:00" - }, - "ActionPoint": { - "Value": 0, - "Total": 0, - "Color": "^0000FF", - "Record": "2020-01-01 00:00:00" - }, - "YellowCoin": { - "Value": 0, - "Color": "^FF8800", - "Record": "2020-01-01 00:00:00" - }, - "PurpleCoin": { - "Value": 0, - "Color": "^7700BB", - "Record": "2020-01-01 00:00:00" - }, - "Core": { - "Value": 0, - "Color": "^AAAAAA", - "Record": "2020-01-01 00:00:00" - }, - "Medal": { - "Value": 0, - "Color": "^FFDD00", - "Record": "2020-01-01 00:00:00" - }, - "Merit": { - "Value": 0, - "Color": "^FFFF00", - "Record": "2020-01-01 00:00:00" - }, - "GuildCoin": { - "Value": 0, - "Color": "^AAAAAA", - "Record": "2020-01-01 00:00:00" - }, - "Storage": { - "Storage": {} - } - }, "Alas": { "Emulator": { "Serial": "auto", @@ -737,7 +669,7 @@ }, "Campaign": { "Name": "D3", - "Event": "war_archives_20221222_cn", + "Event": "war_archives_20230223_cn", "Mode": "normal", "UseClearMode": true, "UseFleetLock": true, diff --git a/alas_wrapped/deploy/Readme.md b/alas_wrapped/deploy/Readme.md index bbdc40adc5..0a471e702a 100644 --- a/alas_wrapped/deploy/Readme.md +++ b/alas_wrapped/deploy/Readme.md @@ -10,7 +10,5 @@ Install Alas by running `python -m deploy.installer` in Alas root folder. Launcher `Alas.exe` is a `.bat` file converted to `.exe` file by [Bat To Exe Converter](https://f2ko.de/programme/bat-to-exe-converter/). -If you have warnings from your anti-virus software, replace `alas.exe` with `deploy/launcher/Alas.bat`. - -In this monorepo, `deploy/launcher/Alas.bat` is a compatibility wrapper that delegates to repository-root `start_alas.bat` (canonical launcher). +If you have warnings from your anti-virus software, replace `alas.exe` with `deploy/launcher/Alas.bat`. They should do the same thing. diff --git a/alas_wrapped/deploy/Windows/template.yaml b/alas_wrapped/deploy/Windows/template.yaml index d266d308e7..1dc91ea9e3 100644 --- a/alas_wrapped/deploy/Windows/template.yaml +++ b/alas_wrapped/deploy/Windows/template.yaml @@ -21,24 +21,21 @@ Deploy: # [Other] Use false to when connected to an untrusted network SSLVerify: true # Update Alas at startup - # [UV-managed] Use false — we manage updates via git, not ALAS - # [Original] Use true - AutoUpdate: false + # [In most cases] Use true + AutoUpdate: true Python: # Filepath of python executable `python.exe` - # [UV-managed] Use './.venv/Scripts/python.exe' # [Easy installer] Use './toolkit/python.exe' - # [Other] Use your own python - PythonExecutable: './.venv/Scripts/python.exe' + # [Other] Use you own python, and its version should be 3.7.6 64bit + PythonExecutable: './toolkit/python.exe' # URL of pypi mirror # [CN user] Use 'https://pypi.tuna.tsinghua.edu.cn/simple' for faster and more stable download # [Other] Use null PypiMirror: null # Install dependencies at startup - # [UV-managed] Use false — UV manages deps outside of ALAS - # [Original] Use true - InstallDependencies: false + # [In most cases] Use true + InstallDependencies: true # Path to requirements.txt # [In most cases] Use 'requirements.txt' # [In AidLux] Use './deploy/AidLux/{version}/requirements.txt', version is default to 0.92 @@ -46,9 +43,9 @@ Deploy: Adb: # Filepath of ADB executable `adb.exe` - # [UV-managed] Use 'adb' (system PATH) or './.venv/Lib/site-packages/adbutils/binaries/adb.exe' # [Easy installer] Use './toolkit/Lib/site-packages/adbutils/binaries/adb.exe' - AdbExecutable: 'adb' + # [Other] Use you own latest ADB, but not the ADB in your emulator + AdbExecutable: './toolkit/Lib/site-packages/adbutils/binaries/adb.exe' # Whether to replace ADB # Chinese emulators (NoxPlayer, LDPlayer, MemuPlayer, MuMuPlayer) use their own ADB, instead of the latest. # Different ADB servers will terminate each other at startup, resulting in disconnection. diff --git a/alas_wrapped/deploy/launcher/Alas.bat b/alas_wrapped/deploy/launcher/Alas.bat index b7c4153ec9..44ca4841b8 100644 --- a/alas_wrapped/deploy/launcher/Alas.bat +++ b/alas_wrapped/deploy/launcher/Alas.bat @@ -1,16 +1,22 @@ +@rem @echo off -setlocal -for %%I in ("%~dp0..\..\..") do set "REPO_ROOT=%%~fI" -set "CANONICAL_LAUNCHER=%REPO_ROOT%\start_alas.bat" +set "_root=%~dp0" +set "_root=%_root:~0,-1%" +cd "%_root%" +echo "%_root% -if not exist "%CANONICAL_LAUNCHER%" ( - echo [ERROR] Canonical launcher not found: %CANONICAL_LAUNCHER% - echo Use repository-root start_alas.bat as the single entrypoint. - exit /b 1 -) +color F0 + +set "_pyBin=%_root%\toolkit" +set "_GitBin=%_root%\toolkit\Git\mingw64\bin" +set "_adbBin=%_root%\toolkit\Lib\site-packages\adbutils\binaries" +set "PATH=%_root%\toolkit\alias;%_root%\toolkit\command;%_pyBin%;%_pyBin%\Scripts;%_GitBin%;%_adbBin%;%PATH%" -echo [INFO] Compatibility wrapper: alas_wrapped\deploy\launcher\Alas.bat -echo [INFO] Delegating to canonical launcher: %CANONICAL_LAUNCHER% -call "%CANONICAL_LAUNCHER%" %* -exit /b %ERRORLEVEL% +title Alas Updater +python -m deploy.installer +if %errorlevel% neq 0 ( + pause >nul +) else ( + start "Alas" "%_root%\toolkit\webapp\alas.exe" +) diff --git a/alas_wrapped/dev_tools/alas2.bat b/alas_wrapped/dev_tools/alas2.bat new file mode 100644 index 0000000000..fb3524a56f --- /dev/null +++ b/alas_wrapped/dev_tools/alas2.bat @@ -0,0 +1,883 @@ +@echo off +rem @SETLOCAL EnableExtensions EnableDelayedExpansion +pushd "%~dp0" +set ver=2.7 +title Alas Run Tool %ver% +:: ----------------------------------------------------------------------------- +rem :check_Permissions +rem echo Administrative permissions required. Detecting permissions... +rem net session >nul 2>&1 +rem if %errorLevel% == 0 ( +rem echo Success: Administrative permissions confirmed. +rem echo Press any to continue... +rem pause >nul +rem call :continue +rem ) else ( +rem echo Failure: Current permissions inadequate. +rem ) +rem pause >nul +:: ----------------------------------------------------------------------------- +:continue +set ALAS_PATH=%~dp0 +:: ----------------------------------------------------------------------------- +set ADB=%ALAS_PATH%toolkit\Lib\site-packages\adbutils\binaries\adb.exe +set PYTHON=%ALAS_PATH%toolkit\python.exe +set GIT=%ALAS_PATH%toolkit\Git\cmd\git.exe +set LMESZINC=https://github.com/LmeSzinc/AzurLaneAutoScript.git +set WHOAMIKYO=https://github.com/whoamikyo/AzurLaneAutoScript.git +set ALAS_ENV=https://github.com/whoamikyo/alas-env.git +set ALAS_ENV_GITEE=https://gitee.com/lmeszinc/alas-env.git +set GITEE_URL=https://gitee.com/lmeszinc/AzurLaneAutoScript.git +set ADB_P=%ALAS_PATH%config\adb_port.ini +set CURL=%ALAS_PATH%toolkit\Git\mingw64\bin\curl.exe +set API_JSON=%ALAS_PATH%log\api_git.json +set config=%~dp0config\alas.ini +set configtemp=%~dp0config\alastemp.ini +set template=%~dp0config\template.ini +set git_log="%GIT% log --pretty=format:%%H%%n%%aI -1" +:: ----------------------------------------------------------------------------- +:first_run +if exist %~dp0config\alas.ini set first_run=1 +if defined first_run ( + call :is_using_git +) else ( + call :not_using_git +) +:: ----------------------------------------------------------------------------- +set using_git= +if exist ".git\" set using_git=1 +if defined using_git ( + call :is_using_git +) else ( + call :not_using_git +) +:: ----------------------------------------------------------------------------- +:is_using_git +setlocal enabledelayedexpansion +for /f "delims=" %%a in (!config!) do ( + set line=%%a + if "x!line:~0,15!"=="xgithub_token = " ( + set github_token=!line:~15! + + ) +) +:: ----------------------------------------------------------------------------- +:bypass_first_run +rem %CURL% -s https://api.github.com/repos/lmeszinc/AzurLaneAutoScript/git/refs/heads/master?access_token=!github_token! > %~dp0log\api_git.json +%CURL% -s https://api.github.com/repos/lmeszinc/AzurLaneAutoScript/commits/master?access_token=!github_token! > %~dp0log\api_git.json +endlocal +rem for /f "skip=5 tokens=2 delims=:," %%I IN (%API_JSON%) DO IF NOT DEFINED sha SET sha=%%I +rem set sha=%sha:"=% +rem set sha=%sha: =% +for /f "skip=1 tokens=2 delims=:," %%I IN (%API_JSON%) DO IF NOT DEFINED sha SET sha=%%I +set sha=%sha:"=% +set sha=%sha: =% +for /f "skip=14 tokens=3 delims=:" %%I IN (%API_JSON%) DO IF NOT DEFINED message SET message=%%I +set message=%message:"=% +set message=%message:,=% +for /f %%i in ('git rev-parse --abbrev-ref HEAD') do set BRANCH=%%i +for /f "delims=" %%i IN ('%GIT% log -1 "--pretty=%%H"') DO set LAST_LOCAL_GIT=%%i +for /f "tokens=1,2" %%A in ('%GIT% log -1 "--format=%%h %%ct" -- .') do ( + set GIT_SHA1=%%A + call :gmTime GIT_CTIME %%B +) +:: ----------------------------------------------------------------------------- +:time_parsed +if %LAST_LOCAL_GIT% == %sha% ( + echo ---------------------------------------------------------------- + echo Remote Git hash: %sha% + echo Remote Git message: %message% + echo ---------------------------------------------------------------- + echo Local Git hash: %LAST_LOCAL_GIT% + echo Local commit date: %GIT_CTIME% + echo Local Branch: %BRANCH% + echo ---------------------------------------------------------------- + echo your ALAS is updated + echo Press any to continue... + pause > NUL + call :adb_kill +) else ( + echo ---------------------------------------------------------------- + echo Remote Git hash: %sha% + echo Remote Git message: %message% + echo ---------------------------------------------------------------- + echo Local Git hash: %LAST_LOCAL_GIT% + echo Local commit date: %GIT_CTIME% + echo Local Branch: %BRANCH% + echo ---------------------------------------------------------------- + popup.exe + choice /t 10 /c yn /d y /m "There is an update for ALAS. Download now?" + if errorlevel 2 call :adb_kill + if errorlevel 1 call :choose_update_mode +) +:: ----------------------------------------------------------------------------- +:not_using_git +set TOOLKIT_GIT=%~dp0toolkit\.git +if not exist %TOOLKIT_GIT% ( + echo You may need to update your dependencies + echo Press any key to update + pause > NUL + call :toolkit_choose +) else ( + call :adb_kill +) +:: ----------------------------------------------------------------------------- +:adb_kill +cls +call %ADB% kill-server > nul 2>&1 +:: ----------------------------------------------------------------------------- +set SCREENSHOT_FOLDER=%~dp0screenshots +if not exist %SCREENSHOT_FOLDER% ( + mkdir %SCREENSHOT_FOLDER% +) +:: ----------------------------------------------------------------------------- +:: if config\adb_port.ini dont exist, will be created + if not exist %ADB_P% ( + cd . > %ADB_P% + ) +:: ----------------------------------------------------------------------------- +:prompt +REM if adb_port is empty, prompt HOST:PORT +set adb_empty=%~dp0config\adb_port.ini +for %%A in (%adb_empty%) do if %%~zA==0 ( + echo Enter your HOST:PORT eg: 127.0.0.1:5555 for default bluestacks + echo If you misstype, you can edit the file in config/adb_port.ini + set /p adb_input= + ) +:: ----------------------------------------------------------------------------- +REM if adb_input = 0 load from adb_port.ini +:adb_input +if [%adb_input%]==[] ( + call :CHECK_BST_BETA + ) else ( + REM write adb_input on adb_port.ini + echo %adb_input% >> %ADB_P% + call :FINDSTR +) +:: ----------------------------------------------------------------------------- +:: Will search for 127.0.0.1:62001 and replace for %ADB_PORT% +:FINDSTR +REM setlocal enableextensions disabledelayedexpansion +set search=127.0.0.1:62001 +set replace=%adb_input% + +for /f "delims=" %%i in ('type "%template%" ^& break ^> "%template%" ') do ( + set line=%%i + setlocal enabledelayedexpansion + >>"%template%" echo(!line:%search%=%replace%! + endlocal + ) +) +call :CHECK_BST_BETA +:: ----------------------------------------------------------------------------- +:CHECK_BST_BETA +reg query HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv >nul +if %errorlevel% equ 0 ( + echo ------------------------------------------------------------------------------------------ + choice /t 10 /c yn /d n /m "Bluestacks Hyper-V BETA detected, would you like to use realtime_connection mode?" + echo ------------------------------------------------------------------------------------------ + if errorlevel 2 call :load + if errorlevel 1 call :realtime_connection +) else ( + call :load +) +:: ----------------------------------------------------------------------------- +:realtime_connection +ECHO. Connecting with realtime mode ... +for /f "tokens=3" %%a in ('reg query HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\Android\Config /v BstAdbPort') do (set /a port = %%a) +set SERIAL_REALTIME=127.0.0.1:%port% +echo ---------------------------------------------------------------- +echo connecting at %SERIAL_REALTIME% +call %ADB% connect %SERIAL_REALTIME% +echo ---------------------------------------------------------------- +call :replace_serial +:: ----------------------------------------------------------------------------- +:replace_serial +set config=%~dp0config\alas.ini +setlocal enabledelayedexpansion +for /f "delims=" %%i in (!config!) do ( + set line=%%i + if "x!line:~0,9!"=="xserial = " ( + set serial=!line:~9! + ) +) +set search=%serial% +set replace=%SERIAL_REALTIME% +echo ---------------------------------------------------------------- +echo Old Serial: %serial% +echo New Serial: %SERIAL_REALTIME% +echo ---------------------------------------------------------------- +echo Press any to continue... +pause > NUL +for /f "delims=" %%i in ('type "%config%" ^& break ^> "%config%" ') do ( + set line=%%i + >>"%config%" echo(!line:%search%=%replace%! + ) +) +endlocal +call :init +:: ----------------------------------------------------------------------------- +:: Deprecated +REM set /a search=104 +REM set replace=serial = %SERIAL_REALTIME% +REM (for /f "tokens=1*delims=:" %%a IN ('findstr /n "^" "%config%"') do ( +REM set Line=%%b +REM IF %%a equ %search% set Line=%replace% +REM setlocal enabledelayedexpansion +REM ECHO(!Line! +REM endlocal +REM ))> %~dp0config\alastemp.ini +REM pause +REM del %config% +REM MOVE %configtemp% %config% +REM ) +:: ----------------------------------------------------------------------------- +:load +if defined first_run ( + call :load_alas +) else ( + call :load_input_serial +) +:: ----------------------------------------------------------------------------- +:load_alas +set config=%~dp0config\alas.ini +setlocal enabledelayedexpansion +for /f "delims=" %%i in (!config!) do ( + set line=%%i + if "x!line:~0,9!"=="xserial = " ( + set serial=!line:~9! + ) +) +call :load_alas_serial +:: ----------------------------------------------------------------------------- +:load_input_serial +echo ---------------------------------------------------------------- +echo connecting at %adb_input% +call %ADB% connect %adb_input% +echo ---------------------------------------------------------------- +call :init +:: ----------------------------------------------------------------------------- +:load_alas_serial +echo ---------------------------------------------------------------- +echo connecting at !serial! +call !ADB! connect !serial! +echo ---------------------------------------------------------------- +call :init +:: ----------------------------------------------------------------------------- +endlocal +:: ----------------------------------------------------------------------------- +:: Deprecated +REM Load adb_port.ini +REM +REM set /p ADB_PORT=<%ADB_P% +REM echo connecting at %ADB_PORT% +REM call %ADB% connect %ADB_PORT% +:: ----------------------------------------------------------------------------- +:init +echo ---------------------------------------------------------------- +echo initializing uiautomator2 +call %PYTHON% -m uiautomator2 init +echo ---------------------------------------------------------------- +echo Press any to continue... +pause > NUL +:: uncomment the pause to catch errors +REM pause +call :alas +:: ----------------------------------------------------------------------------- + +:alas + cls + echo. + echo :: Alas run + echo. + echo Choose your option + echo. + echo 1. EN + echo 2. CN + echo 3. JP + echo 4. UPDATER + echo. + echo :: Type a 'number' and press ENTER + echo :: Type 'exit' to quit + echo. + set /P menu= || Set menu=Nothing + if %menu%==1 call :en + if %menu%==2 call :cn + if %menu%==3 call :jp + if %menu%==4 call :choose_update_mode + if %menu%==exit call :EOF + if %menu%==Nothing call :alas + else ( + cls + echo. + echo :: Incorrect Input Entered + echo. + echo Please type a 'number' or 'exit' + echo Press any key to retry to the menu... + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:en + call %PYTHON% --version >nul + if %errorlevel% == 0 ( + echo ---------------------------------------------------------------- + echo Python Found in %PYTHON% Proceeding.. + echo Opening alas_en.pyw in %ALAS_PATH% + call %PYTHON% alas_en.pyw + pause > NUL + call :alas + ) else ( + echo :: it was not possible to open alas_en.pyw, make sure you have a folder toolkit + echo :: inside AzurLaneAutoScript folder. + echo Alas PATH: %ALAS_PATH% + echo Python Path: %PYTHON% + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:cn + call %PYTHON% --version >nul + if %errorlevel% == 0 ( + echo ---------------------------------------------------------------- + echo Python Found in %PYTHON% Proceeding.. + echo Opening alas_en.pyw in %ALAS_PATH% + call %PYTHON% alas_cn.pyw + pause > NUL + call :alas + ) else ( + echo :: it was not possible to open alas_cn.pyw, make sure you have a folder toolkit + echo :: inside AzurLaneAutoScript folder. + echo Alas PATH: %ALAS_PATH% + echo Python Path: %PYTHON% + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:jp + call %PYTHON% --version >nul + if %errorlevel% == 0 ( + echo ---------------------------------------------------------------- + echo Python Found in %PYTHON% Proceeding.. + echo Opening alas_en.pyw in %ALAS_PATH% + call %PYTHON% alas_jp.pyw + pause > NUL + call :alas + ) else ( + echo :: it was not possible to open alas_jp.pyw, make sure you have a folder toolkit + echo :: inside AzurLaneAutoScript folder. + echo Alas PATH: %ALAS_PATH% + echo Python Path: %PYTHON% + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:updater_menu + cls + echo. + echo :: This update only will work if you downloaded ALAS on + echo :: Release tab and installed with Easy_Install-v2.bat + echo. + echo ::Overwrite local changes:: + echo. + echo. + echo 1) https://github.com/LmeSzinc/AzurLaneAutoScript (Main Repo, When in doubt, use it) + echo 2) https://github.com/whoamikyo/AzurLaneAutoScript (Mirrored Fork) + echo 3) https://github.com/whoamikyo/AzurLaneAutoScript (nightly build, dont use) + echo 4) https://gitee.com/lmeszinc/AzurLaneAutoScript.git (Recommended for CN users) + echo 5) https://github.com/LmeSzinc/AzurLaneAutoScript (Dev build, use only if you know what you are doing) + echo 6) Toolkit tools updater + echo 7) Back to main menu + echo. + echo :: Type a 'number' and press ENTER + echo :: Type 'exit' to quit + echo. + set /P choice= + if %choice%==1 call :LmeSzinc + if %choice%==2 call :whoamikyo + if %choice%==3 call :nightly + if %choice%==4 call :gitee + if %choice%==5 call :LmeSzincD + if %choice%==6 call :toolkit_updater + if %choice%==7 call :alas + if %choice%==exit call :EOF + else ( + cls + echo. + echo :: Incorrect Input Entered + echo. + echo Please type a 'number' or 'exit' + echo Press any key to return to the menu... + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:update_menu_local + cls + echo. + echo :: This update only will work if you downloaded ALAS on + echo :: Release tab and installed with Easy_Install-v2.bat + echo. + echo ::Keep local changes:: + echo. + echo. + echo 1) https://github.com/LmeSzinc/AzurLaneAutoScript (Main Repo, When in doubt, use it) + echo 2) https://github.com/whoamikyo/AzurLaneAutoScript (Mirrored Fork) + echo 3) https://github.com/whoamikyo/AzurLaneAutoScript (nightly build, dont use) + echo 4) https://gitee.com/lmeszinc/AzurLaneAutoScript.git (Recommended for CN users) + echo 5) Back to main menu + echo. + echo :: Type a 'number' and press ENTER + echo :: Type 'exit' to quit + echo. + set /P choice= + if %choice%==1 call :LmeSzinc_local + if %choice%==2 call :whoamikyo_local + if %choice%==3 call :nightly_local + if %choice%==4 call :gitee_local + if %choice%==5 call :alas + if %choice%==exit call :EOF + else ( + cls + echo. + echo :: Incorrect Input Entered + echo. + echo Please type a 'number' or 'exit' + echo Press any key to return to the menu... + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:LmeSzinc + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from LmeSzinc repository.. + call %GIT% fetch origin master + call %GIT% reset --hard origin/master + call %GIT% pull --ff-only origin master + echo DONE! + echo Press any key to proceed + pause > NUL + call :updater_menu + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:LmeSzincD + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from LmeSzinc Dev branch.. + call %GIT% fetch origin dev + call %GIT% reset --hard origin/dev + call %GIT% pull --ff-only origin dev + echo DONE! + echo Press any key to proceed + pause > NUL + call :updater_menu + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:whoamikyo + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from whoamikyo repository.. + call %GIT% fetch whoamikyo master + call %GIT% reset --hard whoamikyo/master + call %GIT% pull --ff-only whoamikyo master + echo DONE! + echo Press any key to proceed + pause > NUL + call :updater_menu + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:nightly + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from whoamikyo nightly repository.. + call %GIT% fetch whoamikyo nightly + call %GIT% reset --hard whoamikyo/nightly + call %GIT% pull --ff-only whoamikyo nightly + echo Press any key to proceed + pause > NUL + call :alas + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:gitee + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from LmeSzinc repository.. + call %GIT% fetch lmeszincgitee master + call %GIT% reset --hard lmeszincgitee/master + call %GIT% pull --ff-only lmeszincgitee master + echo DONE! + echo Press any key to proceed + pause > NUL + call :updater_menu + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +rem :check_connection +rem cls +rem echo. +rem echo :: Checking For Internet Connection to Github... +rem echo. +rem timeout /t 2 /nobreak > NUL + +rem ping -n 1 google.com -w 20000 >nul + +rem if %errorlevel% == 0 ( +rem echo You have a good connection with Github! Proceeding... +rem echo press any to proceed +rem pause > NUL +rem call updater_menu +rem ) else ( +rem echo :: You don't have a good connection out of China +rem echo :: It might be better to update using Gitee +rem echo :: Redirecting... +rem echo. +rem echo Press any key to continue... +rem pause > NUL +rem call start_gitee +rem ) +:: ----------------------------------------------------------------------------- +rem Keep local changes +:: ----------------------------------------------------------------------------- +:choose_update_mode + cls + echo. + echo. + echo ::Choose update method:: + echo. + echo 1) Overwrite local changes (Will undo any local changes) + echo 2) Keep local changes (Useful if you have customized a map) + echo 3) Back to main menu + echo. + echo :: Type a 'number' and press ENTER + echo :: Type 'exit' to quit + echo. + set /P choice= + if %choice%==1 call :updater_menu + if %choice%==2 call :update_menu_local + if %choice%==3 call :alas + if %choice%==exit call EOF + else ( + cls + echo. + echo :: Incorrect Input Entered + echo. + echo Please type a 'number' or 'exit' + echo Press any key to return to the menu... + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:LmeSzinc_local + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from LmeSzinc repository.. + call %GIT% stash + call %GIT% pull origin master + call %GIT% stash pop + echo DONE! + echo Press any key to proceed + pause > NUL + call :update_menu_local + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:whoamikyo_local + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from whoamikyo repository.. + call %GIT% stash + call %GIT% pull whoamikyo master + call %GIT% stash pop + echo DONE! + echo Press any key to proceed + pause > NUL + call :update_menu_local + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:nightly_local + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from whoamikyo nightly repository.. + call %GIT% stash + call %GIT% pull whoamikyo nightly + call %GIT% stash pop + echo Press any key to proceed + pause > NUL + call :update_menu_local + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:gitee_local + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating from LmeSzinc repository.. + call %GIT% stash + call %GIT% pull lmeszincgitee master + call %GIT% stash pop + echo DONE! + echo Press any key to proceed + pause > NUL + call :update_menu_local + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:toolkit_choose + cls + echo. + echo :: This will add the toolkit repository for updating + echo. + echo ::Toolkit:: + echo. + echo. + echo 1) https://github.com/whoamikyo/alas-env.git (Default Github) + echo 2) https://gitee.com/lmeszinc/alas-env.git (Recommended for CN users) + echo 3) Back to main menu + echo. + echo :: Type a 'number' and press ENTER + echo :: Type 'exit' to quit + echo. + set /P choice= + if %choice%==1 call :toolkit_github + if %choice%==2 call :toolkit_gitee + if %choice%==3 call :alas + if %choice%==exit call :EOF + else ( + cls + echo. + echo :: Incorrect Input Entered + echo. + echo Please type a 'number' or 'exit' + echo Press any key to return to the menu... + echo. + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:toolkit_github + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating toolkit.. + call cd toolkit + echo ## initializing toolkit.. + call %GIT% init + call %GIT% config --global core.autocrlf false + echo ## Adding files + echo ## This process may take a while + call %GIT% add -A + echo ## adding origin.. + call %GIT% remote add origin %ALAS_ENV% + echo Fething... + call %GIT% fetch origin master + call %GIT% reset --hard origin/master + echo Pulling... + call %GIT% pull --ff-only origin master + call cd .. + echo DONE! + echo Press any key to proceed + pause > NUL + call :adb_kill + ) else ( + echo :: Git not found, maybe there was an installation issue + echo :: check if you have this directory %GIT% + pause > NUL + call :adb_kill + ) +:: ----------------------------------------------------------------------------- +:toolkit_gitee + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating toolkit.. + call cd toolkit + echo ## initializing toolkit.. + call %GIT% init + call %GIT% config --global core.autocrlf false + echo ## Adding files + echo ## This process may take a while + call %GIT% add -A + echo ## adding origin.. + call %GIT% remote add origin %ALAS_ENV_GITEE% + echo Fething... + call %GIT% fetch origin master + call %GIT% reset --hard origin/master + echo Pulling... + call %GIT% pull --ff-only origin master + call cd .. + echo DONE! + echo Press any key to proceed + pause > NUL + call :adb_kill + ) else ( + echo :: Git not found, maybe there was an installation issue + echo :: check if you have this directory %GIT% + pause > NUL + call :adb_kill + ) +:: ----------------------------------------------------------------------------- +:toolkit_updater + call %GIT% --version >nul + if %errorlevel% == 0 ( + echo GIT Found in %GIT% Proceeding + echo Updating toolkit.. + call cd toolkit + call %GIT% fetch origin master + call %GIT% reset --hard origin/master + echo Pulling... + call %GIT% pull --ff-only origin master + echo DONE! + call cd .. + echo Press any key to proceed + pause > NUL + call :updater_menu + ) else ( + echo :: Git not detected, maybe there was an installation issue + echo check if you have this directory: + echo AzurLaneAutoScript\toolkit\Git\cmd + pause > NUL + call :alas + ) +:: ----------------------------------------------------------------------------- +:gmtime +setlocal +set /a z=%2/86400+719468,d=z%%146097,y=^(d-d/1460+d/36525-d/146096^)/365,d-=365*y+y/4-y/100,m=^(5*d+2^)/153 +set /a d-=^(153*m+2^)/5-1,y+=z/146097*400+m/11,m=^(m+2^)%%12+1 +set /a h=%2/3600%%24,mi=%2%%3600/60,s=%2%%60 +if %m% lss 10 set m=0%m% +if %d% lss 10 set d=0%d% +if %h% lss 10 set h=0%h% +if %mi% lss 10 set mi=0%mi% +if %s% lss 10 set s=0%s% +endlocal & set %1=%y%-%m%-%d% %h%:%mi%:%s% +call :time_parsed +:: ----------------------------------------------------------------------------- +rem :git_update_checker +rem %CURL% -s https://api.github.com/repos/lmeszinc/AzurLaneAutoScript/git/refs/heads/master?access_token=%github_token% > %~dp0log\API_GIT.json +rem FOR /f "skip=5 tokens=2 delims=:," %%I IN (%API_JSON%) DO IF NOT DEFINED sha SET sha=%%I +rem set sha=%sha:"=% +rem set sha=%sha: =% +rem FOR /F "delims=" %%i IN ('%GIT% log -1 "--pretty=%%H"') DO set LAST_LOCAL_GIT=%%i +:: ----------------------------------------------------------------------------- +:: ----------------------------------------------------------------------------- +rem if %LAST_LOCAL_GIT% equ %sha% SET run_update=1 +rem call :alas + +:: ----------------------------------------------------------------------------- +::Add paths +rem call :AddPath %ALAS_PATH% +rem call :AddPath %ADB% +rem call :AddPath %PYTHON% +rem call :AddPath %GIT% + +rem :UpdateEnv +rem ECHO Making updated PATH go live . . . +rem REG delete HKCU\Environment /F /V TEMPVAR > nul 2>&1 +rem setx TEMPVAR 1 > nul 2>&1 +rem REG delete HKCU\Environment /F /V TEMPVAR > nul 2>&1 +:: ----------------------------------------------------------------------------- +rem :AddPath +rem ECHO %PATH% | FINDSTR /C:"%~1" > nul +rem IF ERRORLEVEL 1 ( +rem REG add "HKLM\SYSTEM\CurrentControlset\Control\Session Manager\Environment" /f /v PATH /t REG_SZ /d "%PATH%;%~1" >> add-paths-detail.log +rem IF ERRORLEVEL 0 ( +rem ECHO Adding %1 . . . Success! >> add-paths.log +rem set "PATH=%PATH%;%~1" +rem rem set UPDATE=1 +rem ) ELSE ( +rem ECHO Adding %1 . . . FAILED. Run this script with administrator privileges. >> add-paths.log +rem ) +rem ) ELSE ( +rem ECHO Skipping %1 - Already in PATH >> add-paths.log +rem ) +:: ----------------------------------------------------------------------------- +rem :AddPath +rem ECHO %PATH% | FINDSTR /C:"%~1" > nul +rem IF ERRORLEVEL 1 ( +rem REG add "HKLM\SYSTEM\CurrentControlset\Control\Session Manager\Environment" /f /v PATH /t REG_SZ /d "%PATH%;%~1" > nul 2>&1 +rem IF ERRORLEVEL 0 ( +rem ECHO Adding %1 . . . Success! +rem set "PATH=%PATH%;%~1" +rem set UPDATE=1 +rem ) ELSE ( +rem ECHO Adding %1 . . . FAILED. Run this script with administrator privileges. +rem ) +rem ) ELSE ( +rem ECHO Skipping %1 - Already in PATH +rem ) +:: ----------------------------------------------------------------------------- +:EOF +echo Exiting +pause +exit diff --git a/alas_wrapped/module/base/template.py b/alas_wrapped/module/base/template.py index 53481f30a2..6a16533255 100644 --- a/alas_wrapped/module/base/template.py +++ b/alas_wrapped/module/base/template.py @@ -7,7 +7,6 @@ from module.base.resource import Resource from module.base.utils import * from module.config.server import VALID_SERVER -from module.logger import logger from module.map_detection.utils import Points @@ -115,61 +114,6 @@ def size(self): else: return self.image.shape[0:2][::-1] - def _safe_match_template(self, image, template): - """ - Args: - image (np.ndarray): Image to search in. - template (np.ndarray): Template to search for. - - Returns: - np.ndarray: Result of cv2.matchTemplate - """ - # 2026-01-25 Fix: Added safety check for channel mismatch (Gray vs RGB) to prevent crashes. - image_channels = 1 if len(image.shape) == 2 else 3 - template_channels = 1 if len(template.shape) == 2 else 3 - - coerced = False - if image_channels != template_channels: - coerced = True - if image_channels == 1: - # Image is Gray, Template is RGB -> Convert Template to Gray - template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) - else: - # Image is RGB, Template is Gray -> Convert Image to Gray - image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - - res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) - - if coerced: - _, sim, _, point = cv2.minMaxLoc(res) - # 2026-02-04: Debounce log spam and save debug info for near-misses - if not hasattr(self, '_mismatch_count'): - self._mismatch_count = 0 - self._mismatch_count += 1 - - # Log every 100th occurrence or if similarity is very high - should_log = (self._mismatch_count % 100 == 1) or (sim > 0.8) - - if should_log: - logger.warning(f'Channel mismatch fixed in {self.name}. Sim: {sim:.3f} (Count: {self._mismatch_count})') - - # Targeted Akashi Debugging: Capture near-misses for inspection - if 0.70 < sim < 0.85 and 'AKASHI' in self.name: - from datetime import datetime - now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S-%f') - debug_path = f'./log/debug_mismatch_{self.name}_{now}.png' - try: - h, w = template.shape[:2] - x, y = point - crop = image[y:y+h, x:x+w] - cv2.imwrite(debug_path, crop) - if should_log: - logger.info(f'Saved debug mismatch image to {debug_path}') - except Exception: - pass - - return res - def match(self, image, scaling=1.0, similarity=0.85): """ Args: @@ -180,18 +124,13 @@ def match(self, image, scaling=1.0, similarity=0.85): Returns: bool: If matches. """ - # Targeted Threshold for Akashi: Override similarity if we are looking for the merchant - if 'AKASHI' in self.name: - similarity = 0.75 - scaling = 1 / scaling if scaling != 1.0: image = cv2.resize(image, None, fx=scaling, fy=scaling) if self.is_gif: for template in self.image: - # 2026-01-25 Fix: Use _safe_match_template to handle channel mismatch - res = self._safe_match_template(image, template) + res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) _, sim, _, _ = cv2.minMaxLoc(res) # print(self.file, sim) if sim > similarity: @@ -200,8 +139,7 @@ def match(self, image, scaling=1.0, similarity=0.85): return False else: - # 2026-01-25 Fix: Use _safe_match_template to handle channel mismatch - res = self._safe_match_template(image, self.image) + res = cv2.matchTemplate(image, self.image, cv2.TM_CCOEFF_NORMED) _, sim, _, _ = cv2.minMaxLoc(res) # print(self.file, sim) return sim > similarity @@ -245,8 +183,7 @@ def match_binary(self, image, similarity=0.85): def match_luma(self, image, similarity=0.85): if self.is_gif: - if len(image.shape) == 3: - image = rgb2luma(image) + image = rgb2luma(image) for template in self.image_luma: res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) _, sim, _, _ = cv2.minMaxLoc(res) @@ -257,9 +194,7 @@ def match_luma(self, image, similarity=0.85): return False else: - if len(image.shape) == 3: - image = rgb2luma(image) - res = cv2.matchTemplate(image, self.image_luma, cv2.TM_CCOEFF_NORMED) + res = cv2.matchTemplate(image, self.image, cv2.TM_CCOEFF_NORMED) _, sim, _, _ = cv2.minMaxLoc(res) # print(self.file, sim) return sim > similarity @@ -292,8 +227,7 @@ def match_result(self, image, name=None): float: Similarity Button: """ - # 2026-01-25 Fix: Use _safe_match_template to handle channel mismatch - res = self._safe_match_template(image, self.image) + res = cv2.matchTemplate(image, self.image, cv2.TM_CCOEFF_NORMED) _, sim, _, point = cv2.minMaxLoc(res) # print(self.file, sim) @@ -329,15 +263,13 @@ def match_multi(self, image, scaling=1.0, similarity=0.85, threshold=3, name=Non if self.is_gif: result = [] for template in self.image: - # 2026-01-25 Fix: Use _safe_match_template to handle channel mismatch - res = self._safe_match_template(image, template) + res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) res = np.array(np.where(res > similarity)).T[:, ::-1].tolist() result += res result = np.array(result) else: - # 2026-01-25 Fix: Use _safe_match_template to handle channel mismatch - res = self._safe_match_template(image, self.image) - result = np.array(np.where(res > similarity)).T[:, ::-1] + result = cv2.matchTemplate(image, self.image, cv2.TM_CCOEFF_NORMED) + result = np.array(np.where(result > similarity)).T[:, ::-1] # result: np.array([[x0, y0], [x1, y1], ...) if scaling != 1.0: diff --git a/alas_wrapped/module/campaign/assets.py b/alas_wrapped/module/campaign/assets.py index 064c0ef030..8eb11c412d 100644 --- a/alas_wrapped/module/campaign/assets.py +++ b/alas_wrapped/module/campaign/assets.py @@ -16,11 +16,8 @@ EVENT_20230817_STORY = Button(area={'cn': (610, 320, 670, 380), 'en': (610, 320, 670, 380), 'jp': (610, 320, 670, 380), 'tw': (610, 320, 670, 380)}, color={'cn': (183, 180, 190), 'en': (183, 180, 190), 'jp': (183, 180, 190), 'tw': (183, 180, 190)}, button={'cn': (610, 320, 670, 380), 'en': (610, 320, 670, 380), 'jp': (610, 320, 670, 380), 'tw': (610, 320, 670, 380)}, file={'cn': './assets/cn/campaign/EVENT_20230817_STORY.png', 'en': './assets/en/campaign/EVENT_20230817_STORY.png', 'jp': './assets/jp/campaign/EVENT_20230817_STORY.png', 'tw': './assets/tw/campaign/EVENT_20230817_STORY.png'}) EVENT_20250724_PT_ICON = Button(area={'cn': (1102, 106, 1139, 121), 'en': (1067, 108, 1104, 123), 'jp': (1102, 106, 1139, 121), 'tw': (1102, 106, 1139, 121)}, color={'cn': (95, 103, 93), 'en': (97, 101, 94), 'jp': (95, 103, 93), 'tw': (95, 103, 93)}, button={'cn': (1102, 106, 1139, 121), 'en': (1067, 108, 1104, 123), 'jp': (1102, 106, 1139, 121), 'tw': (1102, 106, 1139, 121)}, file={'cn': './assets/cn/campaign/EVENT_20250724_PT_ICON.png', 'en': './assets/en/campaign/EVENT_20250724_PT_ICON.png', 'jp': './assets/cn/campaign/EVENT_20250724_PT_ICON.png', 'tw': './assets/cn/campaign/EVENT_20250724_PT_ICON.png'}) OCR_COIN = Button(area={'cn': (815, 23, 922, 51), 'en': (815, 23, 922, 51), 'jp': (815, 23, 922, 51), 'tw': (815, 23, 922, 51)}, color={'cn': (61, 61, 73), 'en': (61, 61, 73), 'jp': (61, 61, 73), 'tw': (61, 61, 73)}, button={'cn': (815, 23, 922, 51), 'en': (815, 23, 922, 51), 'jp': (815, 23, 922, 51), 'tw': (815, 23, 922, 51)}, file={'cn': './assets/cn/campaign/OCR_COIN.png', 'en': './assets/en/campaign/OCR_COIN.png', 'jp': './assets/jp/campaign/OCR_COIN.png', 'tw': './assets/tw/campaign/OCR_COIN.png'}) -OCR_COIN_LIMIT = Button(area={'cn': (807, 0, 944, 19), 'en': (807, 0, 944, 19), 'jp': (807, 0, 944, 19), 'tw': (807, 0, 944, 19)}, color={'cn': (206, 206, 206), 'en': (206, 206, 206), 'jp': (206, 206, 206), 'tw': (206, 206, 206)}, button={'cn': (807, 0, 944, 19), 'en': (807, 0, 944, 19), 'jp': (807, 0, 944, 19), 'tw': (807, 0, 944, 19)}, file={'cn': './assets/cn/campaign/OCR_COIN_LIMIT.png', 'en': './assets/en/campaign/OCR_COIN_LIMIT.png', 'jp': './assets/jp/campaign/OCR_COIN_LIMIT.png', 'tw': './assets/tw/campaign/OCR_COIN_LIMIT.png'}) OCR_EVENT_PT = Button(area={'cn': (1196, 109, 1280, 131), 'en': (1190, 109, 1280, 129), 'jp': (1196, 109, 1280, 131), 'tw': (1196, 109, 1280, 131)}, color={'cn': (121, 110, 59), 'en': (88, 78, 51), 'jp': (121, 110, 59), 'tw': (121, 110, 59)}, button={'cn': (1196, 109, 1280, 131), 'en': (1190, 109, 1280, 129), 'jp': (1196, 109, 1280, 131), 'tw': (1196, 109, 1280, 131)}, file={'cn': './assets/cn/campaign/OCR_EVENT_PT.png', 'en': './assets/en/campaign/OCR_EVENT_PT.png', 'jp': './assets/jp/campaign/OCR_EVENT_PT.png', 'tw': './assets/tw/campaign/OCR_EVENT_PT.png'}) -OCR_GEM = Button(area={'cn': (1024, 23, 1137, 51), 'en': (1024, 23, 1137, 51), 'jp': (1024, 23, 1137, 51), 'tw': (1024, 23, 1137, 51)}, color={'cn': (102, 102, 100), 'en': (102, 102, 100), 'jp': (102, 102, 100), 'tw': (102, 102, 100)}, button={'cn': (1024, 23, 1137, 51), 'en': (1024, 23, 1137, 51), 'jp': (1024, 23, 1137, 51), 'tw': (1024, 23, 1137, 51)}, file={'cn': './assets/cn/campaign/OCR_GEM.png', 'en': './assets/cn/campaign/OCR_GEM.png', 'jp': './assets/cn/campaign/OCR_GEM.png', 'tw': './assets/cn/campaign/OCR_GEM.png'}) OCR_OIL = Button(area={'cn': (614, 23, 714, 51), 'en': (614, 23, 714, 51), 'jp': (614, 23, 714, 51), 'tw': (614, 23, 714, 51)}, color={'cn': (64, 65, 79), 'en': (64, 65, 79), 'jp': (64, 65, 79), 'tw': (64, 65, 79)}, button={'cn': (614, 23, 714, 51), 'en': (614, 23, 714, 51), 'jp': (614, 23, 714, 51), 'tw': (614, 23, 714, 51)}, file={'cn': './assets/cn/campaign/OCR_OIL.png', 'en': './assets/en/campaign/OCR_OIL.png', 'jp': './assets/jp/campaign/OCR_OIL.png', 'tw': './assets/tw/campaign/OCR_OIL.png'}) -OCR_OIL_LIMIT = Button(area={'cn': (608, 0, 736, 19), 'en': (608, 0, 736, 19), 'jp': (608, 0, 736, 19), 'tw': (608, 0, 736, 19)}, color={'cn': (202, 202, 202), 'en': (202, 202, 202), 'jp': (202, 202, 202), 'tw': (202, 202, 202)}, button={'cn': (608, 0, 736, 19), 'en': (608, 0, 736, 19), 'jp': (608, 0, 736, 19), 'tw': (608, 0, 736, 19)}, file={'cn': './assets/cn/campaign/OCR_OIL_LIMIT.png', 'en': './assets/en/campaign/OCR_OIL_LIMIT.png', 'jp': './assets/jp/campaign/OCR_OIL_LIMIT.png', 'tw': './assets/tw/campaign/OCR_OIL_LIMIT.png'}) OCR_OIL_CHECK = Button(area={'cn': (573, 30, 592, 49), 'en': (573, 30, 592, 49), 'jp': (573, 30, 592, 49), 'tw': (573, 30, 592, 49)}, color={'cn': (82, 82, 82), 'en': (82, 82, 82), 'jp': (82, 82, 82), 'tw': (82, 82, 82)}, button={'cn': (573, 30, 592, 49), 'en': (573, 30, 592, 49), 'jp': (573, 30, 592, 49), 'tw': (573, 30, 592, 49)}, file={'cn': './assets/cn/campaign/OCR_OIL_CHECK.png', 'en': './assets/en/campaign/OCR_OIL_CHECK.png', 'jp': './assets/jp/campaign/OCR_OIL_CHECK.png', 'tw': './assets/tw/campaign/OCR_OIL_CHECK.png'}) SWITCH_1_HARD = Button(area={'cn': (82, 641, 148, 675), 'en': (87, 642, 148, 676), 'jp': (24, 645, 150, 697), 'tw': (82, 641, 148, 675)}, color={'cn': (233, 141, 128), 'en': (234, 139, 124), 'jp': (219, 116, 106), 'tw': (236, 159, 148)}, button={'cn': (82, 641, 148, 675), 'en': (87, 642, 148, 676), 'jp': (24, 645, 150, 697), 'tw': (82, 641, 148, 675)}, file={'cn': './assets/cn/campaign/SWITCH_1_HARD.png', 'en': './assets/en/campaign/SWITCH_1_HARD.png', 'jp': './assets/jp/campaign/SWITCH_1_HARD.png', 'tw': './assets/tw/campaign/SWITCH_1_HARD.png'}) SWITCH_1_NORMAL = Button(area={'cn': (80, 641, 148, 675), 'en': (79, 638, 147, 675), 'jp': (24, 644, 150, 697), 'tw': (79, 641, 148, 675)}, color={'cn': (157, 180, 227), 'en': (157, 180, 227), 'jp': (143, 169, 222), 'tw': (156, 179, 227)}, button={'cn': (80, 641, 148, 675), 'en': (79, 638, 147, 675), 'jp': (24, 644, 150, 697), 'tw': (79, 641, 148, 675)}, file={'cn': './assets/cn/campaign/SWITCH_1_NORMAL.png', 'en': './assets/en/campaign/SWITCH_1_NORMAL.png', 'jp': './assets/jp/campaign/SWITCH_1_NORMAL.png', 'tw': './assets/tw/campaign/SWITCH_1_NORMAL.png'}) diff --git a/alas_wrapped/module/campaign/campaign_event.py b/alas_wrapped/module/campaign/campaign_event.py index ba7bd4d91a..541cb00a55 100644 --- a/alas_wrapped/module/campaign/campaign_event.py +++ b/alas_wrapped/module/campaign/campaign_event.py @@ -62,10 +62,8 @@ def event_pt_limit_triggered(self): tasks = EVENTS + RAIDS + COALITIONS + GEMS_FARMINGS + HOSPITAL command = self.config.Scheduler_Command if limit <= 0 or command not in tasks: - self.get_event_pt() return False if command in GEMS_FARMINGS and self.stage_is_main(self.config.Campaign_Name): - self.get_event_pt() return False pt = self.get_event_pt() @@ -109,17 +107,8 @@ def triggered_task_balancer(self): Pages: in: page_event or page_sp """ - from module.config.deep import deep_get limit = self.config.TaskBalancer_CoinLimit - coin = deep_get(self.config.data, 'Dashboard.Coin.Value') - logger.attr('Coin Count', coin) - tasks = [ - 'Event', - 'Event2', - 'Raid', - 'GemsFarming', - ] - command = self.config.Scheduler_Command + coin = self.get_coin() # Check Coin if coin == 0: # Avoid wrong/zero OCR result @@ -136,12 +125,11 @@ def triggered_task_balancer(self): return False def handle_task_balancer(self): - if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): - self.config.task_delay(minute=5) - next_task = self.config.TaskBalancer_TaskCall - logger.hr(f'TaskBalancer triggered, switching task to {next_task}') - self.config.task_call(next_task) - self.config.task_stop() + self.config.task_delay(minute=5) + next_task = self.config.TaskBalancer_TaskCall + logger.hr(f'TaskBalancer triggered, switching task to {next_task}') + self.config.task_call(next_task) + self.config.task_stop() def is_event_entrance_available(self): """ diff --git a/alas_wrapped/module/campaign/campaign_ocr.py b/alas_wrapped/module/campaign/campaign_ocr.py index 9dc04193e5..f39a29ae80 100644 --- a/alas_wrapped/module/campaign/campaign_ocr.py +++ b/alas_wrapped/module/campaign/campaign_ocr.py @@ -41,11 +41,21 @@ def _campaign_get_chapter_index(name): @staticmethod def _campaign_ocr_result_process(result): # The result will be like '7--2', because tha dash in game is '–' not '-' - result = result.lower().replace('--', '-').replace('--', '-') - if result.startswith('-'): - result = result[1:] + result = result.replace('--', '-').replace('--', '-').lstrip('-') + + # Replace wrong 'I' from results like 'I1-1', '1I-1', 'I-I', '11-I', 'I4-4', to '1' + # while keeping results like 'isp-2', 'sp1' + def replace_func(match): + segment = match.group(0) + return segment.replace('I', '1') + + result = re.sub(r'[0-9I]+-[0-9I]+', replace_func, result, count=1) + + # Convert '72' to '7-2' if len(result) == 2 and result[0].isdigit(): result = '-'.join(result) + + result = result.lower() return result @staticmethod diff --git a/alas_wrapped/module/campaign/campaign_status.py b/alas_wrapped/module/campaign/campaign_status.py index ac4a494822..45e8179fc1 100644 --- a/alas_wrapped/module/campaign/campaign_status.py +++ b/alas_wrapped/module/campaign/campaign_status.py @@ -1,4 +1,3 @@ -import datetime import re import cv2 @@ -7,17 +6,16 @@ import module.config.server as server from module.base.timer import Timer -from module.campaign.assets import OCR_EVENT_PT, OCR_COIN, OCR_OIL, OCR_COIN_LIMIT, OCR_OIL_LIMIT, OCR_OIL_CHECK from module.base.utils import color_similar, get_color +from module.campaign.assets import OCR_COIN, OCR_EVENT_PT, OCR_OIL, OCR_OIL_CHECK from module.logger import logger from module.ocr.ocr import Digit, Ocr from module.ui.ui import UI -from module.log_res.log_res import LogRes -#if server.server != 'jp': -# OCR_COIN = Digit(OCR_COIN, name='OCR_COIN', letter=(239, 239, 239), threshold=128) -#else: -# OCR_COIN = Digit(OCR_COIN, name='OCR_COIN', letter=(201, 201, 201), threshold=128) +if server.server != 'jp': + OCR_COIN = Digit(OCR_COIN, name='OCR_COIN', letter=(239, 239, 239), threshold=128) +else: + OCR_COIN = Digit(OCR_COIN, name='OCR_COIN', letter=(201, 201, 201), threshold=128) class PtOcr(Ocr): @@ -45,7 +43,7 @@ def pre_process(self, image): class CampaignStatus(UI): - def get_event_pt(self, update=False): + def get_event_pt(self): """ Returns: int: PT amount, or 0 if unable to parse @@ -56,20 +54,17 @@ def get_event_pt(self, update=False): if res: pt = int(res.group(1)) logger.attr('Event_PT', pt) - LogRes(self.config).Pt = pt + return pt else: logger.warning(f'Invalid pt result: {pt}') - pt = 0 - if update: - self.config.update() - return pt + return 0 - def get_coin(self, skip_first_screenshot=True, update=False): + def get_coin(self, skip_first_screenshot=True): """ Returns: int: Coin amount """ - _coin = {} + amount = 0 timeout = Timer(1, count=2).start() while 1: if skip_first_screenshot: @@ -81,17 +76,11 @@ def get_coin(self, skip_first_screenshot=True, update=False): logger.warning('Get coin timeout') break - _coin = { - 'Value': self._get_num(OCR_COIN, 'OCR_COIN'), - 'Limit': self._get_num(OCR_COIN_LIMIT, 'OCR_COIN_LIMIT') - } - if _coin['Value'] >= 100: + amount = OCR_COIN.ocr(self.device.image) + if amount >= 100: break - LogRes(self.config).Coin = _coin - if update: - self.config.update() - return _coin['Value'] + return amount def _get_oil(self): # Update offset @@ -113,32 +102,12 @@ def _get_oil(self): return ocr.ocr(self.device.image) - def _get_num(self, _button, name): - # Update offset - _ = self.appear(OCR_OIL_CHECK) - - color = get_color(self.device.image, OCR_OIL_CHECK.button) - if color_similar(color, OCR_OIL_CHECK.color): - # Original color - if server.server != 'jp': - ocr = Digit(_button, name=name, letter=(247, 247, 247), threshold=128) - else: - ocr = Digit(_button, name=name, letter=(201, 201, 201), threshold=128) - elif color_similar(color, (59, 59, 64)): - # With black overlay - ocr = Digit(_button, name=name, letter=(165, 165, 165), threshold=128) - else: - logger.warning(f'Unexpected OCR_OIL_CHECK color') - ocr = Digit(_button, name=name, letter=(247, 247, 247), threshold=128) - - return ocr.ocr(self.device.image) - - def get_oil(self, skip_first_screenshot=True, update=False): + def get_oil(self, skip_first_screenshot=True): """ Returns: int: Oil amount """ - _oil = {} + amount = 0 timeout = Timer(1, count=2).start() while 1: if skip_first_screenshot: @@ -146,25 +115,19 @@ def get_oil(self, skip_first_screenshot=True, update=False): else: self.device.screenshot() - if not self.appear(OCR_OIL_CHECK, offset=(10, 2)): - logger.info('No oil icon') - self.device.sleep(1) - if timeout.reached(): logger.warning('Get oil timeout') break - _oil = { - 'Value': self._get_num(OCR_OIL, 'OCR_OIL'), - 'Limit': self._get_num(OCR_OIL_LIMIT, 'OCR_OIL_LIMIT') - } - if _oil['Value'] >= 100: + if not self.appear(OCR_OIL_CHECK, offset=(10, 2)): + logger.info('No oil icon') + continue + + amount = self._get_oil() + if amount >= 100: break - LogRes(self.config).Oil = _oil - if update: - self.config.update() - return _oil['Value'] + return amount def is_balancer_task(self): """ diff --git a/alas_wrapped/module/campaign/run.py b/alas_wrapped/module/campaign/run.py index 01a2f1bb26..ad3251c544 100644 --- a/alas_wrapped/module/campaign/run.py +++ b/alas_wrapped/module/campaign/run.py @@ -5,7 +5,6 @@ from module.campaign.campaign_base import CampaignBase from module.campaign.campaign_event import CampaignEvent -from module.shop.shop_status import ShopStatus from module.campaign.campaign_ui import MODE_SWITCH_1 from module.config.config import AzurLaneConfig from module.exception import CampaignEnd, RequestHumanTakeover, ScriptEnd @@ -15,7 +14,7 @@ from module.ui.page import page_campaign -class CampaignRun(CampaignEvent, ShopStatus): +class CampaignRun(CampaignEvent): folder: str name: str stage: str @@ -95,10 +94,7 @@ def triggered_stop_condition(self, oil_check=True): return True # Oil limit if oil_check: - self.status_get_gems() - self.get_coin() - _oil = self.get_oil() - if _oil < max(500, self.config.StopCondition_OilLimit): + if self.get_oil() < max(500, self.config.StopCondition_OilLimit): logger.hr('Triggered stop condition: Oil limit') self.config.task_delay(minute=(120, 240)) return True @@ -423,11 +419,6 @@ def run(self, name, folder='campaign_main', mode='normal', total=0): if self.triggered_stop_condition(oil_check=not self.campaign.is_in_auto_search_menu()): break - # Update config - if len(self.config.modified): - logger.info('Updating config for dashboard') - self.config.update() - # Run self.device.stuck_record_clear() self.device.click_record_clear() @@ -438,10 +429,6 @@ def run(self, name, folder='campaign_main', mode='normal', total=0): logger.info(str(e)) break - # Update config - if len(self.campaign.config.modified): - logger.info('Updating config for dashboard') - self.campaign.config.update() # After run self.run_count += 1 if self.config.StopCondition_RunCount: diff --git a/alas_wrapped/module/coalition/assets.py b/alas_wrapped/module/coalition/assets.py index a869c36968..890b255256 100644 --- a/alas_wrapped/module/coalition/assets.py +++ b/alas_wrapped/module/coalition/assets.py @@ -36,17 +36,17 @@ DAL_SWITCH_SINGLE = Button(area={'cn': (910, 473, 1055, 500), 'en': (915, 475, 1051, 499), 'jp': (917, 474, 1035, 499), 'tw': (945, 478, 1013, 495)}, color={'cn': (223, 223, 223), 'en': (193, 193, 193), 'jp': (215, 215, 215), 'tw': (168, 168, 168)}, button={'cn': (910, 473, 1055, 500), 'en': (915, 475, 1051, 499), 'jp': (917, 474, 1035, 499), 'tw': (945, 478, 1013, 495)}, file={'cn': './assets/cn/coalition/DAL_SWITCH_SINGLE.png', 'en': './assets/en/coalition/DAL_SWITCH_SINGLE.png', 'jp': './assets/jp/coalition/DAL_SWITCH_SINGLE.png', 'tw': './assets/tw/coalition/DAL_SWITCH_SINGLE.png'}) EMPTY_FLAGSHIP = Button(area={'cn': (247, 237, 277, 267), 'en': (247, 237, 277, 267), 'jp': (247, 237, 277, 267), 'tw': (247, 237, 277, 267)}, color={'cn': (76, 64, 56), 'en': (76, 64, 56), 'jp': (76, 64, 56), 'tw': (76, 64, 56)}, button={'cn': (247, 237, 277, 267), 'en': (247, 237, 277, 267), 'jp': (247, 237, 277, 267), 'tw': (247, 237, 277, 267)}, file={'cn': './assets/cn/coalition/EMPTY_FLAGSHIP.png', 'en': './assets/cn/coalition/EMPTY_FLAGSHIP.png', 'jp': './assets/cn/coalition/EMPTY_FLAGSHIP.png', 'tw': './assets/cn/coalition/EMPTY_FLAGSHIP.png'}) EMPTY_VANGUARD = Button(area={'cn': (515, 237, 545, 267), 'en': (515, 237, 545, 267), 'jp': (515, 237, 545, 267), 'tw': (515, 237, 545, 267)}, color={'cn': (52, 52, 53), 'en': (52, 52, 53), 'jp': (52, 52, 53), 'tw': (52, 52, 53)}, button={'cn': (515, 237, 545, 267), 'en': (515, 237, 545, 267), 'jp': (515, 237, 545, 267), 'tw': (515, 237, 545, 267)}, file={'cn': './assets/cn/coalition/EMPTY_VANGUARD.png', 'en': './assets/cn/coalition/EMPTY_VANGUARD.png', 'jp': './assets/cn/coalition/EMPTY_VANGUARD.png', 'tw': './assets/cn/coalition/EMPTY_VANGUARD.png'}) -FASHION_COALITION_CHECK = Button(area={'cn': (102, 19, 177, 51), 'en': (118, 31, 183, 50), 'jp': (101, 18, 175, 51), 'tw': (102, 19, 177, 51)}, color={'cn': (109, 104, 89), 'en': (131, 124, 102), 'jp': (122, 116, 101), 'tw': (109, 104, 89)}, button={'cn': (102, 19, 177, 51), 'en': (118, 31, 183, 50), 'jp': (101, 18, 175, 51), 'tw': (102, 19, 177, 51)}, file={'cn': './assets/cn/coalition/FASHION_COALITION_CHECK.png', 'en': './assets/en/coalition/FASHION_COALITION_CHECK.png', 'jp': './assets/jp/coalition/FASHION_COALITION_CHECK.png', 'tw': './assets/cn/coalition/FASHION_COALITION_CHECK.png'}) +FASHION_COALITION_CHECK = Button(area={'cn': (102, 19, 177, 51), 'en': (118, 31, 183, 50), 'jp': (101, 18, 175, 51), 'tw': (101, 17, 177, 51)}, color={'cn': (109, 104, 89), 'en': (131, 124, 102), 'jp': (122, 116, 101), 'tw': (108, 103, 89)}, button={'cn': (102, 19, 177, 51), 'en': (118, 31, 183, 50), 'jp': (101, 18, 175, 51), 'tw': (101, 17, 177, 51)}, file={'cn': './assets/cn/coalition/FASHION_COALITION_CHECK.png', 'en': './assets/en/coalition/FASHION_COALITION_CHECK.png', 'jp': './assets/jp/coalition/FASHION_COALITION_CHECK.png', 'tw': './assets/tw/coalition/FASHION_COALITION_CHECK.png'}) FASHION_EASY = Button(area={'cn': (136, 223, 199, 263), 'en': (136, 223, 199, 263), 'jp': (136, 223, 199, 263), 'tw': (136, 223, 199, 263)}, color={'cn': (225, 199, 197), 'en': (225, 199, 197), 'jp': (225, 199, 197), 'tw': (225, 199, 197)}, button={'cn': (136, 223, 199, 263), 'en': (136, 223, 199, 263), 'jp': (136, 223, 199, 263), 'tw': (136, 223, 199, 263)}, file={'cn': './assets/cn/coalition/FASHION_EASY.png', 'en': './assets/cn/coalition/FASHION_EASY.png', 'jp': './assets/cn/coalition/FASHION_EASY.png', 'tw': './assets/cn/coalition/FASHION_EASY.png'}) FASHION_EX = Button(area={'cn': (844, 246, 923, 301), 'en': (844, 246, 923, 301), 'jp': (844, 246, 923, 301), 'tw': (844, 246, 923, 301)}, color={'cn': (140, 115, 114), 'en': (140, 115, 114), 'jp': (140, 115, 114), 'tw': (140, 115, 114)}, button={'cn': (844, 246, 923, 301), 'en': (844, 246, 923, 301), 'jp': (844, 246, 923, 301), 'tw': (844, 246, 923, 301)}, file={'cn': './assets/cn/coalition/FASHION_EX.png', 'en': './assets/cn/coalition/FASHION_EX.png', 'jp': './assets/cn/coalition/FASHION_EX.png', 'tw': './assets/cn/coalition/FASHION_EX.png'}) FASHION_HARD = Button(area={'cn': (485, 167, 554, 215), 'en': (485, 167, 554, 215), 'jp': (485, 167, 554, 215), 'tw': (485, 167, 554, 215)}, color={'cn': (152, 136, 129), 'en': (152, 136, 129), 'jp': (152, 136, 129), 'tw': (152, 136, 129)}, button={'cn': (485, 167, 554, 215), 'en': (485, 167, 554, 215), 'jp': (485, 167, 554, 215), 'tw': (485, 167, 554, 215)}, file={'cn': './assets/cn/coalition/FASHION_HARD.png', 'en': './assets/cn/coalition/FASHION_HARD.png', 'jp': './assets/cn/coalition/FASHION_HARD.png', 'tw': './assets/cn/coalition/FASHION_HARD.png'}) -FASHION_MODE_BATTLE = Button(area={'cn': (152, 635, 213, 669), 'en': (108, 644, 188, 668), 'jp': (150, 636, 215, 668), 'tw': (152, 635, 213, 669)}, color={'cn': (140, 133, 117), 'en': (150, 143, 128), 'jp': (144, 137, 119), 'tw': (140, 133, 117)}, button={'cn': (152, 635, 213, 669), 'en': (108, 644, 188, 668), 'jp': (150, 636, 215, 668), 'tw': (152, 635, 213, 669)}, file={'cn': './assets/cn/coalition/FASHION_MODE_BATTLE.png', 'en': './assets/en/coalition/FASHION_MODE_BATTLE.png', 'jp': './assets/jp/coalition/FASHION_MODE_BATTLE.png', 'tw': './assets/cn/coalition/FASHION_MODE_BATTLE.png'}) -FASHION_MODE_STORY = Button(area={'cn': (154, 629, 220, 666), 'en': (117, 642, 195, 667), 'jp': (156, 629, 219, 665), 'tw': (154, 629, 220, 666)}, color={'cn': (141, 134, 116), 'en': (158, 148, 132), 'jp': (151, 143, 123), 'tw': (141, 134, 116)}, button={'cn': (154, 629, 220, 666), 'en': (117, 642, 195, 667), 'jp': (156, 629, 219, 665), 'tw': (154, 629, 220, 666)}, file={'cn': './assets/cn/coalition/FASHION_MODE_STORY.png', 'en': './assets/en/coalition/FASHION_MODE_STORY.png', 'jp': './assets/jp/coalition/FASHION_MODE_STORY.png', 'tw': './assets/cn/coalition/FASHION_MODE_STORY.png'}) +FASHION_MODE_BATTLE = Button(area={'cn': (152, 635, 213, 669), 'en': (108, 644, 188, 668), 'jp': (150, 636, 215, 668), 'tw': (150, 637, 212, 667)}, color={'cn': (140, 133, 117), 'en': (150, 143, 128), 'jp': (144, 137, 119), 'tw': (128, 123, 109)}, button={'cn': (152, 635, 213, 669), 'en': (108, 644, 188, 668), 'jp': (150, 636, 215, 668), 'tw': (150, 637, 212, 667)}, file={'cn': './assets/cn/coalition/FASHION_MODE_BATTLE.png', 'en': './assets/en/coalition/FASHION_MODE_BATTLE.png', 'jp': './assets/jp/coalition/FASHION_MODE_BATTLE.png', 'tw': './assets/tw/coalition/FASHION_MODE_BATTLE.png'}) +FASHION_MODE_STORY = Button(area={'cn': (154, 629, 220, 666), 'en': (117, 642, 195, 667), 'jp': (156, 629, 219, 665), 'tw': (154, 629, 219, 666)}, color={'cn': (141, 134, 116), 'en': (158, 148, 132), 'jp': (151, 143, 123), 'tw': (139, 132, 115)}, button={'cn': (154, 629, 220, 666), 'en': (117, 642, 195, 667), 'jp': (156, 629, 219, 665), 'tw': (154, 629, 219, 666)}, file={'cn': './assets/cn/coalition/FASHION_MODE_STORY.png', 'en': './assets/en/coalition/FASHION_MODE_STORY.png', 'jp': './assets/jp/coalition/FASHION_MODE_STORY.png', 'tw': './assets/tw/coalition/FASHION_MODE_STORY.png'}) FASHION_NORMAL = Button(area={'cn': (322, 295, 392, 334), 'en': (322, 295, 392, 334), 'jp': (322, 295, 392, 334), 'tw': (322, 295, 392, 334)}, color={'cn': (219, 196, 198), 'en': (219, 196, 198), 'jp': (219, 196, 198), 'tw': (219, 196, 198)}, button={'cn': (322, 295, 392, 334), 'en': (322, 295, 392, 334), 'jp': (322, 295, 392, 334), 'tw': (322, 295, 392, 334)}, file={'cn': './assets/cn/coalition/FASHION_NORMAL.png', 'en': './assets/cn/coalition/FASHION_NORMAL.png', 'jp': './assets/cn/coalition/FASHION_NORMAL.png', 'tw': './assets/cn/coalition/FASHION_NORMAL.png'}) FASHION_PT_OCR = Button(area={'cn': (881, 658, 937, 674), 'en': (881, 658, 937, 674), 'jp': (881, 658, 937, 674), 'tw': (881, 658, 937, 674)}, color={'cn': (136, 127, 122), 'en': (136, 127, 122), 'jp': (136, 127, 122), 'tw': (136, 127, 122)}, button={'cn': (881, 658, 937, 674), 'en': (881, 658, 937, 674), 'jp': (881, 658, 937, 674), 'tw': (881, 658, 937, 674)}, file={'cn': './assets/cn/coalition/FASHION_PT_OCR.png', 'en': './assets/cn/coalition/FASHION_PT_OCR.png', 'jp': './assets/cn/coalition/FASHION_PT_OCR.png', 'tw': './assets/cn/coalition/FASHION_PT_OCR.png'}) FASHION_SP = Button(area={'cn': (704, 194, 762, 242), 'en': (704, 194, 762, 242), 'jp': (704, 194, 762, 242), 'tw': (704, 194, 762, 242)}, color={'cn': (146, 133, 135), 'en': (146, 133, 135), 'jp': (146, 133, 135), 'tw': (146, 133, 135)}, button={'cn': (704, 194, 762, 242), 'en': (704, 194, 762, 242), 'jp': (704, 194, 762, 242), 'tw': (704, 194, 762, 242)}, file={'cn': './assets/cn/coalition/FASHION_SP.png', 'en': './assets/cn/coalition/FASHION_SP.png', 'jp': './assets/cn/coalition/FASHION_SP.png', 'tw': './assets/cn/coalition/FASHION_SP.png'}) -FASHION_SWITCH_MULTI = Button(area={'cn': (1075, 457, 1206, 485), 'en': (1076, 457, 1206, 485), 'jp': (1075, 457, 1206, 485), 'tw': (1075, 457, 1206, 485)}, color={'cn': (233, 183, 63), 'en': (201, 158, 54), 'jp': (227, 178, 61), 'tw': (233, 183, 63)}, button={'cn': (1075, 457, 1206, 485), 'en': (1076, 457, 1206, 485), 'jp': (1075, 457, 1206, 485), 'tw': (1075, 457, 1206, 485)}, file={'cn': './assets/cn/coalition/FASHION_SWITCH_MULTI.png', 'en': './assets/en/coalition/FASHION_SWITCH_MULTI.png', 'jp': './assets/jp/coalition/FASHION_SWITCH_MULTI.png', 'tw': './assets/cn/coalition/FASHION_SWITCH_MULTI.png'}) -FASHION_SWITCH_SINGLE = Button(area={'cn': (929, 457, 1059, 485), 'en': (929, 457, 1059, 485), 'jp': (929, 457, 1059, 485), 'tw': (929, 457, 1059, 485)}, color={'cn': (230, 181, 62), 'en': (202, 159, 54), 'jp': (227, 178, 61), 'tw': (230, 181, 62)}, button={'cn': (929, 457, 1059, 485), 'en': (929, 457, 1059, 485), 'jp': (929, 457, 1059, 485), 'tw': (929, 457, 1059, 485)}, file={'cn': './assets/cn/coalition/FASHION_SWITCH_SINGLE.png', 'en': './assets/en/coalition/FASHION_SWITCH_SINGLE.png', 'jp': './assets/jp/coalition/FASHION_SWITCH_SINGLE.png', 'tw': './assets/cn/coalition/FASHION_SWITCH_SINGLE.png'}) +FASHION_SWITCH_MULTI = Button(area={'cn': (1075, 457, 1206, 485), 'en': (1076, 457, 1206, 485), 'jp': (1075, 457, 1206, 485), 'tw': (1075, 457, 1206, 485)}, color={'cn': (233, 183, 63), 'en': (201, 158, 54), 'jp': (227, 178, 61), 'tw': (229, 181, 62)}, button={'cn': (1075, 457, 1206, 485), 'en': (1076, 457, 1206, 485), 'jp': (1075, 457, 1206, 485), 'tw': (1075, 457, 1206, 485)}, file={'cn': './assets/cn/coalition/FASHION_SWITCH_MULTI.png', 'en': './assets/en/coalition/FASHION_SWITCH_MULTI.png', 'jp': './assets/jp/coalition/FASHION_SWITCH_MULTI.png', 'tw': './assets/tw/coalition/FASHION_SWITCH_MULTI.png'}) +FASHION_SWITCH_SINGLE = Button(area={'cn': (929, 457, 1059, 485), 'en': (929, 457, 1059, 485), 'jp': (929, 457, 1059, 485), 'tw': (929, 457, 1059, 485)}, color={'cn': (230, 181, 62), 'en': (202, 159, 54), 'jp': (227, 178, 61), 'tw': (225, 177, 61)}, button={'cn': (929, 457, 1059, 485), 'en': (929, 457, 1059, 485), 'jp': (929, 457, 1059, 485), 'tw': (929, 457, 1059, 485)}, file={'cn': './assets/cn/coalition/FASHION_SWITCH_SINGLE.png', 'en': './assets/en/coalition/FASHION_SWITCH_SINGLE.png', 'jp': './assets/jp/coalition/FASHION_SWITCH_SINGLE.png', 'tw': './assets/tw/coalition/FASHION_SWITCH_SINGLE.png'}) FLEET_NOT_PREPARED = Button(area={'cn': (1008, 310, 1110, 334), 'en': (1008, 310, 1110, 334), 'jp': (1008, 310, 1110, 334), 'tw': (1008, 310, 1110, 334)}, color={'cn': (106, 106, 112), 'en': (106, 106, 112), 'jp': (106, 106, 112), 'tw': (108, 107, 112)}, button={'cn': (1008, 310, 1110, 334), 'en': (1008, 310, 1110, 334), 'jp': (1008, 310, 1110, 334), 'tw': (1008, 310, 1110, 334)}, file={'cn': './assets/cn/coalition/FLEET_NOT_PREPARED.png', 'en': './assets/cn/coalition/FLEET_NOT_PREPARED.png', 'jp': './assets/cn/coalition/FLEET_NOT_PREPARED.png', 'tw': './assets/tw/coalition/FLEET_NOT_PREPARED.png'}) FROSTFALL_COALITION_CHECK = Button(area={'cn': (118, 14, 227, 39), 'en': (118, 16, 221, 36), 'jp': (118, 14, 227, 39), 'tw': (118, 14, 227, 39)}, color={'cn': (145, 161, 200), 'en': (116, 130, 168), 'jp': (150, 166, 204), 'tw': (152, 168, 206)}, button={'cn': (118, 14, 227, 39), 'en': (118, 16, 221, 36), 'jp': (118, 14, 227, 39), 'tw': (118, 14, 227, 39)}, file={'cn': './assets/cn/coalition/FROSTFALL_COALITION_CHECK.png', 'en': './assets/en/coalition/FROSTFALL_COALITION_CHECK.png', 'jp': './assets/jp/coalition/FROSTFALL_COALITION_CHECK.png', 'tw': './assets/tw/coalition/FROSTFALL_COALITION_CHECK.png'}) FROSTFALL_EX = Button(area={'cn': (622, 372, 649, 384), 'en': (622, 372, 649, 384), 'jp': (622, 372, 649, 384), 'tw': (622, 372, 649, 384)}, color={'cn': (198, 152, 252), 'en': (198, 152, 252), 'jp': (198, 152, 252), 'tw': (182, 127, 252)}, button={'cn': (622, 372, 649, 384), 'en': (622, 372, 649, 384), 'jp': (622, 372, 649, 384), 'tw': (622, 372, 649, 384)}, file={'cn': './assets/cn/coalition/FROSTFALL_EX.png', 'en': './assets/en/coalition/FROSTFALL_EX.png', 'jp': './assets/jp/coalition/FROSTFALL_EX.png', 'tw': './assets/tw/coalition/FROSTFALL_EX.png'}) diff --git a/alas_wrapped/module/coalition/coalition.py b/alas_wrapped/module/coalition/coalition.py index 0aeb0673d2..eab449e3da 100644 --- a/alas_wrapped/module/coalition/coalition.py +++ b/alas_wrapped/module/coalition/coalition.py @@ -6,7 +6,6 @@ from module.exception import ScriptEnd, ScriptError from module.logger import logger from module.ocr.ocr import Digit -from module.log_res.log_res import LogRes from module.ui.page import page_campaign_menu @@ -70,8 +69,6 @@ def get_event_pt(self): pt = ocr.ocr(self.device.image) # 999999 seems to be a default value, wait if pt not in [999999]: - LogRes(self.config).Pt = pt - self.config.update() break else: logger.warning('Wait PT timeout, assume it is') diff --git a/alas_wrapped/module/combat/auto_search_combat.py b/alas_wrapped/module/combat/auto_search_combat.py index 9937fb8bb8..5dbf433932 100644 --- a/alas_wrapped/module/combat/auto_search_combat.py +++ b/alas_wrapped/module/combat/auto_search_combat.py @@ -94,7 +94,7 @@ def auto_search_watch_oil(self, checked=False): This will set auto_search_oil_limit_triggered. """ if not checked: - oil = self.get_oil() + oil = self._get_oil() if oil == 0: logger.warning('Oil not found') else: diff --git a/alas_wrapped/module/combat/combat.py b/alas_wrapped/module/combat/combat.py index f2c1feb7f6..818d3de9b6 100644 --- a/alas_wrapped/module/combat/combat.py +++ b/alas_wrapped/module/combat/combat.py @@ -124,6 +124,8 @@ def is_combat_executing(self): return PAUSE_ShadowPuppetry if PAUSE_MaidCafe.match_template_color(self.device.image, offset=(10, 10)): return PAUSE_MaidCafe + if PAUSE_Ancient.match_template_color(self.device.image, offset=(10, 10)): + return PAUSE_Ancient return False def handle_combat_quit(self, offset=(20, 20), interval=3): diff --git a/alas_wrapped/module/combat_ui/assets.py b/alas_wrapped/module/combat_ui/assets.py index f66c58ff1f..cf309cd150 100644 --- a/alas_wrapped/module/combat_ui/assets.py +++ b/alas_wrapped/module/combat_ui/assets.py @@ -5,6 +5,7 @@ # Don't modify it manually. PAUSE = Button(area={'cn': (1158, 40, 1199, 58), 'en': (1155, 38, 1216, 51), 'jp': (1232, 36, 1240, 60), 'tw': (1217, 36, 1225, 59)}, color={'cn': (189, 190, 202), 'en': (164, 169, 181), 'jp': (244, 241, 246), 'tw': (247, 243, 247)}, button={'cn': (1157, 34, 1241, 61), 'en': (1136, 26, 1270, 63), 'jp': (1141, 38, 1220, 57), 'tw': (1157, 34, 1241, 61)}, file={'cn': './assets/cn/combat_ui/PAUSE.png', 'en': './assets/en/combat_ui/PAUSE.png', 'jp': './assets/jp/combat_ui/PAUSE.png', 'tw': './assets/tw/combat_ui/PAUSE.png'}) +PAUSE_Ancient = Button(area={'cn': (1228, 36, 1245, 55), 'en': (1228, 36, 1245, 55), 'jp': (1228, 36, 1245, 55), 'tw': (1228, 36, 1245, 55)}, color={'cn': (172, 161, 144), 'en': (172, 161, 144), 'jp': (172, 161, 144), 'tw': (172, 161, 144)}, button={'cn': (1228, 36, 1245, 55), 'en': (1228, 36, 1245, 55), 'jp': (1228, 36, 1245, 55), 'tw': (1228, 36, 1245, 55)}, file={'cn': './assets/cn/combat_ui/PAUSE_Ancient.png', 'en': './assets/cn/combat_ui/PAUSE_Ancient.png', 'jp': './assets/cn/combat_ui/PAUSE_Ancient.png', 'tw': './assets/cn/combat_ui/PAUSE_Ancient.png'}) PAUSE_Christmas = Button(area={'cn': (1234, 35, 1250, 56), 'en': (1234, 35, 1250, 56), 'jp': (1234, 35, 1250, 56), 'tw': (1234, 35, 1250, 56)}, color={'cn': (158, 181, 210), 'en': (158, 181, 210), 'jp': (158, 181, 210), 'tw': (158, 181, 210)}, button={'cn': (1234, 35, 1250, 56), 'en': (1234, 35, 1250, 56), 'jp': (1234, 35, 1250, 56), 'tw': (1234, 35, 1250, 56)}, file={'cn': './assets/cn/combat_ui/PAUSE_Christmas.png', 'en': './assets/cn/combat_ui/PAUSE_Christmas.png', 'jp': './assets/cn/combat_ui/PAUSE_Christmas.png', 'tw': './assets/cn/combat_ui/PAUSE_Christmas.png'}) PAUSE_Cyber = Button(area={'cn': (1231, 32, 1253, 59), 'en': (1231, 32, 1253, 59), 'jp': (1231, 32, 1253, 59), 'tw': (1231, 32, 1253, 59)}, color={'cn': (40, 140, 157), 'en': (40, 140, 157), 'jp': (40, 140, 157), 'tw': (40, 140, 157)}, button={'cn': (1231, 32, 1253, 59), 'en': (1231, 32, 1253, 59), 'jp': (1231, 32, 1253, 59), 'tw': (1231, 32, 1253, 59)}, file={'cn': './assets/cn/combat_ui/PAUSE_Cyber.png', 'en': './assets/cn/combat_ui/PAUSE_Cyber.png', 'jp': './assets/cn/combat_ui/PAUSE_Cyber.png', 'tw': './assets/cn/combat_ui/PAUSE_Cyber.png'}) PAUSE_DOUBLE_CHECK = Button(area={'cn': (1226, 35, 1231, 60), 'en': (1226, 35, 1231, 61), 'jp': (1226, 35, 1230, 60), 'tw': (1226, 35, 1231, 60)}, color={'cn': (96, 104, 136), 'en': (83, 98, 118), 'jp': (97, 102, 120), 'tw': (96, 104, 136)}, button={'cn': (1226, 35, 1231, 60), 'en': (1226, 35, 1231, 61), 'jp': (1226, 35, 1230, 60), 'tw': (1226, 35, 1231, 60)}, file={'cn': './assets/cn/combat_ui/PAUSE_DOUBLE_CHECK.png', 'en': './assets/en/combat_ui/PAUSE_DOUBLE_CHECK.png', 'jp': './assets/jp/combat_ui/PAUSE_DOUBLE_CHECK.png', 'tw': './assets/tw/combat_ui/PAUSE_DOUBLE_CHECK.png'}) diff --git a/alas_wrapped/module/config/argument/args.json b/alas_wrapped/module/config/argument/args.json index 2bc9941252..fe703db495 100644 --- a/alas_wrapped/module/config/argument/args.json +++ b/alas_wrapped/module/config/argument/args.json @@ -1,206 +1,4 @@ { - "Dashboard": { - "Oil": { - "Value": { - "type": "input", - "value": 0 - }, - "Limit": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^000000" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Coin": { - "Value": { - "type": "input", - "value": 0 - }, - "Limit": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^FFAA33" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Gem": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^FF3333" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Pt": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^00BFFF" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Cube": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^33FFFF" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "ActionPoint": { - "Value": { - "type": "input", - "value": 0 - }, - "Total": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^0000FF" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "YellowCoin": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^FF8800" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "PurpleCoin": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^7700BB" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Core": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^AAAAAA" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Medal": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^FFDD00" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Merit": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^FFFF00" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "GuildCoin": { - "Value": { - "type": "input", - "value": 0 - }, - "Color": { - "type": "input", - "value": "^AAAAAA" - }, - "Record": { - "type": "datetime", - "value": "2020-01-01 00:00:00", - "validate": "datetime" - } - }, - "Storage": { - "Storage": { - "type": "storage", - "value": {}, - "valuetype": "ignore", - "display": "disabled" - } - } - }, "Alas": { "Emulator": { "Serial": { @@ -269,6 +67,7 @@ "cn_android-25", "cn_android-26", "cn_android-27", + "cn_android-28", "cn_ios-0", "cn_ios-1", "cn_ios-2", @@ -394,10 +193,6 @@ "ScreenshotLength": { "type": "input", "value": 1 - }, - "RestartOnUnknownPage": { - "type": "checkbox", - "value": true } }, "Optimization": { @@ -1841,24 +1636,24 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "display": "hide", "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -2122,23 +1917,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -2517,23 +2312,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -2928,22 +2723,22 @@ "type": "state", "value": "campaign_main", "option": [ - "raid_20250116" + "raid_20260212" ], "option_cn": [ - "raid_20250116" + "raid_20260212" ], "option_en": [ - "raid_20250116" + "raid_20260212" ], "option_jp": [ - "raid_20250116" + "raid_20260212" ], "option_tw": [ - "raid_20250116" + "raid_20260212" ], "option_bold": [ - "raid_20250116" + "raid_20260212" ] }, "Mode": { @@ -3327,7 +3122,6 @@ "type": "state", "value": "campaign_main", "option": [ - "coalition_20251120", "coalition_20260122" ], "option_cn": [ @@ -3340,10 +3134,9 @@ "coalition_20260122" ], "option_tw": [ - "coalition_20251120" + "coalition_20260122" ], "option_bold": [ - "coalition_20251120", "coalition_20260122" ] }, @@ -3667,9 +3460,11 @@ "war_archives_20220728_cn", "war_archives_20220915_cn", "war_archives_20221222_cn", + "war_archives_20230223_cn", "war_archives_20231026_cn" ], "option_cn": [ + "war_archives_20230223_cn", "war_archives_20221222_cn", "war_archives_20220915_cn", "war_archives_20231026_cn", @@ -3715,6 +3510,7 @@ "war_archives_20181020_en" ], "option_en": [ + "war_archives_20230223_cn", "war_archives_20221222_cn", "war_archives_20220915_cn", "war_archives_20231026_cn", @@ -3760,6 +3556,7 @@ "war_archives_20181020_en" ], "option_jp": [ + "war_archives_20230223_cn", "war_archives_20221222_cn", "war_archives_20220915_cn", "war_archives_20231026_cn", @@ -3805,6 +3602,7 @@ "war_archives_20181020_en" ], "option_tw": [ + "war_archives_20230223_cn", "war_archives_20221222_cn", "war_archives_20220915_cn", "war_archives_20231026_cn", @@ -4237,23 +4035,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -4649,23 +4447,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -5061,23 +4859,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -5473,23 +5271,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -5875,23 +5673,23 @@ "value": "campaign_main", "option": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ], "option_cn": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_en": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_jp": [ - "event_20231221_cn" + "event_20260226_cn" ], "option_tw": [ "event_20220526_cn" ], "option_bold": [ "event_20220526_cn", - "event_20231221_cn" + "event_20260226_cn" ] }, "Mode": { @@ -6283,22 +6081,22 @@ "type": "state", "value": "campaign_main", "option": [ - "raid_20250116" + "raid_20260212" ], "option_cn": [ - "raid_20250116" + "raid_20260212" ], "option_en": [ - "raid_20250116" + "raid_20260212" ], "option_jp": [ - "raid_20250116" + "raid_20260212" ], "option_tw": [ - "raid_20250116" + "raid_20260212" ], "option_bold": [ - "raid_20250116" + "raid_20260212" ] }, "Mode": { @@ -6510,7 +6308,6 @@ "type": "state", "value": "campaign_main", "option": [ - "coalition_20251120", "coalition_20260122" ], "option_cn": [ @@ -6523,10 +6320,9 @@ "coalition_20260122" ], "option_tw": [ - "coalition_20251120" + "coalition_20260122" ], "option_bold": [ - "coalition_20251120", "coalition_20260122" ] }, @@ -8083,7 +7879,8 @@ "option": [ 2, 3, - 4 + 4, + 5 ] }, "ShipIndex": { @@ -8118,7 +7915,8 @@ 2, 3, 4, - 5 + 5, + 6 ] }, "ShipIndex": { diff --git a/alas_wrapped/module/config/argument/argument.yaml b/alas_wrapped/module/config/argument/argument.yaml index e35a9b7164..4cc3205db9 100644 --- a/alas_wrapped/module/config/argument/argument.yaml +++ b/alas_wrapped/module/config/argument/argument.yaml @@ -2,59 +2,6 @@ # Define arguments. # -------------------- -# ==================== Dashboard ==================== -Oil: - Value: 0 - Limit: 0 - Color: ^000000 - Record: 2020-01-01 00:00:00 -Coin: - Value: 0 - Limit: 0 - Color: ^FFAA33 - Record: 2020-01-01 00:00:00 -Gem: - Value: 0 - Color: ^FF3333 - Record: 2020-01-01 00:00:00 -Pt: - Value: 0 - Color: ^00BFFF - Record: 2020-01-01 00:00:00 -YellowCoin: - Value: 0 - Color: ^FF8800 - Record: 2020-01-01 00:00:00 -PurpleCoin: - Value: 0 - Color: ^7700BB - Record: 2020-01-01 00:00:00 -ActionPoint: - Value: 0 - Total: 0 - Color: ^0000FF - Record: 2020-01-01 00:00:00 -Merit: - Value: 0 - Color: ^FFFF00 - Record: 2020-01-01 00:00:00 -Cube: - Value: 0 - Color: ^33FFFF - Record: 2020-01-01 00:00:00 -Core: - Value: 0 - Color: ^AAAAAA - Record: 2020-01-01 00:00:00 -Medal: - Value: 0 - Color: ^FFDD00 - Record: 2020-01-01 00:00:00 -GuildCoin: - Value: 0 - Color: ^AAAAAA - Record: 2020-01-01 00:00:00 - # ==================== Alas ==================== Scheduler: @@ -141,7 +88,6 @@ Error: mode: yaml value: 'provider: null' ScreenshotLength: 1 - RestartOnUnknownPage: true Optimization: ScreenshotInterval: 0.3 CombatScreenshotInterval: 1.0 @@ -595,7 +541,7 @@ CoreShop: ShipyardDr: ResearchSeries: value: 2 - option: [ 2, 3, 4 ] + option: [ 2, 3, 4, 5 ] ShipIndex: value: 0 option: [ 0, 1, 2, 3, 4, 5, 6 ] @@ -604,7 +550,7 @@ ShipyardDr: Shipyard: ResearchSeries: value: 1 - option: [ 1, 2, 3, 4, 5 ] + option: [ 1, 2, 3, 4, 5, 6 ] ShipIndex: value: 0 option: [ 0, 1, 2, 3, 4, 5, 6 ] diff --git a/alas_wrapped/module/config/argument/gui.yaml b/alas_wrapped/module/config/argument/gui.yaml index d6c36792ad..82302d3ea2 100644 --- a/alas_wrapped/module/config/argument/gui.yaml +++ b/alas_wrapped/module/config/argument/gui.yaml @@ -14,8 +14,6 @@ Button: Stop: ScrollON: ScrollOFF: - DashboardON: - DashboardOFF: ClearLog: Setting: CheckUpdate: @@ -48,26 +46,6 @@ MenuDevelop: Overview: Scheduler: - Dashboard: - SecondsAgo: - MinutesAgo: - HoursAgo: - DaysAgo: - MonthsAgo: - YearsAgo: - NoData: - Oil: - Coin: - Gem: - Cube: - Pt: - YellowCoin: - PurpleCoin: - ActionPoint: - Merit: - Medal: - Core: - GuildCoin: Log: Running: Pending: diff --git a/alas_wrapped/module/config/argument/menu.json b/alas_wrapped/module/config/argument/menu.json index 7a2c2243d8..abdfe9c9b7 100644 --- a/alas_wrapped/module/config/argument/menu.json +++ b/alas_wrapped/module/config/argument/menu.json @@ -23,11 +23,11 @@ "page": "setting", "tasks": [ "EventGeneral", - "Coalition", "Event", "Event2", "Raid", "Hospital", + "Coalition", "MaritimeEscort", "WarArchives" ] @@ -36,13 +36,13 @@ "menu": "collapse", "page": "setting", "tasks": [ - "CoalitionSp", "EventA", "EventB", "EventC", "EventD", "EventSp", - "RaidDaily" + "RaidDaily", + "CoalitionSp" ] }, "Reward": { diff --git a/alas_wrapped/module/config/argument/task.yaml b/alas_wrapped/module/config/argument/task.yaml index 2cfc4b3729..c6f20bb00d 100644 --- a/alas_wrapped/module/config/argument/task.yaml +++ b/alas_wrapped/module/config/argument/task.yaml @@ -71,12 +71,6 @@ Event: EventGeneral: - EventGeneral - TaskBalancer - Coalition: - - Scheduler - - Campaign - - Coalition - - StopCondition - - Emotion Event: - Scheduler - Campaign @@ -106,6 +100,12 @@ Event: - Hospital - StopCondition - Emotion + Coalition: + - Scheduler + - Campaign + - Coalition + - StopCondition + - Emotion MaritimeEscort: - Scheduler - MaritimeEscort @@ -126,12 +126,6 @@ EventDaily: menu: 'collapse' page: 'setting' tasks: - CoalitionSp: - - Scheduler - - Campaign - - Coalition - - StopCondition - - Emotion EventA: - Scheduler - EventDaily @@ -187,6 +181,12 @@ EventDaily: - Campaign - StopCondition - Emotion + CoalitionSp: + - Scheduler + - Campaign + - Coalition + - StopCondition + - Emotion # ==================== Reward ==================== diff --git a/alas_wrapped/module/config/config_generated.py b/alas_wrapped/module/config/config_generated.py index 67e1524dd8..7c057779a7 100644 --- a/alas_wrapped/module/config/config_generated.py +++ b/alas_wrapped/module/config/config_generated.py @@ -9,69 +9,6 @@ class GeneratedConfig: Auto generated configuration """ - # Group `Oil` - Oil_Value = 0 - Oil_Limit = 0 - Oil_Color = '^000000' - Oil_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Coin` - Coin_Value = 0 - Coin_Limit = 0 - Coin_Color = '^FFAA33' - Coin_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Gem` - Gem_Value = 0 - Gem_Color = '^FF3333' - Gem_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Pt` - Pt_Value = 0 - Pt_Color = '^00BFFF' - Pt_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `YellowCoin` - YellowCoin_Value = 0 - YellowCoin_Color = '^FF8800' - YellowCoin_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `PurpleCoin` - PurpleCoin_Value = 0 - PurpleCoin_Color = '^7700BB' - PurpleCoin_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `ActionPoint` - ActionPoint_Value = 0 - ActionPoint_Total = 0 - ActionPoint_Color = '^0000FF' - ActionPoint_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Merit` - Merit_Value = 0 - Merit_Color = '^FFFF00' - Merit_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Cube` - Cube_Value = 0 - Cube_Color = '^33FFFF' - Cube_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Core` - Core_Value = 0 - Core_Color = '^AAAAAA' - Core_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `Medal` - Medal_Value = 0 - Medal_Color = '^FFDD00' - Medal_Record = datetime.datetime(2020, 1, 1, 0, 0) - - # Group `GuildCoin` - GuildCoin_Value = 0 - GuildCoin_Color = '^AAAAAA' - GuildCoin_Record = datetime.datetime(2020, 1, 1, 0, 0) - # Group `Scheduler` Scheduler_Enable = False # True, False Scheduler_NextRun = datetime.datetime(2020, 1, 1, 0, 0) @@ -83,7 +20,7 @@ class GeneratedConfig: # Group `Emulator` Emulator_Serial = 'auto' Emulator_PackageName = 'auto' # auto, com.bilibili.azurlane, com.YoStarEN.AzurLane, com.YoStarJP.AzurLane, com.hkmanjuu.azurlane.gp, com.bilibili.blhx.huawei, com.bilibili.blhx.honor, com.bilibili.blhx.mi, com.tencent.tmgp.bilibili.blhx, com.bilibili.blhx.baidu, com.bilibili.blhx.qihoo, com.bilibili.blhx.nearme.gamecenter, com.bilibili.blhx.vivo, com.bilibili.blhx.mz, com.bilibili.blhx.dl, com.bilibili.blhx.lenovo, com.bilibili.blhx.uc, com.bilibili.blhx.mzw, com.yiwu.blhx.yx15, com.bilibili.blhx.m4399, com.bilibili.blhx.bilibiliMove, com.hkmanjuu.azurlane.gp.mc - Emulator_ServerName = 'disabled' # disabled, cn_android-0, cn_android-1, cn_android-2, cn_android-3, cn_android-4, cn_android-5, cn_android-6, cn_android-7, cn_android-8, cn_android-9, cn_android-10, cn_android-11, cn_android-12, cn_android-13, cn_android-14, cn_android-15, cn_android-16, cn_android-17, cn_android-18, cn_android-19, cn_android-20, cn_android-21, cn_android-22, cn_android-23, cn_android-24, cn_android-25, cn_android-26, cn_android-27, cn_ios-0, cn_ios-1, cn_ios-2, cn_ios-3, cn_ios-4, cn_ios-5, cn_ios-6, cn_ios-7, cn_ios-8, cn_ios-9, cn_ios-10, cn_channel-0, cn_channel-1, cn_channel-2, cn_channel-3, cn_channel-4, en-0, en-1, en-2, en-3, en-4, en-5, jp-0, jp-1, jp-2, jp-3, jp-4, jp-5, jp-6, jp-7, jp-8, jp-9, jp-10, jp-11, jp-12, jp-13, jp-14, jp-15, jp-16, jp-17 + Emulator_ServerName = 'disabled' # disabled, cn_android-0, cn_android-1, cn_android-2, cn_android-3, cn_android-4, cn_android-5, cn_android-6, cn_android-7, cn_android-8, cn_android-9, cn_android-10, cn_android-11, cn_android-12, cn_android-13, cn_android-14, cn_android-15, cn_android-16, cn_android-17, cn_android-18, cn_android-19, cn_android-20, cn_android-21, cn_android-22, cn_android-23, cn_android-24, cn_android-25, cn_android-26, cn_android-27, cn_android-28, cn_ios-0, cn_ios-1, cn_ios-2, cn_ios-3, cn_ios-4, cn_ios-5, cn_ios-6, cn_ios-7, cn_ios-8, cn_ios-9, cn_ios-10, cn_channel-0, cn_channel-1, cn_channel-2, cn_channel-3, cn_channel-4, en-0, en-1, en-2, en-3, en-4, en-5, jp-0, jp-1, jp-2, jp-3, jp-4, jp-5, jp-6, jp-7, jp-8, jp-9, jp-10, jp-11, jp-12, jp-13, jp-14, jp-15, jp-16, jp-17 Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, nemu_ipc, ldopengl Emulator_ControlMethod = 'MaaTouch' # ADB, uiautomator2, minitouch, Hermit, MaaTouch Emulator_ScreenshotDedithering = False @@ -99,7 +36,6 @@ class GeneratedConfig: Error_SaveError = True Error_OnePushConfig = 'provider: null' Error_ScreenshotLength = 1 - Error_RestartOnUnknownPage = True # Group `Optimization` Optimization_ScreenshotInterval = 0.3 @@ -361,13 +297,13 @@ class GeneratedConfig: CoreShop_Filter = 'Array' # Group `ShipyardDr` - ShipyardDr_ResearchSeries = 2 # 2, 3, 4 + ShipyardDr_ResearchSeries = 2 # 2, 3, 4, 5 ShipyardDr_ShipIndex = 0 # 0, 1, 2, 3, 4, 5, 6 ShipyardDr_BuyAmount = 2 ShipyardDr_LastRun = datetime.datetime(2020, 1, 1, 0, 0) # Group `Shipyard` - Shipyard_ResearchSeries = 1 # 1, 2, 3, 4, 5 + Shipyard_ResearchSeries = 1 # 1, 2, 3, 4, 5, 6 Shipyard_ShipIndex = 0 # 0, 1, 2, 3, 4, 5, 6 Shipyard_BuyAmount = 2 Shipyard_LastRun = datetime.datetime(2020, 1, 1, 0, 0) diff --git a/alas_wrapped/module/config/config_updater.py b/alas_wrapped/module/config/config_updater.py index 19c700e9f6..5a95e04ec4 100644 --- a/alas_wrapped/module/config/config_updater.py +++ b/alas_wrapped/module/config/config_updater.py @@ -149,15 +149,6 @@ def gui(self): """ return read_file(filepath_argument('gui')) - @cached_property - def dashboard(self): - """ - - - - """ - return read_file(filepath_argument('dashboard')) - - @cached_property @timer def args(self): @@ -172,12 +163,10 @@ def args(self): """ # Construct args data = {} - # Add dashboard to args - dashboard_and_task = {**self.dashboard,**self.task} - for path, groups in deep_iter(dashboard_and_task, depth=3): - if 'tasks' not in path and 'Dashboard' not in path: + for path, groups in deep_iter(self.task, depth=3): + if 'tasks' not in path: continue - task = path[2] if 'tasks' in path else path[0] + task = path[2] # Add storage to all task groups.append('Storage') for group in groups: diff --git a/alas_wrapped/module/config/i18n/en-US.json b/alas_wrapped/module/config/i18n/en-US.json index 6c99da4e37..153b3f6d97 100644 --- a/alas_wrapped/module/config/i18n/en-US.json +++ b/alas_wrapped/module/config/i18n/en-US.json @@ -66,10 +66,6 @@ "name": "Event General", "help": "" }, - "Coalition": { - "name": "Light & Shadow Fashion Shoot", - "help": "" - }, "Event": { "name": "Event", "help": "" @@ -86,6 +82,10 @@ "name": "Valley Hospital", "help": "" }, + "Coalition": { + "name": "Light & Shadow Fashion Shoot", + "help": "" + }, "MaritimeEscort": { "name": "Maritime Escort", "help": "" @@ -94,10 +94,6 @@ "name": "War Archives", "help": "Due to the lack of maintenance of war archives, continuous clear may not work normally, if Alas runs abnormally, Please manually finish clearing and use auto search" }, - "CoalitionSp": { - "name": "Light & Shadow Fashion Shoot SP", - "help": "" - }, "EventA": { "name": "Event Daily A", "help": "" @@ -122,6 +118,10 @@ "name": "Raid Daily", "help": "" }, + "CoalitionSp": { + "name": "Light & Shadow Fashion Shoot SP", + "help": "" + }, "Commission": { "name": "Commission", "help": "" @@ -279,234 +279,6 @@ "help": "" } }, - "Oil": { - "_info": { - "name": "Oil._info.name", - "help": "Oil._info.help" - }, - "Value": { - "name": "Oil.Value.name", - "help": "Oil.Value.help" - }, - "Limit": { - "name": "Oil.Limit.name", - "help": "Oil.Limit.help" - }, - "Color": { - "name": "Oil.Color.name", - "help": "Oil.Color.help" - }, - "Record": { - "name": "Oil.Record.name", - "help": "Oil.Record.help" - } - }, - "Coin": { - "_info": { - "name": "Coin._info.name", - "help": "Coin._info.help" - }, - "Value": { - "name": "Coin.Value.name", - "help": "Coin.Value.help" - }, - "Limit": { - "name": "Coin.Limit.name", - "help": "Coin.Limit.help" - }, - "Color": { - "name": "Coin.Color.name", - "help": "Coin.Color.help" - }, - "Record": { - "name": "Coin.Record.name", - "help": "Coin.Record.help" - } - }, - "Gem": { - "_info": { - "name": "Gem._info.name", - "help": "Gem._info.help" - }, - "Value": { - "name": "Gem.Value.name", - "help": "Gem.Value.help" - }, - "Color": { - "name": "Gem.Color.name", - "help": "Gem.Color.help" - }, - "Record": { - "name": "Gem.Record.name", - "help": "Gem.Record.help" - } - }, - "Pt": { - "_info": { - "name": "Pt._info.name", - "help": "Pt._info.help" - }, - "Value": { - "name": "Pt.Value.name", - "help": "Pt.Value.help" - }, - "Color": { - "name": "Pt.Color.name", - "help": "Pt.Color.help" - }, - "Record": { - "name": "Pt.Record.name", - "help": "Pt.Record.help" - } - }, - "YellowCoin": { - "_info": { - "name": "YellowCoin._info.name", - "help": "YellowCoin._info.help" - }, - "Value": { - "name": "YellowCoin.Value.name", - "help": "YellowCoin.Value.help" - }, - "Color": { - "name": "YellowCoin.Color.name", - "help": "YellowCoin.Color.help" - }, - "Record": { - "name": "YellowCoin.Record.name", - "help": "YellowCoin.Record.help" - } - }, - "PurpleCoin": { - "_info": { - "name": "PurpleCoin._info.name", - "help": "PurpleCoin._info.help" - }, - "Value": { - "name": "PurpleCoin.Value.name", - "help": "PurpleCoin.Value.help" - }, - "Color": { - "name": "PurpleCoin.Color.name", - "help": "PurpleCoin.Color.help" - }, - "Record": { - "name": "PurpleCoin.Record.name", - "help": "PurpleCoin.Record.help" - } - }, - "ActionPoint": { - "_info": { - "name": "ActionPoint._info.name", - "help": "ActionPoint._info.help" - }, - "Value": { - "name": "ActionPoint.Value.name", - "help": "ActionPoint.Value.help" - }, - "Total": { - "name": "ActionPoint.Total.name", - "help": "ActionPoint.Total.help" - }, - "Color": { - "name": "ActionPoint.Color.name", - "help": "ActionPoint.Color.help" - }, - "Record": { - "name": "ActionPoint.Record.name", - "help": "ActionPoint.Record.help" - } - }, - "Merit": { - "_info": { - "name": "Merit._info.name", - "help": "Merit._info.help" - }, - "Value": { - "name": "Merit.Value.name", - "help": "Merit.Value.help" - }, - "Color": { - "name": "Merit.Color.name", - "help": "Merit.Color.help" - }, - "Record": { - "name": "Merit.Record.name", - "help": "Merit.Record.help" - } - }, - "Cube": { - "_info": { - "name": "Cube._info.name", - "help": "Cube._info.help" - }, - "Value": { - "name": "Cube.Value.name", - "help": "Cube.Value.help" - }, - "Color": { - "name": "Cube.Color.name", - "help": "Cube.Color.help" - }, - "Record": { - "name": "Cube.Record.name", - "help": "Cube.Record.help" - } - }, - "Core": { - "_info": { - "name": "Core._info.name", - "help": "Core._info.help" - }, - "Value": { - "name": "Core.Value.name", - "help": "Core.Value.help" - }, - "Color": { - "name": "Core.Color.name", - "help": "Core.Color.help" - }, - "Record": { - "name": "Core.Record.name", - "help": "Core.Record.help" - } - }, - "Medal": { - "_info": { - "name": "Medal._info.name", - "help": "Medal._info.help" - }, - "Value": { - "name": "Medal.Value.name", - "help": "Medal.Value.help" - }, - "Color": { - "name": "Medal.Color.name", - "help": "Medal.Color.help" - }, - "Record": { - "name": "Medal.Record.name", - "help": "Medal.Record.help" - } - }, - "GuildCoin": { - "_info": { - "name": "GuildCoin._info.name", - "help": "GuildCoin._info.help" - }, - "Value": { - "name": "GuildCoin.Value.name", - "help": "GuildCoin.Value.help" - }, - "Color": { - "name": "GuildCoin.Color.name", - "help": "GuildCoin.Color.help" - }, - "Record": { - "name": "GuildCoin.Record.name", - "help": "GuildCoin.Record.help" - } - }, "Scheduler": { "_info": { "name": "Scheduler", @@ -606,6 +378,7 @@ "cn_android-25": "[国服] 水仙行动", "cn_android-26": "[国服] 冬月计划", "cn_android-27": "[国服] 长弓计划", + "cn_android-28": "[国服] 裁决协议", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", @@ -731,10 +504,6 @@ "ScreenshotLength": { "name": "Record Screenshot(s)", "help": "Number of screenshots saved when exception occurs" - }, - "RestartOnUnknownPage": { - "name": "Restart on Unknown Page", - "help": "When an unknown page is encountered and the server is online, restart the game instead of stopping (only applies when Enable Exception Handling is turned on). After 3 consecutive task failures, ALAS will still request human takeover." } }, "Optimization": { @@ -997,6 +766,7 @@ "event_20250912_cn": "A Dance for Amahara Above", "event_20251023_cn": "Tempesta and Islas de Libertád", "event_20251218_cn": "A Note Through the Firmament", + "event_20260226_cn": "Springtide Inn Online", "raid_20200624": "Air Raid Drills with Essex Rerun", "raid_20210708": "Cross Wave rerun", "raid_20220127": "Mystery Investigation", @@ -1005,8 +775,9 @@ "raid_20230118": "Winter Pathfinder", "raid_20230629": "Reflections of the Oasis", "raid_20240130": "Spring Festive Fiasco", - "raid_20240328": "From Zero to Hero", + "raid_20240328": "From Zero to Hero Rerun", "raid_20250116": "Spring Fashion Festa", + "raid_20260212": "Spring Auction Adventure", "war_archives_20180607_cn": "archives Ink Stained Steel Sakura", "war_archives_20180726_cn": "archives Iris of Light and Dark", "war_archives_20181020_en": "archives Strive Wish and Strategize", @@ -1049,6 +820,7 @@ "war_archives_20220728_cn": "archives Aquilifers Ballade", "war_archives_20220915_cn": "archives Violet Tempest Blooming Lycoris", "war_archives_20221222_cn": "archives Parallel Superimposition", + "war_archives_20230223_cn": "archives Revelations of Dust", "war_archives_20231026_cn": "archives Tempesta and the Fountain of Youth" }, "Mode": { @@ -1501,10 +1273,10 @@ "Mode": { "name": "Raid Mode", "help": "", - "easy": "easy", - "normal": "normal", - "hard": "hard", - "ex": "ex" + "easy": "Easy", + "normal": "Normal", + "hard": "Hard", + "ex": "EX" }, "UseTicket": { "name": "Use Ticket(s)", @@ -2117,7 +1889,8 @@ "help": "", "2": "DR2", "3": "DR3", - "4": "DR4" + "4": "DR4", + "5": "DR5" }, "ShipIndex": { "name": "Ship Index", @@ -2151,7 +1924,8 @@ "2": "PR2", "3": "PR3", "4": "PR4", - "5": "PR5" + "5": "PR5", + "6": "PR6" }, "ShipIndex": { "name": "Ship Index", @@ -2462,7 +2236,7 @@ }, "OpponentChooseMode": { "name": "Opponent Choose Mode", - "help": "IMPORTANT: In Exercise, opponents are ALWAYS sorted by rank (left = highest, right = lowest).\n• leftmost = Fight highest rank player (MAXIMUM merit points/rewards) - RECOMMENDED\n• max_exp = Calculate and fight highest level opponent\n• easiest = Fight lowest level opponent\n• easiest_else_exp = Try easiest first, switch to max_exp if failed", + "help": "", "max_exp": "Most Exp.", "easiest": "Easiest", "leftmost": "Highest ranking", @@ -2470,7 +2244,7 @@ }, "OpponentTrial": { "name": "Each Opponent Try X Time(s)", - "help": "Number of times to retry the SAME opponent before moving to the next one.\nIf you quit battles due to low HP, increase this value (e.g., 10) to retry the same opponent multiple times instead of switching.\nRecommended: 10 for consistent targeting of the best opponent.\n1 ~ Positive Infinity" + "help": "1 ~ Positive Infinity" }, "ExerciseStrategy": { "name": "Exercise Strategy (Keep X number remains)", @@ -2870,8 +2644,6 @@ "Stop": "Stop", "ScrollON": "Auto Scroll ON", "ScrollOFF": "Auto Scroll OFF", - "DashboardON": "Fold", - "DashboardOFF": "Unfold", "ClearLog": "Clear Log", "Setting": "Setting", "CheckUpdate": "Check update", @@ -2904,26 +2676,6 @@ }, "Overview": { "Scheduler": "Scheduler", - "Dashboard": "Gui.Overview.Dashboard", - "SecondsAgo": "Seconds ago", - "MinutesAgo": "Minutes ago", - "HoursAgo": "Hours ago", - "DaysAgo": "Days ago", - "MonthsAgo": "Months ago", - "YearsAgo": "Years ago", - "NoData": "No data", - "Oil": "Oil", - "Coin": "Coin", - "Gem": "Gem", - "Cube": "Cube", - "Pt": "Event Pt", - "YellowCoin": "Operation Supply Coin", - "PurpleCoin": "Special Item Token", - "ActionPoint": "Action Point", - "Merit": "Merit", - "Medal": "Medal", - "Core": "Core Data", - "GuildCoin": "Guild Coin", "Log": "Log", "Running": "Running", "Pending": "Pending", diff --git a/alas_wrapped/module/config/i18n/ja-JP.json b/alas_wrapped/module/config/i18n/ja-JP.json index 4676d758c7..2de0d9a8b3 100644 --- a/alas_wrapped/module/config/i18n/ja-JP.json +++ b/alas_wrapped/module/config/i18n/ja-JP.json @@ -66,10 +66,6 @@ "name": "イベント共通設定", "help": "" }, - "Coalition": { - "name": "特集写真-撮影進行中", - "help": "" - }, "Event": { "name": "イベント海域", "help": "" @@ -86,6 +82,10 @@ "name": "病院探訪", "help": "" }, + "Coalition": { + "name": "特集写真-撮影進行中", + "help": "" + }, "MaritimeEscort": { "name": "Maritime Escort", "help": "" @@ -94,10 +94,6 @@ "name": "作戦履歴", "help": "" }, - "CoalitionSp": { - "name": "特集写真-撮影進行中SP", - "help": "" - }, "EventA": { "name": "毎日イベント海域A", "help": "" @@ -122,6 +118,10 @@ "name": "Raid Daily", "help": "" }, + "CoalitionSp": { + "name": "特集写真-撮影進行中SP", + "help": "" + }, "Commission": { "name": "委託", "help": "" @@ -279,234 +279,6 @@ "help": "" } }, - "Oil": { - "_info": { - "name": "Oil._info.name", - "help": "Oil._info.help" - }, - "Value": { - "name": "Oil.Value.name", - "help": "Oil.Value.help" - }, - "Limit": { - "name": "Oil.Limit.name", - "help": "Oil.Limit.help" - }, - "Color": { - "name": "Oil.Color.name", - "help": "Oil.Color.help" - }, - "Record": { - "name": "Oil.Record.name", - "help": "Oil.Record.help" - } - }, - "Coin": { - "_info": { - "name": "Coin._info.name", - "help": "Coin._info.help" - }, - "Value": { - "name": "Coin.Value.name", - "help": "Coin.Value.help" - }, - "Limit": { - "name": "Coin.Limit.name", - "help": "Coin.Limit.help" - }, - "Color": { - "name": "Coin.Color.name", - "help": "Coin.Color.help" - }, - "Record": { - "name": "Coin.Record.name", - "help": "Coin.Record.help" - } - }, - "Gem": { - "_info": { - "name": "Gem._info.name", - "help": "Gem._info.help" - }, - "Value": { - "name": "Gem.Value.name", - "help": "Gem.Value.help" - }, - "Color": { - "name": "Gem.Color.name", - "help": "Gem.Color.help" - }, - "Record": { - "name": "Gem.Record.name", - "help": "Gem.Record.help" - } - }, - "Pt": { - "_info": { - "name": "Pt._info.name", - "help": "Pt._info.help" - }, - "Value": { - "name": "Pt.Value.name", - "help": "Pt.Value.help" - }, - "Color": { - "name": "Pt.Color.name", - "help": "Pt.Color.help" - }, - "Record": { - "name": "Pt.Record.name", - "help": "Pt.Record.help" - } - }, - "YellowCoin": { - "_info": { - "name": "YellowCoin._info.name", - "help": "YellowCoin._info.help" - }, - "Value": { - "name": "YellowCoin.Value.name", - "help": "YellowCoin.Value.help" - }, - "Color": { - "name": "YellowCoin.Color.name", - "help": "YellowCoin.Color.help" - }, - "Record": { - "name": "YellowCoin.Record.name", - "help": "YellowCoin.Record.help" - } - }, - "PurpleCoin": { - "_info": { - "name": "PurpleCoin._info.name", - "help": "PurpleCoin._info.help" - }, - "Value": { - "name": "PurpleCoin.Value.name", - "help": "PurpleCoin.Value.help" - }, - "Color": { - "name": "PurpleCoin.Color.name", - "help": "PurpleCoin.Color.help" - }, - "Record": { - "name": "PurpleCoin.Record.name", - "help": "PurpleCoin.Record.help" - } - }, - "ActionPoint": { - "_info": { - "name": "ActionPoint._info.name", - "help": "ActionPoint._info.help" - }, - "Value": { - "name": "ActionPoint.Value.name", - "help": "ActionPoint.Value.help" - }, - "Total": { - "name": "ActionPoint.Total.name", - "help": "ActionPoint.Total.help" - }, - "Color": { - "name": "ActionPoint.Color.name", - "help": "ActionPoint.Color.help" - }, - "Record": { - "name": "ActionPoint.Record.name", - "help": "ActionPoint.Record.help" - } - }, - "Merit": { - "_info": { - "name": "Merit._info.name", - "help": "Merit._info.help" - }, - "Value": { - "name": "Merit.Value.name", - "help": "Merit.Value.help" - }, - "Color": { - "name": "Merit.Color.name", - "help": "Merit.Color.help" - }, - "Record": { - "name": "Merit.Record.name", - "help": "Merit.Record.help" - } - }, - "Cube": { - "_info": { - "name": "Cube._info.name", - "help": "Cube._info.help" - }, - "Value": { - "name": "Cube.Value.name", - "help": "Cube.Value.help" - }, - "Color": { - "name": "Cube.Color.name", - "help": "Cube.Color.help" - }, - "Record": { - "name": "Cube.Record.name", - "help": "Cube.Record.help" - } - }, - "Core": { - "_info": { - "name": "Core._info.name", - "help": "Core._info.help" - }, - "Value": { - "name": "Core.Value.name", - "help": "Core.Value.help" - }, - "Color": { - "name": "Core.Color.name", - "help": "Core.Color.help" - }, - "Record": { - "name": "Core.Record.name", - "help": "Core.Record.help" - } - }, - "Medal": { - "_info": { - "name": "Medal._info.name", - "help": "Medal._info.help" - }, - "Value": { - "name": "Medal.Value.name", - "help": "Medal.Value.help" - }, - "Color": { - "name": "Medal.Color.name", - "help": "Medal.Color.help" - }, - "Record": { - "name": "Medal.Record.name", - "help": "Medal.Record.help" - } - }, - "GuildCoin": { - "_info": { - "name": "GuildCoin._info.name", - "help": "GuildCoin._info.help" - }, - "Value": { - "name": "GuildCoin.Value.name", - "help": "GuildCoin.Value.help" - }, - "Color": { - "name": "GuildCoin.Color.name", - "help": "GuildCoin.Color.help" - }, - "Record": { - "name": "GuildCoin.Record.name", - "help": "GuildCoin.Record.help" - } - }, "Scheduler": { "_info": { "name": "Scheduler._info.name", @@ -606,6 +378,7 @@ "cn_android-25": "[国服] 水仙行动", "cn_android-26": "[国服] 冬月计划", "cn_android-27": "[国服] 长弓计划", + "cn_android-28": "[国服] 裁决协议", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", @@ -731,10 +504,6 @@ "ScreenshotLength": { "name": "Error.ScreenshotLength.name", "help": "Error.ScreenshotLength.help" - }, - "RestartOnUnknownPage": { - "name": "Error.RestartOnUnknownPage.name", - "help": "Error.RestartOnUnknownPage.help" } }, "Optimization": { @@ -997,6 +766,7 @@ "event_20250912_cn": "アマハラに舞い奉れ", "event_20251023_cn": "テンペスタと自由群島", "event_20251218_cn": "天穹に響く音謡", + "event_20260226_cn": "春色旅籠Online", "raid_20200624": "特別演習超空強襲波(復刻)", "raid_20210708": "交錯する新たな波 (復刻)", "raid_20220127": "秘密事件調査", @@ -1005,8 +775,9 @@ "raid_20230118": "冬の案内人", "raid_20230629": "緑地伽話", "raid_20240130": "新春宴会狂騒曲", - "raid_20240328": "ゼロから頑張る魔王討伐", + "raid_20240328": "ゼロから頑張る魔王討伐(復刻)", "raid_20250116": "新春華裳協奏曲", + "raid_20260212": "新春玉逸品会", "war_archives_20180607_cn": "檔案 墨染まりし鋼の桜", "war_archives_20180726_cn": "檔案 光と影のアイリス", "war_archives_20181020_en": "檔案 努力希望と計画", @@ -1049,6 +820,7 @@ "war_archives_20220728_cn": "檔案 鋼鷲の冒険譚", "war_archives_20220915_cn": "檔案 赫の涙月 菫の暁風", "war_archives_20221222_cn": "檔案 積重なる事象の幻界", + "war_archives_20230223_cn": "檔案 黙示の遺構", "war_archives_20231026_cn": "檔案 テンペスタと若返りの泉" }, "Mode": { @@ -2117,7 +1889,8 @@ "help": "ShipyardDr.ResearchSeries.help", "2": "2", "3": "3", - "4": "4" + "4": "4", + "5": "5" }, "ShipIndex": { "name": "ShipyardDr.ShipIndex.name", @@ -2151,7 +1924,8 @@ "2": "2", "3": "3", "4": "4", - "5": "5" + "5": "5", + "6": "6" }, "ShipIndex": { "name": "Shipyard.ShipIndex.name", @@ -2870,8 +2644,6 @@ "Stop": "中止", "ScrollON": "自動スクロール ON", "ScrollOFF": "自動スクロール OFF", - "DashboardON": "Gui.Button.DashboardON", - "DashboardOFF": "Gui.Button.DashboardOFF", "ClearLog": "ログクリーニング", "Setting": "設定", "CheckUpdate": "アップデータチェック", @@ -2904,26 +2676,6 @@ }, "Overview": { "Scheduler": "スケジューラー", - "Dashboard": "Gui.Overview.Dashboard", - "SecondsAgo": "Seconds ago", - "MinutesAgo": "Minutes ago", - "HoursAgo": "Hours ago", - "DaysAgo": "Days ago", - "MonthsAgo": "Months ago", - "YearsAgo": "Years ago", - "NoData": "No data", - "Oil": "Oil", - "Coin": "Coin", - "Gem": "Gem", - "Cube": "Cube", - "Pt": "Event Pt", - "YellowCoin": "Operation Supply Coin", - "PurpleCoin": "Special Item Token", - "ActionPoint": "Action Point", - "Merit": "Gui.Overview.Merit", - "Medal": "Gui.Overview.Medal", - "Core": "Gui.Overview.Core", - "GuildCoin": "Gui.Overview.GuildCoin", "Log": "ログ", "Running": "実行中", "Pending": "隊列中", diff --git a/alas_wrapped/module/config/i18n/zh-CN.json b/alas_wrapped/module/config/i18n/zh-CN.json index 5f1641fc1c..5af93089b6 100644 --- a/alas_wrapped/module/config/i18n/zh-CN.json +++ b/alas_wrapped/module/config/i18n/zh-CN.json @@ -66,10 +66,6 @@ "name": "活动通用设置", "help": "" }, - "Coalition": { - "name": "光影风尚-拍摄进行时", - "help": "" - }, "Event": { "name": "活动图", "help": "" @@ -86,6 +82,10 @@ "name": "深谷来信", "help": "" }, + "Coalition": { + "name": "光影风尚-拍摄进行时", + "help": "" + }, "MaritimeEscort": { "name": "商船护航", "help": "" @@ -94,10 +94,6 @@ "name": "作战档案", "help": "由于作战档案缺少维护,开荒功能不一定能正常使用,如果发现Alas运行异常,请手动完成开荒后使用自律寻敌功能" }, - "CoalitionSp": { - "name": "光影风尚-拍摄进行时SP", - "help": "" - }, "EventA": { "name": "活动每日A图", "help": "" @@ -122,6 +118,10 @@ "name": "共斗活动每日", "help": "" }, + "CoalitionSp": { + "name": "光影风尚-拍摄进行时SP", + "help": "" + }, "Commission": { "name": "委托", "help": "" @@ -279,234 +279,6 @@ "help": "" } }, - "Oil": { - "_info": { - "name": "Oil._info.name", - "help": "Oil._info.help" - }, - "Value": { - "name": "Oil.Value.name", - "help": "Oil.Value.help" - }, - "Limit": { - "name": "Oil.Limit.name", - "help": "Oil.Limit.help" - }, - "Color": { - "name": "Oil.Color.name", - "help": "Oil.Color.help" - }, - "Record": { - "name": "Oil.Record.name", - "help": "Oil.Record.help" - } - }, - "Coin": { - "_info": { - "name": "Coin._info.name", - "help": "Coin._info.help" - }, - "Value": { - "name": "Coin.Value.name", - "help": "Coin.Value.help" - }, - "Limit": { - "name": "Coin.Limit.name", - "help": "Coin.Limit.help" - }, - "Color": { - "name": "Coin.Color.name", - "help": "Coin.Color.help" - }, - "Record": { - "name": "Coin.Record.name", - "help": "Coin.Record.help" - } - }, - "Gem": { - "_info": { - "name": "Gem._info.name", - "help": "Gem._info.help" - }, - "Value": { - "name": "Gem.Value.name", - "help": "Gem.Value.help" - }, - "Color": { - "name": "Gem.Color.name", - "help": "Gem.Color.help" - }, - "Record": { - "name": "Gem.Record.name", - "help": "Gem.Record.help" - } - }, - "Pt": { - "_info": { - "name": "Pt._info.name", - "help": "Pt._info.help" - }, - "Value": { - "name": "Pt.Value.name", - "help": "Pt.Value.help" - }, - "Color": { - "name": "Pt.Color.name", - "help": "Pt.Color.help" - }, - "Record": { - "name": "Pt.Record.name", - "help": "Pt.Record.help" - } - }, - "YellowCoin": { - "_info": { - "name": "YellowCoin._info.name", - "help": "YellowCoin._info.help" - }, - "Value": { - "name": "YellowCoin.Value.name", - "help": "YellowCoin.Value.help" - }, - "Color": { - "name": "YellowCoin.Color.name", - "help": "YellowCoin.Color.help" - }, - "Record": { - "name": "YellowCoin.Record.name", - "help": "YellowCoin.Record.help" - } - }, - "PurpleCoin": { - "_info": { - "name": "PurpleCoin._info.name", - "help": "PurpleCoin._info.help" - }, - "Value": { - "name": "PurpleCoin.Value.name", - "help": "PurpleCoin.Value.help" - }, - "Color": { - "name": "PurpleCoin.Color.name", - "help": "PurpleCoin.Color.help" - }, - "Record": { - "name": "PurpleCoin.Record.name", - "help": "PurpleCoin.Record.help" - } - }, - "ActionPoint": { - "_info": { - "name": "ActionPoint._info.name", - "help": "ActionPoint._info.help" - }, - "Value": { - "name": "ActionPoint.Value.name", - "help": "ActionPoint.Value.help" - }, - "Total": { - "name": "ActionPoint.Total.name", - "help": "ActionPoint.Total.help" - }, - "Color": { - "name": "ActionPoint.Color.name", - "help": "ActionPoint.Color.help" - }, - "Record": { - "name": "ActionPoint.Record.name", - "help": "ActionPoint.Record.help" - } - }, - "Merit": { - "_info": { - "name": "Merit._info.name", - "help": "Merit._info.help" - }, - "Value": { - "name": "Merit.Value.name", - "help": "Merit.Value.help" - }, - "Color": { - "name": "Merit.Color.name", - "help": "Merit.Color.help" - }, - "Record": { - "name": "Merit.Record.name", - "help": "Merit.Record.help" - } - }, - "Cube": { - "_info": { - "name": "Cube._info.name", - "help": "Cube._info.help" - }, - "Value": { - "name": "Cube.Value.name", - "help": "Cube.Value.help" - }, - "Color": { - "name": "Cube.Color.name", - "help": "Cube.Color.help" - }, - "Record": { - "name": "Cube.Record.name", - "help": "Cube.Record.help" - } - }, - "Core": { - "_info": { - "name": "Core._info.name", - "help": "Core._info.help" - }, - "Value": { - "name": "Core.Value.name", - "help": "Core.Value.help" - }, - "Color": { - "name": "Core.Color.name", - "help": "Core.Color.help" - }, - "Record": { - "name": "Core.Record.name", - "help": "Core.Record.help" - } - }, - "Medal": { - "_info": { - "name": "Medal._info.name", - "help": "Medal._info.help" - }, - "Value": { - "name": "Medal.Value.name", - "help": "Medal.Value.help" - }, - "Color": { - "name": "Medal.Color.name", - "help": "Medal.Color.help" - }, - "Record": { - "name": "Medal.Record.name", - "help": "Medal.Record.help" - } - }, - "GuildCoin": { - "_info": { - "name": "GuildCoin._info.name", - "help": "GuildCoin._info.help" - }, - "Value": { - "name": "GuildCoin.Value.name", - "help": "GuildCoin.Value.help" - }, - "Color": { - "name": "GuildCoin.Color.name", - "help": "GuildCoin.Color.help" - }, - "Record": { - "name": "GuildCoin.Record.name", - "help": "GuildCoin.Record.help" - } - }, "Scheduler": { "_info": { "name": "任务设置", @@ -606,6 +378,7 @@ "cn_android-25": "[国服] 水仙行动", "cn_android-26": "[国服] 冬月计划", "cn_android-27": "[国服] 长弓计划", + "cn_android-28": "[国服] 裁决协议", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", @@ -731,10 +504,6 @@ "ScreenshotLength": { "name": "出错时,保留最后 X 张截图", "help": "" - }, - "RestartOnUnknownPage": { - "name": "遇到未知页面时重启", - "help": "当遇到未知页面且服务器在线时,重启游戏而不是停止(此功能依赖已启用\"启用异常处理\")。连续3次任务失败后,ALAS仍会请求人工接管。" } }, "Optimization": { @@ -997,6 +766,7 @@ "event_20250912_cn": "起舞于天原之上", "event_20251023_cn": "飓风与自由群岛", "event_20251218_cn": "响彻于天穹之音", + "event_20260226_cn": "春满客栈Online", "raid_20200624": "复刻特别演习埃塞克斯级", "raid_20210708": "复刻穿越彼方的水线", "raid_20220127": "演习神秘事件调查", @@ -1005,8 +775,9 @@ "raid_20230118": "冬日的寻路人", "raid_20230629": "绿洲往事", "raid_20240130": "寰昌宇定家事忙", - "raid_20240328": "从零开始的魔王讨伐之旅", + "raid_20240328": "复刻从零开始的魔王讨伐之旅", "raid_20250116": "华裳巧展喜事长", + "raid_20260212": "春宴怀玉香满庭", "war_archives_20180607_cn": "档案 墨染的钢铁之花", "war_archives_20180726_cn": "档案 光与影的鸢尾之华", "war_archives_20181020_en": "档案 努力希望和计划", @@ -1049,6 +820,7 @@ "war_archives_20220728_cn": "档案 雄鹰的叙事歌", "war_archives_20220915_cn": "档案 紫绛槿岚", "war_archives_20221222_cn": "档案 定向折叠", + "war_archives_20230223_cn": "档案 湮烬尘墟", "war_archives_20231026_cn": "档案 飓风与青春之泉" }, "Mode": { @@ -1504,7 +1276,7 @@ "easy": "简单", "normal": "普通", "hard": "困难", - "ex": "ex" + "ex": "EX" }, "UseTicket": { "name": "使用演习券", @@ -2117,7 +1889,8 @@ "help": "", "2": "二期科研", "3": "三期科研", - "4": "四期科研" + "4": "四期科研", + "5": "五期科研" }, "ShipIndex": { "name": "舰船序号", @@ -2151,7 +1924,8 @@ "2": "二期科研", "3": "三期科研", "4": "四期科研", - "5": "五期科研" + "5": "五期科研", + "6": "六期科研" }, "ShipIndex": { "name": "舰船序号", @@ -2870,8 +2644,6 @@ "Stop": "停止", "ScrollON": "自动滚动 开", "ScrollOFF": "自动滚动 关", - "DashboardON": "折叠", - "DashboardOFF": "展开", "ClearLog": "清空日志", "Setting": "设置", "CheckUpdate": "检查更新", @@ -2904,26 +2676,6 @@ }, "Overview": { "Scheduler": "调度器", - "Dashboard": "Gui.Overview.Dashboard", - "SecondsAgo": "秒前", - "MinutesAgo": "分钟前", - "HoursAgo": "小时前", - "DaysAgo": "天前", - "MonthsAgo": "月前", - "YearsAgo": "年前", - "NoData": "无数据", - "Oil": "石油", - "Coin": "物资", - "Gem": "钻石", - "Cube": "魔方", - "Pt": "活动PT", - "YellowCoin": "大世界黄币", - "PurpleCoin": "大世界紫币", - "ActionPoint": "行动力", - "Merit": "功勋", - "Medal": "勋章", - "Core": "核心数据", - "GuildCoin": "舰队币", "Log": "日志", "Running": "运行中", "Pending": "队列中", @@ -2994,4 +2746,4 @@ "ChooseFile": "选择文件" } } -} +} \ No newline at end of file diff --git a/alas_wrapped/module/config/i18n/zh-TW.json b/alas_wrapped/module/config/i18n/zh-TW.json index 3c3af15c5e..7687ff8aaf 100644 --- a/alas_wrapped/module/config/i18n/zh-TW.json +++ b/alas_wrapped/module/config/i18n/zh-TW.json @@ -66,10 +66,6 @@ "name": "活動通用", "help": "" }, - "Coalition": { - "name": "光影風尚-拍攝進行時", - "help": "" - }, "Event": { "name": "活動圖", "help": "" @@ -86,6 +82,10 @@ "name": "深谷来信", "help": "" }, + "Coalition": { + "name": "光影風尚-拍攝進行時", + "help": "" + }, "MaritimeEscort": { "name": "商船護航", "help": "" @@ -94,10 +94,6 @@ "name": "作戰檔案", "help": "由於作戰檔案缺少維護,開荒功能不一定能正常使用,如果發現Alas運行異常,請手動完成開荒後使用自律尋敵功能" }, - "CoalitionSp": { - "name": "光影風尚-拍攝進行時SP", - "help": "" - }, "EventA": { "name": "活動每日A圖", "help": "" @@ -122,6 +118,10 @@ "name": "共鬥活動每日", "help": "" }, + "CoalitionSp": { + "name": "光影風尚-拍攝進行時SP", + "help": "" + }, "Commission": { "name": "委託", "help": "" @@ -279,234 +279,6 @@ "help": "" } }, - "Oil": { - "_info": { - "name": "Oil._info.name", - "help": "Oil._info.help" - }, - "Value": { - "name": "Oil.Value.name", - "help": "Oil.Value.help" - }, - "Limit": { - "name": "Oil.Limit.name", - "help": "Oil.Limit.help" - }, - "Color": { - "name": "Oil.Color.name", - "help": "Oil.Color.help" - }, - "Record": { - "name": "Oil.Record.name", - "help": "Oil.Record.help" - } - }, - "Coin": { - "_info": { - "name": "Coin._info.name", - "help": "Coin._info.help" - }, - "Value": { - "name": "Coin.Value.name", - "help": "Coin.Value.help" - }, - "Limit": { - "name": "Coin.Limit.name", - "help": "Coin.Limit.help" - }, - "Color": { - "name": "Coin.Color.name", - "help": "Coin.Color.help" - }, - "Record": { - "name": "Coin.Record.name", - "help": "Coin.Record.help" - } - }, - "Gem": { - "_info": { - "name": "Gem._info.name", - "help": "Gem._info.help" - }, - "Value": { - "name": "Gem.Value.name", - "help": "Gem.Value.help" - }, - "Color": { - "name": "Gem.Color.name", - "help": "Gem.Color.help" - }, - "Record": { - "name": "Gem.Record.name", - "help": "Gem.Record.help" - } - }, - "Pt": { - "_info": { - "name": "Pt._info.name", - "help": "Pt._info.help" - }, - "Value": { - "name": "Pt.Value.name", - "help": "Pt.Value.help" - }, - "Color": { - "name": "Pt.Color.name", - "help": "Pt.Color.help" - }, - "Record": { - "name": "Pt.Record.name", - "help": "Pt.Record.help" - } - }, - "YellowCoin": { - "_info": { - "name": "YellowCoin._info.name", - "help": "YellowCoin._info.help" - }, - "Value": { - "name": "YellowCoin.Value.name", - "help": "YellowCoin.Value.help" - }, - "Color": { - "name": "YellowCoin.Color.name", - "help": "YellowCoin.Color.help" - }, - "Record": { - "name": "YellowCoin.Record.name", - "help": "YellowCoin.Record.help" - } - }, - "PurpleCoin": { - "_info": { - "name": "PurpleCoin._info.name", - "help": "PurpleCoin._info.help" - }, - "Value": { - "name": "PurpleCoin.Value.name", - "help": "PurpleCoin.Value.help" - }, - "Color": { - "name": "PurpleCoin.Color.name", - "help": "PurpleCoin.Color.help" - }, - "Record": { - "name": "PurpleCoin.Record.name", - "help": "PurpleCoin.Record.help" - } - }, - "ActionPoint": { - "_info": { - "name": "ActionPoint._info.name", - "help": "ActionPoint._info.help" - }, - "Value": { - "name": "ActionPoint.Value.name", - "help": "ActionPoint.Value.help" - }, - "Total": { - "name": "ActionPoint.Total.name", - "help": "ActionPoint.Total.help" - }, - "Color": { - "name": "ActionPoint.Color.name", - "help": "ActionPoint.Color.help" - }, - "Record": { - "name": "ActionPoint.Record.name", - "help": "ActionPoint.Record.help" - } - }, - "Merit": { - "_info": { - "name": "Merit._info.name", - "help": "Merit._info.help" - }, - "Value": { - "name": "Merit.Value.name", - "help": "Merit.Value.help" - }, - "Color": { - "name": "Merit.Color.name", - "help": "Merit.Color.help" - }, - "Record": { - "name": "Merit.Record.name", - "help": "Merit.Record.help" - } - }, - "Cube": { - "_info": { - "name": "Cube._info.name", - "help": "Cube._info.help" - }, - "Value": { - "name": "Cube.Value.name", - "help": "Cube.Value.help" - }, - "Color": { - "name": "Cube.Color.name", - "help": "Cube.Color.help" - }, - "Record": { - "name": "Cube.Record.name", - "help": "Cube.Record.help" - } - }, - "Core": { - "_info": { - "name": "Core._info.name", - "help": "Core._info.help" - }, - "Value": { - "name": "Core.Value.name", - "help": "Core.Value.help" - }, - "Color": { - "name": "Core.Color.name", - "help": "Core.Color.help" - }, - "Record": { - "name": "Core.Record.name", - "help": "Core.Record.help" - } - }, - "Medal": { - "_info": { - "name": "Medal._info.name", - "help": "Medal._info.help" - }, - "Value": { - "name": "Medal.Value.name", - "help": "Medal.Value.help" - }, - "Color": { - "name": "Medal.Color.name", - "help": "Medal.Color.help" - }, - "Record": { - "name": "Medal.Record.name", - "help": "Medal.Record.help" - } - }, - "GuildCoin": { - "_info": { - "name": "GuildCoin._info.name", - "help": "GuildCoin._info.help" - }, - "Value": { - "name": "GuildCoin.Value.name", - "help": "GuildCoin.Value.help" - }, - "Color": { - "name": "GuildCoin.Color.name", - "help": "GuildCoin.Color.help" - }, - "Record": { - "name": "GuildCoin.Record.name", - "help": "GuildCoin.Record.help" - } - }, "Scheduler": { "_info": { "name": "任務設定", @@ -606,6 +378,7 @@ "cn_android-25": "[国服] 水仙行动", "cn_android-26": "[国服] 冬月计划", "cn_android-27": "[国服] 长弓计划", + "cn_android-28": "[国服] 裁决协议", "cn_ios-0": "[国服] 夏威夷", "cn_ios-1": "[国服] 珊瑚海", "cn_ios-2": "[国服] 中途岛", @@ -731,10 +504,6 @@ "ScreenshotLength": { "name": "出錯時,保留最後 X 張截圖", "help": "" - }, - "RestartOnUnknownPage": { - "name": "遇到未知頁面時重啟", - "help": "當遇到未知頁面且伺服器在線時,重啟遊戲而不是停止(此功能需搭配啟用「啟用異常處理」)。連續3次任務失敗後,ALAS仍會請求人工接管。" } }, "Optimization": { @@ -916,7 +685,7 @@ "coalition_20240627": "歡迎來到童心學院", "coalition_20250626": "迷彩都市的尋蹤者", "coalition_20251120": "DATE A LANE", - "coalition_20260122": "Light & Shadow Fashion Shoot!", + "coalition_20260122": "光影風尚-拍攝進行時", "event_20200227_cn": "Northern Overture", "event_20200312_cn": "斯圖爾特的硝煙", "event_20200326_cn": "Microlayer Medley", @@ -997,6 +766,7 @@ "event_20250912_cn": "起舞於天原之上", "event_20251023_cn": "颶風與自由群島", "event_20251218_cn": "響徹於天穹之音", + "event_20260226_cn": "Springtide Inn Online", "raid_20200624": "特別演習埃塞克斯級(復刻)", "raid_20210708": "復刻穿越彼方的水線", "raid_20220127": "演習神秘事件調查", @@ -1007,6 +777,7 @@ "raid_20240130": "寰昌宇定家事忙", "raid_20240328": "從零開始的魔王討伐之旅", "raid_20250116": "華裳巧展喜事長", + "raid_20260212": "春宴懷玉香滿庭", "war_archives_20180607_cn": "檔案 墨染的鋼鐵之花", "war_archives_20180726_cn": "檔案 光與影的鳶尾之華", "war_archives_20181020_en": "檔案 努力希望和計劃", @@ -1049,6 +820,7 @@ "war_archives_20220728_cn": "檔案 雄鷹的敘事歌", "war_archives_20220915_cn": "檔案 紫絳槿嵐", "war_archives_20221222_cn": "檔案 定向折疊", + "war_archives_20230223_cn": "檔案 湮燼塵墟", "war_archives_20231026_cn": "檔案 飓風與青春之泉" }, "Mode": { @@ -1504,7 +1276,7 @@ "easy": "簡單", "normal": "普通", "hard": "困難", - "ex": "ex" + "ex": "EX" }, "UseTicket": { "name": "使用演習券", @@ -2117,7 +1889,8 @@ "help": "", "2": "二期科研", "3": "三期科研", - "4": "四期科研" + "4": "四期科研", + "5": "五期科研" }, "ShipIndex": { "name": "艦船序號", @@ -2151,7 +1924,8 @@ "2": "二期科研", "3": "三期科研", "4": "四期科研", - "5": "五期科研" + "5": "五期科研", + "6": "六期科研" }, "ShipIndex": { "name": "艦船序號", @@ -2870,8 +2644,6 @@ "Stop": "停止", "ScrollON": "自動滾動 開", "ScrollOFF": "自動滾動 關", - "DashboardON": "開", - "DashboardOFF": "關", "ClearLog": "清空日誌", "Setting": "設定", "CheckUpdate": "檢查更新", @@ -2904,26 +2676,6 @@ }, "Overview": { "Scheduler": "調度器", - "Dashboard": "儀表盤", - "SecondsAgo": "秒前", - "MinutesAgo": "分鐘前", - "HoursAgo": "小時前", - "DaysAgo": "天前", - "MonthsAgo": "月前", - "YearsAgo": "年前", - "NoData": "無數據", - "Oil": "石油", - "Coin": "物資", - "Gem": "鑽石", - "Cube": "魔方", - "Pt": "活動PT", - "YellowCoin": "大世界黃幣", - "PurpleCoin": "大世界紫幣", - "ActionPoint": "行動力", - "Merit": "功勳", - "Medal": "勳章", - "Core": "覈心數據", - "GuildCoin": "艦隊幣", "Log": "日誌", "Running": "執行中", "Pending": "佇列中", diff --git a/alas_wrapped/module/config/server.py b/alas_wrapped/module/config/server.py index e91fa48ce8..3cf519518c 100644 --- a/alas_wrapped/module/config/server.py +++ b/alas_wrapped/module/config/server.py @@ -74,7 +74,7 @@ '小王冠行动', '波茨坦公告', '白色方案', '瓦尔基里行动', '曼哈顿计划', '八月风暴', '秋季旅行', '水星行动', '莱茵河卫兵', '北极光计划', '长戟计划', '暴雨行动', '水仙行动', '冬月计划', - '长弓计划' + '长弓计划', '裁决协议', ], 'cn_ios': [ '夏威夷', '珊瑚海', '中途岛', '铁底湾', '所罗门', '马里亚纳', diff --git a/alas_wrapped/module/config/utils.py b/alas_wrapped/module/config/utils.py index 2cde34af87..f26443fd99 100644 --- a/alas_wrapped/module/config/utils.py +++ b/alas_wrapped/module/config/utils.py @@ -77,6 +77,7 @@ def read_file(file): Returns: dict, list: """ + print(f'read: {file}') if file.endswith('.json'): content = atomic_read_bytes(file) if not content: @@ -91,6 +92,7 @@ def read_file(file): data = {} return data else: + print(f'Unsupported config file extension: {file}') return {} @@ -102,6 +104,7 @@ def write_file(file, data): file (str): data (dict, list): """ + print(f'write: {file}') if file.endswith('.json'): content = json.dumps(data, indent=2, ensure_ascii=False, sort_keys=False, default=str) atomic_write(file, content) @@ -114,7 +117,7 @@ def write_file(file, data): data, default_flow_style=False, encoding='utf-8', allow_unicode=True, sort_keys=False) atomic_write(file, content) else: - pass + print(f'Unsupported config file extension: {file}') def iter_folder(folder, is_dir=False, ext=None): @@ -539,47 +542,5 @@ def type_to_str(typ): return str(typ) -def time_delta(_timedelta): - """ - Output the delta between two times - - Args: - _timedelta : datetime.timedelta - - Returns: - dict : { - 'Y' : int, - 'M' : int, - 'D' : int, - 'h' : int, - 'm' : int, - 's' : int - } - """ - _time_delta = abs(_timedelta.total_seconds()) - d_base = datetime(2010, 1, 1, 0, 0, 0) - d = datetime(2010, 1, 1, 0, 0, 0)-_timedelta - _time_dict = { - 'Y': d.year - d_base.year, - 'M': d.month - d_base.month, - 'D': d.day - d_base.day, - 'h': d.hour - d_base.hour, - 'm': d.minute - d_base.minute, - 's': d.second - d_base.second - } - # _sec ={ - # 'Y': 365*24*60*60, - # 'M': 30*24*60*60, - # 'D': 24*60*60, - # 'h': 60*60, - # 'm': 60, - # 's': 1 - # } - # for _key in _time_dict: - # _time_dict[_key] = int(_time_delta//_sec[_key]) - # _time_delta = _time_delta%_sec[_key] - return _time_dict - - if __name__ == '__main__': get_os_reset_remain() diff --git a/alas_wrapped/module/device/connection.py b/alas_wrapped/module/device/connection.py index 51ecbdcb00..fe5f140123 100644 --- a/alas_wrapped/module/device/connection.py +++ b/alas_wrapped/module/device/connection.py @@ -9,8 +9,6 @@ import uiautomator2 as u2 from adbutils import AdbClient, AdbDevice, AdbTimeout, ForwardItem, ReverseItem -if not hasattr(AdbClient, '_connect') and hasattr(AdbClient, 'make_connection'): - AdbClient._connect = AdbClient.make_connection from adbutils.errors import AdbError from module.base.decorator import Config, cached_property, del_cached_property, run_once diff --git a/alas_wrapped/module/device/connection_attr.py b/alas_wrapped/module/device/connection_attr.py index 5f053c97fd..4cb47065e8 100644 --- a/alas_wrapped/module/device/connection_attr.py +++ b/alas_wrapped/module/device/connection_attr.py @@ -71,28 +71,39 @@ def __init__(self, config): self.config.DEVICE_OVER_HTTP = self.is_over_http @staticmethod - def revise_serial(serial): - serial = serial.replace(' ', '') + def revise_serial(serial: str): + """ + Tons of fool-proof fixes to handle manual serial input + To load a serial: + serial = SerialStr.revise_serial(serial) + """ + serial = serial.strip().replace(' ', '') # 127。0。0。1:5555 serial = serial.replace('。', '.').replace(',', '.').replace(',', '.').replace(':', ':') # 127.0.0.1.5555 serial = serial.replace('127.0.0.1.', '127.0.0.1:') - # Mumu12 5.0 shows double serials, some people may just copy-paste it - # 5555,16384 -> replaced to 5555.16384 + # 5555,16384 (actually "5555.16384" because replace(',', '.')) if '.' in serial: left, _, right = serial.partition('.') - if left.startswith('55') and right.startswith('16'): - serial = right + try: + left = int(left) + right = int(right) + if 5500 < left < 6000 and 16300 < right < 20000: + serial = str(right) + except ValueError: + pass # 16384 - try: - port = int(serial) - if 1000 < port < 65536: - serial = f'127.0.0.1:{port}' - except ValueError: - pass + if serial.isdigit(): + try: + port = int(serial) + if 1000 < port < 65536: + serial = f'127.0.0.1:{port}' + except ValueError: + pass # 夜神模拟器 127.0.0.1:62001 # MuMu模拟器12127.0.0.1:16384 if '模拟' in serial: + import re res = re.search(r'(127\.\d+\.\d+\.\d+:\d+)', serial) if res: serial = res.group(1) @@ -348,7 +359,7 @@ def u2(self) -> u2.Device: device = u2.connect(self.serial) # Stay alive - # device.set_new_command_timeout(604800) # Removed - method dropped in uiautomator2 >= 3.x + device.set_new_command_timeout(604800) - logger.attr('u2.Device', f'Device(serial={self.serial})') + logger.attr('u2.Device', f'Device(atx_agent_url={device._get_atx_agent_url()})') return device diff --git a/alas_wrapped/module/device/device.py b/alas_wrapped/module/device/device.py index 6a14cd8f63..a1519df472 100644 --- a/alas_wrapped/module/device/device.py +++ b/alas_wrapped/module/device/device.py @@ -70,31 +70,23 @@ class Device(Screenshot, Control, AppControl): stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE', 'LOGIN_CHECK'] def __init__(self, *args, **kwargs): - # ConnectionAttr.__init__ (inside super()) initializes self.config. - # Read retry policy from constructor kwargs first to avoid pre-init access. - cfg = kwargs.get('config') - handle_error = bool(getattr(cfg, 'Error_HandleError', False)) - max_retry = 30 if handle_error else 4 - for trial in range(1, max_retry + 1): + for trial in range(4): try: super().__init__(*args, **kwargs) break except EmulatorNotRunningError: - if getattr(self, 'emulator_instance', None) is None: + if trial >= 3: + logger.critical('Failed to start emulator after 3 trial') + raise RequestHumanTakeover + # Try to start emulator + if self.emulator_instance is not None: + self.emulator_start() + else: logger.critical( - f'No emulator with serial "{getattr(cfg, "Emulator_Serial", "unknown")}" found, ' + f'No emulator with serial "{self.config.Emulator_Serial}" found, ' f'please set a correct serial' ) raise RequestHumanTakeover - if trial >= max_retry: - logger.critical(f'Failed to start emulator after {trial} trial') - raise RequestHumanTakeover - if handle_error and trial >= 4: - logger.warning(f'Failed to start emulator after {trial} trial, keep retrying') - # Try to start emulator - self.emulator_start() - if handle_error: - self.sleep(5) # Auto-fill emulator info if IS_WINDOWS and self.config.EmulatorInfo_Emulator == 'auto': diff --git a/alas_wrapped/module/device/method/minitouch.py b/alas_wrapped/module/device/method/minitouch.py index 06a6ed3a7e..d97c2ca884 100644 --- a/alas_wrapped/module/device/method/minitouch.py +++ b/alas_wrapped/module/device/method/minitouch.py @@ -8,11 +8,7 @@ import websockets from adbutils.errors import AdbError -try: - from uiautomator2 import _Service -except ImportError: - class _Service: - pass +from uiautomator2 import _Service from module.base.decorator import Config, cached_property, del_cached_property, has_cached_property from module.base.timer import Timer diff --git a/alas_wrapped/module/device/method/uiautomator_2.py b/alas_wrapped/module/device/method/uiautomator_2.py index e0db47c7f6..fb5274356d 100644 --- a/alas_wrapped/module/device/method/uiautomator_2.py +++ b/alas_wrapped/module/device/method/uiautomator_2.py @@ -119,10 +119,14 @@ class ShellBackgroundResponse: class Uiautomator2(Connection): @retry def screenshot_uiautomator2(self): - # uiautomator2 >= 3.x dropped format='raw'; use 'opencv' to get BGR ndarray directly - image = self.u2.screenshot(format='opencv') + image = self.u2.screenshot(format='raw') + image = np.frombuffer(image, np.uint8) if image is None: - raise ImageTruncated('Empty image after screenshot') + raise ImageTruncated('Empty image after reading from buffer') + + image = cv2.imdecode(image, cv2.IMREAD_COLOR) + if image is None: + raise ImageTruncated('Empty image after cv2.imdecode') cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: diff --git a/alas_wrapped/module/device/method/utils.py b/alas_wrapped/module/device/method/utils.py index f9e6f2cf4b..6c0dd85367 100644 --- a/alas_wrapped/module/device/method/utils.py +++ b/alas_wrapped/module/device/method/utils.py @@ -54,8 +54,7 @@ def shell(self, RETRY_DELAY = 3 # Patch uiautomator2 appdir -if hasattr(u2, 'init'): - u2.init.appdir = os.path.dirname(uiautomator2cache.__file__) +u2.init.appdir = os.path.dirname(uiautomator2cache.__file__) # Patch uiautomator2 logger u2_logger = u2.logger @@ -71,40 +70,39 @@ def setup_logger(*args, **kwargs): u2.setup_logger = setup_logger -if hasattr(u2, 'init'): - u2.init.setup_logger = setup_logger - - - # Patch Initer - class PatchedIniter(u2.init.Initer): - @property - def atx_agent_url(self): - files = { - 'armeabi-v7a': 'atx-agent_{v}_linux_armv7.tar.gz', - # 'arm64-v8a': 'atx-agent_{v}_linux_armv7.tar.gz', - 'arm64-v8a': 'atx-agent_{v}_linux_arm64.tar.gz', - 'armeabi': 'atx-agent_{v}_linux_armv6.tar.gz', - 'x86': 'atx-agent_{v}_linux_386.tar.gz', - 'x86_64': 'atx-agent_{v}_linux_386.tar.gz', - } - name = None - for abi in self.abis: - name = files.get(abi) - if name: - break - if not name: - raise Exception( - "arch(%s) need to be supported yet, please report an issue in github" - % self.abis) - return u2.init.GITHUB_BASEURL + '/atx-agent/releases/download/%s/%s' % ( - u2.version.__atx_agent_version__, name.format(v=u2.version.__atx_agent_version__)) - - @property - def minicap_urls(self): - return [] - - - u2.init.Initer = PatchedIniter +u2.init.setup_logger = setup_logger + + +# Patch Initer +class PatchedIniter(u2.init.Initer): + @property + def atx_agent_url(self): + files = { + 'armeabi-v7a': 'atx-agent_{v}_linux_armv7.tar.gz', + # 'arm64-v8a': 'atx-agent_{v}_linux_armv7.tar.gz', + 'arm64-v8a': 'atx-agent_{v}_linux_arm64.tar.gz', + 'armeabi': 'atx-agent_{v}_linux_armv6.tar.gz', + 'x86': 'atx-agent_{v}_linux_386.tar.gz', + 'x86_64': 'atx-agent_{v}_linux_386.tar.gz', + } + name = None + for abi in self.abis: + name = files.get(abi) + if name: + break + if not name: + raise Exception( + "arch(%s) need to be supported yet, please report an issue in github" + % self.abis) + return u2.init.GITHUB_BASEURL + '/atx-agent/releases/download/%s/%s' % ( + u2.version.__atx_agent_version__, name.format(v=u2.version.__atx_agent_version__)) + + @property + def minicap_urls(self): + return [] + + +u2.init.Initer = PatchedIniter def is_port_using(port_num): @@ -281,19 +279,19 @@ def get_serial_pair(serial): serial (str): Returns: - str, str: `127.0.0.1:5555+{X}` and `emulator-5554+{X}`, 0 <= X <= 32 + tuple[Optional[str], Optional[str]]: `127.0.0.1:5555+{X}` and `emulator-5554+{X}`, 0 <= X <= 32 """ if serial.startswith('127.0.0.1:'): try: port = int(serial[10:]) - if 5555 <= port <= 5555 + 32: + if 5555 <= port <= 5555 + 64: return f'127.0.0.1:{port}', f'emulator-{port - 1}' except (ValueError, IndexError): pass if serial.startswith('emulator-'): try: port = int(serial[9:]) - if 5554 <= port <= 5554 + 32: + if 5554 <= port <= 5554 + 64: return f'127.0.0.1:{port + 1}', f'emulator-{port}' except (ValueError, IndexError): pass @@ -397,19 +395,16 @@ def remove_shell_warning(s): return s -if hasattr(u2, 'init'): - class IniterNoMinicap(u2.init.Initer): - @property - def minicap_urls(self): - """ - Don't install minicap on emulators, return empty urls. +class IniterNoMinicap(u2.init.Initer): + @property + def minicap_urls(self): + """ + Don't install minicap on emulators, return empty urls. - binary from https://github.com/openatx/stf-binaries - only got abi: armeabi-v7a and arm64-v8a - """ - return [] -else: - IniterNoMinicap = None + binary from https://github.com/openatx/stf-binaries + only got abi: armeabi-v7a and arm64-v8a + """ + return [] class Device(u2.Device): @@ -421,8 +416,7 @@ def show_float_window(self, show=True): # Monkey patch -if hasattr(u2, 'init'): - u2.init.Initer = IniterNoMinicap +u2.init.Initer = IniterNoMinicap u2.Device = Device diff --git a/alas_wrapped/module/device/platform/platform_windows.py b/alas_wrapped/module/device/platform/platform_windows.py index 6334402b90..c7fbeb4971 100644 --- a/alas_wrapped/module/device/platform/platform_windows.py +++ b/alas_wrapped/module/device/platform/platform_windows.py @@ -310,9 +310,8 @@ def emulator_start(self): logger.hr('Emulator start', level=1) for _ in range(3): # Stop - stopped = self._emulator_function_wrapper(self._emulator_stop) - if not stopped: - logger.warning('Emulator stop failed, try start anyway') + if not self._emulator_function_wrapper(self._emulator_stop): + return False # Start if self._emulator_function_wrapper(self._emulator_start): # Success @@ -320,12 +319,10 @@ def emulator_start(self): return True else: # Failed to start, stop and start again - if stopped and self._emulator_function_wrapper(self._emulator_stop): + if self._emulator_function_wrapper(self._emulator_stop): continue - logger.warning( - 'Emulator start failed; stop before retry also failed, ' - 'continuing without clean state' - ) + else: + return False logger.error('Failed to start emulator 3 times, stopped') return False @@ -350,4 +347,4 @@ def emulator_stop(self): if __name__ == '__main__': self = PlatformWindows('alas') d = self.emulator_instance - print(d) + print(d) \ No newline at end of file diff --git a/alas_wrapped/module/exception.py b/alas_wrapped/module/exception.py index 0eb33fcea7..e0d3ecf392 100644 --- a/alas_wrapped/module/exception.py +++ b/alas_wrapped/module/exception.py @@ -41,10 +41,6 @@ class GameBugError(Exception): pass -class GameTransportError(Exception): - """Reserved for transport-layer failures (currently not raised directly).""" - - class GameTooManyClickError(Exception): pass diff --git a/alas_wrapped/module/exercise/exercise.py b/alas_wrapped/module/exercise/exercise.py index ebdbd3033e..6b78f76e4c 100644 --- a/alas_wrapped/module/exercise/exercise.py +++ b/alas_wrapped/module/exercise/exercise.py @@ -93,21 +93,11 @@ def _opponent_fleet_check_all(self): super()._opponent_fleet_check_all() def _opponent_sort(self, method=None): - """ - Sort opponents by selection strategy. - - Important: In Azur Lane's Exercise mode, opponents are ALWAYS sorted by rank from left to right. - - Opponent 0 (leftmost) = Highest rank player = Maximum merit points/rewards - - Opponent 3 (rightmost) = Lowest rank player = Minimum merit points/rewards - - Using 'leftmost' mode ensures you always fight the best opponent for maximum rewards. - """ if method is None: method = self.config.Exercise_OpponentChooseMode if method != 'leftmost': return super()._opponent_sort(method=method) else: - # Fight opponents left-to-right: highest rank first return [0, 1, 2, 3] def _exercise_once(self): diff --git a/alas_wrapped/module/exercise/hp_daemon.py b/alas_wrapped/module/exercise/hp_daemon.py index e79bce505d..321a88d24f 100644 --- a/alas_wrapped/module/exercise/hp_daemon.py +++ b/alas_wrapped/module/exercise/hp_daemon.py @@ -76,6 +76,7 @@ def _at_low_hp(self, image, pause=PAUSE): PAUSE_Ninja, PAUSE_ShadowPuppetry, PAUSE_MaidCafe, + PAUSE_Ancient, ]: self.attacker_hp = self._calculate_hp(image, area=ATTACKER_HP_AREA_New.area, reverse=True) self.defender_hp = self._calculate_hp(image, area=DEFENDER_HP_AREA_New.area, reverse=True) diff --git a/alas_wrapped/module/exercise/opponent.py b/alas_wrapped/module/exercise/opponent.py index e6aea7413a..22379def51 100644 --- a/alas_wrapped/module/exercise/opponent.py +++ b/alas_wrapped/module/exercise/opponent.py @@ -8,18 +8,6 @@ from module.ui.assets import BACK_ARROW from module.ui.ui import UI -""" -Exercise Opponent Selection - -In Azur Lane's Exercise/PvP mode: -- Opponents are ALWAYS displayed sorted by rank (left = highest, right = lowest) -- Opponent 0 (leftmost): Highest ranked player, yields maximum merit points -- Opponent 3 (rightmost): Lowest ranked player, yields minimum merit points -- Defeating higher-ranked opponents gives better rewards and more rank points - -Grid layout: [Opponent 0] [Opponent 1] [Opponent 2] [Opponent 3] - (Best) (Good) (Medium) (Weakest) -""" OPPONENT = ButtonGrid(origin=(104, 77), delta=(244, 0), button_shape=(212, 304), grid_shape=(4, 1)) # Mode 'easiest' constants @@ -86,12 +74,6 @@ def get_power(self, image): def get_priority(self, method="max_exp"): """ - Calculate opponent priority for selection strategy. - - Note: This is only used for 'max_exp' and 'easiest' modes. - For 'leftmost' mode, opponents are fought in order [0,1,2,3] without priority calculation, - which is optimal since the game always sorts opponents by rank (leftmost = highest rank). - Args: method: EXERCISE_CHOOSE_MODE @@ -105,7 +87,6 @@ def get_priority(self, method="max_exp"): avg_team_pwr = np.sum(self.power) / team_pwr_div priority = level - avg_team_pwr else: - # max_exp mode: prioritize by total level (approximates difficulty/rewards) priority = np.sum(self.level) / 6 return priority diff --git a/alas_wrapped/module/gacha/gacha_reward.py b/alas_wrapped/module/gacha/gacha_reward.py index ba8be1cc93..41c1af129c 100644 --- a/alas_wrapped/module/gacha/gacha_reward.py +++ b/alas_wrapped/module/gacha/gacha_reward.py @@ -8,7 +8,6 @@ from module.logger import logger from module.ocr.ocr import Digit from module.retire.retirement import Retirement -from module.log_res.log_res import LogRes RECORD_GACHA_OPTION = ('RewardRecord', 'gacha') RECORD_GACHA_SINCE = (0,) @@ -126,8 +125,6 @@ def gacha_calculate(self, target_count, gold_cost, cube_cost): logger.info(f'Able to submit up to {target_count} build orders') self.build_coin_count -= gold_total self.build_cube_count -= cube_total - LogRes(self.config).Cube = self.build_cube_count - self.config.update() return target_count def gacha_goto_pool(self, target_pool): @@ -325,9 +322,6 @@ def gacha_run(self): buy[0] = self.build_ticket_count # Calculate rolls allowed based on configurations and resources buy[1] = self.gacha_calculate(self.config.Gacha_Amount - self.build_ticket_count, gold_cost, cube_cost) - else: - LogRes(self.config).Cube = self.build_cube_count - self.config.update() # Submit 'buy_count' and execute if capable # Cannot use handle_popup_confirm, this window diff --git a/alas_wrapped/module/handler/auto_search.py b/alas_wrapped/module/handler/auto_search.py index d5d0987911..48e67dfbe0 100644 --- a/alas_wrapped/module/handler/auto_search.py +++ b/alas_wrapped/module/handler/auto_search.py @@ -2,6 +2,7 @@ from module.base.button import ButtonGrid from module.base.decorator import Config +from module.base.timer import Timer from module.handler.assets import * from module.handler.enemy_searching import EnemySearchingHandler from module.logger import logger @@ -49,21 +50,14 @@ def _fleet_sidebar(self): origin=(1185, 155 + offset), delta=(0, 111), button_shape=(53, 104), grid_shape=(1, 3), name='FLEET_SIDEBAR') - def _fleet_preparation_sidebar_click(self, index): + def _fleet_preparation_get(self): """ - Args: - index (int): + Returns: + int: 1 for formation 2 for meowfficers 3 for auto search setting - - Returns: - bool: If changed. """ - if index <= 0 or index > 3: - logger.warning(f'Sidebar index cannot be clicked, {index}, limit to 1 through 5 only') - return False - current = 0 total = 0 sidebar = self._fleet_sidebar() @@ -81,46 +75,38 @@ def _fleet_preparation_sidebar_click(self, index): if not current: logger.warning('No fleet sidebar active.') logger.attr('Fleet_sidebar', f'{current}/{total}') - if current == index: - return False + return current - self.device.click(sidebar[0, index - 1]) - return True - - def fleet_preparation_sidebar_ensure(self, index, skip_first_screenshot=True): + def fleet_preparation_sidebar_ensure(self, index): """ Args: index (int): 1 for formation 2 for meowfficers 3 for auto search setting - skip_first_screenshot (bool): - Returns: - bool: whether sidebar could be ensured - at most 3 attempts are made before - return False otherwise True + Returns: + bool: whether sidebar could be ensured + at most 3 attempts are made before + return False otherwise True """ if index <= 0 or index > 5: logger.warning(f'Sidebar index cannot be ensured, {index}, limit 1 through 5 only') return False - counter = 0 - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if self._fleet_preparation_sidebar_click(index): - if counter >= 2: - logger.warning('Sidebar could not be ensured') - return False - counter += 1 - self.device.sleep((0.3, 0.5)) - continue - else: + interval = Timer(1, count=2) + sidebar = self._fleet_sidebar() + for _ in self.loop(timeout=3): + current = self._fleet_preparation_get() + if current == index: return True + if interval.reached(): + self.device.click(sidebar[0, index - 1]) + interval.reset() + continue + else: + logger.warning('Sidebar could not be ensured') + return False def _auto_search_set_click(self, setting): """ diff --git a/alas_wrapped/module/handler/login.py b/alas_wrapped/module/handler/login.py index 7cc4f90d37..f5139ac94f 100644 --- a/alas_wrapped/module/handler/login.py +++ b/alas_wrapped/module/handler/login.py @@ -1,6 +1,3 @@ -import time -from datetime import datetime -from pathlib import Path from typing import Union import numpy as np @@ -11,54 +8,16 @@ import module.config.server as server from module.base.timer import Timer -from module.base.jsonl import append_jsonl from module.base.utils import color_similarity_2d, crop, random_rectangle_point from module.handler.assets import * from module.logger import logger from module.map.assets import * -from module.exception import GameStuckError from module.ui.assets import * from module.ui.page import page_campaign_menu from module.ui.ui import UI class LoginHandler(UI): - LOGIN_MAX_TOTAL_SECONDS = 300 - LOGIN_MAX_NO_PROGRESS_SECONDS = 180 - LOGIN_TRACE_ROTATE_BYTES = 20 * 1024 * 1024 - _RUNTIME_ROOT = Path(__file__).resolve().parents[2] - _trace_write_warned = False - # Side-channel trace file: append-only JSONL so external parsers can - # reconstruct login decisions without touching the normal logger stream. - LOGIN_TRACE_FILE = str(_RUNTIME_ROOT / 'log' / 'login_trace.jsonl') - - def _trace_login_event(self, phase, detected=None, action=None, result=None, error=None, elapsed_ms=None): - # Keep trace writes isolated from bot control flow; telemetry must - # never alter runtime behavior. - payload = { - 'ts': datetime.utcnow().isoformat(timespec='milliseconds') + 'Z', - 'config': getattr(self.config, 'config_name', 'unknown'), - 'phase': phase, - 'detected': detected, - 'action': action, - 'result': result, - 'error': error, - 'elapsed_ms': elapsed_ms, - } - - def _on_trace_error(e): - # Trace logging must never break login flow. - if not self._trace_write_warned: - logger.warning(f'login_trace telemetry disabled: {type(e).__name__}: {e}') - self._trace_write_warned = True - - append_jsonl( - self.LOGIN_TRACE_FILE, - payload, - rotate_bytes=self.LOGIN_TRACE_ROTATE_BYTES, - error_callback=_on_trace_error, - ) - def _handle_app_login(self): """ Pages: @@ -75,55 +34,10 @@ def _handle_app_login(self): confirm_timer = Timer(1.5, count=4).start() orientation_timer = Timer(5) login_success = False - started_at = time.monotonic() - last_progress_at = started_at self.device.stuck_record_clear() self.device.click_record_clear() - self._trace_login_event(phase='start', result='begin', elapsed_ms=0) - - def elapsed_ms(): - return int((time.monotonic() - started_at) * 1000) - - def mark_progress(detected, action, result='progress'): - nonlocal last_progress_at - last_progress_at = time.monotonic() - self._trace_login_event( - phase='progress', - detected=detected, - action=action, - result=result, - elapsed_ms=elapsed_ms(), - ) while 1: - now = time.monotonic() - total_elapsed = now - started_at - idle_elapsed = now - last_progress_at - if total_elapsed > self.LOGIN_MAX_TOTAL_SECONDS: - # Bound total login wall-clock runtime to avoid long blind loops. - self._trace_login_event( - phase='guard', - action='abort', - result='timeout_total', - error=f'elapsed={total_elapsed:.1f}s', - elapsed_ms=elapsed_ms(), - ) - raise GameStuckError( - f'Login timeout after {total_elapsed:.1f}s' - ) - if idle_elapsed > self.LOGIN_MAX_NO_PROGRESS_SECONDS: - # Bound no-progress window so we fail fast when UI is not changing. - self._trace_login_event( - phase='guard', - action='abort', - result='timeout_no_progress', - error=f'idle={idle_elapsed:.1f}s', - elapsed_ms=elapsed_ms(), - ) - raise GameStuckError( - f'Login no progress for {idle_elapsed:.1f}s' - ) - # Watch device rotation if not login_success and orientation_timer.reached(): # Screen may rotate after starting an app @@ -136,14 +50,6 @@ def mark_progress(detected, action, result='progress'): if self.is_in_main(): if confirm_timer.reached(): logger.info('Login to main confirm') - mark_progress(detected='page_main', action='confirm', result='success') - self._trace_login_event( - phase='end', - detected='page_main', - action='return', - result='success', - elapsed_ms=elapsed_ms(), - ) break else: confirm_timer.reset() @@ -151,75 +57,45 @@ def mark_progress(detected, action, result='progress'): # Login if self.match_template_color(LOGIN_CHECK, offset=(30, 30), interval=5): self.device.click(LOGIN_CHECK) - mark_progress(detected='LOGIN_CHECK', action='click') if not login_success: logger.info('Login success') - self._trace_login_event( - phase='login', - detected='LOGIN_CHECK', - action='login_success', - result='success', - elapsed_ms=elapsed_ms(), - ) login_success = True if self.appear(ANDROID_NO_RESPOND, offset=(30, 30), interval=5): logger.warning('Emulator no respond') self.device.click_record_add(ANDROID_NO_RESPOND) self.device.click_record_check() self.device.click(ANDROID_NO_RESPOND, control_check=False) - mark_progress(detected='ANDROID_NO_RESPOND', action='click') continue if self.appear_then_click(LOGIN_ANNOUNCE, offset=(30, 30), interval=5): - mark_progress(detected='LOGIN_ANNOUNCE', action='click') continue if self.appear_then_click(LOGIN_ANNOUNCE_2, offset=(30, 30), interval=5): - mark_progress(detected='LOGIN_ANNOUNCE_2', action='click') continue if self.appear(EVENT_LIST_CHECK, offset=(30, 30), interval=5): self.device.click(BACK_ARROW) - mark_progress(detected='EVENT_LIST_CHECK', action='click_back') continue # Updates and maintenance if self.appear_then_click(MAINTENANCE_ANNOUNCE, offset=(30, 30), interval=5): - mark_progress(detected='MAINTENANCE_ANNOUNCE', action='click') continue if self.appear_then_click(LOGIN_GAME_UPDATE, offset=(30, 30), interval=5): - mark_progress(detected='LOGIN_GAME_UPDATE', action='click') continue if server.server == 'cn' and not login_success: if self.handle_cn_user_agreement(): - mark_progress(detected='CN_USER_AGREEMENT', action='handle') continue # Player return if self.appear_then_click(LOGIN_RETURN_SIGN, offset=(30, 30), interval=5): - mark_progress(detected='LOGIN_RETURN_SIGN', action='click') continue if self.appear_then_click(LOGIN_RETURN_INFO, offset=(30, 30), interval=5): - mark_progress(detected='LOGIN_RETURN_INFO', action='click') continue # Popups if self.handle_popup_confirm('LOGIN'): - mark_progress(detected='POPUP_CONFIRM', action='handle') continue if self.handle_urgent_commission(): - mark_progress(detected='URGENT_COMMISSION', action='handle') continue - # Popups appear at page_main. - # If popup handling succeeds, treat it as successful convergence for - # login and exit immediately. + # Popups appear at page_main if self.ui_page_main_popups(get_ship=login_success): - mark_progress(detected='MAIN_POPUPS', action='handle', result='success') - self._trace_login_event( - phase='end', - detected='MAIN_POPUPS', - action='return', - result='success', - elapsed_ms=elapsed_ms(), - ) return True # Always goto page_main if self.appear_then_click(GOTO_MAIN, offset=(30, 30), interval=5): - mark_progress(detected='GOTO_MAIN', action='click') continue return True @@ -266,17 +142,7 @@ def handle_app_login(self): logger.info('handle_app_login') self.device.screenshot_interval_set(1.0) try: - return self._handle_app_login() - except Exception as e: - # Preserve all existing logger behavior; we only add a side-channel - # event so callers keep current failure semantics. - self._trace_login_event( - phase='error', - action='raise', - result='failed', - error=type(e).__name__, - ) - raise + self._handle_app_login() finally: self.device.screenshot_interval_set() @@ -287,7 +153,6 @@ def app_stop(self): def app_start(self): logger.hr('App start') self.device.app_start() - # Raises on failure; return value is informational for callers that need it. self.handle_app_login() # self.ensure_no_unfinished_campaign() @@ -295,7 +160,6 @@ def app_restart(self): logger.hr('App restart') self.device.app_stop() self.device.app_start() - # Raises on failure; return value is informational for callers that need it. self.handle_app_login() # self.ensure_no_unfinished_campaign() self.config.task_delay(server_update=True) diff --git a/alas_wrapped/module/logger.py b/alas_wrapped/module/logger.py index 4f1c729da0..97dc148dec 100644 --- a/alas_wrapped/module/logger.py +++ b/alas_wrapped/module/logger.py @@ -133,7 +133,7 @@ class Highlighter(RegexHighlighter): # Logger init -logger_debug = True +logger_debug = False logger = logging.getLogger('alas') logger.setLevel(logging.DEBUG if logger_debug else logging.INFO) file_formatter = logging.Formatter( diff --git a/alas_wrapped/module/minigame/minigame.py b/alas_wrapped/module/minigame/minigame.py index 6537dc1de8..7af1520459 100644 --- a/alas_wrapped/module/minigame/minigame.py +++ b/alas_wrapped/module/minigame/minigame.py @@ -3,8 +3,8 @@ from module.logger import logger from module.minigame.assets import * from module.ocr.ocr import Digit -from module.ui.assets import GAME_ROOM_CHECK -from module.ui.page import page_game_room +from module.ui.assets import ACADEMY_GOTO_GAME_ROOM, GAME_ROOM_CHECK +from module.ui.page import page_academy, page_game_room from module.ui.scroll import Scroll from module.ui.ui import UI @@ -131,6 +131,7 @@ def go_to_main_page(self, skip_first_screenshot=True): in: page_game_room main_page/choose_game_page out: page_game_room main_page """ + logger.info('minigame go_to_main_page') while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -183,8 +184,19 @@ def run(self): in: Any page out: page_game_room """ + # TEMP: 2026.02.18 separate self.ui_ensure(page_game_room) into 2 steps + # EN has different page_academy detection, to use ui_ensure(page_game_room), + # ui_goto must use `if self.ui_page_appear(page)` instead of `if self.appear(page.check_button)` + # But that would cause page_main/page_main_white clicking a static switch button + self.ui_ensure(page_academy) + # page_academy -> page_game_room + for _ in self.loop(): + if self.ui_page_appear(page_game_room): + break + if self.ui_page_appear(page_academy, interval=5): + self.device.click(ACADEMY_GOTO_GAME_ROOM) + continue - self.ui_ensure(page_game_room) # game room and choose game have same header, go to game room first self.go_to_main_page() coin_collected = False diff --git a/alas_wrapped/module/os/fleet.py b/alas_wrapped/module/os/fleet.py index c39c40c3e2..6d0f20bf22 100644 --- a/alas_wrapped/module/os/fleet.py +++ b/alas_wrapped/module/os/fleet.py @@ -20,7 +20,7 @@ from module.os.camera import OSCamera from module.os.map_base import OSCampaignMap from module.os_ash.ash import OSAsh -from module.os_combat.combat import Combat +from module.os_combat.combat import Combat, BATTLE_PREPARATION, SIREN_PREPARATION from module.os_handler.assets import AUTO_SEARCH_REWARD, CLICK_SAFE_AREA, IN_MAP, PORT_ENTER from module.os_shop.assets import PORT_SUPPLY_CHECK from module.ui.assets import BACK_ARROW @@ -283,6 +283,13 @@ def wait_until_walk_stable(self, confirm_timer=None, skip_first_screenshot=False clicked_story = False stuck_timer = Timer(20, count=5).start() confirm_timer.reset() + + def abyssal_expected_end(): + # add handle_map_event() because OSCombat.combat_status() removes get_items + if self.handle_map_event(drop=drop): + return False + return self.is_in_map() + for _ in self.loop(skip_first=skip_first_screenshot): # Map event event = self.handle_map_event(drop=drop) @@ -347,7 +354,7 @@ def wait_until_walk_stable(self, confirm_timer=None, skip_first_screenshot=False if self.combat_appear(): # Use ui_back() for testing, because there are too few abyssal loggers every month. # self.ui_back(check_button=self.is_in_map) - self.combat(expected_end=self.is_in_map, fleet_index=self.fleet_show_index, save_get_items=drop) + self.combat(expected_end=abyssal_expected_end, fleet_index=self.fleet_show_index, save_get_items=drop) confirm_timer.reset() stuck_timer.reset() result.add('event') @@ -700,6 +707,7 @@ def boss_leave(self): logger.hr('BOSS leave') # Update local view self.update_os() + self.predict() click_timer = Timer(3) pause_interval = Timer(0.5, count=1) @@ -713,8 +721,13 @@ def boss_leave(self): # Re-enter boss accidentally if pause_interval.reached(): - if self.combat_appear(): - logger.info(f'combat_appear -> {BACK_ARROW}') + if self.appear(BATTLE_PREPARATION): + logger.info(f'{BATTLE_PREPARATION} -> {BACK_ARROW}') + self.device.click(BACK_ARROW) + pause_interval.reset() + continue + if self.appear(SIREN_PREPARATION, offset=(20, 20)): + logger.info(f'{SIREN_PREPARATION} -> {BACK_ARROW}') self.device.click(BACK_ARROW) pause_interval.reset() continue diff --git a/alas_wrapped/module/os/map_operation.py b/alas_wrapped/module/os/map_operation.py index 1330cdfcb3..1f9ea5697d 100644 --- a/alas_wrapped/module/os/map_operation.py +++ b/alas_wrapped/module/os/map_operation.py @@ -47,7 +47,7 @@ def get_zone_name(self): name = ocr.ocr(self.device.image) name = "".join(name.split()) name = name.lower() - name = name.strip('\\/-') + name = name.strip('\\/-—–-') if '-' in name: name = name.split('-')[0] if 'é' in name: # Méditerranée name maps @@ -80,7 +80,7 @@ def get_zone_name(self): # For JP only ocr = Ocr(MAP_NAME, lang='jp', letter=(157, 173, 192), threshold=127, name='OCR_OS_MAP_NAME') name = ocr.ocr(self.device.image) - name = name.strip('\\/-') + name = name.strip('\\/-—–-') self.is_zone_name_hidden = '安全' in name # Remove punctuations for char in '・': @@ -109,7 +109,7 @@ def get_zone_name(self): # For TW only ocr = Ocr(MAP_NAME, lang='tw', letter=(198, 215, 239), threshold=127, name='OCR_OS_MAP_NAME') name = ocr.ocr(self.device.image) - name = name.strip('\\/-') + name = name.strip('\\/-—–-') self.is_zone_name_hidden = '安全' in name # Remove '塞壬要塞海域' if '塞' in name: @@ -123,7 +123,7 @@ def get_zone_name(self): # For CN only ocr = Ocr(MAP_NAME, lang='cnocr', letter=(214, 231, 255), threshold=127, name='OCR_OS_MAP_NAME') name = ocr.ocr(self.device.image) - name = name.strip('\\/-') + name = name.strip('\\/-—–-') self.is_zone_name_hidden = '安全' in name if '-' in name: name = name.split('-')[0] diff --git a/alas_wrapped/module/os_handler/action_point.py b/alas_wrapped/module/os_handler/action_point.py index 337846ca79..3f3eb64d91 100644 --- a/alas_wrapped/module/os_handler/action_point.py +++ b/alas_wrapped/module/os_handler/action_point.py @@ -12,8 +12,6 @@ from module.statistics.item import Item, ItemGrid from module.ui.assets import OS_CHECK from module.ui.ui import UI -from module.config.deep import deep_get -from module.log_res.log_res import LogRes OCR_ACTION_POINT_REMAIN = Digit(ACTION_POINT_REMAIN, letter=(255, 219, 66), name='OCR_ACTION_POINT_REMAIN') OCR_ACTION_POINT_REMAIN_OS = Digit(ACTION_POINT_REMAIN_OS, letter=(239, 239, 239), @@ -138,10 +136,8 @@ def action_point_update(self): if self.config.OS_ACTION_POINT_BOX_USE: total += np.sum(np.array(box) * tuple(ACTION_POINT_BOX.values())) oil = box[0] - LogRes(self.config).Oil = oil + logger.info(f'Action points: {current}({total}), oil: {oil}') - LogRes(self.config).ActionPoint = {'Value': current, 'Total': total} - self.config.update() self._action_point_current = current self._action_point_box = box self._action_point_total = total diff --git a/alas_wrapped/module/os_handler/assets.py b/alas_wrapped/module/os_handler/assets.py index b3f85d8036..70dad15744 100644 --- a/alas_wrapped/module/os_handler/assets.py +++ b/alas_wrapped/module/os_handler/assets.py @@ -33,6 +33,7 @@ MISSION_OVERVIEW_ACCEPT = Button(area={'cn': (1072, 12, 1130, 40), 'en': (1082, 16, 1123, 54), 'jp': (1069, 5, 1132, 43), 'tw': (1069, 10, 1131, 42)}, color={'cn': (230, 193, 168), 'en': (228, 174, 128), 'jp': (224, 166, 120), 'tw': (227, 180, 146)}, button={'cn': (1072, 12, 1130, 40), 'en': (1082, 16, 1123, 54), 'jp': (1069, 5, 1132, 43), 'tw': (1069, 10, 1131, 42)}, file={'cn': './assets/cn/os_handler/MISSION_OVERVIEW_ACCEPT.png', 'en': './assets/en/os_handler/MISSION_OVERVIEW_ACCEPT.png', 'jp': './assets/jp/os_handler/MISSION_OVERVIEW_ACCEPT.png', 'tw': './assets/tw/os_handler/MISSION_OVERVIEW_ACCEPT.png'}) MISSION_OVERVIEW_ACCEPT_SINGLE = Button(area={'cn': (1066, 121, 1138, 149), 'en': (1068, 127, 1138, 149), 'jp': (1067, 121, 1138, 149), 'tw': (1066, 121, 1138, 149)}, color={'cn': (145, 182, 231), 'en': (156, 196, 237), 'jp': (133, 173, 227), 'tw': (145, 182, 231)}, button={'cn': (1066, 121, 1138, 149), 'en': (1068, 127, 1138, 149), 'jp': (1067, 121, 1138, 149), 'tw': (1066, 121, 1138, 149)}, file={'cn': './assets/cn/os_handler/MISSION_OVERVIEW_ACCEPT_SINGLE.png', 'en': './assets/en/os_handler/MISSION_OVERVIEW_ACCEPT_SINGLE.png', 'jp': './assets/jp/os_handler/MISSION_OVERVIEW_ACCEPT_SINGLE.png', 'tw': './assets/cn/os_handler/MISSION_OVERVIEW_ACCEPT_SINGLE.png'}) MISSION_OVERVIEW_CHECK = Button(area={'cn': (127, 17, 262, 42), 'en': (128, 18, 300, 37), 'jp': (126, 16, 263, 42), 'tw': (126, 16, 263, 42)}, color={'cn': (148, 165, 209), 'en': (120, 136, 182), 'jp': (95, 109, 148), 'tw': (147, 164, 208)}, button={'cn': (127, 17, 262, 42), 'en': (128, 18, 300, 37), 'jp': (126, 16, 263, 42), 'tw': (126, 16, 263, 42)}, file={'cn': './assets/cn/os_handler/MISSION_OVERVIEW_CHECK.png', 'en': './assets/en/os_handler/MISSION_OVERVIEW_CHECK.png', 'jp': './assets/jp/os_handler/MISSION_OVERVIEW_CHECK.png', 'tw': './assets/tw/os_handler/MISSION_OVERVIEW_CHECK.png'}) +MISSION_OVERVIEW_EMPTY = Button(area={'cn': (1052, 320, 1067, 390), 'en': (1052, 320, 1067, 390), 'jp': (1052, 320, 1067, 390), 'tw': (1052, 320, 1067, 390)}, color={'cn': (144, 91, 99), 'en': (144, 91, 99), 'jp': (144, 91, 99), 'tw': (144, 91, 99)}, button={'cn': (1052, 320, 1067, 390), 'en': (1052, 320, 1067, 390), 'jp': (1052, 320, 1067, 390), 'tw': (1052, 320, 1067, 390)}, file={'cn': './assets/cn/os_handler/MISSION_OVERVIEW_EMPTY.png', 'en': './assets/en/os_handler/MISSION_OVERVIEW_EMPTY.png', 'jp': './assets/jp/os_handler/MISSION_OVERVIEW_EMPTY.png', 'tw': './assets/tw/os_handler/MISSION_OVERVIEW_EMPTY.png'}) MISSION_OVERVIEW_ENTER = Button(area={'cn': (1111, 672, 1207, 690), 'en': (1112, 662, 1183, 689), 'jp': (1108, 670, 1224, 691), 'tw': (1110, 671, 1207, 690)}, color={'cn': (66, 68, 72), 'en': (60, 67, 78), 'jp': (38, 43, 50), 'tw': (67, 70, 75)}, button={'cn': (1105, 629, 1255, 693), 'en': (1105, 629, 1254, 693), 'jp': (1105, 629, 1254, 693), 'tw': (1105, 629, 1255, 693)}, file={'cn': './assets/cn/os_handler/MISSION_OVERVIEW_ENTER.png', 'en': './assets/en/os_handler/MISSION_OVERVIEW_ENTER.png', 'jp': './assets/jp/os_handler/MISSION_OVERVIEW_ENTER.png', 'tw': './assets/tw/os_handler/MISSION_OVERVIEW_ENTER.png'}) MISSION_QUIT = Button(area={'cn': (1086, 111, 1152, 155), 'en': (1086, 111, 1152, 155), 'jp': (1086, 111, 1152, 155), 'tw': (1086, 111, 1152, 155)}, color={'cn': (152, 38, 35), 'en': (152, 38, 35), 'jp': (152, 38, 35), 'tw': (152, 38, 35)}, button={'cn': (1086, 111, 1152, 155), 'en': (1086, 111, 1152, 155), 'jp': (1086, 111, 1152, 155), 'tw': (1086, 111, 1152, 155)}, file={'cn': './assets/cn/os_handler/MISSION_QUIT.png', 'en': './assets/en/os_handler/MISSION_QUIT.png', 'jp': './assets/jp/os_handler/MISSION_QUIT.png', 'tw': './assets/tw/os_handler/MISSION_QUIT.png'}) ORDER_CHECK = Button(area={'cn': (60, 623, 98, 659), 'en': (60, 623, 98, 659), 'jp': (60, 623, 98, 659), 'tw': (60, 623, 98, 659)}, color={'cn': (60, 77, 122), 'en': (60, 77, 122), 'jp': (60, 77, 122), 'tw': (60, 77, 122)}, button={'cn': (106, 77, 224, 94), 'en': (101, 79, 306, 93), 'jp': (65, 64, 147, 95), 'tw': (106, 76, 226, 95)}, file={'cn': './assets/cn/os_handler/ORDER_CHECK.png', 'en': './assets/en/os_handler/ORDER_CHECK.png', 'jp': './assets/jp/os_handler/ORDER_CHECK.png', 'tw': './assets/tw/os_handler/ORDER_CHECK.png'}) diff --git a/alas_wrapped/module/os_handler/mission.py b/alas_wrapped/module/os_handler/mission.py index e20caa8797..376ac1db97 100644 --- a/alas_wrapped/module/os_handler/mission.py +++ b/alas_wrapped/module/os_handler/mission.py @@ -176,27 +176,28 @@ def os_mission_overview_accept(self): offset=(200, 20), retry_wait=3, additional=self.handle_manjuu, skip_first_screenshot=True) + timeout = 5 + accept_button_timer = Timer(timeout) + self.interval_timer[MISSION_OVERVIEW_ACCEPT_SINGLE.name] = accept_button_timer + self.interval_timer[MISSION_OVERVIEW_ACCEPT.name] = accept_button_timer # MISSION_OVERVIEW_CHECK - confirm_timer = Timer(1, count=3).start() success = True for _ in self.loop(): - if self.handle_manjuu(): - confirm_timer.reset() - continue + # End + if self.appear(MISSION_OVERVIEW_EMPTY, offset=(20, 20)): + success = True + break if self.info_bar_count(): logger.info('Unable to accept missions, because reached the maximum number of missions') success = False break - if self.appear_then_click(MISSION_OVERVIEW_ACCEPT, offset=(20, 20), interval=0.2): - confirm_timer.reset() + + if self.handle_manjuu(): continue - else: - # End - if confirm_timer.reached(): - success = True - break - if self.appear_then_click(MISSION_OVERVIEW_ACCEPT_SINGLE, offset=(20, 20), interval=0.2): - confirm_timer.reset() + # Click + if self.appear_then_click(MISSION_OVERVIEW_ACCEPT, offset=(20, 20), interval=timeout): + continue + if self.appear_then_click(MISSION_OVERVIEW_ACCEPT_SINGLE, offset=(20, 20), interval=timeout): continue # is_in_globe diff --git a/alas_wrapped/module/os_handler/os_status.py b/alas_wrapped/module/os_handler/os_status.py index cf55e45611..8f43a40c29 100644 --- a/alas_wrapped/module/os_handler/os_status.py +++ b/alas_wrapped/module/os_handler/os_status.py @@ -10,7 +10,6 @@ from module.ocr.ocr import Digit from module.os_shop.assets import OS_SHOP_CHECK, OS_SHOP_PURPLE_COINS, SHOP_PURPLE_COINS, SHOP_YELLOW_COINS from module.ui.ui import UI -from module.log_res.log_res import LogRes if server.server != 'jp': OCR_SHOP_YELLOW_COINS = Digit(SHOP_YELLOW_COINS, letter=(239, 239, 239), threshold=160, name='OCR_SHOP_YELLOW_COINS') @@ -77,17 +76,14 @@ def get_yellow_coins(self) -> int: continue else: break - LogRes(self.config).YellowCoin = yellow_coins return yellow_coins def get_purple_coins(self) -> int: if self.appear(OS_SHOP_CHECK): - amount = OCR_OS_SHOP_PURPLE_COINS.ocr(self.device.image) + return OCR_OS_SHOP_PURPLE_COINS.ocr(self.device.image) else: - amount = OCR_SHOP_PURPLE_COINS.ocr(self.device.image) - LogRes(self.config).PurpleCoin = amount - return amount + return OCR_SHOP_PURPLE_COINS.ocr(self.device.image) def os_shop_get_coins(self): self._shop_yellow_coins = self.get_yellow_coins() diff --git a/alas_wrapped/module/os_handler/port.py b/alas_wrapped/module/os_handler/port.py index b2ebca8039..fac02df5ed 100644 --- a/alas_wrapped/module/os_handler/port.py +++ b/alas_wrapped/module/os_handler/port.py @@ -11,13 +11,20 @@ class PortHandler(OSShop): - def port_enter(self, skip_first_screenshot=True): + def port_enter(self): """ Pages: in: IN_MAP out: PORT_CHECK """ - self.ui_click(PORT_ENTER, check_button=PORT_CHECK, skip_first_screenshot=skip_first_screenshot) + logger.info('Port enter') + for _ in self.loop(): + if self.appear(PORT_CHECK, offset=(20, 20)): + break + if self.appear_then_click(PORT_ENTER, offset=(20, 20), interval=5): + continue + if self.handle_map_event(): + continue # Buttons at the bottom has an animation to show pass # Already ensured in ui_click @@ -27,6 +34,7 @@ def port_quit(self, skip_first_screenshot=True): in: PORT_CHECK out: IN_MAP """ + logger.info('Port quit') self.ui_back(appear_button=PORT_CHECK, check_button=self.is_in_map, skip_first_screenshot=skip_first_screenshot) # Buttons at the bottom has an animation to show diff --git a/alas_wrapped/module/raid/assets.py b/alas_wrapped/module/raid/assets.py index 7965f5718b..172c02e781 100644 --- a/alas_wrapped/module/raid/assets.py +++ b/alas_wrapped/module/raid/assets.py @@ -17,6 +17,15 @@ BRISTOL_RAID_EASY = Button(area={'cn': (1151, 490, 1203, 528), 'en': (1155, 504, 1197, 524), 'jp': (1152, 491, 1203, 528), 'tw': (1151, 490, 1204, 519)}, color={'cn': (141, 164, 177), 'en': (145, 167, 177), 'jp': (126, 151, 166), 'tw': (117, 142, 159)}, button={'cn': (1151, 490, 1203, 528), 'en': (1155, 504, 1197, 524), 'jp': (1152, 491, 1203, 528), 'tw': (1151, 490, 1204, 519)}, file={'cn': './assets/cn/raid/BRISTOL_RAID_EASY.png', 'en': './assets/en/raid/BRISTOL_RAID_EASY.png', 'jp': './assets/jp/raid/BRISTOL_RAID_EASY.png', 'tw': './assets/tw/raid/BRISTOL_RAID_EASY.png'}) BRISTOL_RAID_HARD = Button(area={'cn': (1167, 246, 1220, 285), 'en': (1156, 259, 1231, 278), 'jp': (1159, 250, 1229, 285), 'tw': (1170, 249, 1217, 275)}, color={'cn': (136, 160, 173), 'en': (133, 156, 168), 'jp': (145, 168, 181), 'tw': (100, 126, 145)}, button={'cn': (1167, 246, 1220, 285), 'en': (1156, 259, 1231, 278), 'jp': (1159, 250, 1229, 285), 'tw': (1170, 249, 1217, 275)}, file={'cn': './assets/cn/raid/BRISTOL_RAID_HARD.png', 'en': './assets/en/raid/BRISTOL_RAID_HARD.png', 'jp': './assets/jp/raid/BRISTOL_RAID_HARD.png', 'tw': './assets/tw/raid/BRISTOL_RAID_HARD.png'}) BRISTOL_RAID_NORMAL = Button(area={'cn': (1098, 367, 1150, 404), 'en': (1089, 380, 1158, 396), 'jp': (1099, 368, 1149, 404), 'tw': (1097, 367, 1151, 393)}, color={'cn': (129, 152, 165), 'en': (131, 153, 164), 'jp': (119, 146, 163), 'tw': (121, 144, 159)}, button={'cn': (1098, 367, 1150, 404), 'en': (1089, 380, 1158, 396), 'jp': (1099, 368, 1149, 404), 'tw': (1097, 367, 1151, 393)}, file={'cn': './assets/cn/raid/BRISTOL_RAID_NORMAL.png', 'en': './assets/en/raid/BRISTOL_RAID_NORMAL.png', 'jp': './assets/jp/raid/BRISTOL_RAID_NORMAL.png', 'tw': './assets/tw/raid/BRISTOL_RAID_NORMAL.png'}) +CHANGWU_OCR_PT = Button(area={'cn': (1174, 603, 1280, 632), 'en': (1174, 603, 1280, 632), 'jp': (1174, 603, 1280, 632), 'tw': (1174, 603, 1280, 632)}, color={'cn': (183, 118, 101), 'en': (183, 118, 101), 'jp': (183, 118, 101), 'tw': (183, 118, 101)}, button={'cn': (1174, 603, 1280, 632), 'en': (1174, 603, 1280, 632), 'jp': (1174, 603, 1280, 632), 'tw': (1174, 603, 1280, 632)}, file={'cn': './assets/cn/raid/CHANGWU_OCR_PT.png', 'en': './assets/en/raid/CHANGWU_OCR_PT.png', 'jp': './assets/jp/raid/CHANGWU_OCR_PT.png', 'tw': './assets/tw/raid/CHANGWU_OCR_PT.png'}) +CHANGWU_OCR_REMAIN_EASY = Button(area={'cn': (1057, 565, 1116, 585), 'en': (1057, 565, 1116, 585), 'jp': (1057, 565, 1116, 585), 'tw': (1057, 565, 1116, 585)}, color={'cn': (96, 83, 81), 'en': (96, 83, 81), 'jp': (96, 83, 81), 'tw': (96, 83, 81)}, button={'cn': (1057, 565, 1116, 585), 'en': (1057, 565, 1116, 585), 'jp': (1057, 565, 1116, 585), 'tw': (1057, 565, 1116, 585)}, file={'cn': './assets/cn/raid/CHANGWU_OCR_REMAIN_EASY.png', 'en': './assets/en/raid/CHANGWU_OCR_REMAIN_EASY.png', 'jp': './assets/jp/raid/CHANGWU_OCR_REMAIN_EASY.png', 'tw': './assets/tw/raid/CHANGWU_OCR_REMAIN_EASY.png'}) +CHANGWU_OCR_REMAIN_EX = Button(area={'cn': (1138, 26, 1185, 49), 'en': (1138, 26, 1185, 49), 'jp': (1138, 26, 1185, 49), 'tw': (1138, 26, 1185, 49)}, color={'cn': (213, 128, 103), 'en': (213, 128, 103), 'jp': (213, 128, 103), 'tw': (213, 128, 103)}, button={'cn': (1138, 26, 1185, 49), 'en': (1138, 26, 1185, 49), 'jp': (1138, 26, 1185, 49), 'tw': (1138, 26, 1185, 49)}, file={'cn': './assets/cn/raid/CHANGWU_OCR_REMAIN_EX.png', 'en': './assets/en/raid/CHANGWU_OCR_REMAIN_EX.png', 'jp': './assets/jp/raid/CHANGWU_OCR_REMAIN_EX.png', 'tw': './assets/tw/raid/CHANGWU_OCR_REMAIN_EX.png'}) +CHANGWU_OCR_REMAIN_HARD = Button(area={'cn': (1169, 409, 1229, 429), 'en': (1169, 409, 1229, 429), 'jp': (1169, 409, 1229, 429), 'tw': (1169, 409, 1229, 429)}, color={'cn': (100, 86, 82), 'en': (100, 86, 82), 'jp': (100, 86, 82), 'tw': (100, 86, 82)}, button={'cn': (1169, 409, 1229, 429), 'en': (1169, 409, 1229, 429), 'jp': (1169, 409, 1229, 429), 'tw': (1169, 409, 1229, 429)}, file={'cn': './assets/cn/raid/CHANGWU_OCR_REMAIN_HARD.png', 'en': './assets/en/raid/CHANGWU_OCR_REMAIN_HARD.png', 'jp': './assets/jp/raid/CHANGWU_OCR_REMAIN_HARD.png', 'tw': './assets/tw/raid/CHANGWU_OCR_REMAIN_HARD.png'}) +CHANGWU_OCR_REMAIN_NORMAL = Button(area={'cn': (1112, 487, 1170, 506), 'en': (1112, 487, 1170, 506), 'jp': (1112, 487, 1170, 506), 'tw': (1112, 487, 1170, 506)}, color={'cn': (100, 87, 84), 'en': (100, 87, 84), 'jp': (100, 87, 84), 'tw': (100, 87, 84)}, button={'cn': (1112, 487, 1170, 506), 'en': (1112, 487, 1170, 506), 'jp': (1112, 487, 1170, 506), 'tw': (1112, 487, 1170, 506)}, file={'cn': './assets/cn/raid/CHANGWU_OCR_REMAIN_NORMAL.png', 'en': './assets/en/raid/CHANGWU_OCR_REMAIN_NORMAL.png', 'jp': './assets/jp/raid/CHANGWU_OCR_REMAIN_NORMAL.png', 'tw': './assets/tw/raid/CHANGWU_OCR_REMAIN_NORMAL.png'}) +CHANGWU_RAID_EASY = Button(area={'cn': (976, 559, 1032, 590), 'en': (971, 562, 1037, 584), 'jp': (978, 561, 1031, 588), 'tw': (977, 560, 1031, 588)}, color={'cn': (144, 130, 122), 'en': (158, 144, 134), 'jp': (151, 137, 128), 'tw': (162, 147, 137)}, button={'cn': (976, 559, 1032, 590), 'en': (971, 562, 1037, 584), 'jp': (978, 561, 1031, 588), 'tw': (977, 560, 1031, 588)}, file={'cn': './assets/cn/raid/CHANGWU_RAID_EASY.png', 'en': './assets/en/raid/CHANGWU_RAID_EASY.png', 'jp': './assets/jp/raid/CHANGWU_RAID_EASY.png', 'tw': './assets/tw/raid/CHANGWU_RAID_EASY.png'}) +CHANGWU_RAID_EX = Button(area={'cn': (1135, 296, 1209, 331), 'en': (1135, 296, 1209, 331), 'jp': (1135, 296, 1209, 331), 'tw': (1135, 296, 1209, 331)}, color={'cn': (151, 138, 125), 'en': (151, 138, 125), 'jp': (151, 138, 125), 'tw': (151, 138, 125)}, button={'cn': (1135, 296, 1209, 331), 'en': (1135, 296, 1209, 331), 'jp': (1135, 296, 1209, 331), 'tw': (1135, 296, 1209, 331)}, file={'cn': './assets/cn/raid/CHANGWU_RAID_EX.png', 'en': './assets/en/raid/CHANGWU_RAID_EX.png', 'jp': './assets/jp/raid/CHANGWU_RAID_EX.png', 'tw': './assets/tw/raid/CHANGWU_RAID_EX.png'}) +CHANGWU_RAID_HARD = Button(area={'cn': (1087, 403, 1146, 433), 'en': (1083, 407, 1150, 428), 'jp': (1073, 405, 1143, 431), 'tw': (1089, 405, 1145, 432)}, color={'cn': (154, 139, 130), 'en': (173, 157, 145), 'jp': (133, 119, 112), 'tw': (173, 157, 145)}, button={'cn': (1087, 403, 1146, 433), 'en': (1083, 407, 1150, 428), 'jp': (1073, 405, 1143, 431), 'tw': (1089, 405, 1145, 432)}, file={'cn': './assets/cn/raid/CHANGWU_RAID_HARD.png', 'en': './assets/en/raid/CHANGWU_RAID_HARD.png', 'jp': './assets/jp/raid/CHANGWU_RAID_HARD.png', 'tw': './assets/tw/raid/CHANGWU_RAID_HARD.png'}) +CHANGWU_RAID_NORMAL = Button(area={'cn': (1032, 480, 1091, 511), 'en': (1009, 485, 1104, 506), 'jp': (1034, 482, 1089, 510), 'tw': (1033, 481, 1089, 510)}, color={'cn': (144, 130, 122), 'en': (168, 153, 142), 'jp': (146, 131, 123), 'tw': (151, 136, 128)}, button={'cn': (1032, 480, 1091, 511), 'en': (1009, 485, 1104, 506), 'jp': (1034, 482, 1089, 510), 'tw': (1033, 481, 1089, 510)}, file={'cn': './assets/cn/raid/CHANGWU_RAID_NORMAL.png', 'en': './assets/en/raid/CHANGWU_RAID_NORMAL.png', 'jp': './assets/jp/raid/CHANGWU_RAID_NORMAL.png', 'tw': './assets/tw/raid/CHANGWU_RAID_NORMAL.png'}) CHIENWU_OCR_PT = Button(area={'cn': (1166, 604, 1279, 632), 'en': (1166, 604, 1279, 632), 'jp': (1166, 604, 1279, 632), 'tw': (1166, 604, 1279, 632)}, color={'cn': (126, 40, 47), 'en': (126, 40, 47), 'jp': (126, 40, 47), 'tw': (126, 40, 47)}, button={'cn': (1166, 604, 1279, 632), 'en': (1166, 604, 1279, 632), 'jp': (1166, 604, 1279, 632), 'tw': (1166, 604, 1279, 632)}, file={'cn': './assets/cn/raid/CHIENWU_OCR_PT.png', 'en': './assets/cn/raid/CHIENWU_OCR_PT.png', 'jp': './assets/cn/raid/CHIENWU_OCR_PT.png', 'tw': './assets/cn/raid/CHIENWU_OCR_PT.png'}) CHIENWU_OCR_REMAIN_EASY = Button(area={'cn': (1111, 528, 1163, 549), 'en': (1111, 528, 1163, 549), 'jp': (1111, 528, 1163, 549), 'tw': (1111, 528, 1163, 549)}, color={'cn': (174, 153, 133), 'en': (174, 153, 133), 'jp': (174, 153, 133), 'tw': (174, 153, 133)}, button={'cn': (1111, 528, 1163, 549), 'en': (1111, 528, 1163, 549), 'jp': (1111, 528, 1163, 549), 'tw': (1111, 528, 1163, 549)}, file={'cn': './assets/cn/raid/CHIENWU_OCR_REMAIN_EASY.png', 'en': './assets/cn/raid/CHIENWU_OCR_REMAIN_EASY.png', 'jp': './assets/cn/raid/CHIENWU_OCR_REMAIN_EASY.png', 'tw': './assets/cn/raid/CHIENWU_OCR_REMAIN_EASY.png'}) CHIENWU_OCR_REMAIN_EX = Button(area={'cn': (1086, 16, 1152, 44), 'en': (1086, 16, 1152, 44), 'jp': (1086, 16, 1152, 44), 'tw': (1086, 16, 1152, 44)}, color={'cn': (90, 39, 34), 'en': (90, 39, 34), 'jp': (90, 39, 34), 'tw': (90, 39, 34)}, button={'cn': (1086, 16, 1152, 44), 'en': (1086, 16, 1152, 44), 'jp': (1086, 16, 1152, 44), 'tw': (1086, 16, 1152, 44)}, file={'cn': './assets/cn/raid/CHIENWU_OCR_REMAIN_EX.png', 'en': './assets/cn/raid/CHIENWU_OCR_REMAIN_EX.png', 'jp': './assets/cn/raid/CHIENWU_OCR_REMAIN_EX.png', 'tw': './assets/cn/raid/CHIENWU_OCR_REMAIN_EX.png'}) @@ -68,12 +77,13 @@ KUYBYSHEY_RAID_NORMAL = Button(area={'cn': (1045, 423, 1097, 451), 'en': (1036, 424, 1099, 449), 'jp': (1048, 427, 1091, 448), 'tw': (1044, 423, 1096, 452)}, color={'cn': (86, 95, 109), 'en': (81, 92, 105), 'jp': (131, 143, 154), 'tw': (86, 95, 109)}, button={'cn': (1045, 423, 1097, 451), 'en': (1036, 424, 1099, 449), 'jp': (1048, 427, 1091, 448), 'tw': (1044, 423, 1096, 452)}, file={'cn': './assets/cn/raid/KUYBYSHEY_RAID_NORMAL.png', 'en': './assets/en/raid/KUYBYSHEY_RAID_NORMAL.png', 'jp': './assets/jp/raid/KUYBYSHEY_RAID_NORMAL.png', 'tw': './assets/tw/raid/KUYBYSHEY_RAID_NORMAL.png'}) RAID_FLEET_PREPARATION = Button(area={'cn': (983, 577, 1181, 638), 'en': (1041, 592, 1121, 631), 'jp': (983, 579, 1180, 635), 'tw': (983, 577, 1181, 638)}, color={'cn': (236, 188, 115), 'en': (236, 184, 117), 'jp': (235, 183, 103), 'tw': (236, 188, 115)}, button={'cn': (983, 577, 1181, 638), 'en': (1041, 592, 1121, 631), 'jp': (983, 579, 1180, 635), 'tw': (983, 577, 1181, 638)}, file={'cn': './assets/cn/raid/RAID_FLEET_PREPARATION.png', 'en': './assets/en/raid/RAID_FLEET_PREPARATION.png', 'jp': './assets/jp/raid/RAID_FLEET_PREPARATION.png', 'tw': './assets/tw/raid/RAID_FLEET_PREPARATION.png'}) RAID_REWARDS = Button(area={'cn': (836, 127, 900, 169), 'en': (836, 127, 900, 169), 'jp': (836, 127, 900, 169), 'tw': (836, 127, 900, 169)}, color={'cn': (217, 103, 98), 'en': (217, 103, 98), 'jp': (217, 103, 98), 'tw': (217, 103, 98)}, button={'cn': (836, 127, 900, 169), 'en': (836, 127, 900, 169), 'jp': (836, 127, 900, 169), 'tw': (836, 127, 900, 169)}, file={'cn': './assets/cn/raid/RAID_REWARDS.png', 'en': './assets/en/raid/RAID_REWARDS.png', 'jp': './assets/jp/raid/RAID_REWARDS.png', 'tw': './assets/tw/raid/RAID_REWARDS.png'}) +RPG_BACK = Button(area={'cn': (40, 30, 59, 57), 'en': (40, 30, 59, 57), 'jp': (40, 30, 59, 57), 'tw': (40, 30, 59, 57)}, color={'cn': (154, 127, 105), 'en': (154, 127, 105), 'jp': (154, 127, 105), 'tw': (154, 127, 105)}, button={'cn': (40, 30, 59, 57), 'en': (40, 30, 59, 57), 'jp': (40, 30, 59, 57), 'tw': (40, 30, 59, 57)}, file={'cn': './assets/cn/raid/RPG_BACK.png', 'en': './assets/en/raid/RPG_BACK.png', 'jp': './assets/jp/raid/RPG_BACK.png', 'tw': './assets/tw/raid/RPG_BACK.png'}) RPG_GOTO_STAGE = Button(area={'cn': (55, 495, 80, 520), 'en': (55, 495, 80, 520), 'jp': (55, 495, 80, 520), 'tw': (55, 495, 80, 520)}, color={'cn': (174, 168, 160), 'en': (174, 168, 160), 'jp': (174, 168, 160), 'tw': (174, 168, 160)}, button={'cn': (55, 495, 80, 520), 'en': (55, 495, 80, 520), 'jp': (55, 495, 80, 520), 'tw': (55, 495, 80, 520)}, file={'cn': './assets/cn/raid/RPG_GOTO_STAGE.png', 'en': './assets/en/raid/RPG_GOTO_STAGE.png', 'jp': './assets/jp/raid/RPG_GOTO_STAGE.png', 'tw': './assets/tw/raid/RPG_GOTO_STAGE.png'}) RPG_GOTO_STORY = Button(area={'cn': (59, 491, 84, 516), 'en': (59, 491, 84, 516), 'jp': (59, 491, 84, 516), 'tw': (59, 491, 84, 516)}, color={'cn': (182, 122, 105), 'en': (182, 122, 105), 'jp': (182, 122, 105), 'tw': (182, 122, 105)}, button={'cn': (59, 491, 84, 516), 'en': (59, 491, 84, 516), 'jp': (59, 491, 84, 516), 'tw': (59, 491, 84, 516)}, file={'cn': './assets/cn/raid/RPG_GOTO_STORY.png', 'en': './assets/en/raid/RPG_GOTO_STORY.png', 'jp': './assets/jp/raid/RPG_GOTO_STORY.png', 'tw': './assets/tw/raid/RPG_GOTO_STORY.png'}) RPG_HOME = Button(area={'cn': (1222, 29, 1240, 51), 'en': (1222, 29, 1240, 51), 'jp': (1222, 29, 1240, 51), 'tw': (1222, 29, 1240, 51)}, color={'cn': (197, 181, 158), 'en': (197, 181, 158), 'jp': (197, 181, 158), 'tw': (197, 181, 158)}, button={'cn': (1222, 29, 1240, 51), 'en': (1222, 29, 1240, 51), 'jp': (1222, 29, 1240, 51), 'tw': (1222, 29, 1240, 51)}, file={'cn': './assets/cn/raid/RPG_HOME.png', 'en': './assets/en/raid/RPG_HOME.png', 'jp': './assets/jp/raid/RPG_HOME.png', 'tw': './assets/tw/raid/RPG_HOME.png'}) RPG_LEAVE_CITY = Button(area={'cn': (688, 642, 711, 664), 'en': (688, 642, 711, 664), 'jp': (688, 642, 711, 664), 'tw': (688, 642, 711, 664)}, color={'cn': (158, 130, 109), 'en': (158, 130, 109), 'jp': (158, 130, 109), 'tw': (158, 130, 109)}, button={'cn': (688, 642, 711, 664), 'en': (688, 642, 711, 664), 'jp': (688, 642, 711, 664), 'tw': (688, 642, 711, 664)}, file={'cn': './assets/cn/raid/RPG_LEAVE_CITY.png', 'en': './assets/en/raid/RPG_LEAVE_CITY.png', 'jp': './assets/jp/raid/RPG_LEAVE_CITY.png', 'tw': './assets/tw/raid/RPG_LEAVE_CITY.png'}) RPG_RAID_EASY = Button(area={'cn': (149, 561, 179, 591), 'en': (149, 561, 179, 591), 'jp': (149, 561, 179, 591), 'tw': (149, 561, 179, 591)}, color={'cn': (152, 57, 59), 'en': (152, 57, 59), 'jp': (152, 57, 59), 'tw': (152, 57, 59)}, button={'cn': (149, 561, 179, 591), 'en': (149, 561, 179, 591), 'jp': (149, 561, 179, 591), 'tw': (149, 561, 179, 591)}, file={'cn': './assets/cn/raid/RPG_RAID_EASY.png', 'en': './assets/en/raid/RPG_RAID_EASY.png', 'jp': './assets/jp/raid/RPG_RAID_EASY.png', 'tw': './assets/tw/raid/RPG_RAID_EASY.png'}) -RPG_RAID_EX = Button(area={'cn': (949, 518, 976, 565), 'en': (949, 518, 976, 565), 'jp': (949, 518, 976, 565), 'tw': (949, 518, 976, 565)}, color={'cn': (166, 66, 69), 'en': (166, 66, 69), 'jp': (166, 66, 69), 'tw': (166, 66, 69)}, button={'cn': (949, 518, 976, 565), 'en': (949, 518, 976, 565), 'jp': (949, 518, 976, 565), 'tw': (949, 518, 976, 565)}, file={'cn': './assets/cn/raid/RPG_RAID_EX.png', 'en': './assets/en/raid/RPG_RAID_EX.png', 'jp': './assets/jp/raid/RPG_RAID_EX.png', 'tw': './assets/tw/raid/RPG_RAID_EX.png'}) +RPG_RAID_EX = Button(area={'cn': (979, 223, 999, 258), 'en': (979, 223, 999, 258), 'jp': (979, 223, 999, 258), 'tw': (979, 223, 999, 258)}, color={'cn': (231, 198, 84), 'en': (231, 198, 84), 'jp': (231, 198, 84), 'tw': (231, 198, 84)}, button={'cn': (979, 223, 999, 258), 'en': (979, 223, 999, 258), 'jp': (979, 223, 999, 258), 'tw': (979, 223, 999, 258)}, file={'cn': './assets/cn/raid/RPG_RAID_EX.png', 'en': './assets/en/raid/RPG_RAID_EX.png', 'jp': './assets/jp/raid/RPG_RAID_EX.png', 'tw': './assets/tw/raid/RPG_RAID_EX.png'}) RPG_RAID_HARD = Button(area={'cn': (475, 108, 505, 138), 'en': (475, 108, 505, 138), 'jp': (475, 108, 505, 138), 'tw': (475, 108, 505, 138)}, color={'cn': (97, 59, 59), 'en': (97, 59, 59), 'jp': (97, 59, 59), 'tw': (97, 59, 59)}, button={'cn': (475, 108, 505, 138), 'en': (475, 108, 505, 138), 'jp': (475, 108, 505, 138), 'tw': (475, 108, 505, 138)}, file={'cn': './assets/cn/raid/RPG_RAID_HARD.png', 'en': './assets/en/raid/RPG_RAID_HARD.png', 'jp': './assets/jp/raid/RPG_RAID_HARD.png', 'tw': './assets/tw/raid/RPG_RAID_HARD.png'}) RPG_RAID_NORMAL = Button(area={'cn': (313, 259, 343, 289), 'en': (313, 259, 343, 289), 'jp': (313, 259, 343, 289), 'tw': (313, 259, 343, 289)}, color={'cn': (147, 61, 62), 'en': (147, 61, 62), 'jp': (147, 61, 62), 'tw': (147, 61, 62)}, button={'cn': (313, 259, 343, 289), 'en': (313, 259, 343, 289), 'jp': (313, 259, 343, 289), 'tw': (313, 259, 343, 289)}, file={'cn': './assets/cn/raid/RPG_RAID_NORMAL.png', 'en': './assets/en/raid/RPG_RAID_NORMAL.png', 'jp': './assets/jp/raid/RPG_RAID_NORMAL.png', 'tw': './assets/tw/raid/RPG_RAID_NORMAL.png'}) RPG_STATUS_POPUP = Button(area={'cn': (1120, 97, 1144, 121), 'en': (1120, 97, 1144, 121), 'jp': (1120, 97, 1144, 121), 'tw': (1120, 97, 1144, 121)}, color={'cn': (158, 165, 176), 'en': (158, 165, 176), 'jp': (158, 165, 176), 'tw': (158, 165, 176)}, button={'cn': (1120, 97, 1144, 121), 'en': (1120, 97, 1144, 121), 'jp': (1120, 97, 1144, 121), 'tw': (1120, 97, 1144, 121)}, file={'cn': './assets/cn/raid/RPG_STATUS_POPUP.png', 'en': './assets/en/raid/RPG_STATUS_POPUP.png', 'jp': './assets/jp/raid/RPG_STATUS_POPUP.png', 'tw': './assets/tw/raid/RPG_STATUS_POPUP.png'}) diff --git a/alas_wrapped/module/raid/raid.py b/alas_wrapped/module/raid/raid.py index 49eb2e2646..2d160eb8ab 100644 --- a/alas_wrapped/module/raid/raid.py +++ b/alas_wrapped/module/raid/raid.py @@ -2,11 +2,10 @@ import numpy as np import module.config.server as server -from module.base.decorator import run_once from module.base.timer import Timer from module.campaign.campaign_event import CampaignEvent from module.combat.assets import * -from module.exception import OilExhausted, ScriptError +from module.exception import ScriptError from module.logger import logger from module.map.map_operation import MapOperation from module.ocr.ocr import Digit, DigitCounter @@ -16,6 +15,15 @@ from module.ui.page import page_rpg_stage +class RaidCounterPostMixin(DigitCounter): + def after_process(self, result): + # fix result like "915/", "1515" + result = result.strip('/') + if result.isdigit() and len(result) > 2 and result.endswith('15'): + result = f'{result[:-2]}/15' + return result + + class RaidCounter(DigitCounter): def pre_process(self, image): image = super().pre_process(image) @@ -83,6 +91,8 @@ def raid_name_shorten(name): return "RPG" elif name == 'raid_20250116': return 'CHIENWU' + elif name == 'raid_20260212': + return 'CHANGWU' else: raise ScriptError(f'Unknown raid name: {name}') @@ -158,6 +168,11 @@ def raid_ocr(raid, mode): return Digit(button, letter=(247, 223, 222), threshold=128) else: return DigitCounter(button, letter=(0, 0, 0), threshold=128) + elif raid == 'CHANGWU': + if mode == 'ex': + return Digit(button, letter=(255, 239, 215), threshold=128) + else: + return RaidCounterPostMixin(button, lang='cnocr', letter=(154, 148, 133), threshold=128) def pt_ocr(raid): @@ -187,9 +202,44 @@ def pt_ocr(raid): return HuanChangPtOcr(button, letter=(23, 20, 6), threshold=128) elif raid == 'CHIENWU': return Digit(button, letter=(255, 231, 231), threshold=128) + elif raid == 'CHANGWU': + return Digit(button, letter=(255, 239, 215), threshold=128) class Raid(MapOperation, RaidCombat, CampaignEvent): + @property + def _raid_has_oil_icon(self): + """ + Game devs are too asshole to drop oil display for UI design + https://github.com/LmeSzinc/AzurLaneAutoScript/issues/5214 + """ + return False + + def triggered_stop_condition(self, oil_check=False, pt_check=False, coin_check=False): + """ + Returns: + bool: If triggered a stop condition. + """ + # Oil limit + if oil_check: + if self.get_oil() < max(500, self.config.StopCondition_OilLimit): + logger.hr('Triggered stop condition: Oil limit') + self.config.task_delay(minute=(120, 240)) + return True + # Event limit + if pt_check: + if self.event_pt_limit_triggered(): + logger.hr('Triggered stop condition: Event PT limit') + return True + # TaskBalancer + if coin_check: + if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): + logger.hr('Triggered stop condition: Coin limit') + self.handle_task_balancer() + return True + + return False + def combat_preparation(self, balance_hp=False, emotion_reduce=False, auto='combat_auto', fleet_index=1): """ Args: @@ -199,32 +249,20 @@ def combat_preparation(self, balance_hp=False, emotion_reduce=False, auto='comba fleet_index (int): """ logger.info('Combat preparation.') - skip_first_screenshot = True # No need, already waited in `raid_execute_once()` # if emotion_reduce: # self.emotion.wait(fleet_index) - @run_once - def check_oil(): - if self.get_oil() < max(500, self.config.StopCondition_OilLimit): - logger.hr('Triggered oil limit') - raise OilExhausted - - @run_once - def check_coin(): - if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): - logger.hr('Triggered stop condition: Coin limit') - self.handle_task_balancer() - return True - + checked = False for _ in self.loop(): - if self.appear(BATTLE_PREPARATION, offset=(30, 20)): if self.handle_combat_automation_set(auto=auto == 'combat_auto'): continue - check_oil() - check_coin() + if not checked and self._raid_has_oil_icon: + checked = True + if self.triggered_stop_condition(oil_check=True, coin_check=True): + self.config.task_stop() if self.handle_raid_ticket_use(): continue if self.handle_retirement(): @@ -281,7 +319,7 @@ def raid_enter(self, mode, raid, skip_first_screenshot=True): if self.appear(entrance, offset=(10, 10), interval=5): # Items appear from right # Check PT when entrance appear - if self.event_pt_limit_triggered(): + if self.triggered_stop_condition(pt_check=True): self.config.task_stop() self.device.click(entrance) continue @@ -341,7 +379,6 @@ def get_event_pt(self): Pages: in: page_raid """ - from module.log_res.log_res import LogRes skip_first_screenshot = True timeout = Timer(1.5, count=5).start() ocr = pt_ocr(self.config.Campaign_Event) @@ -356,12 +393,10 @@ def get_event_pt(self): pt = ocr.ocr(self.device.image) if timeout.reached(): logger.warning('Wait PT timeout, assume it is') - LogRes(self.config).Pt = pt return pt if pt in [70000, 70001]: continue else: - LogRes(self.config).Pt = pt return pt else: logger.info(f'Raid {self.config.Campaign_Event} does not support PT ocr, skip') diff --git a/alas_wrapped/module/raid/run.py b/alas_wrapped/module/raid/run.py index e561c05ff4..d9a26050b3 100644 --- a/alas_wrapped/module/raid/run.py +++ b/alas_wrapped/module/raid/run.py @@ -1,17 +1,17 @@ from module.base.timer import Timer from module.campaign.campaign_event import CampaignEvent -from module.exception import OilExhausted, ScriptEnd, ScriptError +from module.exception import ScriptEnd, ScriptError from module.logger import logger from module.raid.assets import RAID_REWARDS from module.raid.raid import Raid, raid_ocr -from module.ui.page import page_raid, page_rpg_stage +from module.ui.page import page_campaign_menu, page_raid, page_rpg_stage class RaidRun(Raid, CampaignEvent): run_count: int run_limit: int - def triggered_stop_condition(self): + def triggered_stop_condition(self, oil_check=False, pt_check=False, coin_check=False): """ Returns: bool: If triggered a stop condition. @@ -23,7 +23,7 @@ def triggered_stop_condition(self): self.config.Scheduler_Enable = False return True - return False + return super().triggered_stop_condition(oil_check=oil_check, pt_check=pt_check, coin_check=coin_check) def get_remain(self, mode, skip_first_screenshot=True): """ @@ -93,9 +93,11 @@ def run(self, name='', mode='', total=0): else: logger.info(f'Count: {self.run_count}') - # End - if self.triggered_stop_condition(): - break + # UI switches + if not self._raid_has_oil_icon: + self.ui_ensure(page_campaign_menu) + if self.triggered_stop_condition(oil_check=True, coin_check=True): + break # UI ensure self.device.stuck_record_clear() @@ -108,7 +110,7 @@ def run(self, name='', mode='', total=0): self.disable_event_on_raid() # End for mode EX - if mode == 'ex': + if mode == 'ex' and not self.is_raid_rpg(): if not self.get_remain(mode): logger.info('Triggered stop condition: Zero ' 'raid tickets to do EX mode') @@ -123,10 +125,6 @@ def run(self, name='', mode='', total=0): self.device.click_record_clear() try: self.raid_execute_once(mode=mode, raid=name) - except OilExhausted: - logger.hr('Triggered stop condition: Oil limit') - self.config.task_delay(minute=(120, 240)) - break except ScriptEnd as e: logger.hr('Script end') logger.info(str(e)) diff --git a/alas_wrapped/module/shop/shop_status.py b/alas_wrapped/module/shop/shop_status.py index 472e676820..af0644dfce 100644 --- a/alas_wrapped/module/shop/shop_status.py +++ b/alas_wrapped/module/shop/shop_status.py @@ -2,7 +2,6 @@ from module.ocr.ocr import Digit from module.shop.assets import * from module.ui.ui import UI -from module.log_res.log_res import LogRes if server.server != 'jp': OCR_SHOP_GEMS = Digit(SHOP_GEMS, letter=(255, 243, 82), name='OCR_SHOP_GEMS') @@ -34,8 +33,6 @@ def status_get_gold_coins(self): in: """ amount = OCR_SHOP_GOLD_COINS.ocr(self.device.image) - LogRes(self.config).Coin = amount - self.config.update() return amount def status_get_gems(self): @@ -47,8 +44,6 @@ def status_get_gems(self): in: page_shop, medal shop """ amount = OCR_SHOP_GEMS.ocr(self.device.image) - LogRes(self.config).Gem = amount - self.config.update() return amount def status_get_medal(self): @@ -60,8 +55,6 @@ def status_get_medal(self): in: page_shop, medal shop """ amount = OCR_SHOP_MEDAL.ocr(self.device.image) - LogRes(self.config).Medal = amount - self.config.update() return amount def status_get_merit(self): @@ -73,8 +66,6 @@ def status_get_merit(self): in: page_shop, merit shop """ amount = OCR_SHOP_MERIT.ocr(self.device.image) - LogRes(self.config).Merit = amount - self.config.update() return amount def status_get_guild_coins(self): @@ -86,8 +77,6 @@ def status_get_guild_coins(self): in: page_shop, guild shop """ amount = OCR_SHOP_GUILD_COINS.ocr(self.device.image) - LogRes(self.config).GuildCoin = amount - self.config.update() return amount def status_get_core(self): @@ -99,8 +88,6 @@ def status_get_core(self): in: page_shop, core shop """ amount = OCR_SHOP_CORE.ocr(self.device.image) - LogRes(self.config).Core = amount - self.config.update() return amount def status_get_voucher(self): diff --git a/alas_wrapped/module/storage/storage.py b/alas_wrapped/module/storage/storage.py index a18423c6aa..62be757ff5 100644 --- a/alas_wrapped/module/storage/storage.py +++ b/alas_wrapped/module/storage/storage.py @@ -45,8 +45,12 @@ def _storage_box_template(rarity): def _handle_use_box_amount(self, amount): """ + Args: + amount (int): Expected amount to set + Returns: - bool: if clicked + int: Actual amount set in the UI. + May be less than expected if not enough boxes available. Pages: in: SHOP_BUY_CONFIRM_AMOUNT @@ -84,6 +88,7 @@ def _handle_use_box_amount(self, amount): logger.info(f'Set box amount: {amount}') skip_first = True retry = Timer(1, count=2) + click_count = 0 for _ in self.loop(): if skip_first: skip_first = False @@ -92,13 +97,19 @@ def _handle_use_box_amount(self, amount): diff = amount - current if diff == 0: break + if click_count >= 2: + logger.warning(f'Box amount stuck at {current}, ' + f'requested {amount} but only {current} available') + break if retry.reached(): button = AMOUNT_PLUS if diff > 0 else AMOUNT_MINUS self.device.multi_click(button, n=abs(diff), interval=(0.1, 0.2)) + click_count += 1 retry.reset() - return True + logger.info(f'Box amount set to {current}') + return current def _storage_use_one_box(self, button, amount=1): """ @@ -155,10 +166,10 @@ def _storage_use_one_box(self, button, amount=1): # use match_template_color on BOX_AMOUNT_CONFIRM # a long animation that opens a box, will be on the top of BOX_AMOUNT_CONFIRM if self.match_template_color(BOX_AMOUNT_CONFIRM, offset=(20, 20), interval=5): - self._handle_use_box_amount(amount) + actual = self._handle_use_box_amount(amount) self.device.click(BOX_AMOUNT_CONFIRM) self.interval_reset(BOX_AMOUNT_CONFIRM) - used = amount + used = actual continue if self.appear_then_click(EQUIP_CONFIRM, offset=(20, 20), interval=5): self.interval_reset(MATERIAL_CHECK) diff --git a/alas_wrapped/module/tactical/tactical_class.py b/alas_wrapped/module/tactical/tactical_class.py index 672e705790..b25f13a485 100644 --- a/alas_wrapped/module/tactical/tactical_class.py +++ b/alas_wrapped/module/tactical/tactical_class.py @@ -495,7 +495,7 @@ def tactical_class_receive(self, skip_first_screenshot=True): if self.appear(MISSION_POPUP_GO, offset=self._popup_offset, interval=2): self.device.click(MISSION_POPUP_ACK) continue - if not study_finished and self.appear(TACTICAL_CLASS_CANCEL, offset=(30, 30), interval=2) \ + if self.appear(TACTICAL_CLASS_CANCEL, offset=(30, 30), interval=2) \ and self.appear(TACTICAL_CLASS_START, offset=(30, 30)): if self._tactical_books_choose(): self.dock_select_index = 0 diff --git a/alas_wrapped/module/ui/page.py b/alas_wrapped/module/ui/page.py index 4ea6a94036..2705a53a43 100644 --- a/alas_wrapped/module/ui/page.py +++ b/alas_wrapped/module/ui/page.py @@ -215,10 +215,16 @@ def link(self, button, destination): page_main_white.link(button=MAIN_GOTO_EVENT_LIST_WHITE, destination=page_event_list) # Raid +# before +# page_raid = Page(RAID_CHECK) +# page_raid.link(button=GOTO_MAIN, destination=page_main) +# page_main.link(button=MAIN_GOTO_RAID, destination=page_raid) +# page_main_white.link(button=MAIN_GOTO_RAID_WHITE, destination=page_raid) +# after 2026.02.12 page_raid = Page(RAID_CHECK) page_raid.link(button=GOTO_MAIN, destination=page_main) -page_main.link(button=MAIN_GOTO_RAID, destination=page_raid) -page_main_white.link(button=MAIN_GOTO_RAID_WHITE, destination=page_raid) +page_raid.link(button=BACK_ARROW, destination=page_campaign_menu) +page_campaign_menu.link(button=CAMPAIGN_MENU_GOTO_EVENT, destination=page_raid) # Dock page_dock = Page(DOCK_CHECK) @@ -328,8 +334,10 @@ def link(self, button, destination): page_rpg_story = Page(RPG_GOTO_STAGE) page_rpg_stage.link(button=RPG_GOTO_STORY, destination=page_rpg_story) page_rpg_stage.link(button=RPG_HOME, destination=page_main) +page_rpg_stage.link(button=RPG_BACK, destination=page_campaign_menu) page_rpg_story.link(button=RPG_GOTO_STAGE, destination=page_rpg_stage) page_rpg_story.link(button=RPG_HOME, destination=page_main) +page_rpg_story.link(button=RPG_BACK, destination=page_campaign_menu) page_campaign_menu.link(button=CAMPAIGN_MENU_GOTO_EVENT, destination=page_rpg_stage) # page_main.link(button=MAIN_GOTO_RAID, destination=page_rpg_stage) diff --git a/alas_wrapped/module/ui/setting.py b/alas_wrapped/module/ui/setting.py index 84f3e4b6dd..7a87fd7ef7 100644 --- a/alas_wrapped/module/ui/setting.py +++ b/alas_wrapped/module/ui/setting.py @@ -146,9 +146,7 @@ def _set_execute(self, **kwargs): if clicks: if retry.reached(): for button in clicks: - # Setting toggles can legitimately require repeated taps; - # rely on timeout here instead of global click-loop detector. - self.main.device.click(button, control_check=False) + self.main.device.click(button) retry.reset() else: return True diff --git a/alas_wrapped/module/ui/ui.py b/alas_wrapped/module/ui/ui.py index 55dfbe8346..92ec1a8255 100644 --- a/alas_wrapped/module/ui/ui.py +++ b/alas_wrapped/module/ui/ui.py @@ -1,9 +1,7 @@ from module.base.button import Button from module.base.decorator import run_once from module.base.timer import Timer -from module.coalition.assets import NEONCITY_FLEET_PREPARATION, NEONCITY_PREPARATION_EXIT, DAL_DIFFICULTY_EXIT from module.combat.assets import GET_ITEMS_1, GET_ITEMS_2, GET_SHIP -from module.event_hospital.assets import HOSIPITAL_CLUE_CHECK, HOSPITAL_BATTLE_EXIT from module.exception import (GameNotRunningError, GamePageUnknownError, RequestHumanTakeover) from module.exercise.assets import EXERCISE_PREPARATION @@ -19,7 +17,7 @@ from module.os_handler.assets import (AUTO_SEARCH_REWARD, EXCHANGE_CHECK, RESET_FLEET_PREPARATION, RESET_TICKET_POPUP) from module.raid.assets import * from module.ui.assets import * -from module.ui.page import Page, page_campaign, page_event, page_main, page_main_white, page_sp +from module.ui.page import Page, page_academy, page_campaign, page_event, page_main, page_main_white, page_sp from module.ui_white.assets import * @@ -39,6 +37,11 @@ def ui_page_appear(self, page, offset=(30, 30), interval=0): if self.appear(page_main.check_button, offset=(5, 5), interval=interval): return True return False + # shitty EN localization changing font width of ACADEMY title, + # check other buttons also + if self.config.SERVER == 'en' and page == page_academy: + if self.appear(ACADEMY_GOTO_MUNITIONS, offset=offset, interval=interval): + return True return self.appear(page.check_button, offset=offset, interval=interval) def is_in_main(self, offset=(30, 30), interval=0): @@ -560,10 +563,10 @@ def ui_additional(self, get_ship=True): # return True # Neon city (coalition_20250626) # FASHION (coalition_20260122) reuse NEONCITY - if self.appear(NEONCITY_FLEET_PREPARATION, offset=(20, 20), interval=3): - logger.info(f'{NEONCITY_FLEET_PREPARATION} -> {NEONCITY_PREPARATION_EXIT}') - self.device.click(NEONCITY_PREPARATION_EXIT) - return True + # if self.appear(NEONCITY_FLEET_PREPARATION, offset=(20, 20), interval=3): + # logger.info(f'{NEONCITY_FLEET_PREPARATION} -> {NEONCITY_PREPARATION_EXIT}') + # self.device.click(NEONCITY_PREPARATION_EXIT) + # return True # DATE A LANE (coalition_20251120) # if self.appear_then_click(DAL_DIFFICULTY_EXIT, offset=(20, 20), interval=3): # return True diff --git a/alas_wrapped/module/war_archives/assets.py b/alas_wrapped/module/war_archives/assets.py index 561299fcc7..b95de8d8ce 100644 --- a/alas_wrapped/module/war_archives/assets.py +++ b/alas_wrapped/module/war_archives/assets.py @@ -30,6 +30,7 @@ TEMPLATE_PARALLEL_SUPERIMPOSITION = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_PARALLEL_SUPERIMPOSITION.png', 'en': './assets/cn/war_archives/TEMPLATE_PARALLEL_SUPERIMPOSITION.png', 'jp': './assets/cn/war_archives/TEMPLATE_PARALLEL_SUPERIMPOSITION.png', 'tw': './assets/cn/war_archives/TEMPLATE_PARALLEL_SUPERIMPOSITION.png'}) TEMPLATE_PLEDGE_OF_THE_RADIANT_COURT = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_PLEDGE_OF_THE_RADIANT_COURT.png', 'en': './assets/cn/war_archives/TEMPLATE_PLEDGE_OF_THE_RADIANT_COURT.png', 'jp': './assets/cn/war_archives/TEMPLATE_PLEDGE_OF_THE_RADIANT_COURT.png', 'tw': './assets/cn/war_archives/TEMPLATE_PLEDGE_OF_THE_RADIANT_COURT.png'}) TEMPLATE_PRELUDE_UNDER_THE_MOON = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png', 'en': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png', 'jp': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png', 'tw': './assets/cn/war_archives/TEMPLATE_PRELUDE_UNDER_THE_MOON.png'}) +TEMPLATE_REVELATIONS_OF_DUST = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png', 'en': './assets/en/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png', 'jp': './assets/cn/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png', 'tw': './assets/cn/war_archives/TEMPLATE_REVELATIONS_OF_DUST.png'}) TEMPLATE_RONDO_AT_RAINBOWS_END = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_RONDO_AT_RAINBOWS_END.png', 'en': './assets/en/war_archives/TEMPLATE_RONDO_AT_RAINBOWS_END.png', 'jp': './assets/cn/war_archives/TEMPLATE_RONDO_AT_RAINBOWS_END.png', 'tw': './assets/cn/war_archives/TEMPLATE_RONDO_AT_RAINBOWS_END.png'}) TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png', 'en': './assets/en/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png', 'jp': './assets/cn/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png', 'tw': './assets/cn/war_archives/TEMPLATE_SCHERZO_OF_IRON_AND_BLOOD.png'}) TEMPLATE_SKYBOUND_ORATORIO = Template(file={'cn': './assets/cn/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png', 'en': './assets/en/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png', 'jp': './assets/cn/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png', 'tw': './assets/cn/war_archives/TEMPLATE_SKYBOUND_ORATORIO.png'}) diff --git a/alas_wrapped/module/war_archives/dictionary.py b/alas_wrapped/module/war_archives/dictionary.py index 92bcdf53ca..d7cce38881 100644 --- a/alas_wrapped/module/war_archives/dictionary.py +++ b/alas_wrapped/module/war_archives/dictionary.py @@ -44,4 +44,5 @@ 'war_archives_20231026_cn': TEMPLATE_TEMPESTA_AND_THE_FOUNTAIN_OF_YOUTH, 'war_archives_20220915_cn': TEMPLATE_VIOLET_TEMPEST_BLOOMING_LYCORIS, 'war_archives_20221222_cn': TEMPLATE_PARALLEL_SUPERIMPOSITION, + 'war_archives_20230223_cn': TEMPLATE_REVELATIONS_OF_DUST, } diff --git a/alas_wrapped/module/webui/app.py b/alas_wrapped/module/webui/app.py index 369356a687..5848d8b812 100644 --- a/alas_wrapped/module/webui/app.py +++ b/alas_wrapped/module/webui/app.py @@ -1,4 +1,3 @@ -import re import argparse import json import queue @@ -52,8 +51,6 @@ filepath_config, read_file, ) -from module.config.utils import time_delta -from module.log_res.log_res import LogRes from module.logger import logger from module.ocr.rpc import start_ocr_server_process, stop_ocr_server_process from module.submodule.submodule import load_config @@ -99,34 +96,10 @@ task_handler = TaskHandler() -def timedelta_to_text(delta=None): - time_delta_name_suffix_dict = { - 'Y': 'YearsAgo', - 'M': 'MonthsAgo', - 'D': 'DaysAgo', - 'h': 'HoursAgo', - 'm': 'MinutesAgo', - 's': 'SecondsAgo', - } - time_delta_name_prefix = 'Gui.Overview.' - time_delta_name_suffix = 'NoData' - time_delta_display = '' - if isinstance(delta, dict): - for _key in delta: - if delta[_key]: - time_delta_name_suffix = time_delta_name_suffix_dict[_key] - time_delta_display = delta[_key] - break - time_delta_display = str(time_delta_display) - time_delta_name = time_delta_name_prefix + time_delta_name_suffix - return time_delta_display + t(time_delta_name) - - class AlasGUI(Frame): ALAS_MENU: Dict[str, Dict[str, List[str]]] ALAS_ARGS: Dict[str, Dict[str, Dict[str, Dict[str, str]]]] theme = "default" - _log = RichLog def initial(self) -> None: self.ALAS_MENU = read_file(filepath_args("menu", self.alas_mod)) @@ -469,43 +442,22 @@ def alas_overview(self) -> None: ) log = RichLog("log") - self._log = log - self._log.dashboard_arg_group = LogRes(self.alas_config).groups with use_scope("logs"): - if 'Maa' in self.ALAS_ARGS: - put_scope( - "log-bar", - [ - put_text(t("Gui.Overview.Log")).style( - "font-size: 1.25rem; margin: auto .5rem auto;" - ), - put_scope( - "log-bar-btns", - [ - put_scope("log_scroll_btn"), - ], - ), - ], - ), - else: - put_scope( - "log-bar", - [ - put_text(t("Gui.Overview.Log")).style( - "font-size: 1.25rem; margin: auto .5rem auto;" - ), - put_scope( - "log-bar-btns", - [ - put_scope("log_scroll_btn"), - put_scope("dashboard_btn"), - ], - ), - put_html('
'), - put_scope("dashboard"), - ], - ), + put_scope( + "log-bar", + [ + put_text(t("Gui.Overview.Log")).style( + "font-size: 1.25rem; margin: auto .5rem auto;" + ), + put_scope( + "log-bar-btns", + [ + put_scope("log_scroll_btn"), + ], + ), + ], + ) put_scope("log", [put_html("")]) log.console.width = log.get_width() @@ -520,29 +472,12 @@ def alas_overview(self) -> None: color_off="off", scope="log_scroll_btn", ) - switch_dashboard = BinarySwitchButton( - label_on=t("Gui.Button.DashboardON"), - label_off=t("Gui.Button.DashboardOFF"), - onclick_on=lambda: self.set_dashboard_display(False), - onclick_off=lambda: self.set_dashboard_display(True), - get_state=lambda: log.display_dashboard, - color_on="off", - color_off="on", - scope="dashboard_btn", - ) + self.task_handler.add(switch_scheduler.g(), 1, True) self.task_handler.add(switch_log_scroll.g(), 1, True) - if 'Maa' not in self.ALAS_ARGS: - self.task_handler.add(switch_dashboard.g(), 1, True) self.task_handler.add(self.alas_update_overview_task, 10, True) - if 'Maa' not in self.ALAS_ARGS: - self.task_handler.add(self.alas_update_dashboard, 10, True) self.task_handler.add(log.put_log(self.alas), 0.25, True) - def set_dashboard_display(self, b): - self._log.set_dashboard_display(b) - self.alas_update_dashboard(True) - def _init_alas_config_watcher(self) -> None: def put_queue(path, value): self.modified_config_queue.put({"name": path, "value": value}) @@ -579,7 +514,6 @@ def _save_config( config_updater: AzurLaneConfig = State.config_updater, ) -> None: try: - skip_time_record = False valid = [] invalid = [] config = config_updater.read_file(config_name) @@ -683,105 +617,6 @@ def put_task(func: Function): else: put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--") - def _update_dashboard(self, num=None, groups_to_display=None): - x = 0 - _num = 10000 if num is None else num - _arg_group = self._log.dashboard_arg_group if groups_to_display is None else groups_to_display - time_now = datetime.now().replace(microsecond=0) - for group_name in _arg_group: - group = deep_get(d=self.alas_config.data, keys=f'Dashboard.{group_name}') - if group is None: - continue - - value = str(group['Value']) - if 'Limit' in group.keys(): - value_limit = f' / {group["Limit"]}' - value_total = '' - elif 'Total' in group.keys(): - value_total = f' ({group["Total"]})' - value_limit = '' - elif group_name == 'Pt': - value_limit = ' / ' + re.sub(r'[,.\'",。]', '', - str(deep_get(self.alas_config.data, 'EventGeneral.EventGeneral.PtLimit'))) - if value_limit == ' / 0': - value_limit = '' - else: - value_limit = '' - value_total = '' - # value = value + value_limit + value_total - - value_time = group['Record'] - if value_time is None or value_time == datetime(2020, 1, 1, 0, 0, 0): - value_time = datetime(2023, 1, 1, 0, 0, 0) - - # Handle time delta - if value_time == datetime(2023, 1, 1, 0, 0, 0): - value = 'None' - delta = timedelta_to_text() - else: - delta = timedelta_to_text(time_delta(value_time - time_now)) - if group_name not in self._log.last_display_time.keys(): - self._log.last_display_time[group_name] = '' - if self._log.last_display_time[group_name] == delta and not self._log.first_display: - continue - self._log.last_display_time[group_name] = delta - - # if self._log.first_display: - # Handle width - # value_width = len(value) * 0.7 + 0.6 if value != 'None' else 4.5 - # value_width = str(value_width/1.12) + 'rem' if self.is_mobile else str(value_width) + 'rem' - value_limit = '' if value == 'None' else value_limit - # limit_width = len(value_limit) * 0.7 - # limit_width = str(limit_width) + 'rem' - value_total = '' if value == 'None' else value_total - limit_style = '--dashboard-limit--' if value_limit else '--dashboard-total--' - value_limit = value_limit if value_limit else value_total - # Handle dot color - _color = f"""background-color:{deep_get(d=group, keys='Color').replace('^', '#')}""" - color = f'
' - with use_scope(group_name, clear=True): - put_row( - [ - put_html(color), - put_scope( - f"{group_name}_group", - [ - put_column( - [ - put_row( - [ - put_text(value - ).style(f'--dashboard-value--'), - put_text(value_limit - ).style(limit_style), - ], - ).style('grid-template-columns:min-content auto;align-items: baseline;'), - put_text( - t(f'Gui.Overview.{group_name}') + " - " + delta - ).style('---dashboard-help--') - ], - size="auto auto", - ), - ], - ), - ], - size="20px 1fr" - ).style("height: 1fr"), - x += 1 - if x >= _num: - break - if self._log.first_display: - self._log.first_display = False - - def alas_update_dashboard(self, _clear=False): - if not self.visible: - return - with use_scope("dashboard", clear=_clear): - if not self._log.display_dashboard: - self._update_dashboard(num=4, groups_to_display=['Oil', 'Coin', 'Gem', 'Pt']) - elif self._log.display_dashboard: - self._update_dashboard() - @use_scope("content", clear=True) def alas_daemon_overview(self, task: str) -> None: self.init_menu(name=task) @@ -1100,17 +935,17 @@ def u(state): def dev_utils(self) -> None: self.init_menu(name="Utils") self.set_title(t("Gui.MenuDevelop.Utils")) - put_button(label=t("Gui.MenuDevelop.RaiseException"), onclick=raise_exception) + put_button(label="Raise exception", onclick=raise_exception) def _force_restart(): if State.restart_event is not None: - toast(t("Gui.Toast.AlasRestart"), duration=0, color="error") + toast("Alas will restart in 3 seconds", duration=0, color="error") clearup() State.restart_event.set() else: - toast(t("Gui.Toast.ReloadEnabled"), color="error") + toast("Reload not enabled", color="error") - put_button(label=t("Gui.MenuDevelop.ForceRestart"), onclick=_force_restart) + put_button(label="Force restart", onclick=_force_restart) @use_scope("content", clear=True) def dev_remote(self) -> None: diff --git a/alas_wrapped/module/webui/patch.py b/alas_wrapped/module/webui/patch.py index 1e4d7407f5..6f799d85f7 100644 --- a/alas_wrapped/module/webui/patch.py +++ b/alas_wrapped/module/webui/patch.py @@ -50,20 +50,16 @@ def patch_mimetype(): all deployment, we use the builtin mimetype table only. """ import mimetypes - if mimetypes.inited: - # ohno mimetypes already inited - db = mimetypes.MimeTypes() - mimetypes._db = db - # override global variable - mimetypes.encodings_map = db.encodings_map - mimetypes.suffix_map = db.suffix_map - mimetypes.types_map = db.types_map[True] - mimetypes.common_types = db.types_map[False] - else: - # init db with the default table - db = mimetypes.MimeTypes() - mimetypes._db = db - mimetypes.inited = True + # lock as inited + mimetypes.inited = True + # create a new clean instance + db = mimetypes.MimeTypes(filenames=()) + mimetypes._db = db + # override global variable + mimetypes.encodings_map = db.encodings_map + mimetypes.suffix_map = db.suffix_map + mimetypes.types_map = db.types_map[True] + mimetypes.common_types = db.types_map[False] def fix_py37_subprocess_communicate(): diff --git a/alas_wrapped/module/webui/pin.py b/alas_wrapped/module/webui/pin.py index 80ef586236..4d70243450 100644 --- a/alas_wrapped/module/webui/pin.py +++ b/alas_wrapped/module/webui/pin.py @@ -5,7 +5,7 @@ from pywebio.io_ctrl import Output from pywebio.output import OutputPosition -from pywebio.pin import _pin_output, check_dom_name_value, pin_update +from pywebio.pin import _pin_output, check_dom_name_value def put_input(name, type='text', *, label='', value=None, placeholder=None, readonly=None, datalist=None, diff --git a/alas_wrapped/module/webui/widgets.py b/alas_wrapped/module/webui/widgets.py index 86e8153559..ad879d9ddd 100644 --- a/alas_wrapped/module/webui/widgets.py +++ b/alas_wrapped/module/webui/widgets.py @@ -1,6 +1,5 @@ import copy import json -import pywebio.pin import random import string from typing import Any, Callable, Dict, Generator, List, Optional, TYPE_CHECKING, Union @@ -75,8 +74,6 @@ def set_scroll(self, b: bool) -> None: class RichLog: - last_display_time: dict - def __init__(self, scope, font_width="0.559") -> None: self.scope = scope self.font_width = font_width @@ -96,10 +93,6 @@ def __init__(self, scope, font_width="0.559") -> None: # self._callback_thread = None # self._width = 80 self.keep_bottom = True - self.display_dashboard = False - self.first_display = True - self.last_display_time = {} - self.dashboard_arg_group = None if State.theme == "dark": self.terminal_theme = DARK_TERMINAL_THEME else: @@ -145,11 +138,6 @@ def set_scroll(self, b: bool) -> None: # use for lambda callback function self.keep_bottom = b - def set_dashboard_display(self, b: bool) -> None: - # use for lambda callback function. Copied. - self.display_dashboard = b - self.first_display = True - def get_width(self): js = """ let canvas = document.createElement('canvas'); diff --git a/alas_wrapped/requirements-in.txt b/alas_wrapped/requirements-in.txt index 0dfe2d1d25..3dedf41d90 100644 --- a/alas_wrapped/requirements-in.txt +++ b/alas_wrapped/requirements-in.txt @@ -1,6 +1,6 @@ # Image processing -numpy==1.19.5 -scipy==1.7.3 +numpy==1.16.6 +scipy==1.4.1 pillow opencv-python imageio==2.27.0 @@ -12,7 +12,7 @@ uiautomator2cache==0.3.0.1 wrapt==1.13.1 retrying lz4 -av==12.0.0 +av==10.0.0 psutil==5.9.3 # Utils diff --git a/alas_wrapped/requirements.txt b/alas_wrapped/requirements.txt index 4c4f6a3d30..72ed5c457b 100644 --- a/alas_wrapped/requirements.txt +++ b/alas_wrapped/requirements.txt @@ -1,13 +1,18 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile requirements-in.txt --python-version=3.9 --override=overrides.txt --output-file=requirements.txt --annotation-style=line --only-binary av -adbutils==0.11.0 # via uiautomator2, -r requirements-in.txt +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --annotation-style=line --output-file=requirements.txt requirements-in.txt +# + +adbutils==0.11.0 # via -r requirements-in.txt, uiautomator2 aiofiles==0.7.0 # via -r requirements-in.txt alas-webapp==0.3.7 # via -r requirements-in.txt anyio==1.3.1 # via -r requirements-in.txt apkutils2==1.0.0 # via adbutils asgiref==3.4.1 # via uvicorn async-generator==1.10 # via anyio -av==12.0.0 # via -r requirements-in.txt +av==10.0.0 # via -r requirements-in.txt cached-property==1.5.2 # via uiautomator2 certifi==2021.5.30 # via requests cffi==1.15.0 # via gevent @@ -31,6 +36,7 @@ h11==0.12.0 # via uvicorn httptools==0.4.0 # via uvicorn idna==2.6 # via requests imageio==2.27.0 # via -r requirements-in.txt +importlib-metadata==4.8.1 # via click, prettytable inflection==0.5.1 # via -r requirements-in.txt jellyfish==0.11.2 # via -r requirements-in.txt kiwisolver==1.3.2 # via matplotlib @@ -39,19 +45,19 @@ lxml==4.6.3 # via uiautomator2 lz4==3.1.3 # via -r requirements-in.txt matplotlib==3.4.3 # via gluoncv msgpack==1.0.3 # via zerorpc -mxnet==1.6.0 # via cnocr, -r requirements-in.txt -numpy==1.19.5 # via cnocr, gluoncv, imageio, matplotlib, mxnet, opencv-python, scipy, --override overrides.txt, -r requirements-in.txt +mxnet==1.6.0 # via -r requirements-in.txt, cnocr +numpy==1.16.6 # via -r requirements-in.txt, cnocr, gluoncv, imageio, matplotlib, mxnet, opencv-python, scipy onepush==1.4.0 # via -r requirements-in.txt opencv-python==4.5.3.56 # via -r requirements-in.txt packaging==20.9 # via deprecation, uiautomator2 -pillow==8.3.2 # via cnocr, gluoncv, imageio, matplotlib, uiautomator2, -r requirements-in.txt +pillow==8.3.2 # via -r requirements-in.txt, cnocr, gluoncv, imageio, matplotlib, uiautomator2 portalocker==2.3.2 # via gluoncv prettytable==2.2.1 # via -r requirements-in.txt progress==1.6 # via uiautomator2 psutil==5.9.3 # via -r requirements-in.txt py==1.10.0 # via retry pycparser==2.21 # via cffi -pycryptodome==3.9.9 # via onepush, -r requirements-in.txt +pycryptodome==3.9.9 # via onepush pydantic==1.10.2 # via -r requirements-in.txt pyelftools==0.27 # via apkutils2 pygments==2.12.0 # via rich @@ -61,32 +67,35 @@ python-dateutil==2.8.2 # via matplotlib python-dotenv==0.19.0 # via uvicorn pywebio==1.6.2 # via -r requirements-in.txt pywin32==301 # via portalocker -pyyaml==5.4.1 # via uvicorn, -r requirements-in.txt -pyzmq==22.3.0 # via zerorpc, -r requirements-in.txt +pyyaml==5.4.1 # via -r requirements-in.txt, uvicorn +pyzmq==22.3.0 # via -r requirements-in.txt, zerorpc requests==2.18.4 # via adbutils, gluoncv, mxnet, onepush, uiautomator2 retry==0.9.2 # via adbutils, uiautomator2 retrying==1.3.3 # via -r requirements-in.txt rich==11.2.0 # via -r requirements-in.txt -scipy==1.7.3 # via gluoncv, -r requirements-in.txt -setuptools==81.0.0 # via gevent, zope-event, zope-interface +scipy==1.4.1 # via -r requirements-in.txt, gluoncv six==1.16.0 # via adbutils, cycler, python-dateutil, retrying, uiautomator2 sniffio==1.2.0 # via anyio starlette==0.14.2 # via -r requirements-in.txt tornado==6.1 # via pywebio -tqdm==4.62.3 # via gluoncv, -r requirements-in.txt -typing-extensions==4.3.0 # via pydantic +tqdm==4.62.3 # via -r requirements-in.txt, gluoncv +typing-extensions==4.3.0 # via asgiref, importlib-metadata, pydantic, rich, uvicorn ua-parser==0.10.0 # via user-agents uiautomator2==2.16.17 # via -r requirements-in.txt uiautomator2cache==0.3.0.1 # via -r requirements-in.txt urllib3==1.22 # via requests user-agents==2.2.0 # via pywebio -uvicorn==0.17.6 # via -r requirements-in.txt +uvicorn[standard]==0.17.6 # via -r requirements-in.txt watchgod==0.7 # via uvicorn wcwidth==0.2.5 # via prettytable websockets==10.0 # via uvicorn whichcraft==0.6.1 # via adbutils, uiautomator2 -wrapt==1.13.1 # via deprecated, -r requirements-in.txt +wrapt==1.13.1 # via -r requirements-in.txt, deprecated xmltodict==0.12.0 # via apkutils2 zerorpc==0.6.3 # via -r requirements-in.txt -zope-event==4.5.0 # via gevent -zope-interface==5.4.0 # via gevent +zipp==3.6.0 # via importlib-metadata +zope.event==4.5.0 # via gevent +zope.interface==5.4.0 # via gevent + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/alas_wrapped/webapp/packages/main/src/config.ts b/alas_wrapped/webapp/packages/main/src/config.ts index 7f11332122..b295c5eae3 100644 --- a/alas_wrapped/webapp/packages/main/src/config.ts +++ b/alas_wrapped/webapp/packages/main/src/config.ts @@ -3,20 +3,15 @@ const fs = require('fs'); const path = require('path'); // export const alasPath = 'D:/AzurLaneAutoScript'; -// When running from webapp/, ALAS root is the parent directory. -export const alasPath = path.resolve(process.cwd(), '..'); +export const alasPath = process.cwd(); const file = fs.readFileSync(path.join(alasPath, './config/deploy.yaml'), 'utf8'); const config = yaml.parse(file); const PythonExecutable = config.Deploy.Python.PythonExecutable; const WebuiPort = config.Deploy.Webui.WebuiPort.toString(); -const runConfig = (process.env.ALAS_RUN_CONFIG || '').trim(); export const pythonPath = (path.isAbsolute(PythonExecutable) ? PythonExecutable : path.join(alasPath, PythonExecutable)); export const webuiUrl = `http://127.0.0.1:${WebuiPort}`; export const webuiPath = 'gui.py'; export const webuiArgs = ['--port', WebuiPort, '--electron']; -if (runConfig) { - webuiArgs.push('--run', runConfig); -} export const dpiScaling = Boolean(config.Deploy.Webui.DpiScaling) || (config.Deploy.Webui.DpiScaling === undefined) ; diff --git a/docs/AGENT_MEMORY_2026-03-04.md b/docs/AGENT_MEMORY_2026-03-04.md new file mode 100644 index 0000000000..9e2dc16f7c --- /dev/null +++ b/docs/AGENT_MEMORY_2026-03-04.md @@ -0,0 +1,17 @@ +# Agent Memory (2026-03-04) + +- Permanent operating loop confirmed: + scheduler -> deterministic first -> manual/vision fallback -> blueprint codification -> recovery ladder -> restart fallback. +- MEmu anchor confirmed via admin launch path. +- Canonical implementation branch chosen: `feature/adb-vision-clean`. +- Extra local branches removed: + - `implement/adb-vision-backends-round2` + - `implement/adb-vision-backends-pass2` +- Backend status: + - `droidcast`: implemented with startup + retries. + - `scrcpy`: native `scrcpy screenshot` path. + - `u2`: implemented with forward/start + endpoint retries. +- Test status snapshot: + - `adb_vision/test_server.py`: `19 passed` + - `adb_vision/test_live.py`: `11 skipped` (emulator not reachable at `127.0.0.1:21513`) + diff --git a/docs/IMPLEMENTATION_PLAN_NEXT_PASS.md b/docs/IMPLEMENTATION_PLAN_NEXT_PASS.md new file mode 100644 index 0000000000..6336c7b44a --- /dev/null +++ b/docs/IMPLEMENTATION_PLAN_NEXT_PASS.md @@ -0,0 +1,36 @@ +# adb_vision Backend Implementation Pass 2 + +## Objective + +- Finish the backend pass with deterministic dispatch behavior for screenshot methods. +- Remove ambiguity in the `scrcpy` route so behavior is explicit and reviewable. +- Add/adjust tests for routing and size-based fallback checks. + +## Current state at pass start + +- Branch: `implement/adb-vision-backends-round2` +- Working branch switched to: `implement/adb-vision-backends-pass2` +- Implemented in prior pass: + - `adb_vision/screenshot.py`: DROIDCAST, U2, and SCRCPY-compat backends were wired. + - `adb_vision/test_live.py`: DroidCast skip handling updated. +- Remaining gaps: + - `scrcpy` backend still documents fallback behavior but is not a true scrcpy stream capture. + - `auto` backend order currently includes duplicated screenshot work paths. + - Unit tests still describe old stub assumptions. + +## Execution loop for this pass + +1. Patch screenshot dispatch so auto flow is explicit: + - `droidcast` first, then `u2`, then `screencap`. + - keep `scrcpy` as a compatibility path that is explicit about behavior. +2. Update `screenshot.py` comments/messages to match implementation. +3. Add/adjust unit tests: + - backend order in `auto` + - `scrcpy` compatibility behavior + - rejection of tiny/invalid images +4. Run targeted tests (unit-level) and collect a pass/fail checklist. + +## Risk notes + +- Full native scrcpy stream decoding is still not implemented in this lightweight package. +- `droidcast` and `uiautomator2` remain the practical low-latency capture paths. diff --git a/docs/LLMGuide/README.md b/docs/LLMGuide/README.md new file mode 100644 index 0000000000..5a679e1d81 --- /dev/null +++ b/docs/LLMGuide/README.md @@ -0,0 +1,34 @@ +# LLMGuide — Operator Documentation for LLM Agents + +This folder contains practical documentation written **for and by LLM agents** operating this codebase. Unlike the architecture docs, this is operational truth learned from actually running things. + +## Documents + +| File | What it covers | +|------|---------------| +| `startup_and_operations.md` | How to start ALAS, monitor it, and recover from crashes. **Start here.** | +| `task_catalog.md` | Every ALAS task: what it does, failure modes, when to disable | +| `device_setup.md` | MEmu + ADB + screenshot methods. Root cause of the black screen problem. | +| `llm_control_harness.md` | MCP tools, how to drive the emulator directly, recovery playbooks | + +## The One Thing That Matters Right Now + +**MEmu render mode causes intermittent black screenshots.** + +Until the user changes MEmu → Settings → Display → Render mode → **DirectX or Software** (NOT OpenGL — OpenGL is the GPU mode that causes black frames), the bot will crash every 5-10 minutes with `GameStuckError`. The bot still makes partial progress (commissions collected, etc.) but is unreliable. + +Everything else in this guide assumes you understand that constraint. + +## Guiding Principles (learned the hard way) + +1. **Don't change screenshot method mid-session.** DroidCast needs to be set up in advance. Switching to DroidCast without installing the APK first will kill the session. + +2. **GameStuckError ≠ task is broken.** Usually it means screenshots are unreliable. Fix screenshots before disabling tasks. + +3. **OpsiCrossMonth runs once a month.** Disable it after March 1 reset. Re-enable April 1. + +4. **The venv lives at `alas_wrapped/.venv/`.** It was missing until 2026-03-03. If it disappears, recreate: `uv venv --python=3.9 .venv && uv pip install -r requirements.txt --overrides overrides.txt` + +5. **ALAS writes to PatrickCustom.json while running.** Don't edit it concurrently — you'll corrupt the schedule. Edit when the process is stopped, or edit only `Enable` and `NextRun` fields (ConfigWatcher merges these safely). + +6. **The process exits with code 4 on RequestHumanTakeover.** This is the bot saying "I give up, please look at me." Check `log/error/` for the screenshots and stack trace. diff --git a/docs/LLMGuide/device_setup.md b/docs/LLMGuide/device_setup.md new file mode 100644 index 0000000000..10d44aec9b --- /dev/null +++ b/docs/LLMGuide/device_setup.md @@ -0,0 +1,525 @@ +# Device Setup Guide for LLM Agents (MEmu + ALAS) + +This guide is written for an LLM agent that is operating ALAS on a Windows host with +MEmu (Microvirt) as the Android emulator. It covers every screenshot method available +in ALAS, explains exactly why `adb screencap` returns black frames on MEmu, and documents +every known fix. + +--- + +## 1. ADB Serial for This Setup + +The configured serial for this installation is: + +``` +127.0.0.1:21513 +``` + +This value lives in `alas_wrapped/config/PatrickCustom.json`: + +```json +"Alas": { + "Emulator": { + "Serial": "127.0.0.1:21513", + ... + }, + "EmulatorInfo": { + "Emulator": "MEmuPlayer", + "name": "MEmu", + "path": "C:/Program Files/Microvirt/MEmu/MEmu.exe" + } +} +``` + +MEmu's default ADB port is `21503` for the first instance. Port `21513` is the second +instance (MEmu uses offsets of 10 per instance). Verify the emulator is live before +starting ALAS: + +```bash +adb connect 127.0.0.1:21513 +adb -s 127.0.0.1:21513 shell echo ok +``` + +If `echo ok` hangs or returns an error, the emulator is not ready. ADB responding does +not guarantee the Android boot sequence is complete. Wait until the launcher is visible +on screen before attempting to start ALAS. + +--- + +## 2. How ALAS Selects a Screenshot Method + +The active method is read from `config.Emulator_ScreenshotMethod` on every call. +The full method map is defined in +`alas_wrapped/module/device/screenshot.py` (`Screenshot.screenshot_methods`): + +```python +{ + 'ADB': self.screenshot_adb, + 'ADB_nc': self.screenshot_adb_nc, + 'uiautomator2': self.screenshot_uiautomator2, + 'aScreenCap': self.screenshot_ascreencap, + 'aScreenCap_nc': self.screenshot_ascreencap_nc, + 'DroidCast': self.screenshot_droidcast, + 'DroidCast_raw': self.screenshot_droidcast_raw, + 'scrcpy': self.screenshot_scrcpy, + 'nemu_ipc': self.screenshot_nemu_ipc, + 'ldopengl': self.screenshot_ldopengl, +} +``` + +The current config uses `uiautomator2`. That is the source of the black screenshot +problem described in this guide. + +--- + +## 3. All Screenshot Methods: What Each Requires and MEmu Compatibility + +### 3.1 ADB (`ADB`) + +- **How it works:** Runs `adb shell screencap -p` on the device. The raw RGBA pixel + stream is received over the ADB transport and decoded with `cv2.imdecode`. +- **Requirements:** ADB connection only. No helper process on the device. +- **MEmu compatibility:** Depends on MEmu's GPU rendering mode. On MEmu with the default + OpenGL rendering mode, `adb screencap` returns a pure black image (all pixels zero). + When MEmu is switched to DirectX or software rendering, ADB screencap works correctly. + This is the same root cause as for `uiautomator2` (see section 4). +- **Speed:** Slow. Each call crosses the full ADB pipe. Expect 150-300 ms per frame. + +### 3.2 ADB via Netcat (`ADB_nc`) + +- **How it works:** Same as `ADB` but transfers the raw pixel buffer over a local TCP + socket (`adb_shell_nc`) instead of the ADB protocol. Avoids ADB's CRLF translation. +- **Requirements:** ADB connection. The `nc` (netcat) binary must be available inside + the Android image (it is present in MEmu). +- **MEmu compatibility:** Same as `ADB`. The GPU rendering mode limitation applies + identically because the pixel source is still `screencap`. +- **Speed:** Slightly faster than `ADB` for large buffers. + +### 3.3 uiautomator2 (`uiautomator2`) + +- **How it works:** Uses the `uiautomator2` Python library, which communicates with + the `atx-agent` daemon running inside the Android image. The agent calls Android's + `screenshot()` API internally (backed by `screencap`) and returns a PNG via HTTP. +- **Requirements:** `atx-agent` must be installed and running inside the Android image. + ALAS installs it automatically. MiniCap is explicitly uninstalled on emulators because + it cannot work correctly. +- **MEmu compatibility:** BROKEN with default MEmu settings. This is the configured + method in PatrickCustom.json and it is the direct cause of the persistent black + screenshots seen in the error logs. See section 4 for the full explanation. +- **Speed:** Moderate. HTTP round-trip to the agent adds overhead. + +### 3.4 aScreenCap (`aScreenCap`, `aScreenCap_nc`) + +- **How it works:** ALAS pushes a custom native binary (`bin/ascreencap`) to + `/data/local/tmp/ascreencap` on the device and runs it. The binary reads the + framebuffer directly. The `_nc` variant transfers the result over netcat. +- **Requirements:** ABI-compatible native binary. ALAS includes arm64 and x86_64 + variants. Requires initial push to device. +- **MEmu compatibility:** Also affected by the GPU rendering mode bug because it reads + the Android framebuffer layer, which is black when the GPU compositor is not writing + to it. Not a reliable fix for MEmu. +- **Speed:** Faster than ADB variants. Native binary, minimal encoding overhead. + +### 3.5 DroidCast (`DroidCast`) + +- **How it works:** ALAS pushes `DroidCast_raw-release-1.0.apk` to + `/data/local/tmp/DroidCast_raw.apk` on the device, then launches it as a Java class + with `app_process`: + ``` + CLASSPATH=/data/local/tmp/DroidCast_raw.apk app_process / ink.mol.droidcast_raw.Main + ``` + The process runs an HTTP server on port 53516 inside the Android image. ALAS forwards + that port to the host with `adb forward tcp:53516` and captures PNG frames from + `http://127.0.0.1:/preview`. +- **Requirements:** + - The APK file must exist at `./bin/DroidCast/DroidCast_raw-release-1.0.apk` relative + to the `alas_wrapped/` directory. It is present in this repo. + - `uiautomator2` (atx-agent) must be running inside Android for the process list query + used by `droidcast_stop()`. + - ADB forward port in range `20000-21000` (configured default). + - 10-second startup timeout: ALAS polls `http://127.0.0.1:/` and waits for a 404 + response (which means the server is up but the root route is unregistered). +- **MEmu compatibility:** Works. DroidCast uses Android's `MediaProjection` or display + surface API, which bypasses the framebuffer path that causes the black screenshot bug. + This is the recommended fix method. +- **Speed:** Good. HTTP GET per frame, local TCP. Faster than ADB, slower than IPC methods. + +### 3.6 DroidCast Raw (`DroidCast_raw`) + +- **How it works:** Same APK and launch process as `DroidCast`. The difference is the + API endpoint: `/screenshot` instead of `/preview`. This endpoint returns a raw RGB565 + bitmap (not PNG), which ALAS decodes to RGB888 using bitwise operations. The raw + format skips PNG encoding on the device side. +- **Requirements:** Same as `DroidCast`. +- **MEmu compatibility:** Works, same as `DroidCast`. +- **Speed:** Faster than `DroidCast` because there is no PNG encoding step on the + device side. + +### 3.7 scrcpy (`scrcpy`) + +- **How it works:** ALAS pushes a scrcpy server JAR to the device and starts it, then + reads a continuous H.264 video stream over ADB. The screenshot method returns the most + recent decoded frame from the stream buffer. +- **Requirements:** The scrcpy server JAR at `./bin/scrcpy/scrcpy-server-v1.20.jar`. + The video stream runs continuously regardless of polling frequency. +- **MEmu compatibility:** Works in principle. scrcpy uses `SurfaceControl.screenshot()` + or `DisplayCapture` APIs that can bypass the GPU rendering issue. Not the primary + recommendation because it is more complex to initialize and has higher latency during + startup. The screenshot interval setting is ignored (stream is always live). +- **Speed:** Once started, very fast. The frame is always ready in the buffer. + +### 3.8 nemu_ipc (`nemu_ipc`) + +- **How it works:** Loads `external_renderer_ipc.dll` from the MuMu12 installation + directory and calls `nemu_capture_display()` directly via ctypes. This is a shared + memory / IPC call between the host process and the MuMu12 emulator process. +- **Requirements:** MuMu12 (also called MuMuPlayer12, Netease emulator) version >= 3.8.13. + Requires the DLL at `/shell/sdk/external_renderer_ipc.dll`. +- **MEmu compatibility:** DOES NOT WORK. This method is hard-coded to the MuMu (Nemu/ + Netease) emulator family. The availability check is: + ```python + def nemu_ipc_available(self) -> bool: + if not IS_WINDOWS: + return False + if not self.is_mumu_family: + return False + ... + ``` + `is_mumu_family` returns True only for serials `127.0.0.1:7555` or ports in the range + `16384-17408`. MEmu uses port `21503`/`21513`, so `is_mumu_family` is False and this + method will raise `RequestHumanTakeover` immediately. Do not configure `nemu_ipc` for + MEmu. + +### 3.9 ldopengl (`ldopengl`) + +- **How it works:** Loads `ldopengl64.dll` from the LDPlayer9 installation directory + and calls `CreateScreenShotInstance()` / `IScreenShotClass::Cap()` via ctypes. This + captures the OpenGL framebuffer directly from the emulator's render thread. +- **Requirements:** LDPlayer9 with `ldopengl64.dll` present. +- **MEmu compatibility:** DOES NOT WORK. The availability check requires + `EmulatorInfo_Emulator == 'LDPlayer9'` and `is_ldplayer_bluestacks_family` (port range + `5555-5619`). MEmu does not satisfy either condition. + +--- + +## 4. The Black Screenshot Problem: Root Cause on MEmu + +### What is happening + +MEmu's default GPU rendering mode uses OpenGL hardware acceleration. In this mode, the +game renders to a hardware-accelerated surface that is composited by the GPU. The Android +framebuffer (`/dev/graphics/fb0`) and the surface accessible via `screencap` are not +reliably updated with the composited output. Instead, they return a buffer of all zeros +(pure black, RGB = 0,0,0). + +ALAS detects this in `check_screen_black()` in `screenshot.py`: + +```python +color = get_color(self.image, area=(0, 0, 1280, 720)) +if sum(color) < 1: + logger.warning(f'Received pure black screenshots from emulator, color: {color}') + logger.warning(f'Screenshot method `{self.config.Emulator_ScreenshotMethod}` ' + f'may not work on emulator `{self.serial}`, or the emulator is not fully started') +``` + +The error log in `alas_wrapped/log/error/1772565331621/log.txt` confirms this pattern: +ALAS connected (screen size check passed at `1280x720`), MaaTouch connected, but then +entered an infinite `Unknown ui page` loop. This happens when every screenshot is black +and no page template matches. The bot ran for approximately 10 seconds before giving up +with `Game page unknown`. + +### Why it is intermittent + +Real frames appear occasionally because MEmu sometimes falls back to software rendering +for specific operations, or because the GPU scheduler temporarily flushes the framebuffer. +This is not a reliable behavior. The only durable fix is to change the rendering mode or +use a screenshot method that does not depend on the framebuffer. + +--- + +## 5. Every Known Fix for Black Screenshots on MEmu + +### Fix 1 (Recommended): Switch MEmu to DirectX Rendering + +This should permanently fix `ADB`, `ADB_nc`, `uiautomator2`, and `aScreenCap` methods. + +**Where the setting is:** + +1. Open MEmu Multiple Instance Manager (`MEmuConsole.exe`). +2. For the target instance (e.g., `MEmu`), click the gear icon (Settings). +3. Navigate to the **Engine** or **Graphics** tab. +4. Find the **Render Mode** setting. MEmu has exactly two options: + - `OpenGL` (the default — causes black screenshots on some setups) + - `DirectX` (hardware-accelerated via DirectX — expected to fix black screenshots) +5. Change the mode to **DirectX**. +6. Click OK and restart the MEmu instance. + +After changing the render mode, verify the fix: +```bash +adb -s 127.0.0.1:21513 shell screencap -p > /tmp/test.png +``` +The PNG file should contain the actual screen contents, not a black image. + +**Note:** There is no "Software" render mode in MEmu. Only OpenGL and DirectX exist. + +**Trade-off:** DirectX rendering is hardware-accelerated on the host GPU but writes to a +surface that `screencap` can read. Game performance may differ between OpenGL and DirectX +depending on your GPU — test both if needed. + +For full MEmu configuration details, see `docs/dev/memu_playbook.md`. + +### Fix 2 (No Setting Change Required): Switch to DroidCast + +DroidCast uses Android's `MediaProjection` API or display surface capture instead of +reading the framebuffer. This works regardless of MEmu's render mode. + +**Step 1: Verify the APK is present** + +```bash +ls alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk +``` + +This file is already in the repository. + +**Step 2: Update the config** + +Edit `alas_wrapped/config/PatrickCustom.json`. Change: +```json +"ScreenshotMethod": "uiautomator2" +``` +to: +```json +"ScreenshotMethod": "DroidCast" +``` + +or use `DroidCast_raw` for slightly better performance: +```json +"ScreenshotMethod": "DroidCast_raw" +``` + +**Step 3: What happens at startup** + +When ALAS first runs with `DroidCast` configured, it calls `droidcast_init()`: + +1. Stops any existing DroidCast processes (kills by `ink.mol.droidcast_raw.Main` in + the process command line, via `uiautomator2` process list). +2. Pushes the APK: `adb push ./bin/DroidCast/DroidCast_raw-release-1.0.apk /data/local/tmp/DroidCast_raw.apk` +3. Starts the server: runs `app_process` with the APK as the CLASSPATH. +4. Forwards port 53516 from the device to the host. +5. Polls `http://127.0.0.1:/` for up to 10 seconds until a 404 is returned (indicating + the server is running). +6. Begins capturing frames from `/preview` (DroidCast) or `/screenshot` (DroidCast_raw). + +**Failure modes and their recovery:** + +| Exception | Meaning | Recovery | +|-----------|---------|----------| +| `requests.exceptions.ConnectionError` | DroidCast process died | `droidcast_init()` called automatically | +| `requests.exceptions.ReadTimeout` | Server not responding within 3s | `droidcast_init()` called automatically | +| `DroidCastVersionIncompatible` | Mismatch between expected and actual endpoint | `droidcast_init()` called automatically | +| `ImageTruncated` | Partial response | Retry with no re-init | + +**Note:** DroidCast requires `uiautomator2` (atx-agent) to be running for the +`droidcast_stop()` function to enumerate existing processes. If atx-agent is not running, +DroidCast initialization may fail to clean up stale processes. ALAS handles this by +initializing uiautomator2 before DroidCast. + +### Fix 3: Switch to scrcpy + +Change the screenshot method to `scrcpy` in `PatrickCustom.json`. This also bypasses +the framebuffer. However, scrcpy uses a continuous video stream, which means: + +- Startup is slower (stream negotiation). +- The `Optimization_ScreenshotInterval` setting is ignored; the interval is capped at + 0.1s. +- More resource-intensive than DroidCast. + +Prefer DroidCast over scrcpy for this setup. + +### What Will NOT Fix It + +- Changing `ControlMethod` (MaaTouch, uiautomator2, ADB) has no effect on screenshots. + Touch input and screenshot capture are completely separate subsystems. +- Increasing `ScreenshotInterval` does not help; the frames are black, not slow. +- Restarting ADB server (`adb kill-server && adb start-server`) does not help; the root + cause is in the GPU render path inside MEmu, not the ADB transport. +- Setting `ScreenshotDedithering: true` does not help; it applies noise reduction to + the image, not to the capture mechanism. + +--- + +## 6. Why nemu_ipc Does NOT Work for MEmu + +This is a frequent point of confusion because `nemu_ipc` sounds generic. + +`nemu_ipc` is exclusively for MuMu12 (MuMuPlayer12), which is made by Netease and also +called "Nemu" in Chinese documentation. The IPC mechanism uses a proprietary DLL +(`external_renderer_ipc.dll`) distributed with MuMu12 itself. + +MEmu is made by Microvirt. It is a completely separate emulator with a different +hypervisor, different GPU pipeline, and no `external_renderer_ipc.dll`. + +ALAS enforces this at three levels: + +1. **Port range check**: `is_mumu_family` only returns True for `127.0.0.1:7555` or + ports `16384-17408`. MEmu's port `21513` is outside this range, so `is_mumu_family` + is False. + +2. **Explicit guard in `nemu_ipc_available()`**: + ```python + if not self.is_mumu_family: + return False + ``` + +3. **DLL existence check**: `NemuIpcImpl.__init__()` looks for the DLL at: + - `/shell/sdk/external_renderer_ipc.dll` + - `/nx_device/12.0/shell/sdk/external_renderer_ipc.dll` + + Neither path exists in a Microvirt installation. If `nemu_ipc` is forced anyway, + `NemuIpcIncompatible` is raised and the bot calls `RequestHumanTakeover`. + +**Similarly, `ldopengl` does not work for MEmu.** It requires LDPlayer9's +`ldopengl64.dll`. MEmu has neither this DLL nor the `ldconsole.exe` interface that +LDOpenGL uses to discover the instance PID. + +--- + +## 7. MaaTouch vs uiautomator2 for Control + +The current config: +```json +"ControlMethod": "MaaTouch" +``` + +This is correct and not related to the screenshot problem. + +**MaaTouch** is a high-performance touch input daemon. ALAS pushes +`bin/MaaTouch/maatouchsync` to `/data/local/tmp/maatouchsync` on the device and opens +a persistent socket connection. Touch events are sent as protocol messages over this +socket. The error log confirms MaaTouch initialized successfully: + +``` +MaaTouch stream connected +max_contact: 10; max_x: 1280; max_y: 720; max_pressure: 255 +``` + +**uiautomator2** as a control method uses the atx-agent's `click` RPC call, which is +slower. MaaTouch is the better choice for performance. + +Keep `ControlMethod: MaaTouch` regardless of which screenshot method is chosen. +MaaTouch has no dependency on the GPU rendering path. + +--- + +## 8. Startup Sequence: Verifying Readiness Before Starting ALAS + +The error pattern in the logs (10 seconds of `Unknown ui page` followed by `Game page +unknown`) indicates ALAS started before the emulator or the game was fully ready. + +### Recommended Readiness Check Sequence + +**Step 1: Verify MEmu instance is running** + +```bash +"C:/Program Files/Microvirt/MEmu/memuc.exe" isvmrunning -n MEmu +``` + +Wait until this returns a running status. If the instance name differs (e.g., `MEmu_0`), +adjust accordingly: + +```bash +"C:/Program Files/Microvirt/MEmu/memuc.exe" listvms --running +``` + +**Step 2: Verify ADB can connect** + +```bash +adb connect 127.0.0.1:21513 +adb -s 127.0.0.1:21513 get-state +``` + +Expected output: `device`. If you get `offline` or connection refused, the emulator's +ADB daemon has not started yet. This can take 30-60 seconds after `memuc start`. + +**Step 3: Verify Android has booted** + +```bash +adb -s 127.0.0.1:21513 shell getprop sys.boot_completed +``` + +Expected output: `1`. Any other output (empty, `0`) means Android is still booting. +Poll this until it returns `1`. + +**Step 4: Verify the game is running and on screen** + +```bash +adb -s 127.0.0.1:21513 shell dumpsys window windows | grep mCurrentFocus +``` + +Confirm the focus is on `com.YoStarEN.AzurLane`. If the focus is on the launcher or +another app, the game has not started yet. + +**Step 5: Take a test screenshot to confirm non-black output** + +```bash +adb -s 127.0.0.1:21513 shell screencap -p > /tmp/verify.png +``` + +If the PNG is entirely black, the GPU rendering mode issue is present. Apply Fix 1 or +Fix 2 from section 5 before starting ALAS. + +### Minimum Recommended Wait + +After `memuc start -n MEmu`, allow a minimum of 60 seconds before attempting ADB +commands. After ADB is live (`adb get-state` returns `device`), allow 30 additional +seconds for `sys.boot_completed` to become `1`. After that, allow another 30 seconds +for the game to reach its main screen before ALAS is started. + +--- + +## 9. Summary: Recommended Configuration for This Setup + +The fastest path to a working setup without changing MEmu settings: + +```json +"Alas": { + "Emulator": { + "Serial": "127.0.0.1:21513", + "PackageName": "com.YoStarEN.AzurLane", + "ScreenshotMethod": "DroidCast", + "ControlMethod": "MaaTouch", + "ScreenshotDedithering": false, + "AdbRestart": true + } +} +``` + +The permanent fix that allows any screenshot method: + +1. Open MEmu Settings for the target instance. +2. Change Render Mode from `OpenGL` to `DirectX`. +3. Restart the instance. +4. Restore `ScreenshotMethod` to `uiautomator2` or `ADB` if desired. + +For full MEmu emulator documentation, see `docs/dev/memu_playbook.md`. + +Do not use: `nemu_ipc`, `ldopengl`. These will raise `RequestHumanTakeover` on MEmu. + +--- + +## 10. File Reference + +| File | Purpose | +|------|---------| +| `alas_wrapped/module/device/screenshot.py` | Screenshot dispatcher, `check_screen_black()` | +| `alas_wrapped/module/device/method/adb.py` | `ADB`, `ADB_nc` implementations | +| `alas_wrapped/module/device/method/uiautomator_2.py` | `uiautomator2` screenshot via atx-agent | +| `alas_wrapped/module/device/method/droidcast.py` | `DroidCast`, `DroidCast_raw` implementations | +| `alas_wrapped/module/device/method/ascreencap.py` | `aScreenCap` native binary method | +| `alas_wrapped/module/device/method/scrcpy/scrcpy.py` | `scrcpy` video stream method | +| `alas_wrapped/module/device/method/nemu_ipc.py` | `nemu_ipc` (MuMu12 only, NOT MEmu) | +| `alas_wrapped/module/device/method/ldopengl.py` | `ldopengl` (LDPlayer9 only, NOT MEmu) | +| `alas_wrapped/module/device/connection_attr.py` | `is_mumu_family`, `is_ldplayer_bluestacks_family` | +| `alas_wrapped/module/config/config_manual.py` | `DROIDCAST_FILEPATH_LOCAL`, `DROIDCAST_FILEPATH_REMOTE` | +| `alas_wrapped/config/PatrickCustom.json` | Active runtime configuration | +| `alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk` | DroidCast APK (already present) | diff --git a/docs/LLMGuide/llm_control_harness.md b/docs/LLMGuide/llm_control_harness.md new file mode 100644 index 0000000000..30a0d255f4 --- /dev/null +++ b/docs/LLMGuide/llm_control_harness.md @@ -0,0 +1,916 @@ +# LLM Control Harness Guide + +This document is the definitive reference for an LLM (Claude or Gemini) that needs to take +control of the ALAS automation stack — either to diagnose a crash, perform a manual recovery, +or validate the bot's state before handing back to autonomous operation. + +It covers every MCP tool, every log file, all detection heuristics, and a worked example of +a complete crash-recovery cycle. + +--- + +## 1. System Overview + +ALAS is a Python bot that plays Azur Lane through ADB on a MEmu Android emulator. It runs +as a scheduler process, picking tasks from a queue (Commission, Tactical, Research, etc.) and +executing them one at a time. When a task finishes or fails, ALAS logs the event, saves an +error snapshot, and moves to the next task or requests human takeover. + +The LLM control harness is a thin layer on top: + +``` +[LLM] -- MCP (JSON-RPC) --> [alas_mcp_server.py] --> [ALAS runtime / ADB] +``` + +The MCP server exposes eight callable tools. The LLM uses those tools to observe, interact, +and recover. It never modifies ALAS source or config directly during a recovery session. + +Design principle (from NORTH_STAR.md): deterministic tools handle all normal operations; the +LLM and vision are reserved for recovery when those tools fail or the game reaches an +unexpected state. + +--- + +## 2. MCP Tool Surface + +The MCP server is `agent_orchestrator/alas_mcp_server.py`. It is launched as: + +```bash +cd agent_orchestrator +uv run alas_mcp_server.py --config PatrickCustom +``` + +All tools are registered with FastMCP and exposed over stdio JSON-RPC 2.0. Every call is +appended to `agent_orchestrator/mcp_actions.jsonl` with a sequence number, timestamp, tool +name, arguments, result summary, error string, and duration in milliseconds. + +### 2.1 `adb_screenshot` + +**Signature:** `adb_screenshot() -> dict` + +**What it does:** Captures a full-resolution screenshot from the emulator using the +uiautomator2 ATX agent HTTP path. Does not use `adb shell screencap` — that path reads the +Linux framebuffer, which MEmu's VirtualBox GPU passthrough never populates, producing a blank +3 KB PNG. The ATX agent is the only path that returns real pixel data. + +The call runs in a thread-pool executor. A hard 25-second ceiling is enforced via +`asyncio.wait_for`; if exceeded, the tool raises `RuntimeError("adb_screenshot timed out +after 25s")`. + +**Return value:** +```json +{ + "content": [ + { + "type": "image", + "mimeType": "image/png", + "data": "" + } + ] +} +``` + +The PNG is also saved to `agent_orchestrator/mcp_screenshots/_.png` for +audit purposes. + +**When to use:** As the first step of any diagnosis. Call it before touching anything else. +If it times out or returns a blank image, ADB is broken — proceed to the ADB health check +sequence in section 5. + +**Error signals:** +- `RuntimeError: adb_screenshot timed out after 25s` — ATX agent is unresponsive; the + emulator may have crashed or MEmu GPU rendering mode changed. +- The mcp_actions.jsonl entry shows `"error": "screencap failed: device offline"` — ADB + transport is dead. + +### 2.2 `adb_tap` + +**Signature:** `adb_tap(x: int, y: int) -> str` + +**What it does:** Sends `adb shell input tap X Y` to the emulator. Uses the raw ADB CLI, +not ALAS's MaaTouch daemon. Timeout: 5 seconds. Returns the string `"tapped X,Y"`. + +**When to use:** To dismiss dialogs, confirm prompts, or click known UI elements when the +LLM has taken control and identified a coordinate from a screenshot. The game resolution is +1280x720; all coordinates must be within those bounds. + +**Caveats:** `adb_tap` uses Android's input subsystem, not MaaTouch. It is slightly slower +and less precise than ALAS's normal touch path. Adequate for recovery clicks; do not use it +to drive high-frequency battle automation. + +### 2.3 `adb_swipe` + +**Signature:** `adb_swipe(x1: int, y1: int, x2: int, y2: int, duration_ms: int = 300) -> str` + +**What it does:** Sends `adb shell input swipe X1 Y1 X2 Y2 DURATION_MS`. Timeout: +`5.0 + duration_ms / 1000.0` seconds. Returns `"swiped X1,Y1->X2,Y2"`. + +**When to use:** To scroll through menus, close drawers, or dismiss full-screen overlays +that cannot be dismissed with a tap. + +### 2.4 `adb_get_focus` + +**Signature:** `adb_get_focus() -> dict` + +**What it does:** Runs `adb shell dumpsys window windows`, finds the `mCurrentFocus` line, +and parses the focused package and activity names. Timeout: 8 seconds. + +**Return value:** +```json +{ + "raw": "mCurrentFocus=Window{... u0 com.YoStarEN.AzurLane/com.manjuu.azurlane.MainActivity}", + "package": "com.YoStarEN.AzurLane", + "activity": "com.manjuu.azurlane.MainActivity" +} +``` + +If no window is focused or the line is missing, `package` and `activity` will be `null`. + +**When to use:** +1. Immediately after taking a screenshot, to confirm the game is in the foreground. +2. After calling `adb_launch_game`, to verify the launch succeeded before interacting. +3. When `adb_screenshot` shows a black or unexpected screen, to detect whether another + app (MEmu launcher, system dialog) has taken focus. + +**Healthy values:** +- `package`: `com.YoStarEN.AzurLane` +- `activity`: `com.manjuu.azurlane.MainActivity` or `com.manjuu.azurlane.PrePermissionActivity` + (the latter appears only during startup) + +**Unhealthy signals:** +- `package` is `null` — no app is focused; system or emulator UI is in the way. +- `package` is `com.microvirt.memu` or similar — the MEmu launcher grabbed focus (game + likely crashed to desktop). +- `package` is correct but `activity` contains `Crash` or `Dialog` — the game OS-level + crash reporter is showing. + +### 2.5 `adb_launch_game` + +**Signature:** `adb_launch_game() -> str` + +**What it does:** Sends an explicit Android intent: + +``` +adb shell am start \ + -a android.intent.action.MAIN \ + -c android.intent.category.LAUNCHER \ + -n com.YoStarEN.AzurLane/com.manjuu.azurlane.PrePermissionActivity +``` + +If the game is already running, Android brings the existing process to the foreground instead +of double-launching. Timeout: 10 seconds. Returns `"Azur Lane launch intent sent"`. + +**When to use:** When `adb_get_focus` shows the game is not in the foreground, or when you +have determined the game process has died and need to cold-start it. After calling this, wait +approximately 15-30 seconds and then call `adb_screenshot` to verify the game has loaded. + +### 2.6 `alas_get_current_state` + +**Signature:** `alas_get_current_state() -> str` + +**What it does:** Queries ALAS's internal state machine for the currently recognized UI page. +Returns the page name as a string, e.g. `"page_main"`, `"page_tactical"`, `"page_research"`. +Requires the ALAS context to be initialized. + +**When to use:** To confirm the game is at a known page before invoking `alas_goto` or +`alas_call_tool`. If this raises or returns `"page_unknown"`, the game is not at a recognized +screen. + +**Known page names** (from the ALAS state machine, 43 total): +`page_main`, `page_campaign_menu`, `page_campaign`, `page_fleet`, `page_main_white`, +`page_unknown`, `page_exercise`, `page_daily`, `page_event`, `page_sp`, `page_coalition`, +`page_os`, `page_archives`, `page_reward`, `page_mission`, `page_guild`, `page_commission`, +`page_tactical`, `page_battle_pass`, `page_event_list`, `page_raid`, `page_dock`, +`page_research`, `page_shipyard`, `page_meta`, `page_storage`, `page_reshmenu`, +`page_dormmenu`, `page_dorm`, `page_meowfficer`, `page_academy`, `page_private_quarters`, +`page_game_room`, `page_shop`, `page_munitions`, `page_supply_pack`, `page_build`, +`page_mail`, `page_channel`, `page_rpg_stage`, `page_rpg_story`, `page_rpg_city`, +`page_hospital`. + +### 2.7 `alas_goto` + +**Signature:** `alas_goto(page: str) -> str` + +**What it does:** Instructs ALAS's state machine to navigate from the current page to the +named target page. The state machine calculates the shortest path through the 122-link page +graph and executes the required taps automatically. Raises `ValueError` if the page name is +unknown. + +**When to use:** To return the game to a known starting point before handing back to the +autonomous scheduler. The canonical recovery target is `"page_main"`. + +**Preconditions:** The current page must be recognized. If `alas_get_current_state` returns +`"page_unknown"`, do not call `alas_goto` — the state machine cannot plan a route from an +unknown position. Use `adb_screenshot` plus vision instead to identify where the game is, +then use `adb_tap` to navigate manually to a recognized page first. + +### 2.8 `alas_list_tools` + +**Signature:** `alas_list_tools() -> list[dict]` + +**What it does:** Returns all deterministic ALAS tools registered in the state machine, +as a list of `{name, description, parameters}` dicts. + +**When to use:** To discover what automation tasks are available before calling +`alas_call_tool`. Useful for orientation at the start of a session. + +### 2.9 `alas_call_tool` + +**Signature:** `alas_call_tool(name: str, arguments: dict | None = None) -> Any` + +**What it does:** Invokes a named ALAS tool (from `alas_list_tools`) with the provided +arguments. This is the correct pattern for triggering a task; do not build one MCP tool +per workflow. If the tool raises, the exception propagates and is logged to +`mcp_actions.jsonl`. + +**When to use:** To run a specific ALAS task as part of a recovery or manual drive. For +example, after navigating to `page_main`, you could call `alas_call_tool("commission.run")` +to trigger commission collection. + +### 2.10 `alas_login_ensure_main` + +**Signature:** +``` +alas_login_ensure_main( + max_wait_s: float = 90.0, + poll_interval_s: float = 1.0, + dismiss_popups: bool = True, + get_ship: bool = True, +) -> dict +``` + +**What it does:** Wraps ALAS's deterministic login handler (`alas_wrapped/tools/login.py`). +Polls the current page until the game reaches `page_main`, dismissing popups along the way. +Returns a structured envelope: + +```json +{ + "success": true, + "data": null, + "error": null, + "observed_state": "page_main", + "expected_state": "page_main" +} +``` + +**Warning:** The underlying login handler contains a `while 1:` loop with heavy popup +detection logic (14+ popup types, scipy peak detection). Setting `max_wait_s` to a large +value risks blocking the MCP event loop for that entire duration. Keep `max_wait_s` at 90 +or below. This tool is categorized as a known risk area (see memory: issue #35). + +**When to use:** After cold-starting the game via `adb_launch_game`, when the game has not +yet reached the main lobby and you want ALAS's own popup-dismissal logic to handle the login +sequence automatically. + +--- + +## 3. How to Take a Screenshot and Diagnose Game State + +The LLM's primary observability tool is the screenshot. The game resolution is always 1280x720. + +### Step-by-step diagnosis sequence + +``` +1. Call adb_get_focus + - If package != "com.YoStarEN.AzurLane": + -> Game is not in foreground. Call adb_launch_game, wait 20s, repeat. + - If package is correct: proceed. + +2. Call adb_screenshot + - If it times out: ATX agent is down. See section 5 (crash detection). + - If the returned PNG is black (all pixels near 0): MEmu GPU rendering + issue or emulator freeze. See section 5. + - If the PNG shows a valid game screen: proceed. + +3. Visually interpret the screenshot: + - Main lobby (blue ocean, ship girls, resource bar at top): page_main + - Dark overlay with confirm/cancel buttons: popup dialog + - Loading spinner or progress bar: transitioning between pages + - Network error banner ("Server Unavailable", "Check Connection"): game network failure + - Black screen with Android system chrome: game crashed to desktop + - White screen: possible page_main_white (a variant of the main lobby) + +4. Call alas_get_current_state + - Cross-reference with visual interpretation. + - If state machine says "page_main" and visual confirms: system is healthy. + - If state machine says "page_unknown" but game looks valid: ALAS page detection + failed; use alas_goto("page_main") to attempt recovery. +``` + +### What a blank/black screenshot means + +A black PNG from `adb_screenshot` is almost always an MEmu GPU rendering issue. The +underlying screenshot method is uiautomator2's ATX agent, not `adb shell screencap`. If +the ATX agent returns a black frame, the emulator's GPU pipeline is stalled. Options: +- Use `adb_get_focus` to confirm Android itself is still running. +- If Android is responsive (focus returns a result), the emulator GPU may have glitched. + Consider restarting MEmu via `memuc reboot -i 0`. +- If Android is not responsive, the emulator process has died. Restart via `memuc start`. + +--- + +## 4. How to Detect ALAS Has Crashed + +There are three independent signals. Check them in order from fastest to most reliable. + +### Signal 1: Process check (fastest) + +ALAS runs as a child process of the GUI (`gui.py`). Check whether the process is alive: + +```python +import psutil + +def alas_is_running(config_name: str = "PatrickCustom") -> bool: + for proc in psutil.process_iter(attrs=["name", "cmdline"]): + cmdline = proc.info.get("cmdline") or [] + for arg in cmdline: + if config_name in str(arg): + return True + return False +``` + +If the process is not found, ALAS has exited. This does not tell you why — consult the logs. + +### Signal 2: Log staleness (reliable) + +ALAS writes to `alas_wrapped/log/_PatrickCustom.txt` continuously while running. A +healthy bot produces new log lines every few seconds during task execution, and at least +every 60 seconds during idle cooldown periods. + +Check the modification time of the log file: + +```python +import os, time + +def log_is_stale(threshold_s: int = 120) -> bool: + log_path = "alas_wrapped/log/2026-03-03_PatrickCustom.txt" # use today's date + try: + mtime = os.path.getmtime(log_path) + return (time.time() - mtime) > threshold_s + except FileNotFoundError: + return True # no log at all = definitely not running +``` + +If the log has not been written in 120 seconds during a period when a task should be running, +treat it as a crash indicator. + +### Signal 3: schedule_status.jsonl (most authoritative) + +File: `alas_wrapped/log/schedule_status.jsonl` + +ALAS appends a JSONL record here each time the scheduler loop evaluates the task queue. Each +record contains: + +```json +{ + "ts": "2026-03-03T04:52:09.497Z", + "config": "PatrickCustom", + "current_task": null, + "next_task": "Restart", + "pending": ["Restart", "OpsiCrossMonth", "Commission", "Tactical", ...], + "next_waiting": [], + "pending_count": 23, + "waiting_count": 0, + "source": "scheduler_loop" +} +``` + +**Healthy state:** The `ts` field should be recent (within the last 5 minutes), `pending` +should list tasks, and `current_task` should either be `null` (between tasks) or a task name +(actively running). + +**Stuck/dead state:** The last `ts` is more than 5 minutes old, or `current_task` has been +the same task for an unreasonably long time (Tactical tasks should complete in under 2 +minutes; Commission in under 5 minutes). + +**Parsing the last record:** + +```python +import json +from pathlib import Path + +def read_schedule_status() -> dict | None: + path = Path("alas_wrapped/log/schedule_status.jsonl") + if not path.exists(): + return None + lines = path.read_text(encoding="utf-8").strip().splitlines() + if not lines: + return None + return json.loads(lines[-1]) +``` + +### Signal 4: Error directory + +ALAS saves error snapshots to `alas_wrapped/log/error//` whenever it catches a +critical exception. Each subdirectory contains: +- Several PNG screenshots taken just before the error +- `log.txt` — the relevant log tail + +Check whether new error directories have appeared since the last healthy checkpoint: + +```python +import os +from pathlib import Path + +def recent_errors(since_ms: int) -> list[str]: + error_dir = Path("alas_wrapped/log/error") + if not error_dir.exists(): + return [] + dirs = [ + d.name for d in error_dir.iterdir() + if d.is_dir() and d.name.isdigit() and int(d.name) > since_ms + ] + return sorted(dirs) +``` + +The directory name is the Unix timestamp in milliseconds at the time of the error. The +`log.txt` inside will contain the Python traceback and the final log lines before the crash. + +--- + +## 5. Recovery Playbook + +When ALAS crashes or gets stuck, execute this playbook in order. Stop at the step that +resolves the situation. + +### Step 0: Establish baseline + +``` +1. Read the last line of schedule_status.jsonl — note current_task and ts. +2. Check the PatrickCustom log for recent CRITICAL or ERROR lines. +3. Call adb_get_focus — confirm the game is in foreground. +4. Call adb_screenshot — confirm the screen is visible and sensible. +``` + +### Step 1: Soft recovery (state machine can navigate) + +If `adb_get_focus` confirms the game is in the foreground and `adb_screenshot` shows a +recognizable game screen: + +``` +1. Call alas_get_current_state. + - If it returns a page name (not "page_unknown"): call alas_goto("page_main"). + - If it returns "page_unknown": proceed to Step 2. +2. After alas_goto returns, call alas_get_current_state again to confirm. +3. If confirmed at page_main: ALAS can be restarted (Step 4). +``` + +### Step 2: Manual navigation (state machine cannot navigate) + +If the game screen is visible but unrecognized: + +``` +1. Inspect the screenshot visually. +2. If a popup or overlay is present: + - Identify the dismiss button (usually bottom-right "OK" or "Close"). + - Call adb_tap with the button coordinates. + - Wait 1 second, take another screenshot, repeat until clear. +3. If the game is on a sub-screen (research queue, dorm, etc.): + - Look for a "HOME" button (upper-right, typically around x=1220, y=35 at 1280x720). + - Call adb_tap(1220, 35) to navigate home. + - Take a screenshot, confirm page_main appearance. +4. Once at page_main: call alas_get_current_state to verify, then proceed to Step 4. +``` + +### Step 3: Game process recovery (game has crashed or focus lost) + +If `adb_get_focus` shows the game is not in foreground, or screenshots are black: + +``` +1. Call adb_launch_game. +2. Wait 20 seconds. +3. Call adb_get_focus — confirm package is "com.YoStarEN.AzurLane". +4. Call adb_screenshot — confirm the game screen is visible. +5. If the game shows a login screen or server selection: + - Call alas_login_ensure_main(max_wait_s=90) to handle popups and reach page_main. +6. If the game shows a loading bar: wait up to 60 seconds and retry screenshot. +7. Once at page_main: proceed to Step 4. +``` + +### Step 4: Restart ALAS + +Once the game is confirmed at `page_main`, restart the ALAS process so it resumes autonomous +operation. Do this from a Python subprocess — never from within the MCP server itself. + +**Exact restart command:** + +```bash +cd alas_wrapped +PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe gui.py --run PatrickCustom +``` + +Or using the convenience batch file: + +```bash +cd alas_wrapped +alas.bat +``` + +**From Python:** + +```python +import subprocess +from pathlib import Path + +alas_wrapped = Path("D:/_projects/ALAS/alas_wrapped") +subprocess.Popen( + [str(alas_wrapped / ".venv/Scripts/python.exe"), "gui.py", "--run", "PatrickCustom"], + cwd=str(alas_wrapped), + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, +) +``` + +After starting, watch `schedule_status.jsonl` for a new record with a recent `ts` and +`source: "scheduler_loop"`. This confirms ALAS is running and the scheduler is active. + +### Step 5: Escalate (cannot recover automatically) + +If the game process cannot be started, ADB is unresponsive, or three recovery attempts have +failed: + +``` +1. Write a structured incident summary to stdout for human review: + - Last known state from schedule_status.jsonl + - Last error from log/error//log.txt + - Screenshot from adb_screenshot (if available) + - Steps attempted and outcomes +2. Do not loop indefinitely. One final attempt max. +``` + +--- + +## 6. How to Restart ALAS from Python + +The canonical restart is through `gui.py`, which manages the child process lifecycle. Do not +start `alas.py` directly — the GUI's process manager writes the PID file and handles +log rotation. + +```python +import os +import subprocess +from pathlib import Path + +def restart_alas(config_name: str = "PatrickCustom") -> subprocess.Popen: + alas_wrapped = Path("D:/_projects/ALAS/alas_wrapped") + python = alas_wrapped / ".venv" / "Scripts" / "python.exe" + env = {**os.environ, "PYTHONIOENCODING": "utf-8"} + proc = subprocess.Popen( + [str(python), "gui.py", "--run", config_name], + cwd=str(alas_wrapped), + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return proc +``` + +After calling this: +1. Wait 10 seconds. +2. Poll `schedule_status.jsonl` every 5 seconds until you see a record where `ts` is within + the last 30 seconds. +3. Confirm the process is alive with a `psutil` process check. + +--- + +## 7. Log Files + +All logs are in `alas_wrapped/log/`. They rotate daily; the filename prefix is the date. + +### `_PatrickCustom.txt` + +The primary bot log. ALAS writes every action here, structured as: + +``` +