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.
Files changed (71) hide show
  1. kotonebot/backend/context/context.py +1002 -1002
  2. kotonebot/backend/core.py +6 -49
  3. kotonebot/backend/image.py +36 -5
  4. kotonebot/backend/loop.py +222 -208
  5. kotonebot/backend/ocr.py +7 -1
  6. kotonebot/client/device.py +108 -243
  7. kotonebot/client/host/__init__.py +34 -3
  8. kotonebot/client/host/adb_common.py +7 -9
  9. kotonebot/client/host/custom.py +6 -2
  10. kotonebot/client/host/leidian_host.py +2 -7
  11. kotonebot/client/host/mumu12_host.py +2 -7
  12. kotonebot/client/host/protocol.py +4 -3
  13. kotonebot/client/implements/__init__.py +62 -11
  14. kotonebot/client/implements/adb.py +5 -1
  15. kotonebot/client/implements/nemu_ipc/__init__.py +4 -0
  16. kotonebot/client/implements/uiautomator2.py +6 -2
  17. kotonebot/client/implements/windows.py +7 -3
  18. kotonebot/client/registration.py +1 -1
  19. kotonebot/client/scaler.py +467 -0
  20. kotonebot/config/base_config.py +1 -1
  21. kotonebot/config/config.py +61 -0
  22. kotonebot/core/__init__.py +13 -0
  23. kotonebot/core/entities/base.py +182 -0
  24. kotonebot/core/entities/compound.py +75 -0
  25. kotonebot/core/entities/ocr.py +117 -0
  26. kotonebot/core/entities/template_match.py +198 -0
  27. kotonebot/devtools/__init__.py +42 -0
  28. kotonebot/devtools/cli/__init__.py +6 -0
  29. kotonebot/devtools/cli/main.py +53 -0
  30. kotonebot/devtools/project/project.py +41 -0
  31. kotonebot/devtools/project/scanner.py +202 -0
  32. kotonebot/devtools/project/schema.py +99 -0
  33. kotonebot/devtools/resgen/__init__.py +42 -0
  34. kotonebot/devtools/resgen/codegen.py +331 -0
  35. kotonebot/devtools/resgen/core.py +94 -0
  36. kotonebot/devtools/resgen/parsers.py +360 -0
  37. kotonebot/devtools/resgen/utils.py +158 -0
  38. kotonebot/devtools/resgen/validation.py +115 -0
  39. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  40. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  41. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  42. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  43. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  44. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  45. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  46. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  47. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  48. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  49. kotonebot/devtools/web/dist/index.html +25 -0
  50. kotonebot/devtools/web/server/__init__.py +0 -0
  51. kotonebot/devtools/web/server/rest_api.py +217 -0
  52. kotonebot/devtools/web/server/server.py +85 -0
  53. kotonebot/errors.py +7 -2
  54. kotonebot/interop/win/__init__.py +10 -1
  55. kotonebot/interop/win/_mouse.py +311 -0
  56. kotonebot/interop/win/shake_mouse.py +224 -0
  57. kotonebot/primitives/__init__.py +3 -1
  58. kotonebot/primitives/geometry.py +817 -40
  59. kotonebot/primitives/visual.py +81 -1
  60. kotonebot/ui/pushkit/image_host.py +2 -1
  61. kotonebot/ui/pushkit/wxpusher.py +2 -1
  62. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +4 -1
  63. kotonebot-0.6.0.dist-info/RECORD +105 -0
  64. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  65. kotonebot/client/implements/adb_raw.py +0 -159
  66. kotonebot-0.4.0.dist-info/RECORD +0 -70
  67. /kotonebot/{tools → devtools}/mirror.py +0 -0
  68. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  69. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  70. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +0 -0
  71. {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
+