Skip to content

Commit 13cc4c4

Browse files
committed
feat(timing): 清零规则改用 BattleBegin + 费用条可见性 + 遮挡 hold
清零规则(替代旧 1.5s timeout + 30 帧 out_of_battle reset): - BattleBegin + armed → reset(标题屏清零) - NotInBattle + lf=None 连续 >= 180 帧 → 标记重入清零;下次 InBattle → reset - InBattle 时 None 不计数(部署遮挡不清零) - 暂停不清零(费用条仍可见) 遮挡 hold(lf 大幅回退但不接近 0 → hold): - pw=偏小值:lf 回退到中间值(>= _WRAP_MARGIN)→ hold - pw=0:lf 回退到 0~2 但 prev 不在满条附近 → hold - 正常 wrap:lf 从满条回到 0~2 → 不 hold - 负费切换:neg_changed → 不 hold None 时 hold 显示(不设 -1),删除 reset_timeout 死代码。
1 parent 3e9b10a commit 13cc4c4

1 file changed

Lines changed: 96 additions & 15 deletions

File tree

aao/core/timing/time_source.py

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
核心机制:
77
- 喂帧 ``update(frame)`` → 当前逻辑帧(周期内)。
88
- 周期 wrap 检测:帧显著下降(> _WRAP_MARGIN)→ cycle_counter++、累计 cycle_base。
9+
- 战斗状态检测(仅用于清零规则):InBattle → arm;BattleBegin + armed → reset_timer。
10+
不 gate 费用条读取(暂停/部署/结算时 pause glyph 可能弱化,gate 会丢帧)。
11+
暂停不清零(费用条仍可见);退出战斗后费用条消失 >= 30 帧 → 标记重入清零;
12+
重新 InBattle → reset(进关卡清计时器)。
913
- PLL 外推:当测量帧连续不变(费用条被遮挡/走不满)时,按最近 wrap 间隔估算的周期外推帧。
1014
- 全局计时器 ``total_elapsed = timer_offset + cycle_base + logical_frame``。
11-
- 1.5s 无检测 → 重置
15+
- None 时 hold 显示(不设 -1),不再用 timeout 清零
1216
"""
1317

1418
from __future__ import annotations
@@ -19,12 +23,15 @@
1923

2024
from aao import config
2125
from aao.core.timing import tick
26+
from aao.core.timing.battle_state import BattleState, detect_battle_state
2227
from aao.core.timing.calibration import FullCalibrationData
2328
from aao.utils.logger import logger
2429

25-
RESET_TIMEOUT_S = 1.5
2630
_WRAP_MARGIN = 3 # 帧下降超过此值 → 判定周期 wrap(过滤抖动)
2731
_BOUNDARY_SWITCH_FRAME = 315 # 开局第 315 逻辑帧所在周期为边界周期
32+
# 费用条连续不可见帧数阈值:NotInBattle + lf=None 连续 >= 此帧数
33+
# → 标记重入清零。部署遮挡通常 < 6 秒(180帧@30fps),退出战斗到重进更长。
34+
_NO_COST_BAR_RESET_FRAMES = 180
2835

2936

3037
def format_timer(total_frames: int, fps: int = config.FRAMES_PER_SECOND) -> str:
@@ -61,16 +68,14 @@ def _compute_boundary_cycle(
6168
class TimeSource:
6269
"""费用条时间状态机 + PLL 外推。喂帧驱动,产出周期帧 / 全局计时器。"""
6370

64-
def __init__(self, calibration: FullCalibrationData, reset_timeout: float = RESET_TIMEOUT_S):
71+
def __init__(self, calibration: FullCalibrationData):
6572
self.calibration = calibration
6673
self.profiles = calibration.profiles
67-
self.reset_timeout = reset_timeout
6874

6975
self.cycle_counter = 0
7076
self.cycle_base_frames = 0
7177
self.timer_offset = 0
7278
self.previous_frame = -1 # 显示用(可能含外推)
73-
self.last_detect_time = 0.0
7479
self._total_frames = 0
7580

7681
# 负费(可露希尔)状态
@@ -87,11 +92,24 @@ def __init__(self, calibration: FullCalibrationData, reset_timeout: float = RESE
8792

8893
# PLL 外推状态
8994
self._prev_measured = -1 # 上次测量帧(用于 wrap 检测)
95+
self._prev_cost_is_negative = False # 上次负费状态(排除负费切换的遮挡 hold)
9096
self._stuck_lf: int | None = None # 卡住时的测量帧
9197
self._stuck_since = 0.0 # 卡住开始时间
9298
self._last_wrap_time = 0.0 # 上次 wrap 时间
9399
self._cycle_period = 0.0 # 估算的周期(秒)
94100

101+
# BattleBegin armed 机制(对应 Rust engine.rs:260-265):
102+
# InBattle → arm;BattleBegin + armed → reset_timer + disarm。
103+
# 避免在非战斗界面误清零,只在"刚打完一局 → 进入下一局标题屏"时触发。
104+
self._battle_begin_reset_armed = False
105+
106+
# 费用条消失计数器 + 重入清零标志:
107+
# NotInBattle 且 lf=None(费用条不可见)连续 >= _NO_COST_BAR_RESET_FRAMES 帧
108+
# → 标记 _needs_reentry_reset。下次 InBattle → reset(进关卡清计时器)。
109+
# 暂停时费用条仍可见(lf != None)→ 不计数 → 不清零。
110+
self._no_cost_bar_frames = 0
111+
self._needs_reentry_reset = False
112+
95113
@property
96114
def num_profiles(self) -> int:
97115
return len(self.profiles)
@@ -102,6 +120,34 @@ def active_profile(self):
102120

103121
def update(self, frame: np.ndarray) -> int | None:
104122
"""喂一帧,更新状态。返回当前周期内逻辑帧(含 PLL 外推,None = 未检出)。"""
123+
now = time.time()
124+
125+
# --- 战斗状态检测(仅用于清零规则,不 gate 费用条读取)---
126+
# 对应 Rust engine.rs:253-265。
127+
# InBattle → arm;
128+
# NotInBattle(暂停/部署/结算)→ 仍读费用条,不清零。
129+
# 暂停时费用条仍可见 → 不清零。
130+
# 退出战斗后费用条消失(lf=None 连续 >= _NO_COST_BAR_RESET_FRAMES 帧)→ 标记清零。
131+
# 重新 InBattle 时如果标记了清零 → reset(进关卡清计时器)。
132+
battle_state = detect_battle_state(frame)
133+
if battle_state.is_in_battle:
134+
if self._needs_reentry_reset:
135+
logger.debug("time_source: 重新进入战斗,重置计时器")
136+
self._reset_state()
137+
self._needs_reentry_reset = False
138+
self._battle_begin_reset_armed = True
139+
# reset 后继续读费用条(首帧 lf 作为新起点)
140+
else:
141+
self._battle_begin_reset_armed = True
142+
self._no_cost_bar_frames = 0
143+
elif battle_state is BattleState.BATTLE_BEGIN and self._battle_begin_reset_armed:
144+
logger.debug("time_source: BattleBegin 标题屏 + armed → 重置计时器")
145+
self._reset_state()
146+
self._battle_begin_reset_armed = False
147+
self._needs_reentry_reset = False
148+
self._no_cost_bar_frames = 0
149+
return None
150+
105151
roi = tick.find_cost_bar_roi(frame.shape[1], frame.shape[0])
106152
profile = self.active_profile
107153
total_this = profile.total_frames
@@ -126,7 +172,6 @@ def update(self, frame: np.ndarray) -> int | None:
126172
lf_measured = None
127173
else:
128174
lf_measured = tick.get_logical_frame(frame, roi, profile.pixel_map)
129-
now = time.time()
130175

131176
# 边界后周期去掉 hidden 帧:校准在慢速下检测到的隐藏辉光帧(如 frame 1
132177
# 无独立宽度)在边界后左开右闭周期里不存在,需从帧序列中跳过。
@@ -158,16 +203,48 @@ def update(self, frame: np.ndarray) -> int | None:
158203
lf_measured = total_this - 1 # 之后周期:满条 → frame 29
159204

160205
if lf_measured is None:
161-
self.previous_frame = -1
162-
# 不重置 _prev_measured:保留上次有效值,以便 wrap 瞬间的短暂 None 后仍能检测周期
206+
# 费用条被遮挡或不可见 → hold 显示。
207+
# 同时累计费用条消失帧数:连续不可见 >= 阈值 → 标记重入清零。
208+
# 但只在 NotInBattle 时计数:InBattle 时 None 是遮挡(部署框),
209+
# 不是费用条消失,不应触发清零。
163210
self._stuck_lf = None
164-
if self.last_detect_time and now - self.last_detect_time > self.reset_timeout:
165-
if self.cycle_counter or self.cycle_base_frames or self.timer_offset:
166-
logger.debug("time_source: %ss 无检测,重置", self.reset_timeout)
167-
self._reset_state()
168-
return None
211+
if self._battle_begin_reset_armed and not battle_state.is_in_battle:
212+
self._no_cost_bar_frames += 1
213+
if (
214+
self._no_cost_bar_frames >= _NO_COST_BAR_RESET_FRAMES
215+
and not self._needs_reentry_reset
216+
):
217+
self._needs_reentry_reset = True
218+
logger.debug(
219+
"time_source: 费用条连续 %d 帧不可见,标记重入清零",
220+
self._no_cost_bar_frames,
221+
)
222+
return self.previous_frame if self.previous_frame >= 0 else None
223+
224+
# 费用条可见 → 重置消失计数
225+
self._no_cost_bar_frames = 0
226+
227+
# --- 遮挡 hold ---
228+
# 两种遮挡模式:
229+
# A. lf 回退到中间值(pw=偏小非0)→ hold
230+
# B. lf 回退到 0~2 但 prev 不在满条附近(pw=0,遮挡物是灰色)→ hold
231+
# 正常 wrap:lf 从满条(>= eff_total - _WRAP_MARGIN)回到 0~2 → 不 hold
232+
# 负费切换:eff_total 60↔30 导致 lf 跳变 → 不 hold(neg_changed)
233+
neg_changed = self._cost_is_negative != self._prev_cost_is_negative
234+
prev_high = self._prev_measured >= effective_total - _WRAP_MARGIN
235+
if (
236+
not neg_changed
237+
and self._prev_measured >= 0
238+
and lf_measured < self._prev_measured - _WRAP_MARGIN
239+
and (lf_measured >= _WRAP_MARGIN or not prev_high)
240+
):
241+
return self.previous_frame if self.previous_frame >= 0 else None
169242

170-
# --- wrap 检测(基于测量帧)---
243+
# --- wrap 检测(整数帧差)---
244+
# 旧逻辑:lf 显著下降(> _WRAP_MARGIN)→ 周期 wrap。
245+
# 曾尝试改用相位(prev>0.75 && cur<0.25),但负费切换时
246+
# total_frames_in_cycle 从 60 变 30 导致相位不连续,误判。
247+
# 保持整数帧差,与原始行为一致。
171248
wrapped = False
172249
if self._prev_measured >= 0 and lf_measured < self._prev_measured - _WRAP_MARGIN:
173250
# 更新周期估算
@@ -205,8 +282,8 @@ def update(self, frame: np.ndarray) -> int | None:
205282
lf = lf_measured
206283

207284
self._prev_measured = lf_measured
285+
self._prev_cost_is_negative = self._cost_is_negative
208286
self.previous_frame = lf
209-
self.last_detect_time = now
210287
self._total_frames = self.timer_offset + self.cycle_base_frames + lf
211288
return lf
212289

@@ -230,11 +307,15 @@ def _reset_state(self) -> None:
230307
self._total_frames = 0
231308
self.previous_frame = -1
232309
self._prev_measured = -1
310+
self._prev_cost_is_negative = False
233311
self._stuck_lf = None
234312
self._stuck_since = 0.0
235313
self._last_wrap_time = 0.0
236314
self._cycle_period = 0.0
237315
self._cost_is_negative = False
316+
self._no_cost_bar_frames = 0
317+
self._needs_reentry_reset = False
318+
# 不清 _battle_begin_reset_armed:由 BattleBegin 转移逻辑控制
238319

239320
# --- 查询 ---
240321

0 commit comments

Comments
 (0)