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,
|
sleep,
|
||||||
task,
|
task,
|
||||||
action,
|
action,
|
||||||
use_screenshot
|
use_screenshot,
|
||||||
|
wait
|
||||||
)
|
)
|
||||||
from .backend.util import (
|
from .backend.util import (
|
||||||
Rect,
|
Rect,
|
||||||
|
|
|
@ -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(
|
||||||
*,
|
*,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 的放在最后执行 ===
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.藤田ことね_学園生活)
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 |
|
@ -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