diff --git a/experiments/imgui_test.py b/experiments/imgui_test.py deleted file mode 100644 index 6ab88c1..0000000 --- a/experiments/imgui_test.py +++ /dev/null @@ -1,65 +0,0 @@ -import dearpygui.dearpygui as dpg -import cv2 as cv -import numpy as np - -dpg.create_context() -dpg.create_viewport(title='Custom Title', width=600, height=800) -dpg.setup_dearpygui() - -vid = cv.VideoCapture(0) -ret, frame = vid.read() - -# image size or you can get this from image shape -frame_width = vid.get(cv.CAP_PROP_FRAME_WIDTH) -frame_height = vid.get(cv.CAP_PROP_FRAME_HEIGHT) -video_fps = vid.get(cv.CAP_PROP_FPS) -print(frame_width) -print(frame_height) -print(video_fps) - -print("Frame Array:") -print("Array is of type: ", type(frame)) -print("No. of dimensions: ", frame.ndim) -print("Shape of array: ", frame.shape) -print("Size of array: ", frame.size) -print("Array stores elements of type: ", frame.dtype) -data = np.flip(frame, 2) # because the camera data comes in as BGR and we need RGB -data = data.ravel() # flatten camera data to a 1 d stricture -data = np.asarray(data, dtype=np.float32) # change data type to 32bit floats -texture_data = np.true_divide(data, 255.0) # normalize image data to prepare for GPU - -print("texture_data Array:") -print("Array is of type: ", type(texture_data)) -print("No. of dimensions: ", texture_data.ndim) -print("Shape of array: ", texture_data.shape) -print("Size of array: ", texture_data.size) -print("Array stores elements of type: ", texture_data.dtype) - -with dpg.texture_registry(show=True): - dpg.add_raw_texture(frame.shape[1], frame.shape[0], texture_data, tag="texture_tag", format=dpg.mvFormat_Float_rgb) - -with dpg.window(label="Example Window"): - dpg.add_text("Hello, world") - dpg.add_image("texture_tag") - -dpg.show_metrics() -dpg.show_viewport() -while dpg.is_dearpygui_running(): - - # updating the texture in a while loop the frame rate will be limited to the camera frame rate. - # commenting out the "ret, frame = vid.read()" line will show the full speed that operations and updating a texture can run at - - ret, frame = vid.read() - data = np.flip(frame, 2) - data = data.ravel() - data = np.asarray(data, dtype=np.float32) - texture_data = np.true_divide(data, 255.0) - dpg.set_value("texture_tag", texture_data) - - # to compare to the base example in the open cv tutorials uncomment below - #cv.imshow('frame', frame) - dpg.render_dearpygui_frame() - -vid.release() -#cv.destroyAllWindows() # when using upen cv window "imshow" call this also -dpg.destroy_context() \ No newline at end of file diff --git a/kotonebot/__init__.py b/kotonebot/__init__.py index b6d7acd..7946e21 100644 --- a/kotonebot/__init__.py +++ b/kotonebot/__init__.py @@ -1,15 +1,4 @@ -from kotonebot.client.protocol import DeviceABC +from .client.protocol import DeviceABC from .backend.context import ContextOcr, ContextImage, ContextDebug, device, ocr, image, debug -from .backend.util import Rect, fuzz, regex, contains, grayscale - -# device: DeviceProtocol -# ocr: ContextOcr -# image: ContextImage -# debug: ContextDebug - -# def __getattr__(name: str): -# try: -# return getattr(_c, name) -# except AttributeError: -# return globals()[name] +from .backend.util import Rect, fuzz, regex, contains, grayscaled, grayscale_cached diff --git a/kotonebot/backend/context.py b/kotonebot/backend/context.py index 16bf83e..370fce4 100644 --- a/kotonebot/backend/context.py +++ b/kotonebot/backend/context.py @@ -28,7 +28,7 @@ class ContextOcr: def raw(self, lang: OcrLanguage = 'jp') -> Ocr: """ 返回 `kotonebot.backend.ocr` 中的 Ocr 对象。\n - Ocr 对象与此对象的区别是,此对象会自动截图,而 Ocr 对象需要手动传入图像参数。 + Ocr 对象与此对象(ContextOcr)的区别是,此对象会自动截图,而 Ocr 对象需要手动传入图像参数。 """ match lang: case 'jp': @@ -201,7 +201,7 @@ class ContextImage: time.sleep(0.1) - def expect(self, template: str, mask: str | None = None, threshold: float = 0.9) -> TemplateMatchResult: + def expect(self, template: str | MatLike, mask: str | None = None, threshold: float = 0.9) -> TemplateMatchResult: """ 寻找指定图像。 diff --git a/kotonebot/backend/util.py b/kotonebot/backend/util.py index 09085c0..f74f836 100644 --- a/kotonebot/backend/util.py +++ b/kotonebot/backend/util.py @@ -76,12 +76,12 @@ def cropper_x(x1: float, x2: float) -> Callable[[MatLike], MatLike]: return lambda img: crop_x(img, x1, x2) -def grayscale(img: MatLike | str) -> MatLike: +def grayscaled(img: MatLike | str) -> MatLike: if isinstance(img, str): img = cv2.imread(img) return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) @lru_cache def grayscale_cached(img: MatLike | str) -> MatLike: - return grayscale(img) + return grayscaled(img) diff --git a/kotonebot/client/device/adb.py b/kotonebot/client/device/adb.py index d2de32c..c297c6c 100644 --- a/kotonebot/client/device/adb.py +++ b/kotonebot/client/device/adb.py @@ -35,7 +35,7 @@ class AdbDevice(DeviceABC): def __click_last(self) -> None: if self.last_find is None: - raise ValueError("No last find result") + raise ValueError("No last find result. Make sure you are not calling the 'raw' functions.") self.click(self.last_find) def __click_rect(self, rect: Rect) -> None: diff --git a/kotonebot/tasks/actions/common.py b/kotonebot/tasks/actions/common.py new file mode 100644 index 0000000..836505c --- /dev/null +++ b/kotonebot/tasks/actions/common.py @@ -0,0 +1,132 @@ +from typing import Literal +from logging import getLogger +from time import sleep + +from kotonebot import ( + ocr, + device, + contains, + image, + debug, + regex, + grayscaled, + grayscale_cached +) +from .. import R +from .pdorinku import acquire_pdorinku + +logger = getLogger(__name__) + +def acquire_skill_card(): + """获取技能卡(スキルカード)""" + # TODO: 识别卡片内容,而不是固定选卡 + # TODO: 不硬编码坐标 + CARD_POSITIONS = [ + (157, 820, 128, 128), + (296, 820, 128, 128), + (435, 820, 128, 128), + ] + logger.info("Click first skill card") + device.click(CARD_POSITIONS[0]) + sleep(0.5) + # 确定 + logger.info("Click 受け取る") + device.click(ocr.expect(contains("受け取る")).rect) + # 跳过动画 + device.click(image.expect_wait_any([ + R.InPurodyuusu.PSkillCardIconBlue, + R.InPurodyuusu.PSkillCardIconColorful + ])) + +AcquisitionType = Literal[ + "PDrinkAcquire", # P饮料被动领取 + "PDrinkSelect", # P饮料主动领取 + "PDrinkMax", # P饮料到达上限 + "PSkillCardAcquire", # 技能卡领取 + "PSkillCardSelect", # 技能卡选择 + "PItem", # P物品 + "Clear", # 目标达成 +] +def acquisitions() -> AcquisitionType | None: + """处理行动开始前和结束后可能需要处理的事件,直到到行动页面为止""" + img = device.screenshot_raw() + gray_img = grayscaled(img) + logger.info("Acquisition stuffs...") + + # P饮料被动领取 + logger.info("Check PDrink acquisition...") + if image.raw().find(img, R.InPurodyuusu.PDrinkIcon): + logger.info("Click to finish animation") + device.click_center() + sleep(1) + return "PDrinkAcquire" + # P饮料主动领取 + # if ocr.raw().find(img, contains("受け取るPドリンクを選れでください")): + if image.raw().find(img, R.InPurodyuusu.TextPleaseSelectPDrink): + logger.info("PDrink acquisition") + # 不领取 + # device.click(ocr.expect(contains("受け取らない"))) + # sleep(0.5) + # device.click(image.expect(R.InPurodyuusu.ButtonNotAcquire)) + # sleep(0.5) + # device.click(image.expect(R.InPurodyuusu.ButtonConfirm)) + acquire_pdorinku(index=0) + return "PDrinkSelect" + # P饮料到达上限 + if image.raw().find(img, R.InPurodyuusu.TextPDrinkMax): + device.click(image.expect(R.InPurodyuusu.ButtonLeave)) + sleep(0.7) + # 可能需要点击确认 + device.click(image.expect(R.InPurodyuusu.ButtonConfirm, threshold=0.8)) + return "PDrinkMax" + # 技能卡被动领取(支援卡效果) + logger.info("Check skill card acquisition...") + if image.raw().find_any(img, [ + R.InPurodyuusu.PSkillCardIconBlue, + R.InPurodyuusu.PSkillCardIconColorful + ]): + logger.info("Acquire skill card") + device.click_center() + return "PSkillCardAcquire" + # 技能卡选择 + if ocr.raw().find(img, contains("受け取るスキルカードを選んでください")): + logger.info("Acquire skill card") + acquire_skill_card() + sleep(5) + return "PSkillCardSelect" + # 奖励箱技能卡 + if res := image.raw().find(gray_img, grayscaled(R.InPurodyuusu.LootBoxSkillCard)): + logger.info("Acquire skill card from loot box") + device.click(res.rect) + # 下面就是普通的技能卡选择 + return acquisitions() + # 目标达成 + if image.raw().find(gray_img, grayscale_cached(R.InPurodyuusu.IconClearBlue)): + logger.debug("達成: clicked") + device.click_center() + sleep(5) + # TODO: 可能不存在 達成 NEXT + logger.debug("達成 NEXT: clicked") + device.click_center() + return "Clear" + # P物品 + if image.raw().find(img, R.InPurodyuusu.PItemIconColorful): + logger.info("Click to finish PItem acquisition") + device.click_center() + sleep(1) + return "PItem" + # 支援卡 + # logger.info("Check support card acquisition...") + # 记忆 + # 未跳过剧情 + return None + +if __name__ == '__main__': + from logging import getLogger + import logging + from kotonebot.backend.context import init_context + logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s') + getLogger('kotonebot').setLevel(logging.DEBUG) + getLogger(__name__).setLevel(logging.DEBUG) + init_context() + acquisitions() diff --git a/kotonebot/tasks/actions/in_purodyuusu.py b/kotonebot/tasks/actions/in_purodyuusu.py index ac4247d..6146822 100644 --- a/kotonebot/tasks/actions/in_purodyuusu.py +++ b/kotonebot/tasks/actions/in_purodyuusu.py @@ -1,20 +1,19 @@ import random -import re import time from typing import Literal from typing_extensions import deprecated -import numpy as np import cv2 import unicodedata import logging from time import sleep -from kotonebot import ocr, device, fuzz, contains, image, debug, regex +from kotonebot import ocr, device, contains, image, debug, regex from kotonebot.backend.context import init_context -from kotonebot.backend.util import crop_y, cropper_y, grayscale, grayscale_cached +from kotonebot.backend.util import crop_y, cropper_y from kotonebot.tasks import R from kotonebot.tasks.actions import loading -from kotonebot.tasks.actions.pdorinku import acquire_pdorinku +from .non_lesson_actions import enter_allowance, study_available, enter_study, allowance_available +from .common import acquisitions, AcquisitionType, acquire_skill_card logger = logging.getLogger(__name__) @@ -299,26 +298,7 @@ def remaing_turns_and_points(): turns_ocr = ocr.ocr(turns_img) logger.debug("turns_ocr: %s", turns_ocr) -def acquire_skill_card(): - """获取技能卡(スキルカード)""" - # TODO: 识别卡片内容,而不是固定选卡 - # TODO: 不硬编码坐标 - CARD_POSITIONS = [ - (157, 820, 128, 128), - (296, 820, 128, 128), - (435, 820, 128, 128), - ] - logger.info("Click first skill card") - device.click(CARD_POSITIONS[0]) - sleep(0.5) - # 确定 - logger.info("Click 受け取る") - device.click(ocr.expect(contains("受け取る")).rect) - # 跳过动画 - device.click(image.expect_wait_any([ - R.InPurodyuusu.PSkillCardIconBlue, - R.InPurodyuusu.PSkillCardIconColorful - ])) + def rest(): """执行休息""" @@ -328,83 +308,7 @@ def rest(): # 确定 device.click(image.expect_wait(R.InPurodyuusu.RestConfirmBtn)) -AcquisitionType = Literal[ - "PDrinkAcquire", # P饮料被动领取 - "PDrinkSelect", # P饮料主动领取 - "PDrinkMax", # P饮料到达上限 - "PSkillCardAcquire", # 技能卡被动领取 - "PSkillCardSelect", # 技能卡主动领取 - "PItem", # P物品 - "Clear", # 目标达成 -] -def acquisitions() -> AcquisitionType | None: - """处理行动开始前和结束后可能需要处理的事件,直到到行动页面为止""" - img = device.screenshot_raw() - gray_img = grayscale(img) - logger.info("Acquisition stuffs...") - # P饮料被动领取 - logger.info("Check PDrink acquisition...") - if image.raw().find(img, R.InPurodyuusu.PDrinkIcon): - logger.info("Click to finish animation") - device.click_center() - sleep(1) - return "PDrinkAcquire" - # P饮料主动领取 - # if ocr.raw().find(img, contains("受け取るPドリンクを選れでください")): - if image.raw().find(img, R.InPurodyuusu.TextPleaseSelectPDrink): - logger.info("PDrink acquisition") - # 不领取 - # device.click(ocr.expect(contains("受け取らない"))) - # sleep(0.5) - # device.click(image.expect(R.InPurodyuusu.ButtonNotAcquire)) - # sleep(0.5) - # device.click(image.expect(R.InPurodyuusu.ButtonConfirm)) - acquire_pdorinku(index=0) - return "PDrinkSelect" - # P饮料到达上限 - if image.raw().find(img, R.InPurodyuusu.TextPDrinkMax): - device.click(image.expect(R.InPurodyuusu.ButtonLeave)) - sleep(0.7) - # 可能需要点击确认 - device.click(image.expect(R.InPurodyuusu.ButtonConfirm, threshold=0.8)) - return "PDrinkMax" - # 技能卡被动领取 - logger.info("Check skill card acquisition...") - if image.raw().find_any(img, [ - R.InPurodyuusu.PSkillCardIconBlue, - R.InPurodyuusu.PSkillCardIconColorful - ]): - logger.info("Acquire skill card") - device.click_center() - return "PSkillCardAcquire" - # 技能卡主动领取 - if ocr.raw().find(img, contains("受け取るスキルカードを選んでください")): - logger.info("Acquire skill card") - acquire_skill_card() - sleep(5) - return "PSkillCardSelect" - - # 目标达成 - if image.raw().find(gray_img, grayscale_cached(R.InPurodyuusu.IconClearBlue)): - logger.debug("達成: clicked") - device.click_center() - sleep(5) - # TODO: 可能不存在 達成 NEXT - logger.debug("達成 NEXT: clicked") - device.click_center() - return "Clear" - # P物品 - if image.raw().find(img, R.InPurodyuusu.PItemIconColorful): - logger.info("Click to finish PItem acquisition") - device.click_center() - sleep(1) - return "PItem" - # 支援卡 - # logger.info("Check support card acquisition...") - # 记忆 - # 未跳过剧情 - return None def until_action_scene(): """等待进入行动场景""" @@ -663,7 +567,7 @@ def hajime_regular(week: int = -1, start_from: int = 1): if not enter_recommended_action(): rest() - def week_common(): + def week_lesson(): until_action_scene() executed_action = enter_recommended_action() logger.info("Executed recommended action: %s", executed_action) @@ -677,8 +581,16 @@ def hajime_regular(week: int = -1, start_from: int = 1): rest() until_action_scene() + def week_non_lesson(): + """非练习周。可能可用行动包括:おでかけ、相談、活動支給、授業""" + until_action_scene() + if allowance_available(): + enter_allowance() + # elif study_available(): + # enter_study() + until_action_scene() - def week_final(): + def week_final_lesson(): if enter_recommended_action(final_week=True) != 'lesson': raise ValueError("Failed to enter recommended action on final week.") sleep(5) @@ -709,19 +621,19 @@ def hajime_regular(week: int = -1, start_from: int = 1): produce_end() weeks = [ - week_common, # 1 - week_common, # 2 - week_common, # 3 - week_common, # 4 - week_final, # 5 - week_mid_exam, # 6 - week_common, # 7 - week_common, # 8 - week_common, # 9 - week_common, # 10 - week_common, # 11 - week_final, # 12 - week_final_exam, # 13 + week_lesson, # 1: Vo.レッスン、Da.レッスン、Vi.レッスン + week_lesson, # 2: 授業 + week_lesson, # 3: Vo.レッスン、Da.レッスン、Vi.レッスン、授業 + week_non_lesson, # 4: おでかけ、相談、活動支給 + week_final_lesson, # 5: 追い込みレッスン + week_mid_exam, # 6: 中間試験 + week_non_lesson, # 7: おでかけ、活動支給 + week_non_lesson, # 8: 授業、活動支給 + week_lesson, # 9: Vo.レッスン、Da.レッスン、Vi.レッスン + week_lesson, # 10: Vo.レッスン、Da.レッスン、Vi.レッスン、授業 + week_non_lesson, # 11: おでかけ、相談、活動支給 + week_final_lesson, # 12: 追い込みレッスン + week_final_exam, # 13: 最終試験 ] if week != -1: logger.info("Week %d started.", week) @@ -751,13 +663,13 @@ if __name__ == '__main__': getLogger(__name__).setLevel(logging.DEBUG) init_context() - while not image.wait_for_any([ - R.InPurodyuusu.TextPDiary, # 普通周 - R.InPurodyuusu.ButtonFinalPracticeDance # 离考试剩余一周 - ], timeout=2): - logger.info("Action scene not detected. Retry...") - acquisitions() - sleep(3) + # while not image.wait_for_any([ + # R.InPurodyuusu.TextPDiary, # 普通周 + # R.InPurodyuusu.ButtonFinalPracticeDance # 离考试剩余一周 + # ], timeout=2): + # logger.info("Action scene not detected. Retry...") + # acquisitions() + # sleep(3) # image.wait_for_any([ # R.InPurodyuusu.TextPDiary, # 普通周 @@ -775,7 +687,7 @@ if __name__ == '__main__': # acquisitions() # acquire_pdorinku(0) # image.wait_for(R.InPurodyuusu.InPractice.PDorinkuIcon) - # hajime_regular(start_from=1) + hajime_regular(start_from=9) # until_practice_scene() # device.click(image.expect_wait_any([ # R.InPurodyuusu.PSkillCardIconBlue, diff --git a/kotonebot/tasks/actions/non_lesson_actions.py b/kotonebot/tasks/actions/non_lesson_actions.py new file mode 100644 index 0000000..337eb8c --- /dev/null +++ b/kotonebot/tasks/actions/non_lesson_actions.py @@ -0,0 +1,69 @@ +""" +此文件包含非练习周的行动。 + +具体包括:おでかけ、相談、活動支給、授業 +""" +from time import sleep +from logging import getLogger + +from kotonebot import device, image, ocr, debug +from kotonebot.tasks import R +from .common import acquisitions, AcquisitionType + +logger = getLogger(__name__) + +def allowance_available(): + """ + 判断是否可以执行活動支給。 + """ + return image.expect(R.InPurodyuusu.ButtonTextAllowance) is not None + +def study_available(): + """ + 判断是否可以执行授業。 + """ + return image.expect(R.InPurodyuusu.ButtonTextStudy) is not None + +def enter_study(): + """ + 执行授業。 + """ + raise NotImplementedError("授業功能未实现") + +def enter_allowance(): + """ + 执行活動支給。 + + 前置条件:位于行动页面,且所有行动按钮清晰可见 \n + 结束状态:无 + """ + logger.info("Executing 活動支給.") + # 点击活動支給 [screenshots\allowance\step_1.png] + logger.info("Double clicking on 活動支給.") + device.double_click(image.expect(R.InPurodyuusu.ButtonTextAllowance), interval=1) + # 等待进入页面 + sleep(3) + # 第一个箱子 [screenshots\allowance\step_2.png] + logger.info("Clicking on the first lootbox.") + device.click(image.expect_wait_any([ + R.InPurodyuusu.LootboxSliverLock + ])) + while acquisitions() is None: + logger.info("Waiting for acquisitions finished.") + sleep(2) + # 第二个箱子 + logger.info("Clicking on the second lootbox.") + device.click(image.expect_wait_any([ + R.InPurodyuusu.LootboxSliverLock + ])) + while acquisitions() is None: + logger.info("Waiting for acquisitions finished.") + sleep(2) + logger.info("活動支給 completed.") + # 可能会出现的新动画 + # 技能卡:[screenshots\allowance\step_4.png] + + +def study(): + """授業""" + pass \ No newline at end of file diff --git a/res/sprites/jp/in_purodyuusu/button_text_allowance.png b/res/sprites/jp/in_purodyuusu/button_text_allowance.png new file mode 100644 index 0000000..74d3b90 Binary files /dev/null and b/res/sprites/jp/in_purodyuusu/button_text_allowance.png differ diff --git a/res/sprites/jp/in_purodyuusu/loot_box_skill_card.png b/res/sprites/jp/in_purodyuusu/loot_box_skill_card.png new file mode 100644 index 0000000..dc61591 Binary files /dev/null and b/res/sprites/jp/in_purodyuusu/loot_box_skill_card.png differ diff --git a/res/sprites/jp/in_purodyuusu/lootbox_sliver_lock.png b/res/sprites/jp/in_purodyuusu/lootbox_sliver_lock.png new file mode 100644 index 0000000..9ac59d3 Binary files /dev/null and b/res/sprites/jp/in_purodyuusu/lootbox_sliver_lock.png differ diff --git a/screenshots/allowance/step_1.png b/screenshots/allowance/step_1.png new file mode 100644 index 0000000..d646bbb Binary files /dev/null and b/screenshots/allowance/step_1.png differ diff --git a/screenshots/allowance/step_2.png b/screenshots/allowance/step_2.png new file mode 100644 index 0000000..26860fd Binary files /dev/null and b/screenshots/allowance/step_2.png differ diff --git a/screenshots/allowance/step_4.png b/screenshots/allowance/step_4.png new file mode 100644 index 0000000..9805f02 Binary files /dev/null and b/screenshots/allowance/step_4.png differ diff --git a/tools/R.jinja2 b/tools/R.jinja2 index 57ee559..9c91aa9 100644 --- a/tools/R.jinja2 +++ b/tools/R.jinja2 @@ -14,7 +14,7 @@ class {{ class_name }}: """ 路径:{{ attr.rel_path }}
模块:`{{ attr.class_path|join('.') }}.{{ attr.name }}`
- + """ {%- elif attr.type == 'next_class' -%} {{ attr.name }} = {{ attr.value }}