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,467 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any, overload
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
5
|
+
from cv2.typing import MatLike
|
|
6
|
+
|
|
7
|
+
from kotonebot.primitives.geometry import (
|
|
8
|
+
PointLike, RectLike, Size, SizeLike, AnyPointLike, unify_any_point
|
|
9
|
+
)
|
|
10
|
+
from kotonebot.primitives.geometry import (
|
|
11
|
+
is_point, is_point_f, is_rect, unify_rect,
|
|
12
|
+
Point, PointF, Rect
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
class AbstractScaler(ABC):
|
|
16
|
+
"""用于定义当实际设备分辨率与预期分辨率不一致时缩放行为的接口。
|
|
17
|
+
|
|
18
|
+
该接口定义了包括缩放图像、坐标转换、比例转换在内的方法。
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
# Accept either a `Size` instance or a plain (width, height) tuple.
|
|
22
|
+
self.physical_resolution: SizeLike | None = None
|
|
23
|
+
"""物理分辨率 (width, height)。"""
|
|
24
|
+
self.logic_resolution: SizeLike | None = None
|
|
25
|
+
"""逻辑分辨率 (width, height)。"""
|
|
26
|
+
|
|
27
|
+
def transform_screenshot(self, screenshot: MatLike) -> MatLike:
|
|
28
|
+
"""处理设备画面截图数据。
|
|
29
|
+
|
|
30
|
+
:param screenshot: 原始截图数据。
|
|
31
|
+
:return: 处理后的截图数据。
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def logic_to_physical(self, v: AnyPointLike) -> AnyPointLike: ...
|
|
37
|
+
@overload
|
|
38
|
+
def logic_to_physical(self, v: RectLike) -> RectLike: ...
|
|
39
|
+
def logic_to_physical(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
40
|
+
"""将逻辑坐标转换为物理坐标。
|
|
41
|
+
|
|
42
|
+
:param v: 逻辑坐标点或矩形。
|
|
43
|
+
:return: 转换后的物理坐标点或矩形。
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> scaler.logic_to_physical(Point(10, 20))
|
|
48
|
+
<<< Point(..., ...)
|
|
49
|
+
>>> scaler.logic_to_physical(PointF(10.6, 20.5))
|
|
50
|
+
<<< Point(..., ...)
|
|
51
|
+
>>> scaler.logic_to_physical((10, 20))
|
|
52
|
+
<<< Point(..., ...)
|
|
53
|
+
>>> scaler.logic_to_physical(Rect(10, 20, 30, 40))
|
|
54
|
+
<<< Rect(..., ..., ..., ...)
|
|
55
|
+
>>> scaler.logic_to_physical((10, 20, 30, 40))
|
|
56
|
+
<<< Rect(..., ..., ..., ...)
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def physical_to_logic(self, v: PointLike) -> PointLike: ...
|
|
62
|
+
@overload
|
|
63
|
+
def physical_to_logic(self, v: RectLike) -> RectLike: ...
|
|
64
|
+
def physical_to_logic(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
65
|
+
"""将物理坐标转换为逻辑坐标。
|
|
66
|
+
|
|
67
|
+
:param v: 物理坐标点或矩形。
|
|
68
|
+
:return: 转换后的逻辑坐标点或矩形。
|
|
69
|
+
|
|
70
|
+
Examples
|
|
71
|
+
--------
|
|
72
|
+
见 :meth:`logic_to_physical`。
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
def fractional_to_physical(self, v: PointLike) -> PointLike: ...
|
|
78
|
+
@overload
|
|
79
|
+
def fractional_to_physical(self, v: RectLike) -> RectLike: ...
|
|
80
|
+
def fractional_to_physical(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
81
|
+
"""将比例坐标转换为物理坐标。
|
|
82
|
+
|
|
83
|
+
:param v: 比例坐标点或矩形。
|
|
84
|
+
:return: 转换后的物理坐标点或矩形。
|
|
85
|
+
|
|
86
|
+
Examples
|
|
87
|
+
--------
|
|
88
|
+
见 :meth:`logic_to_physical`。
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def physical_to_fractional(self, v: PointLike) -> PointLike: ...
|
|
94
|
+
@overload
|
|
95
|
+
def physical_to_fractional(self, v: RectLike) -> RectLike: ...
|
|
96
|
+
def physical_to_fractional(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
97
|
+
"""将物理坐标转换为比例坐标。
|
|
98
|
+
|
|
99
|
+
:param v: 物理坐标点或矩形。
|
|
100
|
+
:return: 转换后的比例坐标点或矩形。
|
|
101
|
+
|
|
102
|
+
Examples
|
|
103
|
+
--------
|
|
104
|
+
见 :meth:`logic_to_physical`。
|
|
105
|
+
"""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProportionalScaler(AbstractScaler):
|
|
110
|
+
"""等比例缩放。
|
|
111
|
+
|
|
112
|
+
支持在物理分辨率和逻辑分辨率之间进行等比例缩放转换。
|
|
113
|
+
仅支持等比例缩放,若无法等比例缩放,则会抛出异常。
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
match_rotation: bool = True,
|
|
119
|
+
aspect_ratio_tolerance: float = 0.1
|
|
120
|
+
):
|
|
121
|
+
"""初始化等比例缩放器。"""
|
|
122
|
+
super().__init__()
|
|
123
|
+
|
|
124
|
+
self.match_rotation = match_rotation
|
|
125
|
+
"""分辨率缩放是否自动匹配旋转。
|
|
126
|
+
当目标与真实分辨率的宽高比不一致时,是否允许通过旋转(交换宽高)后再进行匹配。
|
|
127
|
+
|
|
128
|
+
True 表示忽略方向差异,只要宽高比一致就视为可缩放;False 表示必须匹配旋转。
|
|
129
|
+
"""
|
|
130
|
+
self.aspect_ratio_tolerance = aspect_ratio_tolerance
|
|
131
|
+
"""宽高比容差阈值。
|
|
132
|
+
|
|
133
|
+
判断两分辨率宽高比差异是否接受的阈值。
|
|
134
|
+
该值越小,对比例一致性的要求越严格。默认为 0.1(即 10% 容差)。
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def scale_ratio(self) -> float:
|
|
139
|
+
"""获取物理分辨率相对于逻辑分辨率的缩放比例。
|
|
140
|
+
|
|
141
|
+
由于是等比例缩放,长宽的缩放比例应当一致(在容差范围内)。
|
|
142
|
+
"""
|
|
143
|
+
if self.physical_resolution is None:
|
|
144
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
145
|
+
if self.logic_resolution is None:
|
|
146
|
+
return 1.0
|
|
147
|
+
|
|
148
|
+
phy_w, phy_h = self.physical_resolution
|
|
149
|
+
log_w, log_h = self.logic_resolution
|
|
150
|
+
|
|
151
|
+
if self.match_rotation:
|
|
152
|
+
return max(phy_w, phy_h) / max(log_w, log_h)
|
|
153
|
+
|
|
154
|
+
return phy_w / log_w
|
|
155
|
+
|
|
156
|
+
def _aspect_ratio_compatible(
|
|
157
|
+
self, src_size: SizeLike, tgt_size: SizeLike
|
|
158
|
+
) -> bool:
|
|
159
|
+
"""判断两个尺寸在宽高比意义上是否兼容。
|
|
160
|
+
|
|
161
|
+
若 ``self.match_rotation`` 为 True,忽略方向(长边/短边)进行比较。
|
|
162
|
+
判断标准由 ``self.aspect_ratio_tolerance`` 决定(默认 0.1)。
|
|
163
|
+
"""
|
|
164
|
+
src_w, src_h = src_size
|
|
165
|
+
tgt_w, tgt_h = tgt_size
|
|
166
|
+
|
|
167
|
+
# 尺寸必须为正
|
|
168
|
+
if src_w <= 0 or src_h <= 0:
|
|
169
|
+
raise ValueError(f"Source size dimensions must be positive for scaling: {src_size}")
|
|
170
|
+
if tgt_w <= 0 or tgt_h <= 0:
|
|
171
|
+
raise ValueError(f"Target size dimensions must be positive for scaling: {tgt_size}")
|
|
172
|
+
|
|
173
|
+
tolerant = self.aspect_ratio_tolerance
|
|
174
|
+
|
|
175
|
+
# 直接比较宽高比
|
|
176
|
+
if abs((tgt_w / src_w) - (tgt_h / src_h)) <= tolerant:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# 尝试忽略方向差异
|
|
180
|
+
if self.match_rotation:
|
|
181
|
+
ratio_src = max(src_w, src_h) / min(src_w, src_h)
|
|
182
|
+
ratio_tgt = max(tgt_w, tgt_h) / min(tgt_w, tgt_h)
|
|
183
|
+
return abs(ratio_src - ratio_tgt) <= tolerant
|
|
184
|
+
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _assert_scalable(
|
|
188
|
+
self, source: SizeLike, target: SizeLike
|
|
189
|
+
) -> SizeLike:
|
|
190
|
+
"""校验分辨率是否可缩放,并返回调整后的目标分辨率。
|
|
191
|
+
|
|
192
|
+
当 match_rotation 为 True 且源分辨率与目标分辨率的旋转方向不一致时,
|
|
193
|
+
自动交换目标分辨率的宽高,使其与源分辨率的方向保持一致。
|
|
194
|
+
|
|
195
|
+
:param source: 源分辨率 (width, height)
|
|
196
|
+
:param target: 目标分辨率 (width, height)
|
|
197
|
+
:return: 调整后的目标分辨率 (width, height)
|
|
198
|
+
:raises UnscalableResolutionError: 若宽高比不兼容
|
|
199
|
+
"""
|
|
200
|
+
from ..errors import UnscalableResolutionError
|
|
201
|
+
|
|
202
|
+
# 智能调整目标分辨率方向
|
|
203
|
+
adjusted_tgt_size = target
|
|
204
|
+
if self.match_rotation:
|
|
205
|
+
src_w, src_h = source
|
|
206
|
+
tgt_w, tgt_h = target
|
|
207
|
+
|
|
208
|
+
# 判断源分辨率和目标分辨率的方向
|
|
209
|
+
src_is_landscape = src_w > src_h
|
|
210
|
+
tgt_is_landscape = tgt_w > tgt_h
|
|
211
|
+
|
|
212
|
+
# 如果方向不一致,交换目标分辨率的宽高
|
|
213
|
+
if src_is_landscape != tgt_is_landscape:
|
|
214
|
+
adjusted_tgt_size = Size(tgt_h, tgt_w)
|
|
215
|
+
|
|
216
|
+
# 校验调整后的分辨率是否兼容
|
|
217
|
+
if not self._aspect_ratio_compatible(source, adjusted_tgt_size):
|
|
218
|
+
raise UnscalableResolutionError(tuple(target), tuple(source))
|
|
219
|
+
|
|
220
|
+
return adjusted_tgt_size
|
|
221
|
+
|
|
222
|
+
def transform_screenshot(self, screenshot: MatLike) -> MatLike:
|
|
223
|
+
"""处理设备画面截图数据,将物理分辨率缩放到逻辑分辨率。
|
|
224
|
+
|
|
225
|
+
:param screenshot: 原始截图数据。
|
|
226
|
+
:return: 处理后的截图数据。
|
|
227
|
+
"""
|
|
228
|
+
import cv2
|
|
229
|
+
|
|
230
|
+
if self.logic_resolution is None:
|
|
231
|
+
return screenshot
|
|
232
|
+
|
|
233
|
+
target_w, target_h = self.logic_resolution
|
|
234
|
+
h, w = screenshot.shape[:2]
|
|
235
|
+
|
|
236
|
+
# 校验分辨率是否可缩放并获取调整后的目标分辨率
|
|
237
|
+
adjusted_target = self._assert_scalable(Size(w, h), Size(target_w, target_h))
|
|
238
|
+
|
|
239
|
+
return cv2.resize(screenshot, tuple(adjusted_target))
|
|
240
|
+
|
|
241
|
+
def logic_to_physical(self, v: AnyPointLike | RectLike) -> Any:
|
|
242
|
+
"""将逻辑坐标转换为物理坐标。
|
|
243
|
+
|
|
244
|
+
:param v: 逻辑坐标点或矩形。
|
|
245
|
+
:return: 转换后的物理坐标点或矩形。
|
|
246
|
+
"""
|
|
247
|
+
if self.physical_resolution is None:
|
|
248
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
249
|
+
if self.logic_resolution is None:
|
|
250
|
+
return v
|
|
251
|
+
|
|
252
|
+
# 校验分辨率是否可缩放
|
|
253
|
+
self._assert_scalable(self.logic_resolution, self.physical_resolution)
|
|
254
|
+
|
|
255
|
+
ratio = self.scale_ratio
|
|
256
|
+
|
|
257
|
+
# 处理点类型
|
|
258
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
259
|
+
point = unify_any_point(v)
|
|
260
|
+
|
|
261
|
+
new_x = point.x * ratio
|
|
262
|
+
new_y = point.y * ratio
|
|
263
|
+
|
|
264
|
+
if isinstance(point, PointF):
|
|
265
|
+
return PointF(new_x, new_y, name=point.name)
|
|
266
|
+
else:
|
|
267
|
+
return Point(int(new_x), int(new_y), name=point.name)
|
|
268
|
+
|
|
269
|
+
# 处理矩形类型
|
|
270
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
271
|
+
rect = unify_rect(v)
|
|
272
|
+
|
|
273
|
+
new_x = int(rect.x1 * ratio)
|
|
274
|
+
new_y = int(rect.y1 * ratio)
|
|
275
|
+
new_w = int(rect.w * ratio)
|
|
276
|
+
new_h = int(rect.h * ratio)
|
|
277
|
+
|
|
278
|
+
return Rect(new_x, new_y, new_w, new_h, name=rect.name)
|
|
279
|
+
|
|
280
|
+
return v
|
|
281
|
+
|
|
282
|
+
def physical_to_logic(self, v: AnyPointLike | RectLike) -> Any:
|
|
283
|
+
"""将物理坐标转换为逻辑坐标。
|
|
284
|
+
|
|
285
|
+
:param v: 物理坐标点或矩形。
|
|
286
|
+
:return: 转换后的逻辑坐标点或矩形。
|
|
287
|
+
"""
|
|
288
|
+
if self.physical_resolution is None:
|
|
289
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
290
|
+
if self.logic_resolution is None:
|
|
291
|
+
return v
|
|
292
|
+
|
|
293
|
+
# 校验分辨率是否可缩放
|
|
294
|
+
self._assert_scalable(self.logic_resolution, self.physical_resolution)
|
|
295
|
+
|
|
296
|
+
# 类型断言:如果 logic_resolution 不为 None,则 _adjusted_logic_resolution 也不为 None
|
|
297
|
+
assert self.logic_resolution is not None
|
|
298
|
+
|
|
299
|
+
ratio = self.scale_ratio
|
|
300
|
+
|
|
301
|
+
# 处理点类型
|
|
302
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
303
|
+
point = unify_any_point(v)
|
|
304
|
+
|
|
305
|
+
new_x = point.x / ratio
|
|
306
|
+
new_y = point.y / ratio
|
|
307
|
+
|
|
308
|
+
if isinstance(point, PointF):
|
|
309
|
+
return PointF(new_x, new_y, name=point.name)
|
|
310
|
+
else:
|
|
311
|
+
return Point(int(new_x), int(new_y), name=point.name)
|
|
312
|
+
|
|
313
|
+
# 处理矩形类型
|
|
314
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
315
|
+
rect = unify_rect(v)
|
|
316
|
+
|
|
317
|
+
new_x = int(rect.x1 / ratio)
|
|
318
|
+
new_y = int(rect.y1 / ratio)
|
|
319
|
+
new_w = int(rect.w / ratio)
|
|
320
|
+
new_h = int(rect.h / ratio)
|
|
321
|
+
|
|
322
|
+
return Rect(new_x, new_y, new_w, new_h, name=rect.name)
|
|
323
|
+
|
|
324
|
+
return v
|
|
325
|
+
|
|
326
|
+
def fractional_to_physical(self, v: AnyPointLike | RectLike) -> Any:
|
|
327
|
+
"""将比例坐标转换为物理坐标。
|
|
328
|
+
|
|
329
|
+
:param v: 比例坐标点或矩形(0-1范围)。
|
|
330
|
+
:return: 转换后的物理坐标点或矩形。
|
|
331
|
+
"""
|
|
332
|
+
if self.physical_resolution is None:
|
|
333
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
334
|
+
|
|
335
|
+
# 处理点类型
|
|
336
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
337
|
+
point = unify_any_point(v)
|
|
338
|
+
|
|
339
|
+
physical_w, physical_h = self.physical_resolution
|
|
340
|
+
|
|
341
|
+
new_x = point.x * physical_w
|
|
342
|
+
new_y = point.y * physical_h
|
|
343
|
+
|
|
344
|
+
if isinstance(point, PointF):
|
|
345
|
+
return PointF(new_x, new_y, name=point.name)
|
|
346
|
+
else:
|
|
347
|
+
return Point(int(new_x), int(new_y), name=point.name)
|
|
348
|
+
|
|
349
|
+
# 处理矩形类型
|
|
350
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
351
|
+
rect = unify_rect(v)
|
|
352
|
+
|
|
353
|
+
physical_w, physical_h = self.physical_resolution
|
|
354
|
+
|
|
355
|
+
new_x = int(rect.x1 * physical_w)
|
|
356
|
+
new_y = int(rect.y1 * physical_h)
|
|
357
|
+
new_w = int(rect.w * physical_w)
|
|
358
|
+
new_h = int(rect.h * physical_h)
|
|
359
|
+
|
|
360
|
+
return Rect(new_x, new_y, new_w, new_h, name=rect.name)
|
|
361
|
+
|
|
362
|
+
return v
|
|
363
|
+
|
|
364
|
+
def physical_to_fractional(self, v: AnyPointLike | RectLike) -> Any:
|
|
365
|
+
"""将物理坐标转换为比例坐标。
|
|
366
|
+
|
|
367
|
+
:param v: 物理坐标点或矩形。
|
|
368
|
+
:return: 转换后的比例坐标点或矩形(0-1范围)。
|
|
369
|
+
"""
|
|
370
|
+
if self.physical_resolution is None:
|
|
371
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
372
|
+
|
|
373
|
+
# 处理点类型
|
|
374
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
375
|
+
point = unify_any_point(v)
|
|
376
|
+
|
|
377
|
+
physical_w, physical_h = self.physical_resolution
|
|
378
|
+
|
|
379
|
+
new_x = point.x / physical_w
|
|
380
|
+
new_y = point.y / physical_h
|
|
381
|
+
|
|
382
|
+
# 比例坐标总是返回 PointF
|
|
383
|
+
return PointF(new_x, new_y, name=point.name)
|
|
384
|
+
|
|
385
|
+
# 处理矩形类型
|
|
386
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
387
|
+
rect = unify_rect(v)
|
|
388
|
+
|
|
389
|
+
physical_w, physical_h = self.physical_resolution
|
|
390
|
+
|
|
391
|
+
new_x = rect.x1 / physical_w
|
|
392
|
+
new_y = rect.y1 / physical_h
|
|
393
|
+
new_w = rect.w / physical_w
|
|
394
|
+
new_h = rect.h / physical_h
|
|
395
|
+
|
|
396
|
+
# 比例坐标的矩形需要转换为整数,但这里保持浮点精度
|
|
397
|
+
# 实际使用时可能需要根据具体需求调整
|
|
398
|
+
return Rect(int(new_x * 10000), int(new_y * 10000), int(new_w * 10000), int(new_h * 10000), name=rect.name)
|
|
399
|
+
|
|
400
|
+
return v
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class LandscapeGameScaler(ProportionalScaler):
|
|
404
|
+
"""横屏游戏等比例缩放。
|
|
405
|
+
|
|
406
|
+
对于横屏的游戏,通常若两个分辨率的长边一致,那么画面中元素大小也一致。
|
|
407
|
+
因此此缩放器会根据长边进行等比例缩放判断。
|
|
408
|
+
"""
|
|
409
|
+
def __init__(
|
|
410
|
+
self,
|
|
411
|
+
aspect_ratio_tolerance: float = 0.1
|
|
412
|
+
):
|
|
413
|
+
"""初始化横屏等比例缩放器。"""
|
|
414
|
+
super().__init__(
|
|
415
|
+
match_rotation=True,
|
|
416
|
+
aspect_ratio_tolerance=aspect_ratio_tolerance
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def scale_ratio(self) -> float:
|
|
421
|
+
if self.physical_resolution is None:
|
|
422
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
423
|
+
if self.logic_resolution is None:
|
|
424
|
+
return 1.0
|
|
425
|
+
|
|
426
|
+
# 横屏游戏根据长边(max)计算缩放比例
|
|
427
|
+
# Unpack explicitly to support both tuple and Vector2D/Size
|
|
428
|
+
phy_w, phy_h = self.physical_resolution
|
|
429
|
+
log_w, log_h = self.logic_resolution
|
|
430
|
+
|
|
431
|
+
return max(phy_w, phy_h) / max(log_w, log_h)
|
|
432
|
+
|
|
433
|
+
@override
|
|
434
|
+
def _assert_scalable(self, source: SizeLike, target: SizeLike) -> Size:
|
|
435
|
+
return Size(int(source[0] / self.scale_ratio), int(source[1] / self.scale_ratio))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class PortraitGameScaler(ProportionalScaler):
|
|
439
|
+
"""竖屏游戏等比例缩放。
|
|
440
|
+
|
|
441
|
+
对于竖屏的游戏,通常以短边(宽度)为基准进行缩放。
|
|
442
|
+
"""
|
|
443
|
+
def __init__(
|
|
444
|
+
self,
|
|
445
|
+
aspect_ratio_tolerance: float = 0.1
|
|
446
|
+
):
|
|
447
|
+
"""初始化竖屏等比例缩放器。"""
|
|
448
|
+
super().__init__(
|
|
449
|
+
match_rotation=True,
|
|
450
|
+
aspect_ratio_tolerance=aspect_ratio_tolerance
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def scale_ratio(self) -> float:
|
|
455
|
+
if self.physical_resolution is None:
|
|
456
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
457
|
+
if self.logic_resolution is None:
|
|
458
|
+
return 1.0
|
|
459
|
+
|
|
460
|
+
# 竖屏游戏根据短边(min)计算缩放比例
|
|
461
|
+
phy_w, phy_h = self.physical_resolution
|
|
462
|
+
log_w, log_h = self.logic_resolution
|
|
463
|
+
return min(phy_w, phy_h) / min(log_w, log_h)
|
|
464
|
+
|
|
465
|
+
@override
|
|
466
|
+
def _assert_scalable(self, source: SizeLike, target: SizeLike) -> Size:
|
|
467
|
+
return Size(int(source[0] / self.scale_ratio), int(source[1] / self.scale_ratio))
|
kotonebot/config/base_config.py
CHANGED
|
@@ -1,96 +1,103 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
from typing import Generic, TypeVar, Literal
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel, ConfigDict
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
T = TypeVar('T')
|
|
8
|
-
BackendType = Literal['custom', 'mumu12', 'mumu12v5', 'leidian', 'dmm']
|
|
9
|
-
DeviceRecipes = Literal['adb', '
|
|
10
|
-
|
|
11
|
-
class ConfigBaseModel(BaseModel):
|
|
12
|
-
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
13
|
-
|
|
14
|
-
class BackendConfig(ConfigBaseModel):
|
|
15
|
-
type: BackendType = 'custom'
|
|
16
|
-
"""后端类型。"""
|
|
17
|
-
instance_id: str | None = None
|
|
18
|
-
"""模拟器实例 ID。"""
|
|
19
|
-
adb_ip: str = '127.0.0.1'
|
|
20
|
-
"""adb 连接的 ip 地址。"""
|
|
21
|
-
adb_port: int = 5555
|
|
22
|
-
"""adb 连接的端口。"""
|
|
23
|
-
adb_emulator_name: str | None = None
|
|
24
|
-
"""
|
|
25
|
-
adb 连接的模拟器名,用于 自动启动模拟器 功能。
|
|
26
|
-
|
|
27
|
-
雷电模拟器需要设置正确的模拟器名,否则 自动启动模拟器 功能将无法正常工作。
|
|
28
|
-
其他功能不受影响。
|
|
29
|
-
"""
|
|
30
|
-
screenshot_impl: DeviceRecipes = 'adb'
|
|
31
|
-
"""
|
|
32
|
-
截图方法。暂时推荐使用【adb】截图方式。
|
|
33
|
-
|
|
34
|
-
如果使用 remote_windows,需要在 adb_ip 中填写远程 Windows 的 IP 地址,在 adb_port 中填写远程 Windows 的端口号。
|
|
35
|
-
"""
|
|
36
|
-
check_emulator: bool = False
|
|
37
|
-
"""
|
|
38
|
-
检查并启动模拟器
|
|
39
|
-
|
|
40
|
-
启动脚本的时候,如果检测到模拟器未启动,则自动启动模拟器。
|
|
41
|
-
如果模拟器已经启动,则不启动。
|
|
42
|
-
"""
|
|
43
|
-
emulator_path: str | None = None
|
|
44
|
-
"""模拟器 exe 文件路径"""
|
|
45
|
-
emulator_args: str = ""
|
|
46
|
-
"""模拟器启动时的命令行参数"""
|
|
47
|
-
windows_window_title: str = 'gakumas'
|
|
48
|
-
"""Windows 截图方式的窗口标题"""
|
|
49
|
-
windows_ahk_path: str | None = None
|
|
50
|
-
"""Windows 截图方式的 AutoHotkey 可执行文件路径,为 None 时使用默认路径"""
|
|
51
|
-
mumu_background_mode: bool = False
|
|
52
|
-
"""MuMu12 模拟器后台保活模式"""
|
|
53
|
-
target_screenshot_interval: float | None = None
|
|
54
|
-
"""最小截图间隔,单位为秒。为 None 时不限制截图速度。"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
"""
|
|
96
|
-
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Generic, TypeVar, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
BackendType = Literal['custom', 'mumu12', 'mumu12v5', 'leidian', 'dmm']
|
|
9
|
+
DeviceRecipes = Literal['adb', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc', 'windows_background']
|
|
10
|
+
|
|
11
|
+
class ConfigBaseModel(BaseModel):
|
|
12
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
13
|
+
|
|
14
|
+
class BackendConfig(ConfigBaseModel):
|
|
15
|
+
type: BackendType = 'custom'
|
|
16
|
+
"""后端类型。"""
|
|
17
|
+
instance_id: str | None = None
|
|
18
|
+
"""模拟器实例 ID。"""
|
|
19
|
+
adb_ip: str = '127.0.0.1'
|
|
20
|
+
"""adb 连接的 ip 地址。"""
|
|
21
|
+
adb_port: int = 5555
|
|
22
|
+
"""adb 连接的端口。"""
|
|
23
|
+
adb_emulator_name: str | None = None
|
|
24
|
+
"""
|
|
25
|
+
adb 连接的模拟器名,用于 自动启动模拟器 功能。
|
|
26
|
+
|
|
27
|
+
雷电模拟器需要设置正确的模拟器名,否则 自动启动模拟器 功能将无法正常工作。
|
|
28
|
+
其他功能不受影响。
|
|
29
|
+
"""
|
|
30
|
+
screenshot_impl: DeviceRecipes = 'adb'
|
|
31
|
+
"""
|
|
32
|
+
截图方法。暂时推荐使用【adb】截图方式。
|
|
33
|
+
|
|
34
|
+
如果使用 remote_windows,需要在 adb_ip 中填写远程 Windows 的 IP 地址,在 adb_port 中填写远程 Windows 的端口号。
|
|
35
|
+
"""
|
|
36
|
+
check_emulator: bool = False
|
|
37
|
+
"""
|
|
38
|
+
检查并启动模拟器
|
|
39
|
+
|
|
40
|
+
启动脚本的时候,如果检测到模拟器未启动,则自动启动模拟器。
|
|
41
|
+
如果模拟器已经启动,则不启动。
|
|
42
|
+
"""
|
|
43
|
+
emulator_path: str | None = None
|
|
44
|
+
"""模拟器 exe 文件路径"""
|
|
45
|
+
emulator_args: str = ""
|
|
46
|
+
"""模拟器启动时的命令行参数"""
|
|
47
|
+
windows_window_title: str = 'gakumas'
|
|
48
|
+
"""Windows 截图方式的窗口标题"""
|
|
49
|
+
windows_ahk_path: str | None = None
|
|
50
|
+
"""Windows 截图方式的 AutoHotkey 可执行文件路径,为 None 时使用默认路径"""
|
|
51
|
+
mumu_background_mode: bool = False
|
|
52
|
+
"""MuMu12 模拟器后台保活模式"""
|
|
53
|
+
target_screenshot_interval: float | None = None
|
|
54
|
+
"""最小截图间隔,单位为秒。为 None 时不限制截图速度。"""
|
|
55
|
+
cursor_wait_speed: float = -1
|
|
56
|
+
"""
|
|
57
|
+
使用 DMM 版后台挂机功能时,在点击前会尝试等待光标静止,以避免发生点击偏移。
|
|
58
|
+
此项规定了速度小于多少时认为光标静止,单位为像素/秒。
|
|
59
|
+
|
|
60
|
+
-1 表示使用内置默认值,0 表示禁用该功能。
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
class PushConfig(ConfigBaseModel):
|
|
64
|
+
"""推送配置。"""
|
|
65
|
+
|
|
66
|
+
wx_pusher_enabled: bool = False
|
|
67
|
+
"""是否启用 WxPusher 推送。"""
|
|
68
|
+
wx_pusher_app_token: str | None = None
|
|
69
|
+
"""WxPusher 的 app token。"""
|
|
70
|
+
wx_pusher_uid: str | None = None
|
|
71
|
+
"""WxPusher 的 uid。"""
|
|
72
|
+
|
|
73
|
+
free_image_host_key: str | None = None
|
|
74
|
+
"""FreeImageHost API key。用于在推送通知时显示图片。"""
|
|
75
|
+
|
|
76
|
+
class UserConfig(ConfigBaseModel, Generic[T]):
|
|
77
|
+
"""用户可以自由添加、删除的配置数据。"""
|
|
78
|
+
|
|
79
|
+
name: str = 'default_config'
|
|
80
|
+
"""显示名称。通常由用户输入。"""
|
|
81
|
+
id: str = uuid.uuid4().hex
|
|
82
|
+
"""唯一标识符。"""
|
|
83
|
+
category: str = 'default'
|
|
84
|
+
"""类别。如:'global'、'china'、'asia' 等。"""
|
|
85
|
+
description: str = ''
|
|
86
|
+
"""描述。通常由用户输入。"""
|
|
87
|
+
backend: BackendConfig = BackendConfig()
|
|
88
|
+
"""后端配置。"""
|
|
89
|
+
keep_screenshots: bool = False
|
|
90
|
+
"""
|
|
91
|
+
是否保留截图。
|
|
92
|
+
若启用,则会保存每一张截图到 `dumps` 目录下。启用该选项有助于辅助调试。
|
|
93
|
+
"""
|
|
94
|
+
options: T
|
|
95
|
+
"""下游脚本储存的具体数据。"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RootConfig(ConfigBaseModel, Generic[T]):
|
|
99
|
+
version: int = 5
|
|
100
|
+
"""配置版本。"""
|
|
101
|
+
user_configs: list[UserConfig[T]] = []
|
|
102
|
+
"""用户配置。"""
|
|
103
|
+
|