Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions autowsgr/combat/fleet.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,19 @@ def _optional_level(rule: Mapping[str, Any], field: str) -> int | None:
return value


def _optional_relaxed(rule: Mapping[str, Any]) -> bool:
"""读取宽松校验开关;缺省为 False(严格校验)。"""
value = rule.get('relaxed')
if value is None:
return False
if not isinstance(value, bool):
raise TypeError('relaxed 必须是布尔值')
return value


def _selector_from_mapping(
raw: Mapping[str, Any],
*,
relaxed: bool,
inherited: Mapping[str, Any] | None = None,
) -> ShipSelector:
name = _optional_text(raw.get('name'))
Expand All @@ -264,7 +273,7 @@ def _selector_from_mapping(
ship_types=parse_ship_type_codes(source.get('ship_type')),
min_level=_optional_level(source, 'min_level'),
max_level=_optional_level(source, 'max_level'),
relaxed_constraints=relaxed,
relaxed_constraints=_optional_relaxed(raw),
)


Expand All @@ -285,7 +294,7 @@ def fleet_slot_from_api(raw: str | Mapping[str, Any]) -> FleetSlotRule:
raise TypeError('舰队槽位必须是字符串或规则对象')

name = _optional_text(raw.get('name'))
primary = _selector_from_mapping(raw, relaxed=False) if name is not None else None
primary = _selector_from_mapping(raw) if name is not None else None
raw_candidates = raw.get('candidates', [])
if not isinstance(raw_candidates, Sequence) or isinstance(raw_candidates, str):
raise TypeError('candidates 必须是规则对象列表')
Expand All @@ -299,7 +308,7 @@ def fleet_slot_from_api(raw: str | Mapping[str, Any]) -> FleetSlotRule:
max_level=_optional_level(raw, 'max_level'),
)
if isinstance(candidate, str)
else _selector_from_mapping(candidate, relaxed=False)
else _selector_from_mapping(candidate)
for candidate in raw_candidates
)
)
Expand All @@ -320,17 +329,16 @@ def fleet_slot_from_yaml(raw: object) -> FleetSlotRule:

primary: ShipSelector | None = None
if _optional_text(raw.get('name')) is not None:
primary = _selector_from_mapping(raw, relaxed=False)
primary = _selector_from_mapping(raw)
normalized_candidates: list[ShipSelector] = []
for candidate in candidates:
if isinstance(candidate, str):
selector = _selector_from_mapping(
{'name': candidate},
relaxed=False,
inherited=raw,
)
elif isinstance(candidate, Mapping):
selector = _selector_from_mapping(candidate, relaxed=False)
selector = _selector_from_mapping(candidate)
else:
raise TypeError('candidates 只能包含舰名字符串或规则对象')
normalized_candidates.append(selector)
Expand Down
11 changes: 11 additions & 0 deletions autowsgr/context/game_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@


if TYPE_CHECKING:
from datetime import date

from autowsgr.combat.history import CombatResult
from autowsgr.emulator import AndroidController
from autowsgr.infra import UserConfig
Expand Down Expand Up @@ -70,6 +72,12 @@ class GameContext:

ocr: OCREngine
"""OCR 引擎实例 (章节/阵型识别等)。"""
ship_ocr: OCREngine | None = None
"""增强船只识别 OCR 引擎 (RapidOCR, 可选)。

仅在 ``config.ocr.enhanced_ship_ocr`` 开启时由启动器注入;
船只名称 / 等级 / 舰种识别节点优先使用它,其余节点仍走 ``ocr``。
"""

# ── 游戏运行时状态 ──

Expand Down Expand Up @@ -97,6 +105,9 @@ class GameContext:
quick_repair_used: int = 0
"""本次会话已消耗快修数。"""

daily_overlay_date: date | None = None
"""最近一次完成主页面浮层处理的日期 (时间戳 gate: 每日 0 点后首次处理, 跨日失效)。"""

active_fight_tasks: int = 0
"""当前正在执行的战斗任务数。由 TaskScheduler 维护,用于判断是否可以安全执行浴室维修等会占用舰队的操作。"""

