Skip to content

彻底重构:模块化包架构、统一连接器工厂、78 测试、Muon+Lion 训练 - #154

Open
HDAnzz wants to merge 273 commits into
Ancientea:mainfrom
HDAnzz:refactor
Open

彻底重构:模块化包架构、统一连接器工厂、78 测试、Muon+Lion 训练#154
HDAnzz wants to merge 273 commits into
Ancientea:mainfrom
HDAnzz:refactor

Conversation

@HDAnzz

@HDAnzz HDAnzz commented May 2, 2026

Copy link
Copy Markdown

概况

彻底架构重构,将 CannotMax 从平铺脚本集合改造为标准 Python 包(src/cannotmax/),包含 10 个子包、统一连接器工厂、配置驱动识别、78 个自动化测试、预提交钩子和 CI 流水线。版本号 2.0.0-alpha.2。

已完成的重构

包结构

  • 所有源码迁入 src/cannotmax/,子包:configcore/connectorguimodelstrainingpipelinestoolssimulatorutils
  • 入口点:uv run cannotmax(原 python main.py
  • 新增开发命令:uv run cannotmax tools <script>uv run cannotmax pipelines <script>
  • 删除 maafw/(约 50 MB 本地二进制),现由 maafw Python 库提供(安装即附带二进制文件,无需额外配置)

连接器架构

  • ConnectorFactory:基于状态的延迟连接池(IDLE → VALID → INVALID)
  • 单一 connector 替代原有双连接器模式
  • BaseConnector 模板方法:ensure_connected()_capture_internal()_click_internal()
  • MAA Framework 注册表,传统 ADB 回退
  • WinRT 截屏保留在 core/connector/winrt_capture.py(重命名,未删除)

识别系统

  • crop_ratioconfig/app.json 配置驱动,不再硬编码
  • ADB/PC/WIN 各有独立的区域默认值
  • ROINotSelectedError 错误类型、交互式 ROISelector
  • tools/select_crop_ratio.py:实现选择大致区域后自动计算怪物条 ROI 和六个怪物头像以及数字 ROI(相对原图比例的形式)

模型与训练

  • 与 main 分支功能保持一致:Muon + Lion 双优化器、embed_dim=256num_heads=4dropout=0.3
  • 打包版本使用 ONNX 推理(PyTorch 延迟导入,不在打包中携带 torch)

GUI 改进

  • 延迟连接(启动不阻塞),QTimer 异步加载设备列表
  • QMutex 保护模式切换,自动获取期间禁止切换
  • 新增多开管理器(multi_instance.py):并行模拟器控制、崩溃检测、自动恢复
  • 预测复选框控制自动获取时是否加载模型
  • 新增 QMessageBox 深色模式样式修复

测试

  • 78 个单元 + 集成测试(原 0),CI: Windows 运行器
  • 测试分类:导入检查、连接器工厂状态机(20 项)、自动获取状态生命周期(6 项)、输入面板怪物数量读写(4 项)、模型加载与预测(3 项)、识别精度——含怪物头像匹配和数字 OCR 检测(17 项)、怪物区域检测(8 项)、冒烟测试(3 项)
  • 端到端测试标记 @pytest.mark.e2e(需模拟器)

DevOps

  • 预提交钩子:ruff 静态检查、导入排序、格式化
  • package.py + cannotmax.spec 已按重构后路径更新
  • CI 构建工作流支持 PyInstaller + GitHub Release
  • config/app.json 集中管理运行时配置,缺失时自动从默认值创建

Bug 修复

  • PC UI 比例 5 月 1 日更新后发生变化,本分支已按新比例调整(新增 pc_3/4/5.png 状态模板)
  • 打包版跳过 class_to_idx.json 加载(FIELD_FEATURE_COUNT=0),消除无关警告
  • 修复 window_picker.py 缺少 logger、pc_connector.py except 缩进错误
  • 修复 merge_data.py 日期目录长度检查(19→20)
  • PC 端需要通过管理员权限运行(console.py 已内置 _ensure_admin() 自动提权)

破坏性变更

  • main.pyuv run cannotmax(启动:uv run cannotmax
  • train.pyuv run cannotmax train
  • 包内全部使用相对导入,python old_script.py 直接运行不再可用
  • maafw/ 目录已删除;pyproject.toml 要求 maafw>=5.10.2,安装时自动附带 MAA 二进制文件

已知问题

  • 连接器的显示状态在某些情况下可能不准确
  • 模拟器模式跑不通(待排查)
  • 部分 pipelines/tools/ 脚本从原分支直接移动,原分支也存在不适用最新版本的问题,本分支同样未修复
  • 打包后的多开模拟器 exe 占用了与主 exe 同样的依赖空间,体积待优化

测试结果

78 passed, 2 deselected(端到端需要模拟器)

HDAnzz added 30 commits April 29, 2026 11:56
- Fix: base_dir (tools/) -> parent (cannotmax/) -> parent (src/) -> parent (root)
- DATA_DIR now correctly points to project_root/data
- Fix: data_file path from 'arknights.csv' to 'data/arknights.csv'
- FIELD_FEATURE_COUNT is correctly set to 0 in config/constants.py
- Remove terrain feature fields (6 features removed)
- Dataset now only contains monster units (34L + 34R + Result + ImgPath)
- Align with current data format in data/arknights.csv
- Move: src/cannotmax/utils/winrt_capture.py → src/cannotmax/core/winrt_connector.py
- Update imports in recognize.py, main_window.py
- Update __init__.py exports (remove from utils, add to core)
- Align with maa_adb_connector.py structure (both are platform connectors)

Now core/ contains all platform abstraction layers (ADB, WinRT)
- Update WinRTScreenCapture import in legacy/loadData.py
- Remove resolve_maafw_path usage (no longer exists)
- Fix field_recognition.py model path to use relative path
- Rename src/cannotmax/data → src/cannotmax/analytics
- Update imports in main_window.py, simular_history_match_ui.py
- Avoid naming conflict with data/ directory (arknights.csv)

Now: data/ is for CSV/images, analytics/ is for analysis code
- Create src/cannotmax/models/ directory
- Split transformer.py into:
  - dataset.py: ArknightsDataset, TOTAL_FEATURE_COUNT
  - transformer.py: UnitAwareTransformer only
- Update imports in train.py, val.py, predict.py
- Models are now framework-agnostic (no training logic)

Structure:
models/
├── __init__.py
├── dataset.py          # Data loading
└── transformer.py      # Model definition

train.py now imports from .models instead of defining classes
- Move train.py → training/trainer.py
- Move val.py → training/evaluator.py
- Create training/__init__.py with exports
- Update relative imports (from .config → from ..config)
- Remove standalone train.py and val.py from root

Now: training/trainer.py contains training loop, training/evaluator.py contains validation logic
- Remove ArknightsDataset class (moved to models/dataset.py)
- Remove UnitAwareTransformer class (moved to models/transformer.py)
- trainer.py now only contains:
  - Training utilities (train_one_epoch, evaluate)
  - Data preprocessing (preprocess_data)
  - Main training loop (main)
- Uses relative imports: from ..models import UnitAwareTransformer, ArknightsDataset
- Update Quick Start with CLI commands (train, eval, convert)
- Add new directory structure (models/, training/, pipelines/, cli/, console.py)
- Update Entry Points with console.py and module entry points
- Update Capture Modes: winrt_capture.py → winrt_connector.py
- Add Key Files for models/, training/ modules
- Update MAA Integration: remove Win32Controller section, note auto-loaded binaries
- Update Common Agent Pitfalls: remove 3rdparty/maafw, add model loading/data paths notes
- Update Current Phase: Phase 9 - Module Separation (Complete)
- Remove Phase 9 import standardization section (completed)
- Rename src/cannotmax/cli.py → console.py (CLI entry point)
- Update pyproject.toml: cannotmax entry → console:main
- Add src/cannotmax/training/__main__.py for python -m entry
- Update src/cannotmax/pipelines/merge_data.py paths
- CLI now uses argparse with subcommands: train, eval, convert

Entry points:
- cannotmax train
- cannotmax eval
- cannotmax convert -i model.pth -o model.onnx
- python -m src.cannotmax.training
- Add src/cannotmax/config/paths.py with Path objects for common directories:
  - PROJECT_ROOT, DATA_DIR, DATA_CSV
  - IMAGES_DIR, TMP_IMAGES_DIR
  - MODELS_DIR, DEFAULT_MODEL_FILE, DEFAULT_ONNX_FILE
  - MONSTER_DATA_CSV, TEMP_DIR

Import pattern:
  from ..config.paths import DATA_CSV, MODELS_DIR
- Remove os.path.join() in favor of Path division operator
- Replace os.path.exists() with Path.exists()
- Replace os.remove() with Path.unlink()
- Replace os.makedirs() with Path.mkdir(parents=True, exist_ok=True)
- Replace os.walk() with Path.rglob() in data_washer_new.py
- Remove unused 'import os' from 8 files
- Keep os.environ and os.cpu_count() where actively used

Files modified:
- cli/HumanDataCheck.py, pipelines/data_cleaning*.py, pipelines/data_washer_new.py
- core/recognize.py, core/predict_onnx.py, core/field_recognition.py
- core/maa_adb_connector.py, legacy/loadData.py
- Use csv.DictReader instead of csv.reader with index lookups
- Load both '原始名称' and '名称' columns in single pass
- Build id -> {primary, alt} mapping dictionary
- Eliminate O(n²) CSV re-reading inside loop (was O(n×m))
- Simplified fallback logic with unified try-fallback loop
- Complexity: O(n+m) instead of O(n×m)
- Default to GUI when running 'uv run cannotmax' without arguments
- Keep train/eval/convert subcommands for CLI operations
- Use PyQt6 QApplication with high DPI scaling enabled
- PyQt6 enables High DPI scaling by default
- Removed unnecessary setAttribute call
- Corrected class name from MainWindow to ArknightsApp
- MainWindow does not exist in gui/main_window.py
- OCR returns string 'number' but arrays expect int16
- Added int() conversion with safety checks in recognize_and_predict
- Added int() conversion in fill_data method
- Added int() conversion in get_image_name method
- Prevents CUDA device-side assert from string/int type mismatch
- Move cli/convert_model.py -> tools/convert_model.py
- Move cli/HumanDataCheck.py -> tools/human_data_check.py (snake_case)
- Update console.py import path for convert_model
- CLI now only contains console.py (entry point)
- Tools contain standalone utilities (convert_model, human_data_check)

Rationale:
- convert_model and HumanDataCheck are utility scripts, not CLI commands
- console.py is the only true CLI entry point with argparse subcommands
- Changed .cli import to .console (module renamed)
- Fixes 'No name main in module cannotmax.cli' error
- auto_fetch.py: State machine for automated battle data collection
- field_recognition.py: ONNX/PyTorch-based terrain recognition
- maa_adb_connector.py: MAA Framework wrapper (ADB, input, OCR, emulators)
- predict_onnx.py: ONNX fallback inference engine
- predict.py: Primary PyTorch inference with CUDA support
- recognize.py: Template matching + OCR monster recognition
- winrt_connector.py: Windows.Graphics.Capture adapter (已有文档)
- __init__.py: Module overview with component descriptions
- Create core/connector/ package with BaseConnector abstract class
- AdbConnector: ADB emulator support with MAA Framework auto-detection
  - MAA mode: post_screencap/post_click (background)
  - Legacy mode: ADB screencap/input tap
- PcConnector: PC client support with MAA Framework auto-detection
  - MAA mode: FramePool + SendMessageWithCursorPos (background)
  - Legacy mode: WinRT capture + SendInput (foreground)
- Remove legacy/loadData.py (replaced by connector module)
- Update gui/main_window.py: AdbConnectorAdapter -> AdbConnector
- Update core/__init__.py: export new connectors

Both connectors inherit from BaseConnector with unified interface:
- is_connected, screen_width, screen_height
- connect(), capture_screenshot(), click(), get_device_list()

MAA Framework is optional - connectors degrade gracefully if unavailable.
- Fix maa_adb_connector.py: import AdbConnector from .connector instead of ..legacy
- Fix main_window.py: update docstring reference from loadData.AdbConnector
- Remove legacy/loadData.py import from auto_fetch.py (comment only)

All references now use core.connector.AdbConnector/PcConnector
- Rename winrt_connector.py -> winrt_capture.py
  (Not a connector, just WinRT screenshot + SendInput utility)
- Update pc_connector.py import

WinRTScreenCapture is a helper class, not BaseConnector subclass.
- Delete maa_adb_connector.py (AdbConnectorAdapter deprecated)
- Create core/connector/maa_registry.py with:
  - MaaFrameworkDetector: Singleton availability checker
  - ConnectionTypeRegistry: Emulator default addresses (LDPlayer, MuMu, etc.)
  - InputMethodRegistry: Input method enums (maatouch, adb_shell, etc.)
  - MaaAvailability: Enum for MAA states
- Update gui/main_window.py: import from maa_registry instead of maa_adb_connector

Functionality preserved:
- ConnectionTypeRegistry: GUI dropdown options for emulator types
- InputMethodRegistry: GUI dropdown options for input methods
- MaaFrameworkDetector: Status indicator in GUI

Removed:
- AdbConnectorAdapter: Replaced by AdbConnector (MAA auto-detect)
- MaaAdbConnector: Replaced by AdbConnector._init_maa()
- MaaConnectionConfig/AdapterState: No longer needed
… WinRT

- Update recognize.py to use PcConnector.capture_screenshot() instead of WinRTScreenCapture
- PcConnector now supports optional ROI parameter for efficient cropping
- RecognizeMonster.__init__: Initialize PcConnector and call connect()
- RecognizeMonster.run_loop: Use PcConnector.capture_screenshot() for background image
- RecognizeMonster.capture_screenshot: Use PcConnector with roi=bbox for region crop

Benefits:
- Unified screenshot interface (MAA/WinRT auto-detect)
- Cleaner separation: recognize.py doesn't depend on WinRT implementation details
- PcConnector handles MAA Framework fallback automatically
- Split recognize.py into modular components:
  - recognize.py: Core recognition logic (template matching + OCR)
  - roi_selector.py: Interactive ROI selection with OpenCV
  - screenshot_helper.py: Screenshot capture with auto-detection
- RecognizeMonster now composes ROISelector and ScreenshotHelper
- Helper functions (preprocess, find_best_match, etc.) remain in recognize.py
- Maintain backward compatibility: RecognizeMonster API unchanged

Benefits:
- Separation of concerns: recognition vs. input (ROI/screenshot)
- Reusable components: ROISelector and ScreenshotHelper available independently
- Cleaner code structure: recognize.py focused on core algorithm
- Fix AdbController initialization: use named argument 'address' instead of positional
- Fix Win32Controller initialization: ensure proper cleanup on failure
- Add finally blocks to clean up temporary MAA objects
- Prevent AttributeError in MAA Controller.__del__ by ensuring objects
  are either fully initialized or not stored in instance variables

Fixes:
- 'AdbController' object has no attribute '_handle' (MAA bug workaround)
- AdbController.__init__() missing 1 required positional argument: 'address'
- Remove hardcoded relative_regions and relative_regions_nums
- Load from config.get_relative_regions() and get_relative_regions_nums()
- RecognizeMonster stores them as instance attributes in __init__
- Config values match existing hardcoded values exactly:
  - monsters: 6 zones (0.00-0.13, 0.12-0.25, 0.24-0.37, 0.63-0.76, 0.75-0.88, 0.87-1.00)
  - numbers: 6 zones (0.03-0.14, 0.16-0.27, 0.29-0.40, 0.61-0.72, 0.73-0.84, 0.86-0.97)

Benefits:
- Single source of truth in config/settings.py
- Support custom recognition zones via config/recognition_zones.json
- Consistent with DEFAULT_RECOGNITION_ZONES structure
HDAnzz added 21 commits May 4, 2026 01:06
…test_predict, add cannotsim hidden imports

- recognize.py: zones = RECOGNITION_PARAMS.get(mode) if mode != 'WIN' else None
  prevents KeyError for WIN; add results=[] before if/else splits;
  detect zones for all modes but only use detected_zones for WIN region split
- test_predict.py: .model → .session (ONNX uses session not model attr)
- dialogs/__init__.py: explicit re-export to fix F401 unused import
- cannotmax.spec: add cannotsim to hiddenimports for packaged subprocess call
- main_window.py: use -m cannotsim.main_sim in subprocess (already correct)
@liemark
liemark force-pushed the main branch 3 times, most recently from e41373c to 33706c0 Compare May 4, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants