Skip to content
Merged
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
11 changes: 4 additions & 7 deletions deploy/uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
BOOTSTRAPPED_ENV = "AZURPILOT_UV_BOOTSTRAPPED"
BOOTSTRAP_UV_ENV = "AZURPILOT_BOOTSTRAP_UV"
NO_BOOTSTRAP_ENV = "AZURPILOT_NO_UV_BOOTSTRAP"
PYTHON_VERSION = "3.14.3"
DEPENDENCY_SYNC_TIMEOUT = 30 * 60


Expand Down Expand Up @@ -183,10 +182,10 @@ def _uv_python_env(root: Path):

def _managed_python_executable(root: Path) -> Optional[Path]:
install_dir = venv_python_install_dir(root)
for python_home in sorted(install_dir.glob(f"cpython-{PYTHON_VERSION}-*")):
for python_home in sorted(install_dir.glob("cpython-*-*"), reverse=True):
candidates = [
python_home / "python.exe",
python_home / "bin" / "python3.14",
python_home / "bin" / "python3",
Comment on lines +185 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): 基于路径的反向排序方式可能无法稳定地选出版本最高的 cpython 目录

这个循环依赖对诸如 cpython-3.10.5-*cpython-3.9.18-* 的名称进行字典序反向排序。根据具体命名(例如 cpython-3.9.18-*cpython-3.11.0-*)或未来格式变更,字符串排序可能无法反映真实的版本顺序。如果目标是选择最新的解释器,建议从目录名中提取版本,并按 (major, minor, patch) 元组排序,而不是按原始字符串排序,以避免在存在多个安装时选到较旧的版本。

建议实现方式:

DEPENDENCY_SYNC_TIMEOUT = 30 * 60


def _cpython_dir_version(path: Path) -> tuple[int, int, int]:
    """Extract a (major, minor, patch) tuple from a cpython install dir name.

    Expected formats include:
    - cpython-3.10.5-*
    - cpython-3.11-*
    Falls back to (0, 0, 0) if parsing fails so those entries are sorted last.
    """
    name = path.name
    # Expected name pattern: cpython-<version>-<suffix>
    parts = name.split("-")
    if len(parts) < 3 or parts[0] != "cpython":
        return (0, 0, 0)

    version_str = parts[1]
    version_components = version_str.split(".")
    # Normalize to (major, minor, patch) length
    try:
        major = int(version_components[0]) if len(version_components) > 0 else 0
        minor = int(version_components[1]) if len(version_components) > 1 else 0
        patch = int(version_components[2]) if len(version_components) > 2 else 0
    except ValueError:
        # Non-numeric version segment; treat as lowest priority
        return (0, 0, 0)

    return (major, minor, patch)


def _managed_python_executable(root: Path) -> Optional[Path]:
    install_dir = venv_python_install_dir(root)
    python_homes = sorted(
        install_dir.glob("cpython-*-*"),
        key=_cpython_dir_version,
        reverse=True,
    )
    for python_home in python_homes:
Original comment in English

suggestion (bug_risk): Path-based reverse sorting of cpython dirs may not pick the highest version consistently

This loop relies on reverse lexicographic ordering of names like cpython-3.10.5-* and cpython-3.9.18-*. Depending on the exact naming (e.g. cpython-3.9.18-* vs cpython-3.11.0-*) or future format changes, string ordering may not reflect actual version ordering. If the goal is to pick the newest interpreter, consider extracting the version from the directory name and sorting by a (major, minor, patch) tuple rather than the raw string to avoid selecting an older version when multiple installs exist.

Suggested implementation:

DEPENDENCY_SYNC_TIMEOUT = 30 * 60


def _cpython_dir_version(path: Path) -> tuple[int, int, int]:
    """Extract a (major, minor, patch) tuple from a cpython install dir name.

    Expected formats include:
    - cpython-3.10.5-*
    - cpython-3.11-*
    Falls back to (0, 0, 0) if parsing fails so those entries are sorted last.
    """
    name = path.name
    # Expected name pattern: cpython-<version>-<suffix>
    parts = name.split("-")
    if len(parts) < 3 or parts[0] != "cpython":
        return (0, 0, 0)

    version_str = parts[1]
    version_components = version_str.split(".")
    # Normalize to (major, minor, patch) length
    try:
        major = int(version_components[0]) if len(version_components) > 0 else 0
        minor = int(version_components[1]) if len(version_components) > 1 else 0
        patch = int(version_components[2]) if len(version_components) > 2 else 0
    except ValueError:
        # Non-numeric version segment; treat as lowest priority
        return (0, 0, 0)

    return (major, minor, patch)


