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,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))
@@ -1,96 +1,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', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
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
- class PushConfig(ConfigBaseModel):
57
- """推送配置。"""
58
-
59
- wx_pusher_enabled: bool = False
60
- """是否启用 WxPusher 推送。"""
61
- wx_pusher_app_token: str | None = None
62
- """WxPusher 的 app token。"""
63
- wx_pusher_uid: str | None = None
64
- """WxPusher 的 uid。"""
65
-
66
- free_image_host_key: str | None = None
67
- """FreeImageHost API key。用于在推送通知时显示图片。"""
68
-
69
- class UserConfig(ConfigBaseModel, Generic[T]):
70
- """用户可以自由添加、删除的配置数据。"""
71
-
72
- name: str = 'default_config'
73
- """显示名称。通常由用户输入。"""
74
- id: str = uuid.uuid4().hex
75
- """唯一标识符。"""
76
- category: str = 'default'
77
- """类别。如:'global'、'china'、'asia' 等。"""
78
- description: str = ''
79
- """描述。通常由用户输入。"""
80
- backend: BackendConfig = BackendConfig()
81
- """后端配置。"""
82
- keep_screenshots: bool = False
83
- """
84
- 是否保留截图。
85
- 若启用,则会保存每一张截图到 `dumps` 目录下。启用该选项有助于辅助调试。
86
- """
87
- options: T
88
- """下游脚本储存的具体数据。"""
89
-
90
-
91
- class RootConfig(ConfigBaseModel, Generic[T]):
92
- version: int = 5
93
- """配置版本。"""
94
- user_configs: list[UserConfig[T]] = []
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']
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
+ class PushConfig(ConfigBaseModel):
57
+ """推送配置。"""
58
+
59
+ wx_pusher_enabled: bool = False
60
+ """是否启用 WxPusher 推送。"""
61
+ wx_pusher_app_token: str | None = None
62
+ """WxPusher 的 app token。"""
63
+ wx_pusher_uid: str | None = None
64
+ """WxPusher 的 uid。"""
65
+
66
+ free_image_host_key: str | None = None
67
+ """FreeImageHost API key。用于在推送通知时显示图片。"""
68
+
69
+ class UserConfig(ConfigBaseModel, Generic[T]):
70
+ """用户可以自由添加、删除的配置数据。"""
71
+
72
+ name: str = 'default_config'
73
+ """显示名称。通常由用户输入。"""
74
+ id: str = uuid.uuid4().hex
75
+ """唯一标识符。"""
76
+ category: str = 'default'
77
+ """类别。如:'global'、'china'、'asia' 等。"""
78
+ description: str = ''
79
+ """描述。通常由用户输入。"""
80
+ backend: BackendConfig = BackendConfig()
81
+ """后端配置。"""
82
+ keep_screenshots: bool = False
83
+ """
84
+ 是否保留截图。
85
+ 若启用,则会保存每一张截图到 `dumps` 目录下。启用该选项有助于辅助调试。
86
+ """
87
+ options: T
88
+ """下游脚本储存的具体数据。"""
89
+
90
+
91
+ class RootConfig(ConfigBaseModel, Generic[T]):
92
+ version: int = 5
93
+ """配置版本。"""
94
+ user_configs: list[UserConfig[T]] = []
95
+ """用户配置。"""
96
+