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
35 changes: 25 additions & 10 deletions autowsgr/infra/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
from pathlib import Path
from typing import Any, Literal

from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
from pydantic import (
BaseModel,
Field,
ValidationError,
field_validator,
model_validator,
)

from autowsgr.infra.logger import get_logger
from autowsgr.types import (
Expand Down Expand Up @@ -481,8 +487,11 @@ def _coerce_dock_full_mode(cls, v: object) -> object:
return v

# 解装设置
destroy_ship_work_mode: DestroyShipWorkMode = DestroyShipWorkMode.disable
"""解装工作模式"""
destroy_ship_work_mode: DestroyShipWorkMode = Field(default=DestroyShipWorkMode.all)
"""解装范围: all=拆解全部 / destroy_only=拆解舰种 / keep_only=保留舰种。

值别名兼容, 见 :meth:`_coerce_destroy_mode`。
"""
destroy_ship_types: list[ShipType] = Field(default_factory=list)
"""指定舰种列表"""
remove_equipment_mode: bool = True
Expand All @@ -491,14 +500,20 @@ def _coerce_dock_full_mode(cls, v: object) -> object:
@field_validator('destroy_ship_work_mode', mode='before')
@classmethod
def _coerce_destroy_mode(cls, v: object) -> object:
"""允许用中文别名或英文成员名指定解装模式。"""
"""允许用中文别名、英文成员名或旧别名指定解装范围。"""
_alias: dict[str, int] = {
'不启用': 0,
'disable': 0,
'黑名单': 1,
'include': 1,
'白名单': 2,
'exclude': 2,
'拆解全部': 0,
'不启用': 0, # 旧值
'all': 0,
'disable': 0, # 旧值
'拆解舰种': 1,
'黑名单': 1, # 旧值
'destroy_only': 1,
'include': 1, # 旧值
'保留舰种': 2,
'白名单': 2, # 旧值
'keep_only': 2,
'exclude': 2, # 旧值
}
if isinstance(v, str):
key = v.strip()
Comment thread
kuainx marked this conversation as resolved.
Expand Down
2 changes: 1 addition & 1 deletion autowsgr/ops/decisive/chapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _do_dock_full_destroy(self) -> None:
_log.warning('[决战] 船坞已满,执行自动解装')
self._ctrl.click(0.38, 0.565)
if not destroy_ships_auto(self._ctx):
# 白名单覆盖全部舰种, 无可解装对象, 船坞仍满
# 保留舰种覆盖全部舰种, 无可解装对象, 船坞仍满
raise DockFullError('决战中船坞已满,且无可解装舰种')
return
raise DockFullError('决战中检测到船坞已满,且未开启 full_destroy')
18 changes: 9 additions & 9 deletions autowsgr/ops/destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ def destroy_ships_auto(ctx: GameContext, *, from_dialog: bool = False) -> bool:
是否调用本函数由调用方的 ``dock_full_destroy`` / ``full_destroy`` 开关决定;
此处只关心「怎么拆」::

- ``destroy_ship_work_mode == disable``: 不启用舰种分类, 走快速拆解路线
- ``destroy_ship_work_mode == all``: 拆解全部, 不启用舰种分类, 走快速拆解路线
(``ship_types=None`` → 不打开过滤器, 快速全选解装全部)。
- ``include`` (黑名单): 解装 ``destroy_ship_types`` 指定舰种。
- ``exclude`` (白名单): 解装除 ``destroy_ship_types`` 外的所有舰种。
- ``destroy_only`` (拆解舰种): 解装 ``destroy_ship_types`` 指定舰种。
- ``keep_only`` (保留舰种): 解装除 ``destroy_ship_types`` 外的所有舰种。

``remove_equipment`` 取自 ``remove_equipment_mode``。

Expand All @@ -86,18 +86,18 @@ def destroy_ships_auto(ctx: GameContext, *, from_dialog: bool = False) -> bool:
Returns
-------
bool
``True`` 已执行解装; ``False`` 仅在白名单覆盖全部舰种、无可解装对象时返回
``True`` 已执行解装; ``False`` 仅在保留舰种覆盖全部舰种、无可解装对象时返回
(此时船坞仍满, 调用方据此保持 DOCK_FULL)。
"""
cfg = ctx.config
mode = cfg.destroy_ship_work_mode

if mode == DestroyShipWorkMode.disable:
# 不启用舰种分类: 不过滤, 直接走快速全选拆解路线
if mode == DestroyShipWorkMode.all:
# 拆解全部: 不过滤, 直接走快速全选拆解路线
ship_types = None
elif mode == DestroyShipWorkMode.include:
elif mode == DestroyShipWorkMode.destroy_only:
ship_types = cfg.destroy_ship_types or None
else: # exclude (白名单): 解装除指定舰种外的所有
else: # keep_only (保留舰种): 解装除指定舰种外的所有
protected = set(cfg.destroy_ship_types)
# 防战筛选坐标尚未校准,自动解装保留该舰种,避免误点其他分类。
ship_types = [
Expand All @@ -106,7 +106,7 @@ def destroy_ships_auto(ctx: GameContext, *, from_dialog: bool = False) -> bool:
if ShipType.AABG not in protected:
_log.warning('[OPS] 防战筛选坐标未校准,本轮自动解装保留防战')
if not ship_types:
_log.warning('[OPS] 白名单包含全部舰种, 无可解装对象, 跳过')
_log.warning('[OPS] 保留舰种覆盖全部舰种, 无可解装对象, 跳过')
return False

if from_dialog:
Comment thread
kuainx marked this conversation as resolved.
Expand Down
20 changes: 12 additions & 8 deletions autowsgr/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,14 +375,18 @@ def relative_position_in_destroy(self) -> tuple[float, float]:


class DestroyShipWorkMode(IntEnum):
"""拆解工作模式。"""

disable = 0
"""不启用舰种分类"""
include = 1
"""只拆指定舰种"""
exclude = 2
"""拆除指定舰种以外的所有舰种"""
"""解装范围: ``destroy_ship_types`` 列表的用途。"""

all = 0
"""拆解全部 (不按舰种分类, 走快速拆解路线)"""
destroy_only = 1
"""拆解舰种 (只拆列表内舰种)"""
keep_only = 2
"""保留舰种 (列表内舰种保留, 其余全拆)"""

disable = 0 # 旧成员名, 等价 all
include = 1 # 旧成员名, 等价 destroy_only
exclude = 2 # 旧成员名, 等价 keep_only


class DockFullAction(IntEnum):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该改动整体思路清晰,将原先含义模糊的 disable/include/exclude 重命名为更明确的 all/destroy_only/keep_only,并保留了旧成员名作为别名,基本保持了赋值层面的向后兼容。

但存在一个潜在的兼容性问题需要关注:

  1. 枚举别名的 name 属性会发生变化
    Python 中,当多个枚举成员指向同一个值时,后定义的成员会成为先定义成员的“别名”。访问别名成员时,其 .name 返回的是规范成员(第一个定义者)的名字,而不是别名本身的名字。
    例如:

    DestroyShipWorkMode.disable.name   # 现在是 'all'
    DestroyShipWorkMode.include.name   # 现在是 'destroy_only'
    DestroyShipWorkMode.exclude.name   # 现在是 'keep_only'

    而旧代码中这些 .name 分别是 'disable'、'include'、'exclude'。如果项目中有任何代码依赖枚举成员的 .name 做序列化、日志输出、用户展示或持久化,都会在升级后发生行为变化,可能导致数据不一致或显示错误。

  2. 枚举遍历结果会丢失旧成员名
    使用 list(DestroyShipWorkMode) 或类似遍历时,只会得到 all、destroy_only、keep_only,而 disable、include、exclude 不会出现。如果外部代码曾依赖遍历结果中的旧名称,也会受影响。

如果该枚举仅以整数值形式参与配置和逻辑判断,那么本次改动没有问题;但若库的 API 承诺或外部用户可能使用 .name,建议在变更说明中明确指出,或在语义上进行更彻底的兼容处理(例如使用 enum.Enum 的自定义属性或完整保留旧成员为独立枚举值,不过那样就无法同时保持数值映射)。否则,应说明为破坏性变更并规划迁移。

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/context-and-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ UserConfig (顶层)
├── dock_full_destroy: bool # 船坞满自动清空
├── repair_manually: bool # 手动修理模式
├── bathroom_count: int # 修理位置总数
├── destroy_ship_work_mode # 解装模式 (disable/include/exclude)
├── destroy_ship_work_mode # 解装范围 (all/destroy_only/keep_only)
├── destroy_ship_types # 解装舰种列表
└── remove_equipment_mode: bool # 解装前卸装备
```
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ cook(ctx, ...) # 食堂烹饪
**文件**: `autowsgr/ops/destroy.py`

```python
destroy_ships(ctx, ...) # 解装舰船 (黑名单/白名单模式)
destroy_ships(ctx, ...) # 解装舰船 (拆解全部/拆解舰种/保留舰种范围)
```

### 补给
Expand Down
2 changes: 1 addition & 1 deletion docs/features/event-20260930.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
本次不伪造 native 类型或将防战混为导战。

防战解装筛选坐标没有实机样本:显式指定防战解装时,在点击前报错;
自动白名单解装额外保留防战并记录警告。未指定分类的全量解装模式沿用原逻辑。
自动保留舰种解装额外保留防战并记录警告。未指定分类的全量解装模式沿用原逻辑。

## 验证与调试

Expand Down
8 changes: 4 additions & 4 deletions examples/usersettings.full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ repair_manually: false # 是否手动修理
bathroom_count: 2 # 浴室修理槽位数 (auto_bath_repair 空位调度用)

# ── 解装设置 ──
destroy_ship_work_mode: 不启用 # 不启用 / 黑名单 / 白名单
# 不启用: 解装所有舰船
# 黑名单: 解装列表中的舰种
# 白名单: 解装不在列表中的舰种
destroy_ship_work_mode: 拆解全部 # 拆解全部 / 拆解舰种 / 保留舰种
# 拆解全部: 解装所有舰船 (不按舰种分类, 走快速拆解)
# 拆解舰种: 只解装列表中的舰种
# 保留舰种: 解装不在列表中的舰种
destroy_ship_types: [] # 指定舰种列表, 参照 ShipType 枚举使用中文
# - 航母
# - 潜艇

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该变更存在潜在兼容性问题,需要确认:

  1. 配置值变更可能破坏现有逻辑:如果代码其他部分仍按旧值 不启用 / 黑名单 / 白名单 进行匹配,而配置文件中已改为 拆解全部 / 拆解舰种 / 保留舰种,会导致配置解析失败或行为异常。需同步检查并更新所有读取该配置项的代码、文档及示例配置。

  2. 语义转换是否完全等价:旧模式 不启用 注释为“解装所有舰船”,这在语义上容易误解;新模式 拆解全部 更直观。但需确认“黑名单/白名单”与“拆解舰种/保留舰种”在逻辑上一一对应,且不存在取反或边界差异。

  3. “快速拆解”行为是否明确:新注释新增“走快速拆解”这一描述,但旧实现中“解装所有舰船”是否一定走快速拆解?若该字段还影响其他流程,需确认改动后行为符合预期,避免产生性能或功能差异。

建议:仔细核对代码中对 destroy_ship_work_mode 的所有引用,确保字符串常量已同步更新,并补充/更新对应测试用例。

Expand Down
39 changes: 38 additions & 1 deletion testing/infra/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,46 @@ def test_destroy_ship_config(self, tmp_yaml: Callable[[str, str], Path]):
"""
path = tmp_yaml('destroy.yaml', content)
cfg = UserConfig.from_yaml(path)
assert cfg.destroy_ship_work_mode == DestroyShipWorkMode.include
assert cfg.destroy_ship_work_mode == DestroyShipWorkMode.destroy_only
assert len(cfg.destroy_ship_types) == 2

@pytest.mark.parametrize(
('value', 'expected'),
[
('拆解全部', DestroyShipWorkMode.all),
('拆解舰种', DestroyShipWorkMode.destroy_only),
('保留舰种', DestroyShipWorkMode.keep_only),
],
)
def test_destroy_ship_chinese_alias(
self,
tmp_yaml: Callable[[str, str], Path],
value: str,
expected: DestroyShipWorkMode,
):
"""中文别名 拆解全部 / 拆解舰种 / 保留舰种 均映射到对应枚举。"""
content = f"""\
emulator:
type: "雷电"
serial: "emulator-5554"
path: "C:/fake/dnplayer.exe"
destroy_ship_work_mode: {value}
"""
cfg = UserConfig.from_yaml(tmp_yaml('destroy_alias.yaml', content))
assert cfg.destroy_ship_work_mode == expected

def test_destroy_ship_legacy_value_alias(self, tmp_yaml: Callable[[str, str], Path]):
"""旧值别名 (不启用/黑名单/白名单、disable/include/exclude) 仍可用。"""
content = """\
emulator:
type: "雷电"
serial: "emulator-5554"
path: "C:/fake/dnplayer.exe"
destroy_ship_work_mode: 白名单
"""
cfg = UserConfig.from_yaml(tmp_yaml('destroy_legacy.yaml', content))
assert cfg.destroy_ship_work_mode == DestroyShipWorkMode.keep_only


# ── FightConfig ──

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以下是针对该代码变更片段的审核意见:

  1. 旧值别名测试覆盖不足
    test_destroy_ship_legacy_value_alias 的 docstring 声称“不启用/黑名单/白名单、disable/include/exclude 仍可用”,但实际只测试了 白名单 → keep_only 一个用例。建议使用参数化方式覆盖所有声明的旧值别名,避免后续重构时遗漏或破坏其他别名。

  2. 缺少对旧枚举值 include / exclude 的兼容性验证
    原测试断言由 include 改为 destroy_only,这可能是迁移后的预期变化,但新测试并没有显式验证旧值 include / exclude 是否仍然兼容。如果已有用户配置中使用了这些旧值,直接移除会导致兼容性回归。建议补充对旧枚举值的解析测试,确认它们是被继续支持还是会被明确拒绝。

  3. 测试文件名在参数化中重复使用
    test_destroy_ship_chinese_alias 多次传入相同的 destroy_alias.yaml,如果 tmp_yaml fixture 不是每次调用都生成独立临时目录,则可能存在文件覆盖或串扰问题。建议确认 fixture 作用域,或使用不同文件名以避免潜在冲突。

  4. 可维护性优化
    多个测试中重复构造 YAML 内容,可以考虑抽取出公共的 fixture 或辅助函数,减少重复代码,便于后续修改测试基础配置时保持一致。

  5. 暂无安全性或性能问题
    变更中未发现硬编码敏感信息、执行外部代码等风险;测试代码逻辑简单,也无可观察到的性能问题。

Expand Down
40 changes: 20 additions & 20 deletions testing/ops/test_destroy_unit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""destroy_ships_auto 模式调度单元测试 (无设备)。
"""destroy_ships_auto 范围调度单元测试 (无设备)。

验证 disable / include / exclude 三种工作模式 + remove_equipment 的派发逻辑,
验证 all / destroy_only / keep_only 三种解装范围 + remove_equipment 的派发逻辑,
以及 ``from_dialog`` 弹窗直达路线 (点弹窗「解装」直达建造页后复用
destroy_ships)。通过 monkeypatch 拦截导航 / UI 层, 不触发真实 IO。
"""
Expand Down Expand Up @@ -51,39 +51,39 @@ def _fake_destroy_ships(
return calls


def test_disable_uses_quick_route_no_filter(recorded: list[dict]):
"""disable (不启用舰种分类): 不过滤, 走快速拆解路线, 解装全部。"""
def test_all_uses_quick_route_no_filter(recorded: list[dict]):
"""all (拆解全部): 不过滤, 走快速拆解路线, 解装全部。"""
from autowsgr.ops.destroy import destroy_ships_auto

ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.disable))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.all))
assert destroy_ships_auto(ctx) is True
assert recorded == [{'ship_types': None, 'remove_equipment': True}]


