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
1418from __future__ import annotations
1923
2024from aao import config
2125from aao .core .timing import tick
26+ from aao .core .timing .battle_state import BattleState , detect_battle_state
2227from aao .core .timing .calibration import FullCalibrationData
2328from 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
3037def format_timer (total_frames : int , fps : int = config .FRAMES_PER_SECOND ) -> str :
@@ -61,16 +68,14 @@ def _compute_boundary_cycle(
6168class 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