kotonebot 0.4.0__py3-none-any.whl → 0.6.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/backend/context/context.py +1002 -1002
- kotonebot/backend/core.py +6 -49
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +7 -1
- kotonebot/client/device.py +108 -243
- kotonebot/client/host/__init__.py +34 -3
- kotonebot/client/host/adb_common.py +7 -9
- kotonebot/client/host/custom.py +6 -2
- kotonebot/client/host/leidian_host.py +2 -7
- kotonebot/client/host/mumu12_host.py +2 -7
- kotonebot/client/host/protocol.py +4 -3
- kotonebot/client/implements/__init__.py +62 -11
- kotonebot/client/implements/adb.py +5 -1
- kotonebot/client/implements/nemu_ipc/__init__.py +4 -0
- kotonebot/client/implements/uiautomator2.py +6 -2
- kotonebot/client/implements/windows.py +7 -3
- kotonebot/client/registration.py +1 -1
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +1 -1
- kotonebot/config/config.py +61 -0
- 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/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 +7 -2
- kotonebot/interop/win/__init__.py +10 -1
- kotonebot/interop/win/_mouse.py +311 -0
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/primitives/__init__.py +3 -1
- kotonebot/primitives/geometry.py +817 -40
- kotonebot/primitives/visual.py +81 -1
- kotonebot/ui/pushkit/image_host.py +2 -1
- kotonebot/ui/pushkit/wxpusher.py +2 -1
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +4 -1
- kotonebot-0.6.0.dist-info/RECORD +105 -0
- kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
- kotonebot/client/implements/adb_raw.py +0 -159
- kotonebot-0.4.0.dist-info/RECORD +0 -70
- /kotonebot/{tools → devtools}/mirror.py +0 -0
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.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
|
|
@@ -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: ...
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Generic
|
|
2
|
+
from typing_extensions import Unpack, override
|
|
3
|
+
|
|
4
|
+
from kotonebot.devtools.project.schema import BoolProp, FloatProp, ImageProp, RectProp
|
|
5
|
+
from kotonebot.primitives import Rect, ImageSlice
|
|
6
|
+
from kotonebot.devtools import EditorMetadata
|
|
7
|
+
|
|
8
|
+
from .base import Prefab, FindKwargs, GameObjectType, ClickKwargs as _ClickKwargs, WaitKwargs as _WaitKwargs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TemplateMatchFindKargs(FindKwargs[GameObjectType], total=False):
|
|
12
|
+
threshold: float | None
|
|
13
|
+
"""匹配阈值
|
|
14
|
+
|
|
15
|
+
如果指定,则覆盖 TemplateMatchPrefab 中定义的 threshold 属性。
|
|
16
|
+
"""
|
|
17
|
+
colored: bool | None
|
|
18
|
+
"""是否匹配颜色
|
|
19
|
+
|
|
20
|
+
如果指定,则覆盖 TemplateMatchPrefab 中定义的 colored 属性。
|
|
21
|
+
"""
|
|
22
|
+
region: Rect | None
|
|
23
|
+
"""搜索区域
|
|
24
|
+
|
|
25
|
+
如果指定,则覆盖 TemplateMatchPrefab 中定义的 region 属性。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
class ClickKwargs(TemplateMatchFindKargs[GameObjectType], _ClickKwargs[GameObjectType], Generic[GameObjectType], total=False): pass
|
|
29
|
+
class WaitKwargs(TemplateMatchFindKargs[GameObjectType], _WaitKwargs[GameObjectType], Generic[GameObjectType], total=False): pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TemplateMatchPrefab(Prefab[GameObjectType]):
|
|
33
|
+
"""基于模版匹配的 Prefab"""
|
|
34
|
+
template: ImageSlice
|
|
35
|
+
"""[必填] 用于匹配的模版图像"""
|
|
36
|
+
fixed: bool = False
|
|
37
|
+
"""[可选] 是否固定位置。
|
|
38
|
+
|
|
39
|
+
当 `fixed` 为 True 时,匹配将限定在 `template.slice_rect`(若存在)定义的区域内。
|
|
40
|
+
若 `template` 无 `slice_rect`,会在运行时抛出 ValueError,以提示生成代码或资源定义不完整。
|
|
41
|
+
"""
|
|
42
|
+
region: Rect | None = None
|
|
43
|
+
"""[可选] 限定搜索区域
|
|
44
|
+
|
|
45
|
+
默认为 None(全屏搜索)。
|
|
46
|
+
"""
|
|
47
|
+
threshold: float = 0.8
|
|
48
|
+
"""[可选] 匹配阈值
|
|
49
|
+
|
|
50
|
+
范围 0.0 - 1.0,默认为 0.8。
|
|
51
|
+
"""
|
|
52
|
+
colored: bool = False
|
|
53
|
+
"""[可选] 是否匹配颜色
|
|
54
|
+
|
|
55
|
+
默认为 False(不匹配颜色)。
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
class _Editor(EditorMetadata):
|
|
59
|
+
name = '模版'
|
|
60
|
+
description = '基于模版匹配来寻找对象'
|
|
61
|
+
primary_prop = 'template'
|
|
62
|
+
icon = 'media'
|
|
63
|
+
shortcut = 't'
|
|
64
|
+
props = {
|
|
65
|
+
'template': ImageProp(label='模版图像', description='用于匹配的模版图像', default_value=None),
|
|
66
|
+
'fixed': BoolProp(label='固定位置', description='对象位置是否固定不变,若固定可提升匹配速度', default_value=False),
|
|
67
|
+
'region': RectProp(label='搜索区域', description='限定搜索区域以提升匹配速度', default_value=None),
|
|
68
|
+
'threshold': FloatProp(label='匹配阈值', description='模版匹配的相似度阈值,范围 0.0 - 1.0', min=0.0, max=1.0, default_value=0.8),
|
|
69
|
+
'colored': BoolProp(label='匹配颜色', description='是否在匹配时考虑颜色信息', default_value=False),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
@classmethod
|
|
75
|
+
def find(cls, **kwargs: Unpack[TemplateMatchFindKargs[GameObjectType]]) -> GameObjectType | None:
|
|
76
|
+
from kotonebot import image
|
|
77
|
+
predicate = kwargs.get('predicate')
|
|
78
|
+
threshold_override = kwargs.get('threshold')
|
|
79
|
+
threshold = cls.threshold if threshold_override is None else threshold_override
|
|
80
|
+
colored_override = kwargs.get('colored')
|
|
81
|
+
colored = cls.colored if colored_override is None else colored_override
|
|
82
|
+
region = kwargs.get('region', cls.region)
|
|
83
|
+
# If prefab is fixed and no explicit region provided, use template.slice_rect
|
|
84
|
+
if region is None and cls.fixed:
|
|
85
|
+
slice_rect = cls.template.slice_rect
|
|
86
|
+
if slice_rect is None:
|
|
87
|
+
raise ValueError(f"Prefab {cls.__name__} is marked fixed but template has no slice_rect")
|
|
88
|
+
region = slice_rect
|
|
89
|
+
result = image.find(
|
|
90
|
+
cls.template.pixels,
|
|
91
|
+
rect=region,
|
|
92
|
+
threshold=threshold,
|
|
93
|
+
colored=colored,
|
|
94
|
+
)
|
|
95
|
+
if result is None:
|
|
96
|
+
return None
|
|
97
|
+
obj_class = cls._get_object_class()
|
|
98
|
+
obj = obj_class()
|
|
99
|
+
obj.rect = result.rect
|
|
100
|
+
obj.prefab = cls
|
|
101
|
+
if predicate is not None and not predicate(obj):
|
|
102
|
+
return None
|
|
103
|
+
return obj
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
@classmethod
|
|
107
|
+
def find_all(cls, **kwargs: Unpack[TemplateMatchFindKargs[GameObjectType]]) -> list[GameObjectType]:
|
|
108
|
+
from kotonebot import image
|
|
109
|
+
predicate = kwargs.get('predicate')
|
|
110
|
+
threshold_override = kwargs.get('threshold')
|
|
111
|
+
threshold = cls.threshold if threshold_override is None else threshold_override
|
|
112
|
+
colored_override = kwargs.get('colored')
|
|
113
|
+
colored = cls.colored if colored_override is None else colored_override
|
|
114
|
+
region = kwargs.get('region', cls.region)
|
|
115
|
+
if region is None and cls.fixed:
|
|
116
|
+
slice_rect = cls.template.slice_rect
|
|
117
|
+
if slice_rect is None:
|
|
118
|
+
raise ValueError(f"Prefab {cls.__name__} is marked fixed but template has no slice_rect")
|
|
119
|
+
region = slice_rect
|
|
120
|
+
results = image.find_all(
|
|
121
|
+
cls.template.pixels,
|
|
122
|
+
rect=region,
|
|
123
|
+
threshold=threshold,
|
|
124
|
+
colored=colored,
|
|
125
|
+
)
|
|
126
|
+
obj_class = cls._get_object_class()
|
|
127
|
+
objects: list[GameObjectType] = []
|
|
128
|
+
for r in results:
|
|
129
|
+
obj = obj_class()
|
|
130
|
+
obj.rect = r.rect
|
|
131
|
+
obj.prefab = cls
|
|
132
|
+
if predicate is None or predicate(obj):
|
|
133
|
+
objects.append(obj)
|
|
134
|
+
return objects
|
|
135
|
+
|
|
136
|
+
@override
|
|
137
|
+
@classmethod
|
|
138
|
+
def require(cls, **kwargs: Unpack[TemplateMatchFindKargs[GameObjectType]]) -> GameObjectType:
|
|
139
|
+
from kotonebot import image, device
|
|
140
|
+
from kotonebot.backend.image import TemplateNoMatchError
|
|
141
|
+
predicate = kwargs.get('predicate')
|
|
142
|
+
threshold_override = kwargs.get('threshold')
|
|
143
|
+
threshold = cls.threshold if threshold_override is None else threshold_override
|
|
144
|
+
colored_override = kwargs.get('colored')
|
|
145
|
+
colored = cls.colored if colored_override is None else colored_override
|
|
146
|
+
region = kwargs.get('region', cls.region)
|
|
147
|
+
if region is None and cls.fixed:
|
|
148
|
+
slice_rect = cls.template.slice_rect
|
|
149
|
+
if slice_rect is None:
|
|
150
|
+
raise ValueError(f"Prefab {cls.__name__} is marked fixed but template has no slice_rect")
|
|
151
|
+
region = slice_rect
|
|
152
|
+
if predicate is None:
|
|
153
|
+
# 直接使用 expect,未找到会抛出 TemplateNoMatchError
|
|
154
|
+
result = image.expect(
|
|
155
|
+
cls.template.pixels,
|
|
156
|
+
rect=region,
|
|
157
|
+
threshold=threshold,
|
|
158
|
+
colored=colored,
|
|
159
|
+
)
|
|
160
|
+
obj_class = cls._get_object_class()
|
|
161
|
+
obj = obj_class()
|
|
162
|
+
obj.rect = result.rect
|
|
163
|
+
obj.prefab = cls
|
|
164
|
+
return obj
|
|
165
|
+
else:
|
|
166
|
+
# 需要满足 predicate,则遍历所有匹配项
|
|
167
|
+
results = image.find_all(
|
|
168
|
+
cls.template.pixels,
|
|
169
|
+
rect=region,
|
|
170
|
+
threshold=threshold,
|
|
171
|
+
colored=colored,
|
|
172
|
+
)
|
|
173
|
+
obj_class = cls._get_object_class()
|
|
174
|
+
for r in results:
|
|
175
|
+
obj = obj_class()
|
|
176
|
+
obj.rect = r.rect
|
|
177
|
+
if predicate(obj):
|
|
178
|
+
obj.prefab = cls
|
|
179
|
+
return obj
|
|
180
|
+
# 没有任何匹配满足 predicate,抛出未找到异常
|
|
181
|
+
raise TemplateNoMatchError(device.screenshot(), cls.template.pixels)
|
|
182
|
+
|
|
183
|
+
if TYPE_CHECKING:
|
|
184
|
+
# 这些方法只需要重载声明,实际实现由基类提供不变
|
|
185
|
+
@classmethod
|
|
186
|
+
def exists(cls, **kwargs: Unpack[TemplateMatchFindKargs[GameObjectType]]) -> bool: ...
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def click(cls, **kwargs: Unpack[ClickKwargs[GameObjectType]]) -> None: ...
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def wait(cls, **kwargs: Unpack[WaitKwargs[GameObjectType]]) -> GameObjectType: ...
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def try_click(cls, **kwargs: Unpack[ClickKwargs[GameObjectType]]) -> bool: ...
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def try_wait(cls, **kwargs: Unpack[WaitKwargs[GameObjectType]]) -> GameObjectType | None: ...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .resgen import (
|
|
2
|
+
CodeWriter,
|
|
3
|
+
ResourceNode,
|
|
4
|
+
ClassNode,
|
|
5
|
+
SchemaParser,
|
|
6
|
+
StandardGenerator,
|
|
7
|
+
ParserRegistry,
|
|
8
|
+
KotoneV1Parser,
|
|
9
|
+
BasicSpriteParser,
|
|
10
|
+
to_camel_case,
|
|
11
|
+
unify_path,
|
|
12
|
+
build_class_tree,
|
|
13
|
+
ImageProcessor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .project.schema import EditorMetadata
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# core
|
|
20
|
+
"CodeWriter",
|
|
21
|
+
"ResourceNode",
|
|
22
|
+
"ClassNode",
|
|
23
|
+
"SchemaParser",
|
|
24
|
+
|
|
25
|
+
# generator
|
|
26
|
+
"StandardGenerator",
|
|
27
|
+
|
|
28
|
+
# parsers
|
|
29
|
+
"ParserRegistry",
|
|
30
|
+
"KotoneV1Parser",
|
|
31
|
+
"BasicSpriteParser",
|
|
32
|
+
|
|
33
|
+
# utils
|
|
34
|
+
"to_camel_case",
|
|
35
|
+
"unify_path",
|
|
36
|
+
"build_class_tree",
|
|
37
|
+
"ImageProcessor",
|
|
38
|
+
|
|
39
|
+
# plugin
|
|
40
|
+
"EditorMetadata",
|
|
41
|
+
]
|
|
42
|
+
|