Merge commit '1d177be34845495c4051ba642c61198bcea271e8'
This commit is contained in:
commit
e0daedd37c
|
@ -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/
|
||||
##########################
|
||||
|
||||
|
|
|
@ -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
|
66
README.md
66
README.md
|
@ -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 整合
|
||||
|
||||
### 其他
|
||||
* 适配汉化版
|
||||
- [ ] 需要一个合适的方法自动切换不用语言的资源文件
|
||||
- [ ] 需要一个合适的工具来辅助替换模板图片文件
|
||||
- [ ] 收集汉化版本的截图
|
6
justfile
6
justfile
|
@ -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) {
|
||||
|
|
|
@ -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:]:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
# 定位到目标类
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
"""
|
|
@ -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'
|
|
@ -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()
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
from kotonebot.tasks import R
|
||||
from kotonebot.kaa.tasks import R
|
||||
from kotonebot import device, image
|
||||
|
||||
def expect_yes():
|
|
@ -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()
|
|
@ -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
|
||||
|
|
@ -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'
|
|
@ -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())
|
||||
|
|
@ -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)
|
|
@ -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 ''
|
|
@ -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__)
|
|
@ -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__)
|
||||
|
|
@ -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__)
|
||||
|
|
@ -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__)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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__)
|
|
@ -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
|
||||
|
|
@ -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__)
|
|
@ -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))
|
|
@ -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()
|
|
@ -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__)
|
||||
|
|
@ -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()
|
|
@ -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
|
|
@ -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__)
|
|
@ -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'
|
|
@ -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
|
|
@ -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]
|
|
@ -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)
|
|
@ -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"}
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!')
|
Loading…
Reference in New Issue