def test_include_passes_listed_types(recorded: list[dict]):
def test_destroy_only_passes_listed_types(recorded: list[dict]):
from autowsgr.ops.destroy import destroy_ships_auto

types = [ShipType.DD, ShipType.CL]
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.include, types=types, remove_eq=False))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.destroy_only, types=types, remove_eq=False))
assert destroy_ships_auto(ctx) is True
assert recorded == [{'ship_types': types, 'remove_equipment': False}]


def test_include_empty_types_means_all(recorded: list[dict]):
"""include + 空舰种列表 → ship_types=None (不过滤, 全量解装)。"""
def test_destroy_only_empty_types_means_all(recorded: list[dict]):
"""destroy_only + 空舰种列表 → ship_types=None (不过滤, 全量解装)。"""
from autowsgr.ops.destroy import destroy_ships_auto

ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.include))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.destroy_only))
assert destroy_ships_auto(ctx) is True
assert recorded == [{'ship_types': None, 'remove_equipment': True}]


def test_exclude_computes_complement(recorded: list[dict]):
"""exclude (白名单): 解装除指定舰种外的所有非 Other 舰种。"""
def test_keep_only_computes_complement(recorded: list[dict]):
"""keep_only (保留舰种): 解装除指定舰种外的所有非 Other 舰种。"""
from autowsgr.ops.destroy import destroy_ships_auto