def _managed_python_executable(root: Path) -> Optional[Path]:
    install_dir = venv_python_install_dir(root)
    python_homes = sorted(
        install_dir.glob("cpython-*-*"),
        key=_cpython_dir_version,
        reverse=True,
    )
    for python_home in python_homes:

python_home / "bin" / "python",
]
for candidate in candidates:
Expand All @@ -204,7 +203,7 @@ def _venv_python_works(root: Path) -> bool:
[
str(python),
"-c",
"import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 14) else 1)",
"import sys; raise SystemExit(0)",
],
cwd=str(root),
stdout=subprocess.DEVNULL,
Expand Down Expand Up @@ -335,10 +334,10 @@ def _ensure_self_contained_python(
outputs: Optional[list[str]] = None,
deadline: float | None = None,
):
env = _uv_python_env(root)
if _venv_python_works(root) and _managed_python_executable(root):
return

env = _uv_python_env(root)
managed_python = _managed_python_executable(root)
if managed_python is None:
command = [
Expand All @@ -349,7 +348,6 @@ def _ensure_self_contained_python(
venv_python_install_dir(root),
"--no-bin",
"--managed-python",
PYTHON_VERSION,
]
_run_and_collect(
command,
Expand All @@ -365,7 +363,6 @@ def _ensure_self_contained_python(
"python",
"find",
"--managed-python",
PYTHON_VERSION,
]
output = _run_output(
command,
Expand Down
18 changes: 9 additions & 9 deletions module/coalition/coalition_scuttle.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
OPTS_INFO_D,
EXP_INFO_D, EXP_INFO_A, EXP_INFO_B, EXP_INFO_S
)
from module.handler.assets import GET_MISSION
from module.coalition.assets import *
from module.coalition.combat import CoalitionCombat
from module.coalition.coalition import Coalition
Expand All @@ -21,6 +20,7 @@ class CoalitionScuttleCombat(CoalitionCombat):

triggered_normal_end = False
_is_shipwreck = False # 当前战斗是否为沉船D评价
_is_s_rank = False # 当前战斗是否为S评价

def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expected_end=None):
"""
Expand Down Expand Up @@ -91,10 +91,8 @@ def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expecte
break
# D评价结算界面:S/A/B评价的动画过渡帧可能短暂误匹配D评价模板,
# 但只有真正的沉船才会出现OPTS_INFO_D弹窗。
# 此处仅break退出循环,不设置沉船标记(未经过OPTS_INFO_D确认)。
# 真正的沉船由上方OPTS_INFO_D路径触发并设置_is_shipwreck。
# 此处不设置沉船标记(未经过OPTS_INFO_D确认),让后续S/A/B条件覆盖。
if self.appear(BATTLE_STATUS_D) or self.appear(EXP_INFO_D):
self._withdraw = True
break
if confirm_timer.reached():
self._withdraw = True
Expand All @@ -111,7 +109,8 @@ def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expecte

# S评价或自动搜索运行中
if self.appear(BATTLE_STATUS_S) or self.appear(EXP_INFO_S) \
or self.appear(GET_MISSION) or self.is_auto_search_running():
or self.is_auto_search_running():
self._is_s_rank = True
self.device.screenshot_interval_set()
break

Expand All @@ -136,6 +135,7 @@ def coalition_combat(self):
while 1:
logger.hr(f'{self.FUNCTION_NAME_BASE}{self.battle_count}', level=2)
self._is_shipwreck = False
self._is_s_rank = False
# 仅第一场战斗扣减2心情(关卡进入代价),后续战斗不再扣减
self.auto_search_combat_execute(
emotion_reduce=self.battle_count == 0,
Expand Down Expand Up @@ -387,10 +387,10 @@ def run(self, event='', mode='', fleet='', total=0):
if self.config.StopCondition_RunCount:
self.config.StopCondition_RunCount -= 1

# SP关卡非D评价(沉船):视为已通过,延迟至服务器刷新
# D评价视为未通过,继续出击
if mode == 'sp' and self.triggered_normal_end and not self._is_shipwreck:
logger.info('SP以非D评价通过')
# SP关卡仅S评价视为已通过,延迟至服务器刷新
# A/B/C/D评价均视为未通过,继续出击
if mode == 'sp' and self._is_s_rank and not self._is_shipwreck:
logger.info('SP以S评价通过')
self.config.task_delay(server_update=True)
self.config.task_stop()

Expand Down
42 changes: 34 additions & 8 deletions module/combat/auto_search_combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ class AutoSearchCombat(MapOperation, Combat, CampaignStatus):
_auto_search_status_confirm (bool): 自动搜索状态是否已确认。
_withdraw (bool): 是否已执行撤退。
_defeat_count (int): 战败次数。
_shipwreck_emotion_reduced (bool): 沉船心情扣减是否已执行,防止重复扣减。
_auto_search_emotion_reduce (bool): 当前战斗是否启用心情扣减。
_auto_search_fleet_index (int): 当前战斗的舰队索引。
auto_search_oil_limit_triggered (bool): 石油限制是否已触发。
auto_search_coin_limit_triggered (bool): 物资限制是否已触发。
"""
_auto_search_in_stage_timer = Timer(3, count=6)
_auto_search_status_confirm = False
_withdraw = False
_defeat_count = 0
_shipwreck_emotion_reduced = False
_auto_search_emotion_reduce = False
_auto_search_fleet_index = 1
auto_search_oil_limit_triggered = False
auto_search_coin_limit_triggered = False

Expand Down Expand Up @@ -314,24 +320,23 @@ def auto_search_combat_execute(self, emotion_reduce, fleet_index, battle=None, e
if self.handle_get_ship():
continue
if self.appear_then_click(OPTS_INFO_D, offset=(30, 30), interval=2):
if emotion_reduce:
if emotion_reduce and not self._shipwreck_emotion_reduced:
self.emotion.reduce(fleet_index, shipwreck=True)
self._shipwreck_emotion_reduced = True
self._withdraw = True
break
# D评价结算界面(BATTLE_STATUS_D / EXP_INFO_D)
# S/A/B评价的动画过渡帧可能短暂误匹配D评价模板,
# 但只有真正的沉船才会出现OPTS_INFO_D弹窗。
# 此处仅break退出循环,不扣减shipwreck心情。
# 真正的沉船扣减由上方OPTS_INFO_D路径触发。
# 退出后由auto_search_combat_status()中的handle_battle_status()处理结算画面。
# 此处不设置 _withdraw,让后续S/A/B评价条件覆盖误匹配。
# 真正的D评价会先被上方OPTS_INFO_D捕获。
if self.appear(BATTLE_STATUS_D) or self.appear(EXP_INFO_D):
self._withdraw = True
break
if confirm_timer.reached():
# 结算确认超时:不扣心情、不盲目点击OPTS_INFO_D
# 只设置_withdraw让status处理,status中检测到OPTS_INFO_D才扣心情
logger.warning('[自动搜索-战斗] 结算确认超时,进入status处理')
self._withdraw = True
self.device.click(OPTS_INFO_D)
if emotion_reduce:
self.emotion.reduce(fleet_index, shipwreck=True)
confirm_timer.reset()
break
if self.appear(BATTLE_STATUS_A) or self.appear(BATTLE_STATUS_B) \
Expand Down Expand Up @@ -505,6 +510,24 @@ def auto_search_combat_status(self):
if self.handle_mission_popup_ack():
continue

# 处理战斗结算界面——SABC评价在自动搜索中可能快速自动过渡,
# 若截图恰好捕获到结算画面则点击推进并记录评价
# D评价点击BATTLE_STATUS_D后,会出现OPTS_INFO_D沉船弹窗
if self.handle_battle_status():
continue
if self.handle_exp_info():
continue
# 检测D评价(沉船)弹窗——这是沉船的确认性标志(二次确认)
# 只有OPTS_INFO_D出现才确认是真正的D评价并扣心情
# S/A/B/C转场误匹配BATTLE_STATUS_D不会出现OPTS_INFO_D,不会扣心情
if self.appear(OPTS_INFO_D, offset=(30, 30)):
logger.info('[自动搜索-结算] 检测到沉船弹窗,进入撤退处理')
if self._auto_search_emotion_reduce and not self._shipwreck_emotion_reduced:
self.emotion.reduce(self._auto_search_fleet_index, shipwreck=True)
self._shipwreck_emotion_reduced = True
self._withdraw = True
continue

# Handle low emotion combat
# Combat status
if self._auto_search_status_confirm:
Expand All @@ -529,6 +552,9 @@ def auto_search_combat(self, emotion_reduce=None, fleet_index=1, battle=None):
"""
emotion_reduce = emotion_reduce if emotion_reduce is not None else self.emotion.is_calculate

self._auto_search_emotion_reduce = emotion_reduce
self._auto_search_fleet_index = fleet_index
self._shipwreck_emotion_reduced = False
self.auto_search_combat_execute(emotion_reduce=emotion_reduce, fleet_index=fleet_index, battle=battle)
self.auto_search_combat_status()

Expand Down
Loading