kotonebot 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +73 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows/__init__.py +1 -0
- kotonebot/client/implements/windows/print_window.py +133 -0
- kotonebot/client/implements/windows/send_message.py +324 -0
- kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +103 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +13 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/interop/win/window.py +89 -0
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
- kotonebot-0.7.0.dist-info/RECORD +109 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
- kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import TYPE_CHECKING, Callable
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from kotonebot import Loop
|
|
7
|
+
from kotonebot.client.scaler import AbstractScaler
|
|
8
|
+
from kotonebot.primitives import Size
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DeviceConfig:
|
|
12
|
+
default_scaler_factory: 'Callable[[], AbstractScaler]'
|
|
13
|
+
"""Device 类默认使用缩放方法类的构造器。
|
|
14
|
+
|
|
15
|
+
构造 Device 实例时,可以在构造方法中指定 scaler。若未指定,则使用此处的默认值。
|
|
16
|
+
默认使用的 scaler 为 :class:`ProportionalScaler`,即等比例缩放。对于非等比例缩放,
|
|
17
|
+
会直接抛出异常。
|
|
18
|
+
"""
|
|
19
|
+
default_logic_resolution: Size | None
|
|
20
|
+
"""Device 默认逻辑分辨率。
|
|
21
|
+
|
|
22
|
+
若为 None,则不进行缩放。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LoopConfig:
|
|
28
|
+
loop_callbacks: 'list[Callable[[Loop], None]]' = field(default_factory=list)
|
|
29
|
+
"""全局 Loop 回调函数。
|
|
30
|
+
|
|
31
|
+
每次 Loop 循环一次时,都会调用此处的处理函数。
|
|
32
|
+
可以在这里放置一些需要全局处理的内容,如网络错误等。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Config:
|
|
37
|
+
device: DeviceConfig
|
|
38
|
+
loop: LoopConfig
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_global_config: ContextVar[Config | None] = ContextVar('kotonebot_global_config', default=None)
|
|
42
|
+
|
|
43
|
+
def conf(*, auto_create: bool = True) -> Config:
|
|
44
|
+
"""获取全局配置对象。
|
|
45
|
+
|
|
46
|
+
:return: 全局配置对象。
|
|
47
|
+
"""
|
|
48
|
+
c = _global_config.get()
|
|
49
|
+
if c is None:
|
|
50
|
+
if not auto_create:
|
|
51
|
+
raise RuntimeError('Global config is not set.')
|
|
52
|
+
from kotonebot.client.scaler import ProportionalScaler
|
|
53
|
+
c = Config(
|
|
54
|
+
device=DeviceConfig(
|
|
55
|
+
default_scaler_factory=lambda: ProportionalScaler(),
|
|
56
|
+
default_logic_resolution=None
|
|
57
|
+
),
|
|
58
|
+
loop=LoopConfig()
|
|
59
|
+
)
|
|
60
|
+
_global_config.set(c)
|
|
61
|
+
return c
|
kotonebot/config/manager.py
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Type, Generic, TypeVar
|
|
3
|
-
|
|
4
|
-
from .base_config import RootConfig, UserConfig
|
|
5
|
-
|
|
6
|
-
T = TypeVar('T')
|
|
7
|
-
|
|
8
|
-
def load_config(
|
|
9
|
-
config_path: str,
|
|
10
|
-
*,
|
|
11
|
-
type: Type[T],
|
|
12
|
-
use_default_if_not_found: bool = True
|
|
13
|
-
) -> RootConfig[T]:
|
|
14
|
-
"""
|
|
15
|
-
从指定路径读取配置文件
|
|
16
|
-
|
|
17
|
-
:param config_path: 配置文件路径
|
|
18
|
-
:param use_default_if_not_found: 如果配置文件不存在,是否使用默认配置
|
|
19
|
-
"""
|
|
20
|
-
if not os.path.exists(config_path):
|
|
21
|
-
if use_default_if_not_found:
|
|
22
|
-
return RootConfig[type]()
|
|
23
|
-
else:
|
|
24
|
-
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
25
|
-
|
|
26
|
-
with open(config_path, 'r', encoding='utf-8') as f:
|
|
27
|
-
return RootConfig[type].model_validate_json(f.read())
|
|
28
|
-
|
|
29
|
-
def save_config(
|
|
30
|
-
config: RootConfig[T],
|
|
31
|
-
config_path: str,
|
|
32
|
-
):
|
|
33
|
-
"""将配置保存到指定路径"""
|
|
34
|
-
RootConfig[T].model_validate(config)
|
|
35
|
-
with open(config_path, 'w+', encoding='utf-8') as f:
|
|
36
|
-
f.write(config.model_dump_json(indent=4))
|
|
1
|
+
import os
|
|
2
|
+
from typing import Type, Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from .base_config import RootConfig, UserConfig
|
|
5
|
+
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
def load_config(
|
|
9
|
+
config_path: str,
|
|
10
|
+
*,
|
|
11
|
+
type: Type[T],
|
|
12
|
+
use_default_if_not_found: bool = True
|
|
13
|
+
) -> RootConfig[T]:
|
|
14
|
+
"""
|
|
15
|
+
从指定路径读取配置文件
|
|
16
|
+
|
|
17
|
+
:param config_path: 配置文件路径
|
|
18
|
+
:param use_default_if_not_found: 如果配置文件不存在,是否使用默认配置
|
|
19
|
+
"""
|
|
20
|
+
if not os.path.exists(config_path):
|
|
21
|
+
if use_default_if_not_found:
|
|
22
|
+
return RootConfig[type]()
|
|
23
|
+
else:
|
|
24
|
+
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
25
|
+
|
|
26
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
27
|
+
return RootConfig[type].model_validate_json(f.read())
|
|
28
|
+
|
|
29
|
+
def save_config(
|
|
30
|
+
config: RootConfig[T],
|
|
31
|
+
config_path: str,
|
|
32
|
+
):
|
|
33
|
+
"""将配置保存到指定路径"""
|
|
34
|
+
RootConfig[T].model_validate(config)
|
|
35
|
+
with open(config_path, 'w+', encoding='utf-8') as f:
|
|
36
|
+
f.write(config.model_dump_json(indent=4))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .entities.base import Prefab
|
|
2
|
+
from .entities.template_match import TemplateMatchPrefab, TemplateMatchFindKargs
|
|
3
|
+
from .entities.ocr import OcrPrefab, OcrFindKargs
|
|
4
|
+
from .entities.base import GameObject
|
|
5
|
+
from .entities.base import GameObjectType
|
|
6
|
+
from .entities.compound import AnyOf
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'Prefab', 'TemplateMatchPrefab', 'OcrPrefab',
|
|
10
|
+
'GameObject',
|
|
11
|
+
'GameObjectType',
|
|
12
|
+
'AnyOf'
|
|
13
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import Any, Callable, Type, cast, get_args
|
|
4
|
+
from typing_extensions import Generic, TypeVar, Unpack, TypedDict
|
|
5
|
+
|
|
6
|
+
from kotonebot.primitives import Rect
|
|
7
|
+
from kotonebot.backend.context.context import manual_context
|
|
8
|
+
|
|
9
|
+
GameObjectType = TypeVar('GameObjectType', bound='GameObject', default='GameObject')
|
|
10
|
+
|
|
11
|
+
class FindKwargs(TypedDict, Generic[GameObjectType], total=False):
|
|
12
|
+
predicate: 'Callable[[GameObjectType], bool] | None'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ClickKwargs(FindKwargs[GameObjectType], Generic[GameObjectType], total=False):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class WaitKwargs(FindKwargs[GameObjectType], Generic[GameObjectType], total=False):
|
|
19
|
+
timeout: float | None
|
|
20
|
+
interval: float | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Prefab(Generic[GameObjectType], ABC):
|
|
24
|
+
__object_class__: Type[GameObjectType] | None = None
|
|
25
|
+
display_name: str | None = None
|
|
26
|
+
"""展示名称
|
|
27
|
+
|
|
28
|
+
可选,用于在编辑器或日志中显示更友好的名称。
|
|
29
|
+
如果未设置,则使用类名。
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _get_object_class(cls) -> Type[GameObjectType]:
|
|
34
|
+
"""
|
|
35
|
+
核心魔法:获取用于实例化的类。
|
|
36
|
+
优先使用显式定义的 object_class,
|
|
37
|
+
如果没有,则尝试从泛型定义中推断。
|
|
38
|
+
"""
|
|
39
|
+
# 1. 如果用户手动定义了,直接用
|
|
40
|
+
if cls.__object_class__ is not None:
|
|
41
|
+
return cls.__object_class__
|
|
42
|
+
|
|
43
|
+
# 2. 尝试从 __orig_bases__ 推断
|
|
44
|
+
# 遍历基类,寻找 Prefab[T] 的定义
|
|
45
|
+
for base in getattr(cls, "__orig_bases__", []):
|
|
46
|
+
origin = getattr(base, "__origin__", None)
|
|
47
|
+
# 检查这个基类是不是 Prefab (或者其子类)
|
|
48
|
+
if origin is not None and issubclass(origin, Prefab):
|
|
49
|
+
args = get_args(base)
|
|
50
|
+
if args and isinstance(args[0], type) and issubclass(args[0], GameObject):
|
|
51
|
+
# 缓存结果,下次不用再推断
|
|
52
|
+
cls.__object_class__ = args[0]
|
|
53
|
+
return cls.__object_class__
|
|
54
|
+
# 3. 如果都失败了,回退到默认的 GameObject
|
|
55
|
+
# (这通常发生在用户没有指定泛型参数时,如 class MyPrefab(TemplateMatchPrefab): ...)
|
|
56
|
+
return cast(Type[GameObjectType], GameObject)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def find(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> GameObjectType | None:
|
|
60
|
+
"""在屏幕画面中寻找当前 Prefab,并返回对应的第一个 GameObject 实例。
|
|
61
|
+
|
|
62
|
+
:return: 寻找结果。如果没有找到,返回 None。
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def find_all(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> list[GameObjectType]:
|
|
68
|
+
"""在屏幕画面中寻找当前 Prefab,并返回对应的所有 GameObject 实例。
|
|
69
|
+
|
|
70
|
+
:return: 寻找结果列表。如果没有找到,返回空列表。
|
|
71
|
+
"""
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def require(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> GameObjectType:
|
|
76
|
+
"""在屏幕画面中寻找当前 Prefab,并返回对应的第一个 GameObject 实例。
|
|
77
|
+
|
|
78
|
+
此方法与 find 类似,但如果没有找到任何结果,则会抛出异常。
|
|
79
|
+
|
|
80
|
+
:raises: 如果没有找到,抛出异常。
|
|
81
|
+
:return: 寻找结果。
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def exists(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> bool:
|
|
87
|
+
"""判断当前 Prefab 是否存在于屏幕画面中。
|
|
88
|
+
|
|
89
|
+
此方法为 find 的简化版,仅返回是否存在。
|
|
90
|
+
相当于 ``Prefab.find(...) is not None``。
|
|
91
|
+
|
|
92
|
+
:return: 如果存在,返回 True;否则返回 False。
|
|
93
|
+
"""
|
|
94
|
+
return cls.find(**kwargs) is not None
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def click(cls, **kwargs: Unpack[ClickKwargs[GameObjectType]]) -> None:
|
|
98
|
+
"""在屏幕画面中寻找当前 Prefab,并点击第一个找到的 GameObject 实例。
|
|
99
|
+
|
|
100
|
+
该方法会调用 require 方法,因此如果没有找到任何结果,则会抛出异常。
|
|
101
|
+
"""
|
|
102
|
+
return cls.require(**kwargs).click()
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def try_click(cls, **kwargs: Unpack[ClickKwargs[GameObjectType]]) -> bool:
|
|
106
|
+
"""尝试点击当前 Prefab 的第一个找到的 GameObject 实例。
|
|
107
|
+
|
|
108
|
+
:return: 如果找到了对象并成功点击,返回 True;否则返回 False。
|
|
109
|
+
"""
|
|
110
|
+
obj = cls.find(**kwargs)
|
|
111
|
+
if obj is not None:
|
|
112
|
+
obj.click()
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def wait(
|
|
118
|
+
cls,
|
|
119
|
+
**kwargs: Unpack[WaitKwargs[GameObjectType]],
|
|
120
|
+
) -> GameObjectType:
|
|
121
|
+
"""等待当前 Prefab 出现。
|
|
122
|
+
|
|
123
|
+
若指定时间内未找到,则抛出超时异常(wait 不再返回 None)。
|
|
124
|
+
"""
|
|
125
|
+
# 从 kwargs 中分离出用于等待控制的参数,剩下的传递给 `find`
|
|
126
|
+
timeout = kwargs.pop("timeout", None)
|
|
127
|
+
interval = kwargs.pop("interval", None)
|
|
128
|
+
start_time = time.time()
|
|
129
|
+
ctx = manual_context('auto')
|
|
130
|
+
with ctx:
|
|
131
|
+
while True:
|
|
132
|
+
obj = cls.find(**kwargs)
|
|
133
|
+
if obj is not None:
|
|
134
|
+
return obj
|
|
135
|
+
from kotonebot import sleep
|
|
136
|
+
sleep(interval or 1.0)
|
|
137
|
+
if timeout is not None:
|
|
138
|
+
elapsed = time.time() - start_time
|
|
139
|
+
if elapsed >= timeout:
|
|
140
|
+
raise TimeoutError(f"Timeout when waiting for {cls.__name__}({timeout} s)")
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def try_wait(
|
|
144
|
+
cls,
|
|
145
|
+
**kwargs: Unpack[WaitKwargs[GameObjectType]],
|
|
146
|
+
) -> GameObjectType | None:
|
|
147
|
+
"""尝试等待当前 Prefab 出现。
|
|
148
|
+
|
|
149
|
+
若指定时间内未找到,则返回 None。
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
return cls.wait(**kwargs)
|
|
153
|
+
except TimeoutError:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
class GameObject:
|
|
157
|
+
"""## GameObject
|
|
158
|
+
GameObject(游戏对象),游戏物体/UI 的基类,所有通过一系列方式从屏幕画面上寻找到的结果都应以 GameObject 的形式展示。
|
|
159
|
+
|
|
160
|
+
GameObject 本身仅包含基础属性。如果你需要自定义 GameObject 的属性或行为,可以继承 GameObject 并使用你自己的类。
|
|
161
|
+
"""
|
|
162
|
+
rect: Rect
|
|
163
|
+
"""对象在屏幕上的范围"""
|
|
164
|
+
display_name: str | None = None
|
|
165
|
+
"""展示名称
|
|
166
|
+
|
|
167
|
+
可选,用于在编辑器或日志中显示更友好的名称。
|
|
168
|
+
如果未设置,则使用类名。
|
|
169
|
+
"""
|
|
170
|
+
prefab: type[Prefab[Any]]
|
|
171
|
+
"""当前对象对应的 Prefab 类"""
|
|
172
|
+
|
|
173
|
+
def click(self) -> None:
|
|
174
|
+
"""点击当前对象的中心位置。"""
|
|
175
|
+
from kotonebot import device
|
|
176
|
+
device.click(self.rect.center)
|
|
177
|
+
|
|
178
|
+
def double_click(self) -> None:
|
|
179
|
+
"""双击当前对象的中心位置。"""
|
|
180
|
+
from kotonebot import device
|
|
181
|
+
device.double_click(*self.rect.center)
|
|
182
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Type, Sequence, cast, Any
|
|
2
|
+
from typing_extensions import Unpack, override
|
|
3
|
+
from kotonebot.core.entities.base import FindKwargs, GameObjectType
|
|
4
|
+
from .base import Prefab
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AnyOf(Prefab[Any]):
|
|
8
|
+
"""
|
|
9
|
+
复合 Prefab,用于匹配给定的任意一个 Prefab。
|
|
10
|
+
|
|
11
|
+
作为一个 Class,它可以通过两种方式使用:
|
|
12
|
+
1. 继承定义:
|
|
13
|
+
class MyButton(AnyOf):
|
|
14
|
+
options = [ConfirmButton, CancelButton]
|
|
15
|
+
|
|
16
|
+
2. 动态泛型(推荐):
|
|
17
|
+
MyButton = AnyOf[ConfirmButton, CancelButton]
|
|
18
|
+
"""
|
|
19
|
+
options: Sequence[Type[Prefab]] = []
|
|
20
|
+
|
|
21
|
+
def __class_getitem__(cls, items: Type[Prefab] | tuple[Type[Prefab], ...]):
|
|
22
|
+
"""
|
|
23
|
+
允许使用 AnyOf[PrefabA, PrefabB] 的语法动态生成子类。
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(items, tuple):
|
|
26
|
+
items = (items,)
|
|
27
|
+
|
|
28
|
+
# 动态创建一个新的类,名字由所有子 Prefab 的名字拼接而成
|
|
29
|
+
name = f"AnyOf_{'_'.join(i.__name__ for i in items)}"
|
|
30
|
+
return type(name, (cls,), {'options': items})
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
@classmethod
|
|
34
|
+
def find(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> GameObjectType | None:
|
|
35
|
+
# Cast kwargs to Any to bypass contravariance checks on predicate
|
|
36
|
+
unsafe_kwargs = cast(dict[str, Any], kwargs)
|
|
37
|
+
|
|
38
|
+
for prefab in cls.options:
|
|
39
|
+
obj = prefab.find(**unsafe_kwargs)
|
|
40
|
+
if obj is not None:
|
|
41
|
+
# 强制转换为 GameObjectType,假设用户定义的 options 均符合泛型约束
|
|
42
|
+
return cast(GameObjectType, obj)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
@classmethod
|
|
47
|
+
def find_all(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> list[GameObjectType]:
|
|
48
|
+
unsafe_kwargs = cast(dict[str, Any], kwargs)
|
|
49
|
+
results: list[GameObjectType] = []
|
|
50
|
+
for prefab in cls.options:
|
|
51
|
+
found = prefab.find_all(**unsafe_kwargs)
|
|
52
|
+
# 列表协变问题,需要强制转换
|
|
53
|
+
results.extend(cast(list[GameObjectType], found))
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
@classmethod
|
|
58
|
+
def exists(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> bool:
|
|
59
|
+
unsafe_kwargs = cast(dict[str, Any], kwargs)
|
|
60
|
+
for prefab in cls.options:
|
|
61
|
+
if prefab.exists(**unsafe_kwargs):
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
@classmethod
|
|
67
|
+
def require(cls, **kwargs: Unpack[FindKwargs[GameObjectType]]) -> GameObjectType:
|
|
68
|
+
unsafe_kwargs = cast(dict[str, Any], kwargs)
|
|
69
|
+
for prefab in cls.options:
|
|
70
|
+
obj = prefab.find(**unsafe_kwargs)
|
|
71
|
+
if obj is not None:
|
|
72
|
+
return cast(GameObjectType, obj)
|
|
73
|
+
|
|
74
|
+
names = ", ".join([p.__name__ for p in cls.options])
|
|
75
|
+
raise RuntimeError(f"AnyOf: Could not find any of the following prefabs: [{names}]")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Generic, TYPE_CHECKING
|
|
2
|
+
from typing_extensions import Unpack, override
|
|
3
|
+
|
|
4
|
+
from kotonebot.devtools.project.schema import StrProp, RectProp
|
|
5
|
+
from kotonebot.primitives import Rect
|
|
6
|
+
from kotonebot.devtools import EditorMetadata
|
|
7
|
+
|
|
8
|
+
from .base import Prefab, FindKwargs, GameObjectType, ClickKwargs as _ClickKwargs, WaitKwargs as _WaitKwargs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OcrFindKargs(FindKwargs[GameObjectType], total=False):
|
|
12
|
+
region: Rect | None
|
|
13
|
+
"""搜索区域
|
|
14
|
+
|
|
15
|
+
如果指定,则覆盖 OcrPrefab 中定义的 region 属性。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
class ClickKwargs(OcrFindKargs[GameObjectType], _ClickKwargs[GameObjectType], Generic[GameObjectType], total=False): pass
|
|
19
|
+
class WaitKwargs(OcrFindKargs[GameObjectType], _WaitKwargs[GameObjectType], Generic[GameObjectType], total=False): pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OcrPrefab(Prefab[GameObjectType]):
|
|
23
|
+
"""基于 Ocr 的 Prefab"""
|
|
24
|
+
pattern: str
|
|
25
|
+
region: Rect | None = None
|
|
26
|
+
|
|
27
|
+
class _Editor(EditorMetadata):
|
|
28
|
+
name = 'OCR'
|
|
29
|
+
description = '基于 OCR + 文字匹配来识别对象'
|
|
30
|
+
primary_prop = 'region'
|
|
31
|
+
icon = 'search-text'
|
|
32
|
+
shortcut = 'o'
|
|
33
|
+
props = {
|
|
34
|
+
'pattern': StrProp(label='匹配文本', description='用于匹配的文本内容', default_value=''),
|
|
35
|
+
'region': RectProp(label='搜索区域', description='限定搜索区域以提升识别速度', default_value=None),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
@classmethod
|
|
40
|
+
def find(cls, **kwargs: Unpack[OcrFindKargs[GameObjectType]]) -> GameObjectType | None:
|
|
41
|
+
from kotonebot import ocr
|
|
42
|
+
predicate = kwargs.get('predicate')
|
|
43
|
+
region = kwargs.get('region', cls.region)
|
|
44
|
+
result = ocr.find(cls.pattern, rect=region)
|
|
45
|
+
if result is None:
|
|
46
|
+
return None
|
|
47
|
+
obj_class = cls._get_object_class()
|
|
48
|
+
obj = obj_class()
|
|
49
|
+
# 使用原图坐标
|
|
50
|
+
obj.rect = result.original_rect
|
|
51
|
+
obj.prefab = cls
|
|
52
|
+
if predicate is not None and not predicate(obj):
|
|
53
|
+
return None
|
|
54
|
+
return obj
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
@classmethod
|
|
58
|
+
def find_all(cls, **kwargs: Unpack[OcrFindKargs[GameObjectType]]) -> list[GameObjectType]:
|
|
59
|
+
from kotonebot import ocr
|
|
60
|
+
predicate = kwargs.get('predicate')
|
|
61
|
+
region = kwargs.get('region', cls.region)
|
|
62
|
+
# 获取所有 OCR 结果后按文本过滤
|
|
63
|
+
results = ocr.ocr(rect=region)
|
|
64
|
+
obj_class = cls._get_object_class()
|
|
65
|
+
objects: list[GameObjectType] = []
|
|
66
|
+
for r in results:
|
|
67
|
+
if r.text == cls.pattern:
|
|
68
|
+
obj = obj_class()
|
|
69
|
+
obj.rect = r.original_rect
|
|
70
|
+
obj.prefab = cls
|
|
71
|
+
if predicate is None or predicate(obj):
|
|
72
|
+
objects.append(obj)
|
|
73
|
+
return objects
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
@classmethod
|
|
77
|
+
def require(cls, **kwargs: Unpack[OcrFindKargs[GameObjectType]]) -> GameObjectType:
|
|
78
|
+
from kotonebot import ocr, device
|
|
79
|
+
from kotonebot.backend.ocr import TextNotFoundError
|
|
80
|
+
predicate = kwargs.get('predicate')
|
|
81
|
+
region = kwargs.get('region', cls.region)
|
|
82
|
+
if predicate is None:
|
|
83
|
+
result = ocr.expect(cls.pattern, rect=region)
|
|
84
|
+
obj_class = cls._get_object_class()
|
|
85
|
+
obj = obj_class()
|
|
86
|
+
obj.rect = result.original_rect
|
|
87
|
+
obj.prefab = cls
|
|
88
|
+
return obj
|
|
89
|
+
else:
|
|
90
|
+
# 扫描所有 OCR 结果,匹配文本并套用 predicate
|
|
91
|
+
results = ocr.ocr(rect=cls.region)
|
|
92
|
+
obj_class = cls._get_object_class()
|
|
93
|
+
for r in results:
|
|
94
|
+
if r.text == cls.pattern:
|
|
95
|
+
obj = obj_class()
|
|
96
|
+
obj.rect = r.original_rect
|
|
97
|
+
if predicate(obj):
|
|
98
|
+
obj.prefab = cls
|
|
99
|
+
return obj
|
|
100
|
+
raise TextNotFoundError(cls.pattern, device.screenshot())
|
|
101
|
+
|
|
102
|
+
if TYPE_CHECKING:
|
|
103
|
+
# 这些方法只需要重载声明,实际实现由基类提供不变
|
|
104
|
+
@classmethod
|
|
105
|
+
def exists(cls, **kwargs: Unpack[OcrFindKargs[GameObjectType]]) -> bool: ...
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def click(cls, **kwargs: Unpack[ClickKwargs[GameObjectType]]) -> None: ...
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def wait(cls, **kwargs: Unpack[WaitKwargs[GameObjectType]]) -> GameObjectType: ...
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def try_click(cls, **kwargs: Unpack[ClickKwargs[GameObjectType]]) -> bool: ...
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def try_wait(cls, **kwargs: Unpack[WaitKwargs[GameObjectType]]) -> GameObjectType | None: ...
|