Merge commit '1d177be34845495c4051ba642c61198bcea271e8'

This commit is contained in:
XcantloadX 2025-05-16 08:23:38 +08:00
commit e0daedd37c
70 changed files with 647 additions and 286 deletions

6
.gitignore vendored
View File

@ -2,8 +2,9 @@
tests/output_images
tests/output_images/*
R.py
kotonebot/tasks/sprites
kotonebot/tasks/metadata.py
kotonebot/kaa/sprites
kotonebot/kaa/metadata.py
kotonebot/kaa/resources
kotonebot-ui/node_modules
kotonebot-ui/.vite
dumps*/
@ -16,7 +17,6 @@ messages/
logs/
traces/
version
kotonebot/tasks/resources
cache/
##########################

View File

@ -1,5 +1,5 @@
graft kotonebot/tasks/sprites
graft kotonebot/tasks/resources
graft kotonebot/kaa/sprites
graft kotonebot/kaa/resources
prune tests
prune tools
prune experiments

View File

@ -71,36 +71,58 @@ TODO
## 开发
见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md)
## 贡献
非常欢迎 PR。
你可以从 Github Issue 中选择一个 Issue 解决,或者从下面的路线图里选一个任务讨论。
## 路线图
下面是待实现的功能:
(带删除线标记的为已完成)
* 培育
* 允许指定领取 P 饮料、P 物品、技能卡的领取选择优先级
* ~~允许指定行动优先级~~
* 自动使用 P 饮料
* 支持非凡(アノマリー)属性偶像的自动培育
* ~~支持琴音的自动培育~~
* 允许优先选择活动加成高的偶像进行培育
* 支持 MASTER 培育、NIA 培育
* 竞赛
* 按分数差距优先选择
### 培育
* 允许指定领取 P 饮料、P 物品、技能卡的领取选择优先级
* ~~允许指定行动优先级~~
* 自动使用 P 饮料
* ~~支持非凡(アノマリー)属性偶像的自动培育~~
* ~~支持琴音的自动培育~~
* 允许优先选择活动加成高的偶像进行培育
* ~~支持 MASTER 培育~~
* 支持 NIA 培育
### 日常
* ~~竞赛按分数差距优先选择~~
* ~~自动硬币扭蛋(コインガシャ)~~
* 调度
* ~~模拟器启停~~
* 常驻运行与自动运行
* 命令行接口
* 尝试接入 ALAS
### 调度
* ~~模拟器启停~~
* 记录任务执行时间与次数,避免重复执行。例如竞赛每天只执行一次
* 常驻运行与自动运行
* ~~命令行接口~~
* 尝试接入 ALAS
### UI
* UI
* 使用 Flet/Flutter 重写 UI
* 分离脚本与 UI允许 UI 与脚本分别独立运行
* 启动器
* 使用 C# 替换当前的简易 .bat 文件
* Linux 支持
* Android 支持
* 使用 Python for Android 移植 kaa 到 Android 平台
* 使用 Shizuku 执行 ADB 命令
* 使用 Pyjnius 绕过 ADB ,使用无障碍直接控制设备
* 开发工具
* 将开发工具通过 VSCode 扩展与 VSCode 整合
### 跨平台
* Android 支持
- [ ] 使用 Python for Android 移植 kaa 到 Android 平台
- [ ] 解决 native 依赖编译问题
- [ ] 需要一个适合移动端的 UI
- [ ] 调用 Shizuku 执行 ADB 命令
- [ ] 使用 Pyjnius 绕过 ADB ,调用无障碍直接控制设备
* Linux 支持
### 开发工具
* 使用 Konva.js 重构 ImageAnnotation 工具
* 将开发工具通过 VSCode 扩展与 VSCode 整合
### 其他
* 适配汉化版
- [ ] 需要一个合适的方法自动切换不用语言的资源文件
- [ ] 需要一个合适的工具来辅助替换模板图片文件
- [ ] 收集汉化版本的截图

View File

