diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index bbfb9cd751..548480fc02 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -36,6 +36,10 @@ "close_game": "关闭游戏", "close_game_wait_duration": "关闭游戏的等待时长", "close_game_wait_duration_help": "距离下一个任务超过此分钟数时关闭游戏(上方选项为前往庭院时无效);设为0表示禁用", + "close_game_night_start": "关闭游戏的时段-开始", + "close_game_night_start_help": "仅在该时刻之后(含)才允许关闭游戏;与下方结束时刻相同表示不限时段", + "close_game_night_end": "关闭游戏的时段-结束", + "close_game_night_end_help": "仅在该时刻之前(不含)才允许关闭游戏。例如 00:00 - 07:00 表示只在凌晨关游戏,白天队列为空时把游戏留在原地,方便同一台设备随时手动接手", "close_emulator_wait_duration": "关闭模拟器的等待时长", "close_emulator_wait_duration_help": "距离下一个任务超过此分钟数时关闭模拟器;设为0表示禁用", "emulator_startup_lead_time": "模拟器预热时间", diff --git a/script.py b/script.py index 72fadaccea..90585cbe79 100644 --- a/script.py +++ b/script.py @@ -407,6 +407,7 @@ def _handle_wait_during_idle(self, next_run: datetime) -> bool: strategy_map = { "close_game": self._wait_close_game, "goto_main": self._wait_goto_main, + "stay_there": self._wait_stay_there, } func = strategy_map.get(method) if func is None: @@ -469,6 +470,31 @@ def _wait_close_game(self, next_run: datetime) -> bool: self.run("Restart") return True + # 时段闸门: 只在 [night_start, night_end) 内才关游戏。 + # 白天/晚上把游戏留在原地, 方便用户在挂机的同一台设备上随时手动接手。 + # start == end 视为不限制 (维持旧行为)。 + night_start = self._time_to_timedelta( + self.config.script.optimization.close_game_night_start) + night_end = self._time_to_timedelta( + self.config.script.optimization.close_game_night_end) + if night_start != night_end and not self._in_close_game_window(night_start, night_end): + # 分段等待: 不能一次 wait_until(next_run) 睡过凌晨边界, 否则醒来时 + # 判断点已过, 整夜都不会关游戏。改为等到 min(下个任务, 下个凌晨边界), + # 到点返回让调度器重新进来, 那时已落在允许窗口内。 + now = datetime.now() + boundary = (datetime.combine(now.date(), (datetime.min + night_start).time()) + if night_start else now.replace(hour=0, minute=0, second=0, microsecond=0)) + while boundary <= now: + boundary += timedelta(days=1) + segment_end = min(next_run, boundary) + logger.info(f"Close game is disabled outside " + f"{night_start} - {night_end}, keep game running " + f"(re-evaluate at {segment_end})") + self.device.release_during_wait() + if not self.wait_until(segment_end): + return False + return True + if close_game_wait <= timedelta(0): logger.info("Close game during wait") self.device.app_stop() @@ -516,6 +542,25 @@ def _wait_goto_main(self, next_run: datetime) -> bool: self.device.release_during_wait() return self.wait_until(next_run) + @staticmethod + def _in_close_game_window(night_start: timedelta, night_end: timedelta) -> bool: + """ + 当前是否处于允许关闭游戏的时段。 + + Args: + night_start: 窗口起点 (距离 0 点的偏移) + night_end: 窗口终点 (不含) + + Returns: + bool + """ + now = datetime.now() + cur = timedelta(hours=now.hour, minutes=now.minute, seconds=now.second) + if night_start < night_end: + return night_start <= cur < night_end + # 跨 0 点的窗口, 例如 23:00 - 07:00 + return cur >= night_start or cur < night_end + def _wait_stay_there(self, next_run: datetime) -> bool: if self._emulator_down: logger.info("Stay_there during wait (emulator is down, with preheat)") diff --git a/tasks/Script/config_optimization.py b/tasks/Script/config_optimization.py index 421a46504d..c469519984 100644 --- a/tasks/Script/config_optimization.py +++ b/tasks/Script/config_optimization.py @@ -10,6 +10,10 @@ class WhenTaskQueueEmpty(str, Enum): GOTO_MAIN = 'goto_main' CLOSE_GAME = 'close_game' + # GUI(argument.yaml) 早已提供 stay_there 选项, 但枚举缺少该值, + # 导致用户选中后被 pydantic 拒绝 / 或走过 _wait_stay_there 的 fallback 分支。 + # 补上后 _wait_stay_there() 才可达。 + STAY_THERE = 'stay_there' class ScheduleRule(str, Enum): FILTER = 'Filter' # 默认的基于过滤器,(按照开发者设定的调度规则进行调度) @@ -27,6 +31,13 @@ class Optimization(BaseModel): description='when_task_queue_empty_help') close_game_wait_duration: Time = Field(default=Time(minute=0), description='close_game_wait_duration_help') + # 仅在 [night_start, night_end) 这个时段内才允许「关闭游戏」。 + # 默认 00:00 - 07:00: 白天/晚上把游戏留在原地, 方便同一台设备随时手动接手玩; + # 若 start == end 表示不做时段限制 (维持旧行为)。 + close_game_night_start: Time = Field(default=Time(hour=0, minute=0), + description='close_game_night_start_help') + close_game_night_end: Time = Field(default=Time(hour=7, minute=0), + description='close_game_night_end_help') close_emulator_wait_duration: Time = Field(default=Time(minute=0), description='close_emulator_wait_duration_help') emulator_startup_lead_time: Time = Field(default=Time(minute=2),