feat(*): 优化若干流程

1. 添加 wait() 函数,允许在截图前等待指定时间
2. 以调试模式启动时,新增了删除原有调试 dump 文件夹时对文件被占用的处理
3. 修复了培育任务无法从已有培育流程开始的问题
4. 修复了购买推荐商品会卡住的问题
5. 优化未读交流判断逻辑提高准确率
6. 新增练习/考试中对本回合已无可用手牌情况的判断
7. 优化若干流程在 u2 截图模式下的逻辑
8. 消息推送新增失败时自动重试
This commit is contained in:
XcantloadX 2025-02-16 17:11:35 +08:00
parent be7acd3102
commit 726515774e
17 changed files with 152 additions and 37 deletions

View File

@ -13,7 +13,8 @@ from .backend.context import (
sleep, sleep,
task, task,
action, action,
use_screenshot use_screenshot,
wait
) )
from .backend.util import ( from .backend.util import (
Rect, Rect,

View File

@ -141,7 +141,7 @@ def sleep(seconds: float, /):
""" """
可中断的 sleep 函数 可中断的 sleep 函数
建议使用 `context.sleep()` 代替 `time.sleep()` 建议使用本函数代替 `time.sleep()`
这样能以最快速度响应用户请求中断 这样能以最快速度响应用户请求中断
""" """
global vars global vars
@ -655,6 +655,7 @@ class ContextDevice(Device):
""" """
截图返回截图数据同时更新当前上下文的截图数据 截图返回截图数据同时更新当前上下文的截图数据
""" """
global next_wait, last_screenshot_time, next_wait_time
current = ContextStackVars.ensure_current() current = ContextStackVars.ensure_current()
if force: if force:
current._inherit_screenshot = None current._inherit_screenshot = None
@ -662,6 +663,13 @@ class ContextDevice(Device):
img = current._inherit_screenshot img = current._inherit_screenshot
current._inherit_screenshot = None current._inherit_screenshot = None
else: else:
if next_wait == 'screenshot':
delta = time.time() - last_screenshot_time
if delta < next_wait_time:
sleep(next_wait_time - delta)
last_screenshot_time = time.time()
next_wait_time = 0
next_wait = None
img = self._device.screenshot() img = self._device.screenshot()
current._screenshot = img current._screenshot = img
return img return img
@ -691,7 +699,7 @@ class Context(Generic[T]):
ip = self.config.current.backend.adb_ip ip = self.config.current.backend.adb_ip
port = self.config.current.backend.adb_port port = self.config.current.backend.adb_port
# TODO: 处理链接失败情况 # TODO: 处理链接失败情况
self.__device = ContextDevice(create_device(f'{ip}:{port}', 'adb_raw')) self.__device = ContextDevice(create_device(f'{ip}:{port}', 'adb'))
def inject( def inject(
self, self,
@ -763,6 +771,15 @@ def use_screenshot(*args: MatLike | None) -> MatLike:
return img return img
return device.screenshot() return device.screenshot()
WaitBeforeType = Literal['screenshot']
def wait(at_least: float = 0.3, *, before: WaitBeforeType) -> None:
global next_wait, next_wait_time
if before == 'screenshot':
if time.time() - last_screenshot_time < at_least:
next_wait = 'screenshot'
next_wait_time = at_least
# 这里 Context 类还没有初始化,但是 tasks 中的脚本可能已经引用了这里的变量 # 这里 Context 类还没有初始化,但是 tasks 中的脚本可能已经引用了这里的变量
# 为了能够动态更新这里变量的值,这里使用 Forwarded 类再封装一层, # 为了能够动态更新这里变量的值,这里使用 Forwarded 类再封装一层,
# 将调用转发到实际的稍后初始化的 Context 类上 # 将调用转发到实际的稍后初始化的 Context 类上
@ -781,7 +798,10 @@ debug: ContextDebug = cast(ContextDebug, Forwarded(name="debug"))
"""调试工具。""" """调试工具。"""
config: ContextConfig = cast(ContextConfig, Forwarded(name="config")) config: ContextConfig = cast(ContextConfig, Forwarded(name="config"))
"""配置数据。""" """配置数据。"""
last_screenshot_time: float = -1
"""上一次截图的时间。"""
next_wait: WaitBeforeType | None = None
next_wait_time: float = 0
def init_context( def init_context(
*, *,

View File

@ -7,8 +7,11 @@ from pathlib import Path
from threading import Thread from threading import Thread
from . import debug from . import debug
from kotonebot import logging
from kotonebot.backend.context import init_context from kotonebot.backend.context import init_context
logger = logging.getLogger(__name__)
def _task_thread(task_module: str): def _task_thread(task_module: str):
"""任务线程。""" """任务线程。"""
runpy.run_module(task_module, run_name="__main__") runpy.run_module(task_module, run_name="__main__")
@ -57,7 +60,18 @@ if __name__ == "__main__":
os.makedirs(save_path) os.makedirs(save_path)
if args.clear: if args.clear:
if debug.auto_save_to_folder: if debug.auto_save_to_folder:
shutil.rmtree(debug.auto_save_to_folder) try:
logger.info(f"Removing {debug.auto_save_to_folder}")
shutil.rmtree(debug.auto_save_to_folder)
except PermissionError:
logger.warning(f"Failed to remove {debug.auto_save_to_folder}. Trying to remove all contents instead.")
for root, dirs, files in os.walk(debug.auto_save_to_folder):
for file in files:
try:
os.remove(os.path.join(root, file))
except PermissionError:
raise
# 初始化上下文 # 初始化上下文
module_name, class_name = args.config_type.rsplit('.', 1) module_name, class_name = args.config_type.rsplit('.', 1)

View File

@ -240,13 +240,13 @@ class UntilImage:
self.sd.result = self.result self.sd.result = self.result
class SimpleDispatcher: class SimpleDispatcher:
def __init__(self, name: str, *, interval: float = 0.2): def __init__(self, name: str, *, min_interval: float = 0.3):
self.name = name self.name = name
self.logger = logging.getLogger(f'SimpleDispatcher of {name}') self.logger = logging.getLogger(f'SimpleDispatcher of {name}')
self.blocks: list[Callable] = [] self.blocks: list[Callable] = []
self.finished: bool = False self.finished: bool = False
self.result: Any | None = None self.result: Any | None = None
self.interval = interval self.min_interval = min_interval
self.timeout_value: float | None = None self.timeout_value: float | None = None
self.timeout_critical: bool = False self.timeout_critical: bool = False
self.__last_run_time: float = 0 self.__last_run_time: float = 0
@ -306,8 +306,8 @@ class SimpleDispatcher:
while True: while True:
logger.debug(f'Running dispatcher "{self.name}"') logger.debug(f'Running dispatcher "{self.name}"')
time_delta = time.time() - self.__last_run_time time_delta = time.time() - self.__last_run_time
if time_delta < self.interval: if time_delta < self.min_interval:
sleep(self.interval - time_delta) sleep(self.min_interval - time_delta)
for block in self.blocks: for block in self.blocks:
block() block()
if self.finished: if self.finished:

View File

@ -16,7 +16,7 @@ from kotonebot import (
from ..game_ui import CommuEventButtonUI from ..game_ui import CommuEventButtonUI
from .pdorinku import acquire_pdorinku from .pdorinku import acquire_pdorinku
from kotonebot.backend.dispatch import SimpleDispatcher from kotonebot.backend.dispatch import SimpleDispatcher
from kotonebot.tasks.actions.commu import check_and_skip_commu from kotonebot.tasks.actions.commu import handle_unread_commu
logger = getLogger(__name__) logger = getLogger(__name__)
@ -109,11 +109,15 @@ def acquisitions() -> AcquisitionType | None:
if image.find(R.InPurodyuusu.TextPDrinkMax): if image.find(R.InPurodyuusu.TextPDrinkMax):
logger.info("PDrink max found") logger.info("PDrink max found")
while True: while True:
if image.find(R.InPurodyuusu.ButtonLeave, colored=True): # TODO: 这里会因为截图速度过快,截图截到中间状态的弹窗。
# 然后又因为从截图、识别、发出点击到实际点击中间又延迟,
# 过了这段时间后,原来中间状态按钮所在的位置已经变成了其他
# 的东西,导致误点击
if image.find(R.InPurodyuusu.ButtonLeave, colored=True): # mark
device.click() device.click()
elif image.find(R.Common.ButtonConfirm): elif image.find(R.Common.ButtonConfirm):
device.click() device.click()
break break
device.screenshot() device.screenshot()
return "PDrinkMax" return "PDrinkMax"
# 技能卡领取 # 技能卡领取
@ -134,7 +138,7 @@ def acquisitions() -> AcquisitionType | None:
device.click_center() device.click_center()
sleep(5) sleep(5)
# TODO: 可能不存在 達成 NEXT # TODO: 可能不存在 達成 NEXT
logger.debug("達成 NEXT: clicked") logger.debug("達成 NEXT: clicked") # TODO: 需要截图
device.click_center() device.click_center()
return "Clear" return "Clear"
# P物品领取 # P物品领取
@ -153,7 +157,7 @@ def acquisitions() -> AcquisitionType | None:
return "NetworkError" return "NetworkError"
# 跳过未读交流 # 跳过未读交流
logger.debug("Check skip commu...") logger.debug("Check skip commu...")
if check_and_skip_commu(img): if handle_unread_commu(img):
return "SkipCommu" return "SkipCommu"
# === 需要 OCR 的放在最后执行 === # === 需要 OCR 的放在最后执行 ===

View File

@ -3,6 +3,8 @@ import logging
from cv2.typing import MatLike from cv2.typing import MatLike
from kotonebot.backend.util import Countdown, Interval
from .. import R from .. import R
from kotonebot import device, image, color, user, rect_expand, until, action, sleep, use_screenshot from kotonebot import device, image, color, user, rect_expand, until, action, sleep, use_screenshot
@ -17,8 +19,8 @@ def is_at_commu():
def skip_commu(): def skip_commu():
device.click(image.expect_wait(R.Common.ButtonCommuSkip)) device.click(image.expect_wait(R.Common.ButtonCommuSkip))
@action('检查并跳过交流', screenshot_mode='manual') @action('检查未读交流', screenshot_mode='manual')
def check_and_skip_commu(img: MatLike | None = None) -> bool: def handle_unread_commu(img: MatLike | None = None) -> bool:
""" """
检查当前是否处在未读交流并自动跳过 检查当前是否处在未读交流并自动跳过
@ -36,14 +38,39 @@ def check_and_skip_commu(img: MatLike | None = None) -> bool:
ret = True ret = True
logger.debug('Fast forward button found. Check commu') logger.debug('Fast forward button found. Check commu')
button_bg_rect = rect_expand(skip_btn.rect, 10, 10, 50, 10) button_bg_rect = rect_expand(skip_btn.rect, 10, 10, 50, 10)
colors = color.raw().dominant_color(img, 2, rect=button_bg_rect) def is_fastforwarding():
RANGE = ((20, 65, 95), (180, 100, 100)) nonlocal img
if not any(color.raw().in_range(c, RANGE) for c in colors): assert img is not None
colors = color.raw().dominant_color(img, 2, rect=button_bg_rect)
RANGE = ((20, 65, 95), (180, 100, 100))
return any(color.raw().in_range(c, RANGE) for c in colors)
# 防止截图速度过快时,截图到了未加载完全的画面
cd = Interval()
hit = 0
HIT_THRESHOLD = 2
while True:
if not is_fastforwarding():
logger.debug("Unread commu hit %d/%d", hit, HIT_THRESHOLD)
hit += 1
else:
hit = 0
break
if hit >= HIT_THRESHOLD:
break
cd.wait()
img = device.screenshot()
should_skip = hit >= HIT_THRESHOLD
if not should_skip:
logger.info('Fast forwarding. No action needed.')
return False
if should_skip:
user.info('发现未读交流', [img]) user.info('发现未读交流', [img])
logger.debug('Not fast forwarding. Click fast forward button') logger.debug('Not fast forwarding. Click fast forward button')
device.click(skip_btn) device.click(skip_btn)
sleep(0.7) sleep(0.7)
if image.find(R.Common.ButtonConfirm): if image.wait_for(R.Common.ButtonConfirm, timeout=5):
logger.debug('Click confirm button') logger.debug('Click confirm button')
device.click() device.click()
else: else:

View File

@ -9,17 +9,17 @@ import cv2
import numpy as np import numpy as np
from cv2.typing import MatLike from cv2.typing import MatLike
from kotonebot.backend.context.context import use_screenshot
from .. import R from .. import R
from . import loading from . import loading
from ..common import conf from ..common import conf
from .scenes import at_home from .scenes import at_home
from .common import until_acquisition_clear, acquisitions, commut_event
from kotonebot.errors import UnrecoverableError from kotonebot.errors import UnrecoverableError
from kotonebot.backend.context.context import use_screenshot
from .common import until_acquisition_clear, acquisitions, commut_event
from kotonebot.backend.util import AdaptiveWait, Countdown, crop, cropped from kotonebot.backend.util import AdaptiveWait, Countdown, crop, cropped
from kotonebot.backend.dispatch import DispatcherContext, SimpleDispatcher from kotonebot.backend.dispatch import DispatcherContext, SimpleDispatcher
from kotonebot import ocr, device, contains, image, regex, action, sleep, color, Rect from kotonebot import ocr, device, contains, image, regex, action, sleep, color, Rect, wait
from .non_lesson_actions import ( from .non_lesson_actions import (
enter_allowance, allowance_available, study_available, enter_study, enter_allowance, allowance_available, study_available, enter_study,
is_rest_available, rest is_rest_available, rest
@ -516,6 +516,7 @@ def exam(type: Literal['mid', 'final']):
wait = cycle([0.1, 0.3, 0.5]) wait = cycle([0.1, 0.3, 0.5])
tries = 1 tries = 1
no_card_cd = Countdown(sec=4)
while True: while True:
start_time = time.time() start_time = time.time()
img = device.screenshot() img = device.screenshot()
@ -526,6 +527,17 @@ def exam(type: Literal['mid', 'final']):
continue continue
card_count = skill_card_count(img) card_count = skill_card_count(img)
if card_count == 0:
# 处理本回合已无剩余手牌的情况
# TODO: 使用模板匹配而不是 OCR提升速度
no_remaining_card = ocr.find(contains("0枚"), rect=R.InPurodyuusu.BoxNoSkillCard)
if no_remaining_card:
# TODO: HARD CODEDED
SKIP_POSITION = (621, 739, 85, 85)
device.click(SKIP_POSITION)
no_card_cd.reset()
continue
if card_count > 0: if card_count > 0:
inner_tries = 0 inner_tries = 0
while True: while True:
@ -579,9 +591,9 @@ def produce_end():
# 等待选择封面画面 [screenshots/produce_end/select_cover.jpg] # 等待选择封面画面 [screenshots/produce_end/select_cover.jpg]
# 次へ # 次へ
logger.info("Waiting for select cover screen...") logger.info("Waiting for select cover screen...")
wait = AdaptiveWait(timeout=60 * 5, max_interval=20) aw = AdaptiveWait(timeout=60 * 5, max_interval=20)
while not image.find(R.InPurodyuusu.ButtonNextNoIcon): while not image.find(R.InPurodyuusu.ButtonNextNoIcon):
wait() aw()
device.click(0, 0) device.click(0, 0)
# 选择封面 # 选择封面
logger.info("Use default cover.") logger.info("Use default cover.")
@ -657,12 +669,12 @@ def produce_end():
if image.find(R.InPurodyuusu.ButtonNextNoIcon): if image.find(R.InPurodyuusu.ButtonNextNoIcon):
logger.debug("Click next") logger.debug("Click next")
device.click() device.click()
sleep(0.2) wait(0.5, before='screenshot')
# [screenshots/produce_end/end_complete.png] # [screenshots/produce_end/end_complete.png]
elif image.find(R.InPurodyuusu.ButtonComplete): elif image.find(R.InPurodyuusu.ButtonComplete):
logger.debug("Click complete") logger.debug("Click complete")
device.click(image.expect_wait(R.InPurodyuusu.ButtonComplete)) device.click(image.expect_wait(R.InPurodyuusu.ButtonComplete))
sleep(0.2) wait(0.5, before='screenshot')
break break
# 点击结束后可能还会弹出来: # 点击结束后可能还会弹出来:
@ -957,7 +969,7 @@ if __name__ == '__main__':
# produce_end() # produce_end()
# hajime_pro(start_from=15) # hajime_pro(start_from=16)
# exam('mid') # exam('mid')
stage = (detect_regular_produce_scene()) stage = (detect_regular_produce_scene())
hajime_regular_from_stage(stage) hajime_regular_from_stage(stage)

View File

@ -5,6 +5,9 @@
""" """
from logging import getLogger from logging import getLogger
from kotonebot.backend.dispatch import SimpleDispatcher
from kotonebot.backend.util import Interval
from .. import R from .. import R
from ..game_ui import CommuEventButtonUI, EventButton from ..game_ui import CommuEventButtonUI, EventButton
from .common import acquisitions, AcquisitionType from .common import acquisitions, AcquisitionType
@ -83,6 +86,7 @@ def enter_allowance():
logger.debug("Waiting for 活動支給 screen.") logger.debug("Waiting for 活動支給 screen.")
acquisitions() acquisitions()
# 领取奖励 # 领取奖励
it = Interval()
while True: while True:
# TODO: 检测是否在行动页面应当单独一个函数 # TODO: 检测是否在行动页面应当单独一个函数
if image.find_multi([ if image.find_multi([
@ -93,9 +97,11 @@ def enter_allowance():
if image.find(R.InPurodyuusu.LootboxSliverLock): if image.find(R.InPurodyuusu.LootboxSliverLock):
logger.info("Click on lootbox.") logger.info("Click on lootbox.")
device.click() device.click()
sleep(0.5) # 防止点击了第一个箱子后立马点击了第二个
continue continue
if acquisitions() is not None: if acquisitions() is not None:
continue continue
it.wait()
logger.info("活動支給 completed.") logger.info("活動支給 completed.")
@action('判断是否可以休息') @action('判断是否可以休息')
@ -110,10 +116,12 @@ def is_rest_available():
def rest(): def rest():
"""执行休息""" """执行休息"""
logger.info("Rest for this week.") logger.info("Rest for this week.")
# 点击休息 (SimpleDispatcher('in_produce.rest')
device.click(image.expect_wait(R.InPurodyuusu.Rest)) # 点击休息
# 确定 .click(R.InPurodyuusu.Rest)
device.click(image.expect_wait(R.InPurodyuusu.RestConfirmBtn)) # 确定
.click(R.InPurodyuusu.RestConfirmBtn, finish=True)
).run()
if __name__ == '__main__': if __name__ == '__main__':
from kotonebot.backend.context import manual_context, init_context from kotonebot.backend.context import manual_context, init_context

View File

@ -36,6 +36,7 @@ def check_and_goto_mission() -> bool:
def claim_mission_reward(name: str): def claim_mission_reward(name: str):
"""领取任务奖励""" """领取任务奖励"""
# [screenshots/mission/daily.png] # [screenshots/mission/daily.png]
image.expect_wait(R.Common.ButtonIconArrowShort)
if image.find(R.Common.ButtonIconArrowShort, colored=True): if image.find(R.Common.ButtonIconArrowShort, colored=True):
logger.info(f'Claiming {name} mission reward.') logger.info(f'Claiming {name} mission reward.')
device.click() device.click()

View File

@ -2,6 +2,7 @@ import logging
from itertools import cycle from itertools import cycle
from typing import Optional, Literal from typing import Optional, Literal
from kotonebot.backend.context.context import wait
from kotonebot.ui import user from kotonebot.ui import user
from kotonebot.backend.util import Countdown from kotonebot.backend.util import Countdown
from kotonebot.backend.dispatch import SimpleDispatcher from kotonebot.backend.dispatch import SimpleDispatcher
@ -9,7 +10,7 @@ from kotonebot.backend.dispatch import SimpleDispatcher
from . import R from . import R
from .common import conf, PIdol from .common import conf, PIdol
from .actions.scenes import at_home, goto_home from .actions.scenes import at_home, goto_home
from .actions.in_purodyuusu import hajime_pro, hajime_regular from .actions.in_purodyuusu import hajime_pro, hajime_regular, resume_regular_produce
from kotonebot import device, image, ocr, task, action, sleep, equals, contains from kotonebot import device, image, ocr, task, action, sleep, equals, contains
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -79,6 +80,7 @@ def select_idol(target_titles: list[str] | PIdol):
# 如果不是,就挨个选中,判断名称 # 如果不是,就挨个选中,判断名称
for r in results: for r in results:
device.click(r) device.click(r)
sleep(0.3)
device.screenshot() device.screenshot()
if all(ocr.find_all(_target_titles, rect=R.Produce.KbIdolOverviewName)): if all(ocr.find_all(_target_titles, rect=R.Produce.KbIdolOverviewName)):
found = True found = True
@ -113,6 +115,8 @@ def resume_produce():
# [res/sprites/jp/produce/produce_resume.png] # [res/sprites/jp/produce/produce_resume.png]
logger.info('Click resume button.') logger.info('Click resume button.')
device.click(image.expect_wait(R.Produce.ButtonResume)) device.click(image.expect_wait(R.Produce.ButtonResume))
# 继续流程
resume_regular_produce()
@action('执行培育', screenshot_mode='manual-inherit') @action('执行培育', screenshot_mode='manual-inherit')
def do_produce(idol: PIdol, mode: Literal['regular', 'pro']) -> bool: def do_produce(idol: PIdol, mode: Literal['regular', 'pro']) -> bool:
@ -154,11 +158,13 @@ def do_produce(idol: PIdol, mode: Literal['regular', 'pro']) -> bool:
# 2. 选择支援卡 自动编成 [screenshots/produce/select_support_card.png] # 2. 选择支援卡 自动编成 [screenshots/produce/select_support_card.png]
ocr.expect_wait(contains('サポート'), rect=R.Produce.BoxStepIndicator) ocr.expect_wait(contains('サポート'), rect=R.Produce.BoxStepIndicator)
device.click(image.expect_wait(R.Produce.ButtonAutoSet)) device.click(image.expect_wait(R.Produce.ButtonAutoSet))
wait(0.5, before='screenshot')
device.click(image.expect_wait(R.Common.ButtonConfirm, colored=True)) device.click(image.expect_wait(R.Common.ButtonConfirm, colored=True))
device.click(image.expect_wait(R.Common.ButtonNextNoIcon)) device.click(image.expect_wait(R.Common.ButtonNextNoIcon, colored=True))
# 3. 选择回忆 自动编成 [screenshots/produce/select_memory.png] # 3. 选择回忆 自动编成 [screenshots/produce/select_memory.png]
ocr.expect_wait(contains('メモリー'), rect=R.Produce.BoxStepIndicator) ocr.expect_wait(contains('メモリー'), rect=R.Produce.BoxStepIndicator)
device.click(image.expect_wait(R.Produce.ButtonAutoSet)) device.click(image.expect_wait(R.Produce.ButtonAutoSet))
wait(0.5, before='screenshot')
device.screenshot() device.screenshot()
(SimpleDispatcher('do_produce.step_3') (SimpleDispatcher('do_produce.step_3')
.click(R.Common.ButtonNextNoIcon) .click(R.Common.ButtonNextNoIcon)
@ -236,10 +242,13 @@ if __name__ == '__main__':
file_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s')) file_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s'))
logging.getLogger().addHandler(file_handler) logging.getLogger().addHandler(file_handler)
from kotonebot.backend.context import init_context import time
from kotonebot.backend.context import init_context, manual_context
from kotonebot.tasks.common import BaseConfig from kotonebot.tasks.common import BaseConfig
init_context(config_type=BaseConfig) init_context(config_type=BaseConfig)
conf().produce.enabled = True conf().produce.enabled = True
conf().produce.mode = 'regular'
conf().produce.idols = [PIdol.花海佑芽_学園生活]
produce_task() produce_task()
# a() # a()
# select_idol(PIdol.藤田ことね_学園生活) # select_idol(PIdol.藤田ことね_学園生活)

View File

@ -71,6 +71,7 @@ def dispatch_recommended_items():
logger.info(f'Start purchasing recommended items.') logger.info(f'Start purchasing recommended items.')
while True: while True:
device.screenshot()
if image.find(R.Daily.TextShopRecommended): if image.find(R.Daily.TextShopRecommended):
logger.info(f'Clicking on recommended item.') # TODO: 计数 logger.info(f'Clicking on recommended item.') # TODO: 计数
device.click() device.click()

View File

@ -6,7 +6,7 @@ from . import R
from .common import Priority from .common import Priority
from .actions.loading import loading from .actions.loading import loading
from .actions.scenes import at_home, goto_home from .actions.scenes import at_home, goto_home
from .actions.commu import is_at_commu, check_and_skip_commu from .actions.commu import is_at_commu, handle_unread_commu
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@task('启动游戏', priority=Priority.START_GAME) @task('启动游戏', priority=Priority.START_GAME)
@ -42,7 +42,7 @@ def start_game():
elif image.find(R.Common.ButtonIconClose): elif image.find(R.Common.ButtonIconClose):
device.click() device.click()
# [screenshots/startup/birthday.png] # [screenshots/startup/birthday.png]
elif check_and_skip_commu(): elif handle_unread_commu():
pass pass
else: else:
device.click_center() device.click_center()

View File

@ -10,6 +10,21 @@ from .. import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def retry(func):
"""
装饰器当函数发生 ConnectionResetError 时自动重试三次
"""
def wrapper(*args, **kwargs):
for i in range(3):
try:
return func(*args, **kwargs)
except ConnectionResetError:
if i == 2: # 最后一次重试失败
raise
logger.warning(f'ConnectionResetError raised when calling {func}, retrying {i+1}/{3}')
continue
return wrapper
def ask( def ask(
question: str, question: str,
options: list[str], options: list[str],
@ -40,6 +55,7 @@ def _save_local(
logger.verbose('saving image to local: %s', f'{file_name}_{i}.png') logger.verbose('saving image to local: %s', f'{file_name}_{i}.png')
cv2.imwrite(f'{file_name}_{i}.png', image) cv2.imwrite(f'{file_name}_{i}.png', image)
@retry
def push( def push(
title: str, title: str,
message: str, message: str,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1 @@
{"definitions":{"c74f2151-74b0-4b47-bf80-356c48f431e0":{"name":"InPurodyuusu.BoxNoSkillCard","displayName":"手札のスキルカ学ドが0枚です","type":"hint-box","annotationId":"c74f2151-74b0-4b47-bf80-356c48f431e0","useHintRect":false}},"annotations":[{"id":"c74f2151-74b0-4b47-bf80-356c48f431e0","type":"rect","data":{"x1":180,"y1":977,"x2":529,"y2":1026}}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

@ -0,0 +1 @@
{"definitions":{"582d36c0-0916-4706-9833-4fbc026701f5":{"name":"InPurodyuusu.BoxPDrinkMaxConfirmTitle","displayName":"P饮料溢出 不领取弹窗标题","type":"hint-box","annotationId":"582d36c0-0916-4706-9833-4fbc026701f5","useHintRect":false}},"annotations":[{"id":"582d36c0-0916-4706-9833-4fbc026701f5","type":"rect","data":{"x1":46,"y1":829,"x2":270,"y2":876}}]}