@ -51,7 +51,7 @@ generate-metadata: env
from pathlib import Path
with open("WHATS_NEW.md", "r", encoding="utf-8") as f:
content = f.read()
metadata_path = Path("kotonebot/tasks/metadata.py")
metadata_path = Path("kotonebot/kaa/metadata.py")
metadata_path.parent.mkdir(parents=True, exist_ok=True)
with open(metadata_path, "w", encoding="utf-8") as f:
f.write(f'WHATS_NEW = """\n{content}\n"""')
@ -68,10 +68,10 @@ extract-game-data:
#!{{shebang_pwsh}}
Write-Host "Extracting game data..."
New-Item -ItemType File -Force -Path .\kotonebot\tasks\resources\__init__.py
New-Item -ItemType File -Force -Path .\kotonebot\kaa\resources\__init__.py
$currentHash = git -C .\submodules\gakumasu-diff rev-parse HEAD
$hashFile = ".\kotonebot\tasks\resources\game_ver.txt"
$hashFile = ".\kotonebot\kaa\resources\game_ver.txt"
$shouldUpdate = $true
if (Test-Path $hashFile) {

View File

@ -1,22 +1,17 @@
import io
import os
import logging
import pkgutil
import importlib
import threading
from typing_extensions import Self
from dataclasses import dataclass, field
import threading
import traceback
import os
import zipfile
import cv2
from datetime import datetime
import io
from typing import Any, Literal, Callable, Generic, TypeVar, ParamSpec
from kotonebot.backend.context import Task, Action
from kotonebot.backend.context import init_context, vars
from kotonebot.backend.context import task_registry, action_registry, current_callstack, Task, Action
from kotonebot.client.host.protocol import Instance
from kotonebot.ui import user
from kotonebot.client.host.protocol import Instance
from kotonebot.backend.context import init_context, vars
from kotonebot.backend.context import task_registry, action_registry, Task, Action
log_stream = io.StringIO()
stream_handler = logging.StreamHandler(log_stream)
@ -39,41 +34,6 @@ class RunStatus:
def interrupt(self):
vars.interrupted.set()
def _save_error_report(
exception: Exception,
*,
path: str | None = None
) -> str:
"""
保存错误报告
:param path: 保存的路径若为 `None`则保存到 `./reports/{YY-MM-DD HH-MM-SS}.zip`
:return: 保存的路径
"""
from kotonebot import device
try:
if path is None:
path = f'./reports/{datetime.now().strftime("%Y-%m-%d %H-%M-%S")}.zip'
exception_msg = '\n'.join(traceback.format_exception(exception))
task_callstack = '\n'.join([f'{i+1}. name={task.name} priority={task.priority}' for i, task in enumerate(current_callstack)])
screenshot = device.screenshot()
logs = log_stream.getvalue()
with open('config.json', 'r', encoding='utf-8') as f:
config_content = f.read()
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
with zipfile.ZipFile(path, 'w') as zipf:
zipf.writestr('exception.txt', exception_msg)
zipf.writestr('task_callstack.txt', task_callstack)
zipf.writestr('screenshot.png', cv2.imencode('.png', screenshot)[1].tobytes())
zipf.writestr('config.json', config_content)
zipf.writestr('logs.txt', logs)
return path
except Exception as e:
logger.exception(f'Failed to save error report:')
return ''
# Modified from https://stackoverflow.com/questions/70982565/how-do-i-make-an-event-listener-with-decorators-in-python
Params = ParamSpec('Params')
Return = TypeVar('Return')
@ -125,11 +85,12 @@ class KotoneBot:
def __init__(
self,
module: str,
config_path: str,
config_type: type = dict[str, Any],
*,
debug: bool = False,
resume_on_error: bool = False,
auto_save_error_report: bool = True,
auto_save_error_report: bool = False,
):
"""
初始化 KotoneBot
@ -141,6 +102,7 @@ class KotoneBot:
:param auto_save_error_report: 是否自动保存错误报告
"""
self.module = module
self.config_path = config_path
self.config_type = config_type
# HACK: 硬编码
self.current_config: int | str = 0
@ -150,6 +112,9 @@ class KotoneBot:
self.events = KotoneBotEvents()
self.backend_instance: Instance | None = None
if self.auto_save_error_report:
raise NotImplementedError('auto_save_error_report not implemented yet.')
def initialize(self):
"""
初始化并载入所有任务和动作
@ -176,7 +141,7 @@ class KotoneBot:
from kotonebot.client.host import create_custom
from kotonebot.config.manager import load_config
# HACK: 硬编码
config = load_config('config.json', type=self.config_type)
config = load_config(self.config_path, type=self.config_type)
config = config.user_configs[0]
logger.info('Checking backend...')
if config.backend.type == 'custom' and config.backend.check_emulator:
@ -207,7 +172,7 @@ class KotoneBot:
按优先级顺序运行所有任务
"""
self.check_backend()
init_context(config_type=self.config_type)
init_context(config_path=self.config_path, config_type=self.config_type)
vars.interrupted.clear()
if by_priority:
@ -238,7 +203,7 @@ class KotoneBot:
logger.exception(f'Error: ')
report_path = None
if self.auto_save_error_report:
report_path = _save_error_report(e)
raise NotImplementedError
self.events.task_status_changed.trigger(task, 'error')
if not self.resume_on_error:
for task1 in tasks[tasks.index(task)+1:]:

View File

@ -255,7 +255,7 @@ class ContextStackVars:
class ContextOcr:
def __init__(self, context: 'Context'):
self.context = context
self.__engine = jp
self.__engine = jp()
def raw(self, lang: OcrLanguage = 'jp') -> Ocr:
"""
@ -264,9 +264,9 @@ class ContextOcr:
"""
match lang:
case 'jp':
return jp
return jp()
case 'en':
return en
return en()
case _:
raise ValueError(f"Invalid language: {lang}")
@ -601,9 +601,9 @@ class ContextDebug:
V = TypeVar('V')
class ContextConfig(Generic[T]):
def __init__(self, context: 'Context', config_type: Type[T] = dict[str, Any]):
def __init__(self, context: 'Context', config_path: str = 'config.json', config_type: Type[T] = dict[str, Any]):
self.context = context
self.config_path: str = 'config.json'
self.config_path: str = config_path
self.current_key: int | str = 0
self.config_type: Type = config_type
self.root = load_config(self.config_path, type=config_type)
@ -730,13 +730,13 @@ class ContextDevice(Device):
class Context(Generic[T]):
def __init__(self, config_type: Type[T], screenshot_impl: Optional[DeviceImpl] = None):
def __init__(self, config_path: str, config_type: Type[T], screenshot_impl: Optional[DeviceImpl] = None):
self.__ocr = ContextOcr(self)
self.__image = ContextImage(self)
self.__color = ContextColor(self)
self.__vars = ContextGlobalVars()
self.__debug = ContextDebug(self)
self.__config = ContextConfig[T](self, config_type)
self.__config = ContextConfig[T](self, config_path, config_type)
ip = self.config.current.backend.adb_ip
port = self.config.current.backend.adb_port
@ -851,6 +851,7 @@ next_wait_time: float = 0
def init_context(
*,
config_path: str = 'config.json',
config_type: Type[T] = dict[str, Any],
force: bool = False,
screenshot_impl: Optional[DeviceImpl] = None
@ -858,6 +859,7 @@ def init_context(
"""
初始化 Context 模块
:param config_path: 配置文件路径
:param config_type: 配置数据类类型
配置数据类必须继承自 pydantic `BaseModel`
默认为 `dict[str, Any]`即普通的 JSON 数据不包含任何类型信息
@ -869,7 +871,7 @@ def init_context(
global _c, device, ocr, image, color, vars, debug, config
if _c is not None and not force:
return
_c = Context(config_type=config_type, screenshot_impl=screenshot_impl)
_c = Context(config_path=config_path, config_type=config_type, screenshot_impl=screenshot_impl)
device._FORWARD_getter = lambda: _c.device # type: ignore
ocr._FORWARD_getter = lambda: _c.ocr # type: ignore
image._FORWARD_getter = lambda: _c.image # type: ignore

View File

@ -14,7 +14,6 @@ import cv2
import uvicorn
from thefuzz import fuzz
from pydantic import BaseModel
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, Response
from fastapi import FastAPI, WebSocket, HTTPException
from fastapi.middleware.cors import CORSMiddleware
@ -143,7 +142,7 @@ def list_dir(path: str) -> list[File]:
@app.get("/api/resources/autocomplete")
def autocomplete(class_path: str) -> list[str]:
from kotonebot.tasks import R # HACK: hardcode
from kotonebot.kaa.tasks import R
class_names = class_path.split(".")[:-1]
target_class = R
# 定位到目标类

View File

@ -20,21 +20,6 @@ from ..util import Rect, lf_path
from .debug import result as debug_result, debug
logger = logging.getLogger(__name__)
# TODO: 这个路径需要能够独立设置
_engine_jp = RapidOCR(
rec_model_path=lf_path('models/japan_PP-OCRv4_rec_infer.onnx'),
use_det=True,
use_cls=False,
use_rec=True,
)
_engine_en = RapidOCR(
rec_model_path=lf_path('models/en_PP-OCRv3_rec_infer.onnx'),
use_det=True,
use_cls=False,
use_rec=True,
)
StringMatchFunction = Callable[[str], bool]
REGEX_NUMBERS = re.compile(r'\d+')
@ -483,13 +468,42 @@ class Ocr:
raise TextNotFoundError(text, img)
return ret
# TODO: 这个路径需要能够独立设置
_engine_jp: RapidOCR | None = None
_engine_en: RapidOCR | None = RapidOCR(
rec_model_path=lf_path('models/en_PP-OCRv3_rec_infer.onnx'),
use_det=True,
use_cls=False,
use_rec=True,
)
def jp() -> Ocr:
"""
日语 OCR 引擎
"""
global _engine_jp
if _engine_jp is None:
_engine_jp = RapidOCR(
rec_model_path=lf_path('models/japan_PP-OCRv3_rec_infer.onnx'),
use_det=True,
use_cls=False,
use_rec=True,
)
return Ocr(_engine_jp)
jp = Ocr(_engine_jp)
"""日语 OCR 引擎。"""
en = Ocr(_engine_en)
"""英语 OCR 引擎。"""
def en() -> Ocr:
"""
英语 OCR 引擎
"""
global _engine_en
if _engine_en is None:
_engine_en = RapidOCR(
rec_model_path=lf_path('models/en_PP-OCRv3_rec_infer.onnx'),
use_det=True,
use_cls=False,
use_rec=True,
)
return Ocr(_engine_en)
if __name__ == '__main__':

View File

@ -2,7 +2,7 @@ import runpy
import logging
import argparse
from kotonebot.tasks.common import BaseConfig
from kotonebot.kaa.common import BaseConfig
def run_script(script_path: str) -> None:

View File

@ -2,7 +2,7 @@ import os
import json
import shutil
from importlib import resources
from typing import Literal, Dict, NamedTuple, Tuple, TypeVar, Generic, Any
from typing import Literal, TypeVar, Any
from typing_extensions import assert_never
from enum import IntEnum, Enum
@ -162,7 +162,7 @@ class DailyMoneyShopItems(IntEnum):
return [(cls.to_ui_text(item), item) for item in cls if cls._is_note(item)]
def to_resource(self):
from . import R
from kotonebot.kaa.tasks import R
match self:
case DailyMoneyShopItems.Recommendations:
return R.Daily.TextShopRecommended
@ -357,6 +357,7 @@ class ProduceConfig(ConfigBaseModel):
ProduceAction.ALLOWANCE,
ProduceAction.OUTING,
ProduceAction.STUDY,
ProduceAction.CONSULT,
ProduceAction.REST,
]
"""
@ -486,10 +487,10 @@ def conf() -> BaseConfig:
return c.options
def sprite_path(path: str) -> str:
standalone = os.path.join('kotonebot/tasks/sprites', path)
standalone = os.path.join('kotonebot/kaa/sprites', path)
if os.path.exists(standalone):
return standalone
return str(resources.files('kotonebot.tasks.sprites') / path)
return str(resources.files('kotonebot.kaa.sprites') / path)
def upgrade_config() -> str | None:
"""

View File

@ -2,7 +2,7 @@ import os
import sqlite3
from typing import Any, cast
from kotonebot.tasks import resources as res
from kotonebot.kaa import resources as res
_db: sqlite3.Connection | None = None
_db_path = cast(str, res.__path__)[0] + '/game.db'

View File

@ -1,13 +1,7 @@
from dataclasses import dataclass
from typing import Literal, overload
import cv2
import numpy as np
from cv2.typing import MatLike
from kotonebot.backend.image import TemplateMatchResult
from kotonebot.tasks import R
from kotonebot import action, color, image
from kotonebot.backend.color import HsvColor
from kotonebot.util import Rect
@ -94,7 +88,6 @@ class WhiteFilter(HsvColorFilter):
if __name__ == '__main__':
from pprint import pprint as print
from kotonebot.backend.context import init_context, manual_context, device
init_context()

View File

@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import Sequence
from kotonebot.tasks import R
from ..tasks import R
from kotonebot.backend.core import HintBox
from kotonebot.backend.color import HsvColor
from kotonebot import action, device, ocr, sleep, Rect

View File

@ -1,4 +1,4 @@
from kotonebot.tasks import R
from kotonebot.kaa.tasks import R
from kotonebot import device, image
def expect_yes():

View File

@ -1,20 +1,17 @@
import os
import logging
from typing import cast
from importlib import resources
import cv2
import numpy as np
from cv2.typing import MatLike
from kotonebot.tasks import R
from kotonebot.tasks.util import paths
from kotonebot.util import Rect, cv2_imread
from kotonebot.tasks.game_ui import Scrollable
from kotonebot.backend.debug import result, img
from kotonebot import device, color, action, sleep, contains
from kotonebot.tasks.image_db import ImageDatabase, HistDescriptor, FileDataSource
from kotonebot.backend.preprocessor import HsvColorRemover, HsvColorsRemover
from kotonebot.kaa.tasks import R
from kotonebot.kaa.util import paths
from kotonebot.util import Rect
from kotonebot.kaa.game_ui import Scrollable
from kotonebot import device, action
from kotonebot.kaa.image_db import ImageDatabase, HistDescriptor, FileDataSource
from kotonebot.backend.preprocessor import HsvColorsRemover
logger = logging.getLogger(__name__)
_db: ImageDatabase | None = None
@ -152,15 +149,12 @@ def locate_idol(skin_id: str):
def test():
from kotonebot.backend.context import init_context, manual_context, device
from kotonebot.backend.context import init_context, manual_context
init_context()
manual_context().begin()
locate_idol('i_card-skin-fktn-3-006')
if __name__ == '__main__':
from pprint import pprint as print
from kotonebot.util import cv2_imread
from kotonebot.backend.preprocessor import HsvColorFilter
test()

View File

@ -2,7 +2,7 @@ from typing import Literal, overload
from kotonebot.backend.image import TemplateMatchResult
from kotonebot.tasks import R
from ..tasks import R
from .common import WhiteFilter
from kotonebot import action, device, image

View File

@ -167,7 +167,7 @@ class ImageDatabase:
if __name__ == '__main__':
from kotonebot.tasks.image_db.db import Db
from kotonebot.kaa.image_db.db import Db
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
imgs_path = r'E:\GithubRepos\KotonesAutoAssistant.worktrees\dev\kotonebot\tasks\resources\idol_cards'
needle_path = r'D:\05.png'

View File

@ -1,8 +1,9 @@
import sys
import runpy
import logging
import argparse
import importlib.metadata
import runpy
from datetime import datetime
from .kaa import Kaa
from kotonebot.backend.context import tasks_from_id, task_registry
@ -12,10 +13,12 @@ version = importlib.metadata.version('ksaa')
# 主命令
psr = argparse.ArgumentParser(description='Command-line interface for Kotone\'s Auto Assistant')
psr.add_argument('-v', '--version', action='version', version='kaa v' + version)
# psr.add_argument('-c', '--config', required=False, help='Path to the configuration file. Default: ./config.json')
psr.add_argument('-c', '--config', default='./config.json', help='Path to the configuration file. Default: ./config.json')
psr.add_argument('-lp', '--log-path', default=None, help='Path to the log file. Does not log to file if not specified. Default: None')
psr.add_argument('-ll', '--log-level', default='DEBUG', help='Log level. Default: DEBUG')
# 子命令
subparsers = psr.add_subparsers(dest='subcommands')
subparsers = psr.add_subparsers(dest='subcommands', title='Subcommands')
# task 子命令
task_psr = subparsers.add_parser('task', help='Task related commands')
@ -37,18 +40,28 @@ _kaa: Kaa | None = None
def kaa() -> Kaa:
global _kaa
if _kaa is None:
_kaa = Kaa()
_kaa = Kaa(psr.parse_args().config)
_kaa.initialize()
return _kaa
def task_invoke() -> int:
tasks_args = psr.parse_args().task_ids
assert isinstance(tasks_args, list)
if not tasks_args:
print('No tasks specified.')
return -1
kaa().set_log_level(logging.DEBUG)
# 设置日志
log_level = getattr(logging, psr.parse_args().log_level, None)
if log_level is None:
raise ValueError(f'Invalid log level: {psr.parse_args().log_level}')
kaa().set_log_level(log_level)
if psr.parse_args().log_path is not None:
kaa().add_file_logger(psr.parse_args().log_path)
# 执行任务
print(tasks_args)
if len(tasks_args) == 1 and tasks_args[0] == '*':
if '*' in tasks_args:
if len(tasks_args) > 1:
raise ValueError('Cannot specify other tasks when using wildcard.')
kaa().run_all()
else:
kaa().run(tasks_from_id(tasks_args))
@ -85,10 +98,14 @@ def main():
sys.exit(task_invoke())
elif args.task_command == 'list':
sys.exit(task_list())
else:
raise ValueError(f'Unknown task command: {args.task_command}')
elif args.subcommands == 'remote-server':
sys.exit(remote_server())
elif args.subcommands is None:
log_filename = datetime.now().strftime('logs/%y-%m-%d-%H-%M-%S.log')
kaa().set_log_level(logging.DEBUG)
kaa().add_file_logger(log_filename)
from .gr import main as gr_main
gr_main(kaa())

View File

@ -8,12 +8,12 @@ from typing import List, Dict, Tuple, Literal, Generator
import cv2
import gradio as gr
from kotonebot.tasks.main import Kaa
from kotonebot.tasks.db import IdolCard
from kotonebot.kaa.main import Kaa
from kotonebot.kaa.db import IdolCard
from kotonebot.config.manager import load_config, save_config
from kotonebot.config.base_config import UserConfig, BackendConfig
from kotonebot.backend.context import task_registry, ContextStackVars
from kotonebot.tasks.common import (
from kotonebot.kaa.common import (
BaseConfig, APShopItems, CapsuleToysConfig, ClubRewardConfig, PurchaseConfig, ActivityFundsConfig,
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
MissionRewardConfig, DailyMoneyShopItems, ProduceAction,
@ -1120,7 +1120,7 @@ class KotoneBotUI:
def _create_whats_new_tab(self) -> None:
"""创建更新日志标签页,并显示最新版本更新内容"""
with gr.Tab("更新日志"):
from kotonebot.tasks.metadata import WHATS_NEW
from kotonebot.kaa.metadata import WHATS_NEW
gr.Markdown(WHATS_NEW)
def _create_screen_tab(self) -> None:
@ -1178,7 +1178,7 @@ class KotoneBotUI:
return app
def main(kaa: Kaa | None = None) -> None:
kaa = kaa or Kaa()
kaa = kaa or Kaa('./config.json')
ui = KotoneBotUI(kaa)
app = ui.create_ui()
app.launch(inbrowser=True, show_error=True)

93
kotonebot/kaa/main/kaa.py Normal file
View File

@ -0,0 +1,93 @@
import io
import os
import logging
import importlib.metadata
import traceback
import zipfile
from datetime import datetime
import cv2
from kotonebot import KotoneBot
from ..common import BaseConfig, upgrade_config
# 初始化日志
log_formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(name)s] %(message)s')
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.CRITICAL)
log_stream = io.StringIO()
stream_handler = logging.StreamHandler(log_stream)
stream_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] - %(message)s'))
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
logging.getLogger("kotonebot").setLevel(logging.DEBUG)
logger = logging.getLogger(__name__)
# 升级配置
upgrade_msg = upgrade_config()
class Kaa(KotoneBot):
"""
琴音小助手 kaa 主类由其他 GUI/TUI 调用
"""
def __init__(self, config_path: str):
super().__init__(module='kotonebot.kaa.tasks', config_path=config_path, config_type=BaseConfig)
self.upgrade_msg = upgrade_msg
self.version = importlib.metadata.version('ksaa')
logger.info('Version: %s', self.version)
def add_file_logger(self, log_path: str):
log_dir = os.path.abspath(os.path.dirname(log_path))
os.makedirs(log_dir, exist_ok=True)
file_handler = logging.FileHandler(log_path, encoding='utf-8')
file_handler.setFormatter(log_formatter)
root_logger.addHandler(file_handler)
def set_log_level(self, level: int):
console_handler.setLevel(level)
def dump_error_report(
self,
exception: Exception,
*,
path: str | None = None
) -> str:
"""
保存错误报告
:param path: 保存的路径若为 `None`则保存到 `./reports/{YY-MM-DD HH-MM-SS}.zip`
:return: 保存的路径
"""
from kotonebot import device
from kotonebot.backend.context import current_callstack
try:
if path is None:
path = f'./reports/{datetime.now().strftime("%Y-%m-%d %H-%M-%S")}.zip'
exception_msg = '\n'.join(traceback.format_exception(exception))
task_callstack = '\n'.join(
[f'{i + 1}. name={task.name} priority={task.priority}' for i, task in enumerate(current_callstack)])
screenshot = device.screenshot()
logs = log_stream.getvalue()
with open(self.config_path, 'r', encoding='utf-8') as f:
config_content = f.read()
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
with zipfile.ZipFile(path, 'w') as zipf:
zipf.writestr('exception.txt', exception_msg)
zipf.writestr('task_callstack.txt', task_callstack)
zipf.writestr('screenshot.png', cv2.imencode('.png', screenshot)[1].tobytes())
zipf.writestr('config.json', config_content)
zipf.writestr('logs.txt', logs)
return path
except Exception as e:
logger.exception(f'Failed to save error report:')
return ''

View File

@ -3,10 +3,10 @@ import logging
from cv2.typing import MatLike
from .. import R
from ..game_ui import dialog
from kotonebot.util import Interval, Countdown
from kotonebot.tasks.game_ui import WhiteFilter
from kotonebot.kaa.tasks import R
from kotonebot.kaa.game_ui import dialog
from kotonebot.util import Countdown
from kotonebot.kaa.game_ui import WhiteFilter
from kotonebot import device, image, user, action, use_screenshot
logger = logging.getLogger(__name__)

View File

@ -4,9 +4,9 @@ from logging import getLogger
import cv2
import numpy as np
from kotonebot import image, device, debug, action, sleep
from kotonebot import image, device, action, sleep
from kotonebot.backend.debug import result
from .. import R
from kotonebot.kaa.tasks import R
logger = getLogger(__name__)

View File

@ -1,10 +1,10 @@
import logging
from .. import R
from kotonebot.kaa.tasks import R
from kotonebot.util import Interval
from kotonebot.tasks.game_ui import dialog
from kotonebot.tasks.game_ui import toolbar_home
from kotonebot import device, image, action, until, sleep
from kotonebot.kaa.game_ui import dialog
from kotonebot.kaa.game_ui import toolbar_home
from kotonebot import device, image, action, sleep
logger = logging.getLogger(__name__)

View File

@ -1,8 +1,8 @@
import logging
from typing import NamedTuple
from datetime import timedelta
from .. import R
from kotonebot import action, ocr, device, regex
from kotonebot.kaa.tasks import R
from kotonebot import action, ocr, regex
logger = logging.getLogger(__name__)

View File

@ -1,8 +1,8 @@
"""收取活动费"""
import logging
from .. import R
from ..common import conf
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, color

View File

@ -1,8 +1,8 @@
"""领取礼物(邮箱)"""
import logging
from .. import R
from ..common import conf
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from ..actions.scenes import at_home, goto_home
from kotonebot import device, image, task, color, rect_expand, sleep

View File

@ -3,9 +3,8 @@ import logging
from typing import Literal
from datetime import timedelta
from .. import R
from ..common import conf
from ..actions.loading import wait_loading_end
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, action, ocr, contains, cropped, rect_expand, color, sleep, regex

View File

@ -1,9 +1,9 @@
"""扭蛋机,支持任意次数的任意扭蛋类型"""
import logging
from .. import R
from ..common import conf
from ..game_ui.scrollable import Scrollable
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui.scrollable import Scrollable
from ..actions.scenes import at_home, goto_home
from kotonebot.backend.image import TemplateMatchResult
from kotonebot import task, action, device, image, sleep, Interval

View File

@ -1,9 +1,9 @@
"""领取社团奖励,并尽可能地给其他人送礼物"""
import logging
from .. import R
from ..common import conf
from ..game_ui import toolbar_menu
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui import toolbar_menu
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, sleep, ocr

View File

@ -2,9 +2,9 @@
import logging
from gettext import gettext as _
from .. import R
from ..common import conf
from ..game_ui import WhiteFilter
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui import WhiteFilter
from ..actions.scenes import at_home, goto_home
from ..actions.loading import wait_loading_end
from kotonebot import device, image, ocr, color, action, task, user, rect_expand, sleep, contains

View File

@ -1,8 +1,8 @@
"""领取任务奖励"""
import logging
from .. import R
from ..common import conf, Priority
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf, Priority
from ..actions.loading import wait_loading_end
from ..actions.scenes import at_home, goto_home
from kotonebot import device, image, color, task, action, rect_expand, sleep

View File

@ -1,13 +1,12 @@
"""从商店购买物品"""
import logging
from typing import Optional
from typing_extensions import deprecated
from .. import R
from ..common import conf, DailyMoneyShopItems
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf, DailyMoneyShopItems
from kotonebot.util import cropped
from kotonebot import task, device, image, ocr, action, sleep
from kotonebot.backend.dispatch import DispatcherContext, SimpleDispatcher, dispatcher
from kotonebot import task, device, image, action, sleep
from kotonebot.backend.dispatch import SimpleDispatcher
from ..actions.scenes import goto_home, goto_shop, at_daily_shop
logger = logging.getLogger(__name__)

View File

@ -1,9 +1,9 @@
"""升级一张支援卡,优先升级低等级支援卡"""
import logging
from .. import R
from ..common import conf
from ..game_ui.scrollable import Scrollable
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui.scrollable import Scrollable
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, sleep

View File

@ -6,7 +6,7 @@ import _thread
import threading
from kotonebot.ui import user
from .common import Priority, conf
from kotonebot.kaa.common import Priority, conf
from kotonebot import task, action, config, device
logger = logging.getLogger(__name__)

View File

View File

@ -4,13 +4,12 @@ from typing import Callable, NamedTuple, Literal
import cv2
import numpy as np
from cv2.typing import MatLike
from .. import R
from ..common import conf
from ..game_ui import dialog
from ..util.trace import trace
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui import dialog
from kotonebot.kaa.util.trace import trace
from kotonebot import action, Interval, Countdown, device, image, sleep, ocr, contains, use_screenshot, color, Rect
class SkillCard(NamedTuple):
@ -187,6 +186,7 @@ def do_cards(
card_rects = calc_card_position(card_count)
card_rect = card_rects[0]
device.double_click(card_rect[:4])
sleep(2)
timeout_cd.reset()
# 结束条件
if card_count == 0 and end_predicate():
@ -392,6 +392,5 @@ def detect_recommended_card(
return filtered_results[0]
if __name__ == '__main__':
import cv2
img = cv2.imread(r'E:\GithubRepos\KotonesAutoAssistant.worktrees\dev\kotonebot-resource\sprites\jp\in_purodyuusu\produce_exam_1.png')
img = cv2.imread(r'/kotonebot-resource/sprites/jp/in_purodyuusu/produce_exam_1.png')
print(skill_card_count(img))

View File

@ -0,0 +1,308 @@
from typing import Literal
from logging import getLogger
from kotonebot.kaa.tasks import R
from kotonebot import (
ocr,
device,
image,
action,
sleep,
Interval,
)
from .p_drink import acquire_p_drink
from kotonebot.util import measure_time
from kotonebot.kaa.common import conf
from kotonebot.kaa.tasks.actions.loading import loading
from kotonebot.kaa.game_ui import CommuEventButtonUI, dialog, badge
from kotonebot.kaa.tasks.actions.commu import handle_unread_commu
logger = getLogger(__name__)
@action('领取技能卡', screenshot_mode='manual-inherit')
def acquire_skill_card():
"""获取技能卡(スキルカード)"""
# TODO: 识别卡片内容,而不是固定选卡
# TODO: 不硬编码坐标
logger.debug("Locating all skill cards...")
it = Interval()
cards = None
card_clicked = False
target_card = None
while True:
device.screenshot()
it.wait()
# 是否显示技能卡选择指导的对话框
# [kotonebot-resource/sprites/jp/in_purodyuusu/screenshot_show_skill_card_select_guide_dialog.png]
if image.find(R.InPurodyuusu.TextSkillCardSelectGuideDialogTitle):
# 默认就是显示,直接确认
dialog.yes()
continue
if not cards:
cards = image.find_all_multi([
R.InPurodyuusu.A,
R.InPurodyuusu.M
])
if not cards:
logger.warning("No skill cards found. Skip acquire.")
return
cards = sorted(cards, key=lambda x: (x.position[0], x.position[1]))
logger.info(f"Found {len(cards)} skill cards")
# 判断是否有推荐卡
rec_badges = image.find_all(R.InPurodyuusu.TextRecommend)
rec_badges = [card.rect for card in rec_badges]
if rec_badges:
cards = [card.rect for card in cards]
matches = badge.match(cards, rec_badges, 'mb')
logger.debug("Recommend card badge matches: %s", matches)
# 选第一个推荐卡
target_match = next(filter(lambda m: m.badge is not None, matches), None)
if target_match:
target_card = target_match.object
else:
target_card = cards[0]
else:
logger.debug("No recommend badge found. Pick first card.")
target_card = cards[0].rect
continue
if not card_clicked and target_card is not None:
logger.debug("Click target skill card")
device.click(target_card)
card_clicked = True
sleep(0.2)
continue
if acquire_btn := image.find(R.InPurodyuusu.AcquireBtnDisabled):
logger.debug("Click acquire button")
device.click(acquire_btn)
break
@action('选择P物品', screenshot_mode='auto')
def select_p_item():
"""
前置条件P物品选择对话框受け取るPアイテムを選んでください;\n
结束状态P物品获取动画
"""
# 前置条件 [screenshots/produce/in_produce/select_p_item.png]
# 前置条件 [screenshots/produce/in_produce/claim_p_item.png]
POSTIONS = [
(157, 820, 128, 128), # x, y, w, h
(296, 820, 128, 128),
(435, 820, 128, 128),
] # TODO: HARD CODED
device.click(POSTIONS[0])
sleep(0.5)
device.click(ocr.expect_wait('受け取る'))
@action('技能卡自选强化', screenshot_mode='manual-inherit')
def hanlde_skill_card_enhance():
"""
前置条件技能卡强化对话框\n
结束状态技能卡强化动画结束后瞬间
:return: 是否成功处理对话框
"""
# 前置条件 [kotonebot-resource\sprites\jp\in_purodyuusu\screenshot_skill_card_enhane.png]
# 结束状态 [screenshots/produce/in_produce/skill_card_enhance.png]
cards = image.find_multi([
R.InPurodyuusu.A,
R.InPurodyuusu.M
])
if cards is None:
logger.info("No skill cards found")
return False
logger.debug("Clicking first skill card.")
device.click(cards)
it = Interval()
while True:
device.screenshot()
if image.find(R.InPurodyuusu.ButtonEnhance, colored=True):
logger.debug("Enhance button found")
device.click()
break
it.wait()
logger.debug("Handle skill card enhance finished.")
@action('技能卡自选删除', screenshot_mode='manual-inherit')
def handle_skill_card_removal():
"""
前置条件技能卡删除对话框\n
结束状态技能卡删除动画结束后瞬间
"""
# 前置条件 [kotonebot-resource\sprites\jp\in_purodyuusu\screenshot_remove_skill_card.png]
card = image.find_multi([
R.InPurodyuusu.A,
R.InPurodyuusu.M
])
if card is None:
logger.info("No skill cards found")
return False
device.click(card)
it = Interval()
while True:
device.screenshot()
if image.find(R.InPurodyuusu.ButtonRemove):
device.click()
logger.debug("Remove button clicked.")
break
it.wait()
logger.debug("Handle skill card removal finished.")
AcquisitionType = Literal[
"PDrinkAcquire", # P饮料被动领取
"PDrinkSelect", # P饮料主动领取
"PDrinkMax", # P饮料到达上限
"PSkillCardAcquire", # 技能卡领取
"PSkillCardSelect", # 技能卡选择
"PSkillCardEnhanced", # 技能卡强化
"PSkillCardEnhanceSelect", # 技能卡自选强化
"PSkillCardRemoveSelect", # 技能卡自选删除
"PSkillCardEvent", # 技能卡事件(随机强化、删除、更换)
"PItemClaim", # P物品领取
"PItemSelect", # P物品选择
"Clear", # 目标达成
"ClearNext", # 目标达成 NEXT
"NetworkError", # 网络中断弹窗
"SkipCommu", # 跳过交流
"Loading", # 加载画面
]
@measure_time()
@action('处理培育事件', screenshot_mode='manual')
def fast_acquisitions() -> AcquisitionType | None:
"""处理行动开始前和结束后可能需要处理的事件"""
img = device.screenshot()
logger.info("Acquisition stuffs...")
# 加载画面
if loading():
logger.info("Loading...")
return "Loading"
device.click(10, 10)
# 跳过未读交流
logger.debug("Check skip commu...")
if conf().produce.skip_commu and handle_unread_commu(img):
return "SkipCommu"
device.click(10, 10)
# P饮料到达上限
logger.debug("Check PDrink max...")
# TODO: 需要封装一个更好的实现方式。比如 wait_stable
if image.find(R.InPurodyuusu.TextPDrinkMax):
logger.debug("PDrink max found")
device.screenshot()
if image.find(R.InPurodyuusu.TextPDrinkMax):
# 有对话框标题,但是没找到确认按钮
# 可能是需要勾选一个饮料
if not image.find(R.InPurodyuusu.ButtonLeave, colored=True):
logger.info("No leave button found, click checkbox")
device.click(image.expect(R.Common.CheckboxUnchecked, colored=True))
sleep(0.2)
device.screenshot()
if leave := image.find(R.InPurodyuusu.ButtonLeave, colored=True):
logger.info("Leave button found")
device.click(leave)
return "PDrinkMax"
# P饮料到达上限 确认提示框
# [kotonebot-resource\sprites\jp\in_purodyuusu\screenshot_pdrink_max_confirm.png]
if image.find(R.InPurodyuusu.TextPDrinkMaxConfirmTitle):
logger.debug("PDrink max confirm found")
device.screenshot()
if image.find(R.InPurodyuusu.TextPDrinkMaxConfirmTitle):
if confirm := image.find(R.Common.ButtonConfirm):
logger.info("Confirm button found")
device.click(confirm)
return "PDrinkMax"
device.click(10, 10)
# 技能卡自选强化
if image.find(R.InPurodyuusu.IconTitleSkillCardEnhance):
if hanlde_skill_card_enhance():
return "PSkillCardEnhanceSelect"
device.click(10, 10)
# 技能卡自选删除
if image.find(R.InPurodyuusu.IconTitleSkillCardRemoval):
if handle_skill_card_removal():
return "PSkillCardRemoveSelect"
device.click(10, 10)
# 网络中断弹窗
logger.debug("Check network error popup...")
if (image.find(R.Common.TextNetworkError)
and (btn_retry := image.find(R.Common.ButtonRetry))
):
logger.info("Network error popup found")
device.click(btn_retry)
return "NetworkError"
device.click(10, 10)
# 物品选择对话框
logger.debug("Check award select dialog...")
if image.find(R.InPurodyuusu.TextClaim):
logger.info("Award select dialog found.")
# P饮料选择
logger.debug("Check PDrink select...")
if image.find(R.InPurodyuusu.TextPDrink):
logger.info("PDrink select found")
acquire_p_drink()
return "PDrinkSelect"
# 技能卡选择
logger.debug("Check skill card select...")
if image.find(R.InPurodyuusu.TextSkillCard):
logger.info("Acquire skill card found")
acquire_skill_card()
return "PSkillCardSelect"
# P物品选择
logger.debug("Check PItem select...")
if image.find(R.InPurodyuusu.TextPItem):
logger.info("Acquire PItem found")
select_p_item()
return "PItemSelect"
device.click(10, 10)
return None
def until_acquisition_clear():
"""
处理各种奖励弹窗直到没有新的奖励弹窗为止
前置条件任意\n
结束条件任意
"""
interval = Interval(0.6)
while fast_acquisitions():
interval.wait()
ORANGE_RANGE = ((14, 87, 23)), ((37, 211, 255))
@action('处理交流事件', screenshot_mode='manual-inherit')
def commu_event():
ui = CommuEventButtonUI([ORANGE_RANGE])
buttons = ui.all(description=False, title=True)
if len(buttons) > 1:
for button in buttons:
# 冲刺课程,跳过处理
if '重点' in button.title:
return False
logger.info(f"Found commu event: {buttons}")
logger.info("Select first choice")
if buttons[0].selected:
device.click(buttons[0])
else:
device.double_click(buttons[0])
return True
return False
if __name__ == '__main__':
from logging import getLogger
import logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s')
getLogger('kotonebot').setLevel(logging.DEBUG)
getLogger(__name__).setLevel(logging.DEBUG)
select_p_item()

View File

@ -1,7 +1,7 @@
from typing import Literal
from logging import getLogger
from .. import R
from kotonebot.kaa import R
from kotonebot import (
ocr,
device,
@ -12,10 +12,10 @@ from kotonebot import (
)
from .p_drink import acquire_p_drink
from kotonebot.util import measure_time
from kotonebot.tasks.common import conf
from kotonebot.tasks.actions.loading import loading
from kotonebot.tasks.game_ui import CommuEventButtonUI, dialog, badge
from kotonebot.tasks.actions.commu import handle_unread_commu
from kotonebot.kaa.common import conf
from kotonebot.kaa.tasks.actions.loading import loading
from kotonebot.kaa.game_ui import CommuEventButtonUI, dialog, badge
from kotonebot.kaa.tasks.actions.commu import handle_unread_commu
logger = getLogger(__name__)

View File

@ -1,19 +1,19 @@
import logging
from typing_extensions import assert_never
from typing import Literal, NamedTuple
from typing import Literal
from .. import R
from kotonebot.kaa.tasks import R
from ..actions import loading
from ..game_ui import WhiteFilter, dialog
from kotonebot.kaa.game_ui import WhiteFilter, dialog
from ..actions.scenes import at_home
from .cards import do_cards, CardDetectResult
from ..actions.commu import handle_unread_commu
from kotonebot.errors import UnrecoverableError
from kotonebot.util import Countdown, Interval, cropped
from kotonebot.backend.dispatch import DispatcherContext
from ..common import ProduceAction, RecommendCardDetectionMode, conf
from kotonebot.kaa.common import ProduceAction, RecommendCardDetectionMode, conf
from ..produce.common import until_acquisition_clear, commu_event, fast_acquisitions
from kotonebot import ocr, device, contains, image, regex, action, sleep, Rect, wait
from kotonebot import ocr, device, contains, image, regex, action, sleep, wait
from ..produce.non_lesson_actions import (
enter_allowance, allowance_available,
study_available, enter_study,
@ -836,7 +836,7 @@ if __name__ == '__main__':
logging.getLogger().addHandler(file_handler)
from kotonebot.backend.context import init_context, manual_context
from ..common import BaseConfig
from kotonebot.kaa.common import BaseConfig
from kotonebot.backend.debug import debug
init_context(config_type=BaseConfig)
manual_context().begin()

View File

@ -5,13 +5,12 @@
"""
from logging import getLogger
from kotonebot.tasks.game_ui import dialog
from kotonebot.kaa.game_ui import dialog
from .. import R
from ..common import conf
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from ..produce.common import fast_acquisitions
from ..game_ui.commu_event_buttons import CommuEventButtonUI
from kotonebot.kaa.game_ui.commu_event_buttons import CommuEventButtonUI
from kotonebot.util import Countdown, Interval
from kotonebot.errors import UnrecoverableError
from kotonebot import device, image, action, sleep

View File

@ -1,6 +1,6 @@
from logging import getLogger
from .. import R
from kotonebot.kaa.tasks import R
from kotonebot import device, image, action, sleep
logger = getLogger(__name__)

View File

@ -8,11 +8,11 @@ from kotonebot.util import Countdown, Interval
from kotonebot.backend.context.context import wait
from kotonebot.backend.dispatch import SimpleDispatcher
from .. import R
from ..common import conf
from ..game_ui import dialog
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui import dialog
from ..actions.scenes import at_home, goto_home
from ..game_ui.idols_overview import locate_idol
from kotonebot.kaa.game_ui.idols_overview import locate_idol
from ..produce.in_purodyuusu import hajime_pro, hajime_regular, hajime_master, resume_pro_produce, resume_regular_produce, \
resume_master_produce
from kotonebot import device, image, ocr, task, action, sleep, contains
@ -248,7 +248,7 @@ def do_produce(
dialog.no()
elif image.find(R.Common.ButtonNextNoIcon):
device.click()
elif image.find(R.Produce.TextStepIndicator2):
if image.find(R.Produce.TextStepIndicator2):
break
# 2. 选择支援卡 自动编成 [screenshots/produce/screenshot_produce_start_2_support_card.png]
image.expect_wait(R.Produce.TextStepIndicator2)
@ -364,9 +364,9 @@ if __name__ == '__main__':
logging.getLogger().addHandler(file_handler)
import time
from kotonebot.backend.context import init_context, manual_context
from kotonebot.tasks.common import BaseConfig
from kotonebot.util import Profiler
from kotonebot.backend.context import init_context
from kotonebot.kaa.common import BaseConfig
init_context(config_type=BaseConfig)
conf().produce.enabled = True
conf().produce.mode = 'pro'

View File

@ -3,8 +3,8 @@ import os
import ctypes
import logging
from . import R
from .common import Priority, conf
from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import Priority, conf
from .actions.loading import loading
from kotonebot.util import Countdown, Interval
from .actions.scenes import at_home, goto_home

View File

@ -1,7 +1,7 @@
import os
from typing import cast
from kotonebot.tasks import resources as res
from kotonebot.kaa import resources as res
CACHE = os.path.join('cache')
RESOURCE = cast(list[str], res.__path__)[0]

View File

View File

@ -1,43 +0,0 @@
import os
import logging
import importlib.metadata
from datetime import datetime
from kotonebot import KotoneBot
from ..common import BaseConfig, upgrade_config
# 初始化日志
os.makedirs('logs', exist_ok=True)
log_formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(name)s] %(message)s')
log_filename = datetime.now().strftime('logs/%y-%m-%d-%H-%M-%S.log')
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.CRITICAL)
file_handler = logging.FileHandler(log_filename, encoding='utf-8')
file_handler.setFormatter(log_formatter)
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
logging.getLogger("kotonebot").setLevel(logging.DEBUG)
logger = logging.getLogger(__name__)
# 升级配置
upgrade_msg = upgrade_config()
class Kaa(KotoneBot):
"""
琴音小助手 kaa 主类由其他 GUI/TUI 调用
"""
def __init__(self):
super().__init__(module='kotonebot.tasks', config_type=BaseConfig)
self.upgrade_msg = upgrade_msg
self.version = importlib.metadata.version('ksaa')
logger.info('Version: %s', self.version)
def set_log_level(self, level: int):
console_handler.setLevel(level)

View File

@ -48,7 +48,7 @@ dependencies = [
package-dir = { "kotonebot" = "kotonebot" }
[project.scripts]
kaa = "kotonebot.tasks.main.cli:main"
kaa = "kotonebot.kaa.main.cli:main"
[tool.setuptools.dynamic]
version = {file = "./version"}

View File

@ -51,11 +51,11 @@ class TestOcr(unittest.TestCase):
assert bounding_box(points) == (5, 5, 0, 0)
def test_ocr_basic(self):
result = jp.ocr(self.img)
result = jp().ocr(self.img)
self.assertGreater(len(result), 0)
def test_ocr_rect(self):
result = jp.ocr(self.img, rect=(147, 614, 417, 32), pad=True)
result = jp().ocr(self.img, rect=(147, 614, 417, 32), pad=True)
self.assertEqual(result[0].text, '受け取るPドリンクを選んでください。')
x, y, w, h = result[0].original_rect
self.assertAlmostEqual(x, 147, delta=10)
@ -63,7 +63,7 @@ class TestOcr(unittest.TestCase):
self.assertAlmostEqual(w, 417, delta=10)
self.assertAlmostEqual(h, 32, delta=10)
result = jp.ocr(self.img, rect=(147, 614, 417, 32), pad=False)
result = jp().ocr(self.img, rect=(147, 614, 417, 32), pad=False)
self.assertEqual(result[0].text, '受け取るPドリンクを選んでください。')
x, y, w, h = result[0].original_rect
self.assertAlmostEqual(x, 147, delta=10)
@ -72,9 +72,9 @@ class TestOcr(unittest.TestCase):
self.assertAlmostEqual(h, 32, delta=10)
def test_find(self):
self.assertTrue(jp.find(self.img, '中間まで'))
self.assertTrue(jp.find(self.img, '受け取るPドリンクを選んでください。'))
self.assertTrue(jp.find(self.img, '受け取る'))
self.assertTrue(jp().find(self.img, '中間まで'))
self.assertTrue(jp().find(self.img, '受け取るPドリンクを選んでください。'))
self.assertTrue(jp().find(self.img, '受け取る'))
class TestOcrResult(unittest.TestCase):

View File

@ -1,5 +1,5 @@
from unittest import TestCase
from kotonebot.tasks.game_ui.badge import match, BadgeResult
from kotonebot.kaa.game_ui.badge import match, BadgeResult
from kotonebot.util import Rect
def rect_from_center(x: int, y: int) -> Rect:

View File

@ -82,7 +82,7 @@ class BaseTestCase(unittest.TestCase):
cls.device = MockDevice()
from kotonebot.backend.debug.server import start_server
from kotonebot.backend.debug import debug
from kotonebot.tasks.common import BaseConfig
from kotonebot.kaa.common import BaseConfig
debug.enabled = True
# debug.wait_for_message_sent = True
start_server()

View File

@ -3,7 +3,7 @@
####### 此文件为自动生成,请勿编辑 #######
####### AUTO GENERATED. DO NOT EDIT. #######
{%- endif %}
from kotonebot.tasks.common import sprite_path
from kotonebot.kaa.common import sprite_path
from kotonebot.backend.core import Image, HintBox, HintPoint

View File

@ -17,10 +17,10 @@ print("拉取资源...")
manifest = gom.fetch()
print("提取 P 偶像卡资源...")
base_path = './kotonebot/tasks/resources/idol_cards'
base_path = './kotonebot/kaa/resources/idol_cards'
os.makedirs(base_path, exist_ok=True)
db = sqlite3.connect("./kotonebot/tasks/resources/game.db")
db = sqlite3.connect("./kotonebot/kaa/resources/game.db")
cursor = db.execute("""
SELECT
IC.id AS cardId,

View File

@ -457,12 +457,12 @@ if __name__ == '__main__':
parser.add_argument('-i', '--ide', help='IDE 类型', default=ide_type())
args = parser.parse_args()
if os.path.exists(r'kotonebot\tasks\sprites'):
shutil.rmtree(r'kotonebot\tasks\sprites')
if os.path.exists(r'kotonebot\kaa\sprites'):
shutil.rmtree(r'kotonebot\kaa\sprites')
path = PATH + '\\jp'
files = scan_png_files(path)
sprites = load_sprites(path, files)
sprites = copy_sprites(sprites, r'kotonebot\tasks\sprites')
sprites = copy_sprites(sprites, r'kotonebot\kaa\sprites')
classes = make_classes(sprites, args.ide)
env = jinja2.Environment(loader=jinja2.FileSystemLoader('./tools'))
@ -470,9 +470,9 @@ if __name__ == '__main__':
template = env.get_template('R.jinja2')
print(f'Rendering template: {template.name}')
with open('./kotonebot/tasks/R.py', 'w', encoding='utf-8') as f:
with open('./kotonebot/kaa/tasks/R.py', 'w', encoding='utf-8') as f:
f.write(template.render(data=classes, production=args.production))
print('Creating __init__.py')
with open('./kotonebot/tasks/sprites/__init__.py', 'w', encoding='utf-8') as f:
with open('./kotonebot/kaa/sprites/__init__.py', 'w', encoding='utf-8') as f:
f.write('')
print('All done!')