kotonebot 0.5.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 (103) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -312
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +1002 -1002
  6. kotonebot/backend/context/task_action.py +183 -183
  7. kotonebot/backend/core.py +86 -129
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/image.py +36 -5
  15. kotonebot/backend/loop.py +222 -208
  16. kotonebot/backend/ocr.py +535 -535
  17. kotonebot/backend/preprocessor.py +103 -103
  18. kotonebot/client/__init__.py +9 -9
  19. kotonebot/client/device.py +369 -529
  20. kotonebot/client/fast_screenshot.py +377 -377
  21. kotonebot/client/host/__init__.py +43 -43
  22. kotonebot/client/host/adb_common.py +101 -107
  23. kotonebot/client/host/custom.py +118 -118
  24. kotonebot/client/host/leidian_host.py +196 -196
  25. kotonebot/client/host/mumu12_host.py +353 -353
  26. kotonebot/client/host/protocol.py +214 -214
  27. kotonebot/client/host/windows_common.py +58 -58
  28. kotonebot/client/implements/__init__.py +65 -70
  29. kotonebot/client/implements/adb.py +89 -89
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -188
  34. kotonebot/client/implements/uiautomator2.py +85 -85
  35. kotonebot/client/implements/windows.py +176 -176
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/client/scaler.py +467 -0
  39. kotonebot/config/base_config.py +96 -96
  40. kotonebot/config/config.py +61 -0
  41. kotonebot/config/manager.py +36 -36
  42. kotonebot/core/__init__.py +13 -0
  43. kotonebot/core/entities/base.py +182 -0
  44. kotonebot/core/entities/compound.py +75 -0
  45. kotonebot/core/entities/ocr.py +117 -0
  46. kotonebot/core/entities/template_match.py +198 -0
  47. kotonebot/devtools/__init__.py +42 -0
  48. kotonebot/devtools/cli/__init__.py +6 -0
  49. kotonebot/devtools/cli/main.py +53 -0
  50. kotonebot/{tools → devtools}/mirror.py +354 -354
  51. kotonebot/devtools/project/project.py +41 -0
  52. kotonebot/devtools/project/scanner.py +202 -0
  53. kotonebot/devtools/project/schema.py +99 -0
  54. kotonebot/devtools/resgen/__init__.py +42 -0
  55. kotonebot/devtools/resgen/codegen.py +331 -0
  56. kotonebot/devtools/resgen/core.py +94 -0
  57. kotonebot/devtools/resgen/parsers.py +360 -0
  58. kotonebot/devtools/resgen/utils.py +158 -0
  59. kotonebot/devtools/resgen/validation.py +115 -0
  60. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  61. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  62. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  63. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  64. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  65. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  66. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  67. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  68. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  69. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  70. kotonebot/devtools/web/dist/index.html +25 -0
  71. kotonebot/devtools/web/server/__init__.py +0 -0
  72. kotonebot/devtools/web/server/rest_api.py +217 -0
  73. kotonebot/devtools/web/server/server.py +85 -0
  74. kotonebot/errors.py +76 -76
  75. kotonebot/interop/win/__init__.py +11 -9
  76. kotonebot/interop/win/_mouse.py +310 -310
  77. kotonebot/interop/win/message_box.py +313 -313
  78. kotonebot/interop/win/reg.py +37 -37
  79. kotonebot/interop/win/shake_mouse.py +224 -0
  80. kotonebot/interop/win/shortcut.py +43 -43
  81. kotonebot/interop/win/task_dialog.py +513 -513
  82. kotonebot/logging/__init__.py +2 -2
  83. kotonebot/logging/log.py +17 -17
  84. kotonebot/primitives/__init__.py +19 -17
  85. kotonebot/primitives/geometry.py +1067 -862
  86. kotonebot/primitives/visual.py +143 -63
  87. kotonebot/ui/file_host/sensio.py +36 -36
  88. kotonebot/ui/file_host/tmp_send.py +54 -54
  89. kotonebot/ui/pushkit/__init__.py +3 -3
  90. kotonebot/ui/pushkit/image_host.py +88 -88
  91. kotonebot/ui/pushkit/protocol.py +13 -13
  92. kotonebot/ui/pushkit/wxpusher.py +54 -54
  93. kotonebot/ui/user.py +148 -148
  94. kotonebot/util.py +436 -436
  95. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +84 -82
  96. kotonebot-0.6.0.dist-info/RECORD +105 -0
  97. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  98. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +673 -673
  99. kotonebot/client/implements/adb_raw.py +0 -163
  100. kotonebot-0.5.0.dist-info/RECORD +0 -71
  101. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  102. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  103. {kotonebot-0.5.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
@@ -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: ...