protected = [ShipType.CV]
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.exclude, types=protected))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.keep_only, types=protected))
assert destroy_ships_auto(ctx) is True

call = recorded[0]
Expand All @@ -96,12 +96,12 @@ def test_exclude_computes_complement(recorded: list[dict]):
assert call['remove_equipment'] is True


def test_exclude_all_types_returns_false(recorded: list[dict]):
"""白名单覆盖全部非 Other 舰种 → 无可解装对象 → 返回 False。"""
def test_keep_only_all_types_returns_false(recorded: list[dict]):
"""保留舰种覆盖全部非 Other 舰种 → 无可解装对象 → 返回 False。"""
from autowsgr.ops.destroy import destroy_ships_auto

all_real = [t for t in ShipType if t is not ShipType.Other]
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.exclude, types=all_real))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.keep_only, types=all_real))
assert destroy_ships_auto(ctx) is False
assert recorded == []

Expand Down Expand Up @@ -136,7 +136,7 @@ def test_from_dialog_clicks_dialog_then_reuses_destroy_ships(
)

types = [ShipType.DD, ShipType.CL]
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.include, types=types, remove_eq=False))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.destroy_only, types=types, remove_eq=False))
assert destroy_ships_auto(ctx, from_dialog=True) is True
# 先点弹窗「解装」直达建造页, 再复用 destroy_ships (其 goto_page 幂等直达)
assert calls == [
Expand All @@ -160,12 +160,12 @@ def test_default_skips_dialog_entry(self, monkeypatch: pytest.MonkeyPatch):
lambda *_a, **_k: calls.append(('destroy',)),
)

ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.disable))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.all))
assert destroy_ships_auto(ctx) is True
assert calls == [('destroy',)]

