feat(*): 优化若干流程
1. 添加 wait() 函数,允许在截图前等待指定时间 2. 以调试模式启动时,新增了删除原有调试 dump 文件夹时对文件被占用的处理 3. 修复了培育任务无法从已有培育流程开始的问题 4. 修复了购买推荐商品会卡住的问题 5. 优化未读交流判断逻辑提高准确率 6. 新增练习/考试中对本回合已无可用手牌情况的判断 7. 优化若干流程在 u2 截图模式下的逻辑 8. 消息推送新增失败时自动重试
This commit is contained in:
parent
be7acd3102
commit
726515774e
|
@ -13,7 +13,8 @@ from .backend.context import (
|
|||
sleep,
|
||||
task,
|
||||
action,
|
||||
use_screenshot
|
||||
use_screenshot,
|
||||
wait
|
||||
)
|
||||
from .backend.util import (
|
||||
Rect,
|
||||
|
|
|
@ -141,7 +141,7 @@ def sleep(seconds: float, /):
|
|||
"""
|
||||
可中断的 sleep 函数。
|
||||
|
||||
建议使用 `context.sleep()` 代替 `time.sleep()`,
|
||||
建议使用本函数代替 `time.sleep()`,
|
||||
这样能以最快速度响应用户请求中断。
|
||||
"""
|
||||
global vars
|
||||
|
@ -655,6 +655,7 @@ class ContextDevice(Device):
|
|||
"""
|
||||
截图。返回截图数据,同时更新当前上下文的截图数据。
|
||||
"""
|
||||
global next_wait, last_screenshot_time, next_wait_time
|
||||
current = ContextStackVars.ensure_current()
|
||||
if force:
|
||||
current._inherit_screenshot = None
|
||||
|
@ -662,6 +663,13 @@ class ContextDevice(Device):
|
|||
img = current._inherit_screenshot
|
||||
current._inherit_screenshot = None
|
||||
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()
|
||||
current._screenshot = img
|
||||
return img
|
||||
|
@ -691,7 +699,7 @@ class Context(Generic[T]):
|
|||
ip = self.config.current.backend.adb_ip
|
||||
port = self.config.current.backend.adb_port
|
||||
# TODO: 处理链接失败情况
|
||||
self.__device = ContextDevice(create_device(f'{ip}:{port}', 'adb_raw'))
|
||||
self.__device = ContextDevice(create_device(f'{ip}:{port}', 'adb'))
|
||||
|
||||
def inject(
|
||||
self,
|
||||
|
@ -763,6 +771,15 @@ def use_screenshot(*args: MatLike | None) -> MatLike:
|
|||
return img
|
||||
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 中的脚本可能已经引用了这里的变量
|
||||
# 为了能够动态更新这里变量的值,这里使用 Forwarded 类再封装一层,
|
||||
# 将调用转发到实际的稍后初始化的 Context 类上
|
||||
|
@ -781,7 +798,10 @@ debug: ContextDebug = cast(ContextDebug, Forwarded(name="debug"))
|
|||
"""调试工具。"""
|
||||
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(
|
||||
*,
|
||||
|
|
|
@ -7,8 +7,11 @@ from pathlib import Path
|
|||
from threading import Thread
|
||||
|
||||
from . import debug
|
||||
from kotonebot import logging
|
||||
from kotonebot.backend.context import init_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _task_thread(task_module: str):
|
||||
"""任务线程。"""
|
||||
runpy.run_module(task_module, run_name="__main__")
|
||||
|
@ -57,7 +60,18 @@ if __name__ == "__main__":
|
|||
os.makedirs(save_path)
|
||||
if args.clear:
|
||||
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)
|
||||
|
|
|
@ -240,13 +240,13 @@ class UntilImage:
|
|||
self.sd.result = self.result
|
||||
|
||||
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.logger = logging.getLogger(f'SimpleDispatcher of {name}')
|
||||
self.blocks: list[Callable] = []
|
||||
self.finished: bool = False
|
||||
self.result: Any | None = None
|
||||
self.interval = interval
|
||||
self.min_interval = min_interval
|
||||
self.timeout_value: float | None = None
|
||||
self.timeout_critical: bool = False
|
||||
self.__last_run_time: float = 0
|
||||
|
@ -306,8 +306,8 @@ class SimpleDispatcher:
|
|||
while True:
|
||||
logger.debug(f'Running dispatcher "{self.name}"')
|
||||
time_delta = time.time() - self.__last_run_time
|
||||
if time_delta < self.interval:
|
||||
sleep(self.interval - time_delta)
|
||||
if time_delta < self.min_interval:
|
||||
sleep(self.min_interval - time_delta)
|
||||
for block in self.blocks:
|
||||
block()
|
||||
if self.finished:
|
||||
|
|
|
@ -16,7 +16,7 @@ from kotonebot import (
|
|||
from ..game_ui import CommuEventButtonUI
|
||||
from .pdorinku import acquire_pdorinku
|
||||
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__)
|
||||
|
||||
|
@ -109,11 +109,15 @@ def acquisitions() -> AcquisitionType | None:
|
|||
if image.find(R.InPurodyuusu.TextPDrinkMax):
|
||||
logger.info("PDrink max found")
|
||||
while True:
|
||||
if image.find(R.InPurodyuusu.ButtonLeave, colored=True):
|
||||
# TODO: 这里会因为截图速度过快,截图截到中间状态的弹窗。
|
||||
# 然后又因为从截图、识别、发出点击到实际点击中间又延迟,
|
||||
# 过了这段时间后,原来中间状态按钮所在的位置已经变成了其他
|
||||
# 的东西,导致误点击
|
||||
if image.find(R.InPurodyuusu.ButtonLeave, colored=True): # mark
|
||||
device.click()
|
||||
elif image.find(R.Common.ButtonConfirm):
|
||||
device.click()
|
||||
break
|
||||
break
|
||||
device.screenshot()
|
||||
return "PDrinkMax"
|
||||
# 技能卡领取
|
||||
|
@ -134,7 +138,7 @@ def acquisitions() -> AcquisitionType | None:
|
|||
device.click_center()
|
||||
sleep(5)
|
||||
# TODO: 可能不存在 達成 NEXT
|
||||
logger.debug("達成 NEXT: clicked")
|
||||
logger.debug("達成 NEXT: clicked") # TODO: 需要截图
|
||||
device.click_center()
|
||||
return "Clear"
|
||||
# P物品领取
|
||||
|
@ -153,7 +157,7 @@ def acquisitions() -> AcquisitionType | None:
|
|||
return "NetworkError"
|
||||
# 跳过未读交流
|
||||
logger.debug("Check skip commu...")
|
||||
if check_and_skip_commu(img):
|
||||
if handle_unread_commu(img):
|
||||
return "SkipCommu"
|
||||
|
||||
# === 需要 OCR 的放在最后执行 ===
|
||||
|
|
|
@ -3,6 +3,8 @@ import logging
|
|||
|
||||
from cv2.typing import MatLike
|
||||
|
||||
from kotonebot.backend.util import Countdown, Interval
|
||||
|
||||
from .. import R
|
||||
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():
|
||||
device.click(image.expect_wait(R.Common.ButtonCommuSkip))
|
||||
|
||||
@action('检查并跳过交流', screenshot_mode='manual')
|
||||
def check_and_skip_commu(img: MatLike | None = None) -> bool:
|
||||
@action('检查未读交流', screenshot_mode='manual')
|
||||
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
|
||||
logger.debug('Fast forward button found. Check commu')
|
||||
button_bg_rect = rect_expand(skip_btn.rect, 10, 10, 50, 10)
|
||||
colors = color.raw().dominant_color(img, 2, rect=button_bg_rect)
|
||||
RANGE = ((20, 65, 95), (180, 100, 100))
|
||||
if not any(color.raw().in_range(c, RANGE) for c in colors):
|
||||
def is_fastforwarding():
|
||||
nonlocal img
|
||||
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])
|
||||
logger.debug('Not fast forwarding. Click fast forward button')
|
||||
device.click(skip_btn)
|
||||
sleep(0.7)
|
||||
if image.find(R.Common.ButtonConfirm):
|
||||
if image.wait_for(R.Common.ButtonConfirm, timeout=5):
|
||||
logger.debug('Click confirm button')
|
||||
device.click()
|
||||
else:
|
||||
|
|
|
@ -9,17 +9,17 @@ import cv2
|
|||
import numpy as np
|
||||
from cv2.typing import MatLike
|
||||
|
||||
from kotonebot.backend.context.context import use_screenshot
|
||||
|
||||
from .. import R
|
||||
from . import loading
|
||||
from ..common import conf
|
||||
from .scenes import at_home
|
||||
from .common import until_acquisition_clear, acquisitions, commut_event
|
||||
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.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 (
|
||||
enter_allowance, allowance_available, study_available, enter_study,
|
||||
is_rest_available, rest
|
||||
|
@ -516,6 +516,7 @@ def exam(type: Literal['mid', 'final']):
|
|||
|
||||
wait = cycle([0.1, 0.3, 0.5])
|
||||
tries = 1
|
||||
no_card_cd = Countdown(sec=4)
|
||||
while True:
|
||||
start_time = time.time()
|
||||
img = device.screenshot()
|
||||
|
@ -526,6 +527,17 @@ def exam(type: Literal['mid', 'final']):
|
|||
continue
|
||||
|
||||
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:
|
||||
inner_tries = 0
|
||||
while True:
|
||||
|
@ -579,9 +591,9 @@ def produce_end():
|
|||
# 等待选择封面画面 [screenshots/produce_end/select_cover.jpg]
|
||||
# 次へ
|
||||
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):
|
||||
wait()
|
||||
aw()
|
||||
device.click(0, 0)
|
||||
# 选择封面
|
||||
logger.info("Use default cover.")
|
||||
|
@ -657,12 +669,12 @@ def produce_end():
|
|||
if image.find(R.InPurodyuusu.ButtonNextNoIcon):
|
||||
logger.debug("Click next")
|
||||
device.click()
|
||||
sleep(0.2)
|
||||
wait(0.5, before='screenshot')
|
||||
# [screenshots/produce_end/end_complete.png]
|
||||
elif image.find(R.InPurodyuusu.ButtonComplete):
|
||||
logger.debug("Click complete")
|
||||
device.click(image.expect_wait(R.InPurodyuusu.ButtonComplete))
|
||||
sleep(0.2)
|
||||
wait(0.5, before='screenshot')
|
||||
break
|
||||
|
||||
# 点击结束后可能还会弹出来:
|
||||
|
@ -957,7 +969,7 @@ if __name__ == '__main__':
|
|||
# produce_end()
|
||||
|
||||
|
||||
# hajime_pro(start_from=15)
|
||||
# hajime_pro(start_from=16)
|
||||
# exam('mid')
|
||||
stage = (detect_regular_produce_scene())
|
||||
hajime_regular_from_stage(stage)
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
"""
|
||||
from logging import getLogger
|
||||
|
||||
from kotonebot.backend.dispatch import SimpleDispatcher
|
||||
from kotonebot.backend.util import Interval
|
||||
|
||||
from .. import R
|
||||
from ..game_ui import CommuEventButtonUI, EventButton
|
||||
from .common import acquisitions, AcquisitionType
|
||||
|
@ -83,6 +86,7 @@ def enter_allowance():
|
|||
logger.debug("Waiting for 活動支給 screen.")
|
||||
acquisitions()
|
||||
# 领取奖励
|
||||
it = Interval()
|
||||
while True:
|
||||
# TODO: 检测是否在行动页面应当单独一个函数
|
||||
if image.find_multi([
|
||||
|
@ -93,9 +97,11 @@ def enter_allowance():
|
|||
if image.find(R.InPurodyuusu.LootboxSliverLock):
|
||||
logger.info("Click on lootbox.")
|
||||
device.click()
|
||||
sleep(0.5) # 防止点击了第一个箱子后立马点击了第二个
|
||||
continue
|
||||
if acquisitions() is not None:
|
||||
continue
|
||||
it.wait()
|
||||
logger.info("活動支給 completed.")
|
||||
|
||||
@action('判断是否可以休息')
|
||||
|
@ -110,10 +116,12 @@ def is_rest_available():
|
|||
def rest():
|
||||
"""执行休息"""
|
||||
logger.info("Rest for this week.")
|
||||
# 点击休息
|
||||
device.click(image.expect_wait(R.InPurodyuusu.Rest))
|
||||
# 确定
|
||||
device.click(image.expect_wait(R.InPurodyuusu.RestConfirmBtn))
|
||||
(SimpleDispatcher('in_produce.rest')
|
||||
# 点击休息
|
||||
.click(R.InPurodyuusu.Rest)
|
||||
# 确定
|
||||
.click(R.InPurodyuusu.RestConfirmBtn, finish=True)
|
||||
).run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kotonebot.backend.context import manual_context, init_context
|
||||
|
|
|
@ -36,6 +36,7 @@ def check_and_goto_mission() -> bool:
|
|||
def claim_mission_reward(name: str):
|
||||
"""领取任务奖励"""
|
||||
# [screenshots/mission/daily.png]
|
||||
image.expect_wait(R.Common.ButtonIconArrowShort)
|
||||
if image.find(R.Common.ButtonIconArrowShort, colored=True):
|
||||
logger.info(f'Claiming {name} mission reward.')
|
||||
device.click()
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
from itertools import cycle
|
||||
from typing import Optional, Literal
|
||||
|
||||
from kotonebot.backend.context.context import wait
|
||||
from kotonebot.ui import user
|
||||
from kotonebot.backend.util import Countdown
|
||||
from kotonebot.backend.dispatch import SimpleDispatcher
|
||||
|
@ -9,7 +10,7 @@ from kotonebot.backend.dispatch import SimpleDispatcher
|
|||
from . import R
|
||||
from .common import conf, PIdol
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -79,6 +80,7 @@ def select_idol(target_titles: list[str] | PIdol):
|
|||
# 如果不是,就挨个选中,判断名称
|
||||
for r in results:
|
||||
device.click(r)
|
||||
sleep(0.3)
|
||||
device.screenshot()
|
||||
if all(ocr.find_all(_target_titles, rect=R.Produce.KbIdolOverviewName)):
|
||||
found = True
|
||||
|
@ -113,6 +115,8 @@ def resume_produce():
|
|||
# [res/sprites/jp/produce/produce_resume.png]
|
||||
logger.info('Click resume button.')
|
||||
device.click(image.expect_wait(R.Produce.ButtonResume))
|
||||
# 继续流程
|
||||
resume_regular_produce()
|
||||
|
||||
@action('执行培育', screenshot_mode='manual-inherit')
|
||||
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]
|
||||
ocr.expect_wait(contains('サポート'), rect=R.Produce.BoxStepIndicator)
|
||||
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.ButtonNextNoIcon))
|
||||
device.click(image.expect_wait(R.Common.ButtonNextNoIcon, colored=True))
|
||||
# 3. 选择回忆 自动编成 [screenshots/produce/select_memory.png]
|
||||
ocr.expect_wait(contains('メモリー'), rect=R.Produce.BoxStepIndicator)
|
||||
device.click(image.expect_wait(R.Produce.ButtonAutoSet))
|
||||
wait(0.5, before='screenshot')
|
||||
device.screenshot()
|
||||
(SimpleDispatcher('do_produce.step_3')
|
||||
.click(R.Common.ButtonNextNoIcon)
|
||||
|
@ -236,10 +242,13 @@ if __name__ == '__main__':
|
|||
file_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s'))
|
||||
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
|
||||
init_context(config_type=BaseConfig)
|
||||
conf().produce.enabled = True
|
||||
conf().produce.mode = 'regular'
|
||||
conf().produce.idols = [PIdol.花海佑芽_学園生活]
|
||||
produce_task()
|
||||
# a()
|
||||
# select_idol(PIdol.藤田ことね_学園生活)
|
|
@ -71,6 +71,7 @@ def dispatch_recommended_items():
|
|||
logger.info(f'Start purchasing recommended items.')
|
||||
|
||||
while True:
|
||||
device.screenshot()
|
||||
if image.find(R.Daily.TextShopRecommended):
|
||||
logger.info(f'Clicking on recommended item.') # TODO: 计数
|
||||
device.click()
|
||||
|
|
|
@ -6,7 +6,7 @@ from . import R
|
|||
from .common import Priority
|
||||
from .actions.loading import loading
|
||||
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__)
|
||||
|
||||
@task('启动游戏', priority=Priority.START_GAME)
|
||||
|
@ -42,7 +42,7 @@ def start_game():
|
|||
elif image.find(R.Common.ButtonIconClose):
|
||||
device.click()
|
||||
# [screenshots/startup/birthday.png]
|
||||
elif check_and_skip_commu():
|
||||
elif handle_unread_commu():
|
||||
pass
|
||||
else:
|
||||
device.click_center()
|
||||
|
|
|
@ -10,6 +10,21 @@ from .. import logging
|
|||
|
||||
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(
|
||||
question: str,
|
||||
options: list[str],
|
||||
|
@ -40,6 +55,7 @@ def _save_local(
|
|||
logger.verbose('saving image to local: %s', f'{file_name}_{i}.png')
|
||||
cv2.imwrite(f'{file_name}_{i}.png', image)
|
||||
|
||||
@retry
|
||||
def push(
|
||||
title: str,
|
||||
message: str,
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
|
@ -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 |
|
@ -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}}]}
|
Loading…
Reference in New Issue