Expand Down
3 changes: 3 additions & 0 deletions autowsgr/infra/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class OCRConfig(BaseModel):
"""是否使用 GPU 加速"""
mirror: OcrMirror = OcrMirror.modelscope
"""EasyOCR 模型下载镜像源"""
enhanced_ship_ocr: bool = False
"""是否启用增强船只识别 OCR (RapidOCR)。默认关闭保持原行为;开启后
船只名称 / 等级 / 舰种识别节点优先使用 RapidOCR,无需额外下载模型。"""

# 舰名匹配
ship_name_match_confidence: float = Field(
Expand Down
73 changes: 68 additions & 5 deletions autowsgr/ops/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from __future__ import annotations

import time
from datetime import date
from typing import TYPE_CHECKING

from autowsgr.infra.logger import get_logger
Expand Down Expand Up @@ -60,6 +61,15 @@
_OVERLAY_DISMISS_DELAY: float = 1.0
"""消除浮层后的等待时间 (秒)。"""

_OVERLAY_DISMISS_MAX: int = 5
"""每日浮层消除的最大尝试次数 (新闻 → 签到 → 确认 → 二次确认 → 兜底)。"""

_OVERLAY_DISMISS_WAIT: float = 1.5
"""消除每个浮层后的等待时间 (秒) — 弹窗按顺序逐个出现, 需要间隔等待。"""

_OVERLAY_CONFIRM_TIMEOUT: float = 3.0
"""确认弹窗兜底的最大等待时限 (秒) — 用于检测不到浮层签名但存在确认按钮的弹窗。"""

_RECOVERY_TO_MAIN_TIMEOUT: float = 20.0
"""页面异常恢复时,尝试回到主界面的超时 (秒)。"""

Expand Down Expand Up @@ -223,21 +233,72 @@ def restart_game(
start_game(ctrl, package, startup_timeout=startup_timeout)


def go_main_page(ctx: GameContext, *, dismiss_overlays: bool = True) -> None: # noqa: ARG001
def handle_daily_overlays(ctx: GameContext) -> None:
"""每日首次主页面浮层处理 (新闻公告 → 每日签到 → 活动预约)。

时间戳 gate: 每天 0 点后第一次调用才真正截图检测并消除浮层,
当天已处理过则直接返回 (零开销), 避免每次回主页都重复检测。

触发时机 (启动 / 任务执行前 / 挂机等待) 均可安全调用:
- 登录后弹出「新闻公告」→ 勾选「不再显示」+ 点左上角 X
- 接着弹出「每日签到」→ 点领取 + 确认操作
- 可能再弹出「活动预约」→ 点关闭 + 二次确认

按弹窗出现顺序逐个消除, 最多尝试 :data:`_OVERLAY_DISMISS_MAX` 次,
直到画面干净或无浮层可关为止。
"""
today = date.today() # noqa: DTZ011 # 本地墙上时钟 (游戏 0 点刷新)
if ctx.daily_overlay_date == today:
return
# 先标记再执行: 即使本次消除中断, 当天也不会反复重试
ctx.daily_overlay_date = today

page = MainPage(ctx)
for attempt in range(_OVERLAY_DISMISS_MAX):
# ① 已知浮层 (新闻 / 签到 / 预约) 直接消除
if page.dismiss_current_overlay():
_log.info(
'[Startup] 主页面浮层已消除 ({}/{})',
attempt + 1,
_OVERLAY_DISMISS_MAX,
)
time.sleep(_OVERLAY_DISMISS_WAIT)
continue

# ② 确认弹窗兜底: 检测不到浮层签名, 但可能有「确认」按钮
# (如签到奖励确认、二次确认) — 用确认模板轮子点掉
from autowsgr.ui.utils import confirm_operation

if confirm_operation(
ctx.ctrl,
must_confirm=False,
timeout=_OVERLAY_CONFIRM_TIMEOUT,
):
_log.info('[Startup] 确认弹窗兜底: 点击确认 ({}/{})', attempt + 1, _OVERLAY_DISMISS_MAX)
time.sleep(_OVERLAY_DISMISS_WAIT)
continue

# ③ 既无浮层又无确认按钮 → 主页干净, 流程完成
return


def go_main_page(ctx: GameContext, *, dismiss_overlays: bool = True) -> None:
"""确保当前处于游戏主页面。

1. 若设置了 ``dismiss_overlays``,先消除登录浮层
2. 调用 :func:`~autowsgr.ops.navigate.goto_page` 导航到主页面
1. 调用 :func:`~autowsgr.ops.navigate.goto_page` 导航到主页面
2. 若设置了 ``dismiss_overlays``,导航后消除登录浮层

Parameters
----------
ctx:
游戏上下文。
dismiss_overlays:
是否先消除登录浮层,默认 ``True``。
是否消除登录浮层,默认 ``True``。
"""
_log.info('[Startup] 导航到主页面')
goto_page(ctx, PageName.MAIN)
if dismiss_overlays:
handle_daily_overlays(ctx)


def recover_to_main_or_restart(
Expand Down Expand Up @@ -278,7 +339,7 @@ def ensure_game_ready(
app: GameAPP | str = GameAPP.official,
*,
startup_timeout: float = _STARTUP_TIMEOUT,
dismiss_overlays: bool = True, # noqa: ARG001
dismiss_overlays: bool = True,
) -> None:
"""确保游戏已启动并处于可识别页面。

Expand Down Expand Up @@ -320,3 +381,5 @@ def ensure_game_ready(
_log.info('[Startup] 游戏已在运行')

_log.info('[Startup] 游戏就绪')
if dismiss_overlays:
handle_daily_overlays(ctx)
20 changes: 18 additions & 2 deletions autowsgr/scheduler/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ def ctrl(self) -> AndroidController:
# ── OCR ──

def create_ocr(self) -> OCREngine:
"""根据配置创建 EasyOCR 引擎。"""
"""根据配置创建通用 OCR 引擎 (EasyOCR)。"""
cfg = self.config
_log.info('[Launcher] 创建 EasyOCR 引擎')
_log.info('[Launcher] 创建通用 OCR 引擎: EasyOCR')
gpu = cfg.ocr.gpu
gpu_override = os.getenv('AUTOWSGR_OCR_GPU_MODE', '').lower()
if gpu_override == 'cuda':
Expand Down Expand Up @@ -167,6 +167,19 @@ def create_ocr(self) -> OCREngine:
_log.info('[Launcher] OCR用户舰名别名加载(有效别名:{})', loaded_aliases)
return self._ocr

def create_ship_ocr(self) -> OCREngine | None:
"""根据配置创建增强船只识别 OCR 引擎 (RapidOCR / PP-OCR)。

仅在 ``cfg.ocr.enhanced_ship_ocr`` 开启时创建;
默认关闭,返回 ``None``,船只识别节点继续使用默认 EasyOCR。
"""
cfg = self.config
if not cfg.ocr.enhanced_ship_ocr:
_log.info('[Launcher] 增强船只识别 OCR 未开启,船只识别节点继续使用 EasyOCR')
return None
_log.info('[Launcher] 创建增强船只识别 OCR 引擎: RapidOCR (PP-OCR)')
return OCREngine.create(engine='rapidocr', gpu=cfg.ocr.gpu)

# ── 构造 GameContext ──

def build_context(self) -> GameContext:
Expand All @@ -187,7 +200,10 @@ def build_context(self) -> GameContext:
ctrl=self.ctrl,
config=self.config,
ocr=self._ocr,
ship_ocr=self.create_ship_ocr(),
)
ship_engine = 'RapidOCR (PP-OCR)' if ctx.ship_ocr is not None else '未启用 (沿用 EasyOCR)'
_log.info('[Launcher] OCR 加载完成: 通用引擎=EasyOCR, 船只识别引擎={}', ship_engine)
_log.info('[Launcher] GameContext 已构建')
return ctx

Expand Down
21 changes: 21 additions & 0 deletions autowsgr/scheduler/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,30 @@ def run(self) -> list[FightTask]:
task.name,
task.times,
)
self._ensure_main_page_clean()
self._run_task(task)

self._print_summary()
return list(self._tasks)

def _ensure_main_page_clean(self) -> None:
"""常驻弹窗检查: 每日首次消除主页面浮层 (新闻公告 / 每日签到 / 活动预约)。

在任务执行前、挂机等待时调用; 内部由
:func:`autowsgr.ops.startup.handle_daily_overlays` 的时间戳 gate 短路:
每天 0 点后第一次调用才真正截图检测并消除, 当天已处理过则直接返回,
不干扰正常流程。
"""
try:
from autowsgr.ops.startup import handle_daily_overlays

handle_daily_overlays(self._ctx)
except Exception as exc:
_log.opt(exception=True).warning(
'[Scheduler] 主页面浮层检查失败: {}',
exc,
)

def _run_task(self, task: FightTask) -> None:
"""执行单个任务的全部轮次。"""
# 统一用 BatchRunnerAdapter 包装: 完整保留 run()→list[CombatResult],
Expand Down Expand Up @@ -356,6 +375,8 @@ def run_daily(self) -> None:
pass
else:
# 队列空:所有触发器暂无任务 (常规战打满 / 只等远征定时) → 挂机等待
# 挂机期间顺带做常驻弹窗检查, 保证回到主页面时浮层不残留
self._ensure_main_page_clean()
time.sleep(self._idle_sleep)

_log.info('[Scheduler] 收到停止信号, 调度结束')
Expand Down
4 changes: 4 additions & 0 deletions autowsgr/server/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ class FleetShipRuleRequest(BaseModel):
ship_type: list[str] | None = Field(default=None, description='允许的舰种列表(如 [ss, ssg])')
min_level: int | None = Field(default=None, ge=1, description='等级下限(含)')
max_level: int | None = Field(default=None, ge=1, description='等级上限(含)')
relaxed: bool = Field(
default=False,
description='宽松校验:舰名必须命中,等级/舰种尽力而为(识别失败或不匹配也放行)',
)

@field_validator('name')
@classmethod
Expand Down
10 changes: 10 additions & 0 deletions autowsgr/ui/battle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ def __init__(self, ctx: GameContext, ocr: OCREngine | None = None) -> None:
self._ctx = ctx
self._ctrl = ctx.ctrl
self._ocr = ocr or ctx.ocr
self._ship_ocr = getattr(ctx, 'ship_ocr', None)

@property
def _preferred_ocr(self) -> OCREngine | None:
"""返回船只识别优先使用的 OCR 引擎。

增强船只识别 (RapidOCR) 开启时优先使用它,未开启时回退默认 OCR。
只有名称 / 等级 / 舰种等船只信息节点应使用本属性,其余节点仍用 ``_ocr``。
"""
return self._ship_ocr or self._ocr

# ── 页面识别 ──────────────────────────────────────────────────────────

Expand Down
19 changes: 19 additions & 0 deletions autowsgr/ui/battle/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,22 @@
.. note::
坐标为估算值,实际使用前应根据截图校准。
"""


# ═══════════════════════════════════════════════════════════════════════════════
# 舰船舰种 OCR 裁切区域
# ═══════════════════════════════════════════════════════════════════════════════

SHIP_TYPE_CROP: dict[int, tuple[float, float, float, float]] = {
0: (0.0450, 0.5220, 0.1600, 0.5750),
1: (0.1600, 0.5220, 0.2750, 0.5750),
2: (0.2750, 0.5220, 0.3900, 0.5750),
3: (0.3900, 0.5220, 0.5050, 0.5750),
4: (0.5050, 0.5220, 0.6200, 0.5750),
5: (0.6200, 0.5220, 0.7350, 0.5750),
}
"""出征准备页 6 个舰船槽位的舰种文本 OCR 裁切区域 (x1, y1, x2, y2)。

坐标取自 ``logs/ocr_regions_1280x720.yaml`` 的 ``ship_type`` 槽位
(如 ``轻巡(J国)`` / ``潜艇(G国)`` 等舰种文字区域)。
"""
Loading
Loading