def test_from_dialog_exhausted_whitelist_skips_all(self, monkeypatch: pytest.MonkeyPatch):
"""白名单覆盖全部舰种 → 弹窗不点、解装不执行。"""
"""保留舰种覆盖全部舰种 → 弹窗不点、解装不执行。"""
from autowsgr.ops.destroy import destroy_ships_auto

calls: list[tuple] = []
Expand All @@ -181,6 +181,6 @@ def test_from_dialog_exhausted_whitelist_skips_all(self, monkeypatch: pytest.Mon
)

all_real = [t for t in ShipType if t is not ShipType.Other]
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.exclude, types=all_real))
ctx = _FakeCtx(_FakeConfig(DestroyShipWorkMode.keep_only, types=all_real))
assert destroy_ships_auto(ctx, from_dialog=True) is False
assert calls == []
Comment thread
kuainx marked this conversation as resolved.
2 changes: 1 addition & 1 deletion testing/ops/test_normal_fight_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ def test_destroy_success_marks_destroyed_keeps_flag(self, monkeypatch: pytest.Mo
assert result.dock_full_destroyed is True

def test_destroy_exhausted_whitelist_keeps_flag(self, monkeypatch: pytest.MonkeyPatch):
"""白名单覆盖全部舰种 → 无可解装对象 → 保持 DOCK_FULL。"""
"""保留舰种覆盖全部舰种 → 无可解装对象 → 保持 DOCK_FULL。"""
import autowsgr.ops.destroy as destroy_module

monkeypatch.setattr(destroy_module, 'destroy_ships_auto', lambda _ctx, **_k: False)
Expand Down
8 changes: 4 additions & 4 deletions usersettings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ repair_manually: false # 是否手动修理
bathroom_count: 2 # 浴室修理槽位数 (auto_bath_repair 空位调度用)

# ── 解装设置 ──
destroy_ship_work_mode: 不启用 # 不启用 / 黑名单 / 白名单
# 不启用: 解装所有舰船
# 黑名单: 解装列表中的舰种
# 白名单: 解装不在列表中的舰种
destroy_ship_work_mode: 拆解全部 # 拆解全部 / 拆解舰种 / 保留舰种
# 拆解全部: 解装所有舰船 (不按舰种分类, 走快速拆解)
# 拆解舰种: 只解装列表中的舰种
# 保留舰种: 解装不在列表中的舰种
destroy_ship_types: [] # 指定舰种列表, 参照 ShipType 枚举使用中文
# - 航母
# - 潜艇
Comment thread
kuainx marked this conversation as resolved.
Expand Down
Loading