MaaFw 4.5.5__py3-none-manylinux2014_x86_64.whl → 5.4.0__py3-none-manylinux2014_x86_64.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.

Potentially problematic release.


This version of MaaFw might be problematic. Click here for more details.

maa/controller.py CHANGED
@@ -1,12 +1,13 @@
1
1
  import json
2
+ import os
3
+ import numpy
2
4
  from abc import abstractmethod
3
5
  from ctypes import c_int32
4
- import os
5
6
  from pathlib import Path
6
- from typing import Any, Dict, Optional, Union
7
+ from typing import Any, Dict, Optional, Tuple, Union
7
8
 
8
9
  from .buffer import ImageBuffer, StringBuffer
9
- from .notification_handler import NotificationHandler
10
+ from .event_sink import EventSink, NotificationType
10
11
  from .define import *
11
12
  from .job import Job, JobWithResult
12
13
  from .library import Library
@@ -14,13 +15,14 @@ from .library import Library
14
15
  __all__ = [
15
16
  "AdbController",
16
17
  "DbgController",
18
+ "PlayCoverController",
17
19
  "Win32Controller",
20
+ "GamepadController",
18
21
  "CustomController",
19
22
  ]
20
23
 
21
24
 
22
25
  class Controller:
23
- _notification_handler: Optional[NotificationHandler]
24
26
  _handle: MaaControllerHandle
25
27
  _own: bool
26
28
 
@@ -42,16 +44,63 @@ class Controller:
42
44
  Library.framework().MaaControllerDestroy(self._handle)
43
45
 
44
46
  def post_connection(self) -> Job:
47
+ """异步连接设备 / Asynchronously connect device
48
+
49
+ 这是一个异步操作,会立即返回一个 Job 对象
50
+ This is an asynchronous operation that immediately returns a Job object
51
+
52
+ Returns:
53
+ Job: 作业对象,可通过 status/wait 查询状态 / Job object, can query status via status/wait
54
+ """
45
55
  ctrl_id = Library.framework().MaaControllerPostConnection(self._handle)
46
56
  return self._gen_ctrl_job(ctrl_id)
47
57
 
48
- def post_click(self, x: int, y: int) -> Job:
49
- ctrl_id = Library.framework().MaaControllerPostClick(self._handle, x, y)
58
+ def post_click(self, x: int, y: int, contact: int = 0, pressure: int = 1) -> Job:
59
+ """异步点击 / Asynchronously click
60
+
61
+ 这是一个异步操作,会立即返回一个 Job 对象
62
+ This is an asynchronous operation that immediately returns a Job object
63
+
64
+ Args:
65
+ x: x 坐标 / x coordinate
66
+ y: y 坐标 / y coordinate
67
+ contact: 触点编号 (Adb 控制器: 手指编号; Win32 控制器: 鼠标按键 0:左键, 1:右键, 2:中键) / Contact number (Adb controller: finger number; Win32 controller: mouse button 0:left, 1:right, 2:middle)
68
+ pressure: 触点力度 / Contact pressure
69
+
70
+ Returns:
71
+ Job: 作业对象,可通过 status/wait 查询状态 / Job object, can query status via status/wait
72
+ """
73
+ ctrl_id = Library.framework().MaaControllerPostClickV2(
74
+ self._handle, x, y, contact, pressure
75
+ )
50
76
  return self._gen_ctrl_job(ctrl_id)
51
77
 
52
- def post_swipe(self, x1: int, y1: int, x2: int, y2: int, duration: int) -> Job:
53
- ctrl_id = Library.framework().MaaControllerPostSwipe(
54
- self._handle, x1, y1, x2, y2, duration
78
+ def post_swipe(
79
+ self,
80
+ x1: int,
81
+ y1: int,
82
+ x2: int,
83
+ y2: int,
84
+ duration: int,
85
+ contact: int = 0,
86
+ pressure: int = 1,
87
+ ) -> Job:
88
+ """滑动 / Swipe
89
+
90
+ Args:
91
+ x1: 起点 x 坐标 / Start x coordinate
92
+ y1: 起点 y 坐标 / Start y coordinate
93
+ x2: 终点 x 坐标 / End x coordinate
94
+ y2: 终点 y 坐标 / End y coordinate
95
+ duration: 滑动时长(毫秒) / Swipe duration in milliseconds
96
+ contact: 触点编号 (Adb 控制器: 手指编号; Win32 控制器: 鼠标按键 0:左键, 1:右键, 2:中键) / Contact number (Adb controller: finger number; Win32 controller: mouse button 0:left, 1:right, 2:middle)
97
+ pressure: 触点力度 / Contact pressure
98
+
99
+ Returns:
100
+ Job: 作业对象 / Job object
101
+ """
102
+ ctrl_id = Library.framework().MaaControllerPostSwipeV2(
103
+ self._handle, x1, y1, x2, y2, duration, contact, pressure
55
104
  )
56
105
  return self._gen_ctrl_job(ctrl_id)
57
106
 
@@ -62,30 +111,78 @@ class Controller:
62
111
  return self.post_click_key(key)
63
112
 
64
113
  def post_click_key(self, key: int) -> Job:
114
+ """单击按键 / Click key
115
+
116
+ Args:
117
+ key: 虚拟键码 / Virtual key code
118
+
119
+ Returns:
120
+ Job: 作业对象 / Job object
121
+ """
65
122
  ctrl_id = Library.framework().MaaControllerPostClickKey(self._handle, key)
66
123
  return self._gen_ctrl_job(ctrl_id)
67
124
 
68
125
  def post_key_down(self, key: int) -> Job:
126
+ """按下键 / Key down
127
+
128
+ Args:
129
+ key: 虚拟键码 / Virtual key code
130
+
131
+ Returns:
132
+ Job: 作业对象 / Job object
133
+ """
69
134
  ctrl_id = Library.framework().MaaControllerPostKeyDown(self._handle, key)
70
135
  return self._gen_ctrl_job(ctrl_id)
71
136
 
72
137
  def post_key_up(self, key: int) -> Job:
138
+ """抬起键 / Key up
139
+
140
+ Args:
141
+ key: 虚拟键码 / Virtual key code
142
+
143
+ Returns:
144
+ Job: 作业对象 / Job object
145
+ """
73
146
  ctrl_id = Library.framework().MaaControllerPostKeyUp(self._handle, key)
74
147
  return self._gen_ctrl_job(ctrl_id)
75
148
 
76
149
  def post_input_text(self, text: str) -> Job:
150
+ """输入文本 / Input text
151
+
152
+ Args:
153
+ text: 要输入的文本 / Text to input
154
+
155
+ Returns:
156
+ Job: 作业对象 / Job object
157
+ """
77
158
  ctrl_id = Library.framework().MaaControllerPostInputText(
78
159
  self._handle, text.encode()
79
160
  )
80
161
  return self._gen_ctrl_job(ctrl_id)
81
162
 
82
163
  def post_start_app(self, intent: str) -> Job:
164
+ """启动应用 / Start app
165
+
166
+ Args:
167
+ intent: 目标应用 (Adb 控制器: package name 或 activity) / Target app (Adb controller: package name or activity)
168
+
169
+ Returns:
170
+ Job: 作业对象 / Job object
171
+ """
83
172
  ctrl_id = Library.framework().MaaControllerPostStartApp(
84
173
  self._handle, intent.encode()
85
174
  )
86
175
  return self._gen_ctrl_job(ctrl_id)
87
176
 
88
177
  def post_stop_app(self, intent: str) -> Job:
178
+ """关闭应用 / Stop app
179
+
180
+ Args:
181
+ intent: 目标应用 (Adb 控制器: package name) / Target app (Adb controller: package name)
182
+
183
+ Returns:
184
+ Job: 作业对象 / Job object
185
+ """
89
186
  ctrl_id = Library.framework().MaaControllerPostStopApp(
90
187
  self._handle, intent.encode()
91
188
  )
@@ -94,6 +191,17 @@ class Controller:
94
191
  def post_touch_down(
95
192
  self, x: int, y: int, contact: int = 0, pressure: int = 1
96
193
  ) -> Job:
194
+ """按下 / Touch down
195
+
196
+ Args:
197
+ x: x 坐标 / x coordinate
198
+ y: y 坐标 / y coordinate
199
+ contact: 触点编号 (Adb 控制器: 手指编号; Win32 控制器: 鼠标按键 0:左键, 1:右键, 2:中键) / Contact number (Adb controller: finger number; Win32 controller: mouse button 0:left, 1:right, 2:middle)
200
+ pressure: 触点力度 / Contact pressure
201
+
202
+ Returns:
203
+ Job: 作业对象 / Job object
204
+ """
97
205
  ctrl_id = Library.framework().MaaControllerPostTouchDown(
98
206
  self._handle, contact, x, y, pressure
99
207
  )
@@ -102,16 +210,57 @@ class Controller:
102
210
  def post_touch_move(
103
211
  self, x: int, y: int, contact: int = 0, pressure: int = 1
104
212
  ) -> Job:
213
+ """移动 / Move
214
+
215
+ Args:
216
+ x: x 坐标 / x coordinate
217
+ y: y 坐标 / y coordinate
218
+ contact: 触点编号 (Adb 控制器: 手指编号; Win32 控制器: 鼠标按键 0:左键, 1:右键, 2:中键) / Contact number (Adb controller: finger number; Win32 controller: mouse button 0:left, 1:right, 2:middle)
219
+ pressure: 触点力度 / Contact pressure
220
+
221
+ Returns:
222
+ Job: 作业对象 / Job object
223
+ """
105
224
  ctrl_id = Library.framework().MaaControllerPostTouchMove(
106
225
  self._handle, contact, x, y, pressure
107
226
  )
108
227
  return self._gen_ctrl_job(ctrl_id)
109
228
 
110
229
  def post_touch_up(self, contact: int = 0) -> Job:
230
+ """抬起 / Touch up
231
+
232
+ Args:
233
+ contact: 触点编号 (Adb 控制器: 手指编号; Win32 控制器: 鼠标按键 0:左键, 1:右键, 2:中键) / Contact number (Adb controller: finger number; Win32 controller: mouse button 0:left, 1:right, 2:middle)
234
+
235
+ Returns:
236
+ Job: 作业对象 / Job object
237
+ """
111
238
  ctrl_id = Library.framework().MaaControllerPostTouchUp(self._handle, contact)
112
239
  return self._gen_ctrl_job(ctrl_id)
113
240
 
241
+ def post_scroll(self, dx: int, dy: int) -> Job:
242
+ """滚动 / Scroll
243
+
244
+ Args:
245
+ dx: 水平滚动距离,正值向右滚动,负值向左滚动 / Horizontal scroll distance, positive for right, negative for left
246
+ dy: 垂直滚动距离,正值向上滚动,负值向下滚动 / Vertical scroll distance, positive for up, negative for down
247
+
248
+ Returns:
249
+ Job: 作业对象 / Job object
250
+
251
+ Note:
252
+ 不是所有控制器都支持滚动操作 / Not all controllers support scroll operation
253
+ 建议使用 120 的整数倍(WHEEL_DELTA)以获得最佳兼容性 / Using multiples of 120 (WHEEL_DELTA) is recommended
254
+ """
255
+ ctrl_id = Library.framework().MaaControllerPostScroll(self._handle, dx, dy)
256
+ return self._gen_ctrl_job(ctrl_id)
257
+
114
258
  def post_screencap(self) -> JobWithResult:
259
+ """截图 / Screenshot
260
+
261
+ Returns:
262
+ JobWithResult: 作业对象,可通过 result 获取截图 / Job object, can get screenshot via result
263
+ """
115
264
  ctrl_id = Library.framework().MaaControllerPostScreencap(self._handle)
116
265
  return JobWithResult(
117
266
  ctrl_id,
@@ -122,6 +271,22 @@ class Controller:
122
271
 
123
272
  @property
124
273
  def cached_image(self) -> numpy.ndarray:
274
+ """获取最新一次截图 / Get the latest screenshot
275
+
276
+ Returns:
277
+ numpy.ndarray: 截图图像 / Screenshot image
278
+
279
+ Raises:
280
+ RuntimeError: 如果获取失败
281
+
282
+ Note:
283
+ 返回的图像是经过缩放的,尺寸根据截图目标尺寸设置(长边/短边)而定,可能与设备原始分辨率不同。
284
+ 使用 resolution 属性可获取设备的原始(未缩放)分辨率。
285
+
286
+ The returned image is scaled according to the screenshot target size settings (long side / short side).
287
+ The image dimensions may differ from the raw device resolution.
288
+ Use the resolution property to get the raw (unscaled) device resolution.
289
+ """
125
290
  image_buffer = ImageBuffer()
126
291
  if not Library.framework().MaaControllerCachedImage(
127
292
  self._handle, image_buffer._handle
@@ -129,18 +294,101 @@ class Controller:
129
294
  raise RuntimeError("Failed to get cached image.")
130
295
  return image_buffer.get()
131
296
 
297
+ def post_shell(self, cmd: str, timeout: int = 20000) -> JobWithResult:
298
+ """执行 shell 命令 (仅 ADB 控制器) / Execute shell command (ADB only)
299
+
300
+ Args:
301
+ cmd: shell 命令 / shell command
302
+ timeout: 超时时间(毫秒),默认 20000 / Timeout in milliseconds, default 20000
303
+
304
+ Returns:
305
+ JobWithResult: 作业对象,可通过 result 获取命令输出 / Job object, can get output via result
306
+ """
307
+ ctrl_id = Library.framework().MaaControllerPostShell(
308
+ self._handle, cmd.encode("utf-8"), timeout
309
+ )
310
+ return JobWithResult(
311
+ ctrl_id,
312
+ self._status,
313
+ self._wait,
314
+ self._get_shell_output,
315
+ )
316
+
317
+ @property
318
+ def shell_output(self) -> str:
319
+ """获取最近一次 shell 命令输出 / Get the latest shell command output
320
+
321
+ Returns:
322
+ str: shell 命令输出 / shell command output
323
+
324
+ Raises:
325
+ RuntimeError: 如果获取失败
326
+ """
327
+ string_buffer = StringBuffer()
328
+ if not Library.framework().MaaControllerGetShellOutput(
329
+ self._handle, string_buffer._handle
330
+ ):
331
+ raise RuntimeError("Failed to get shell output.")
332
+ return string_buffer.get()
333
+
132
334
  @property
133
335
  def connected(self) -> bool:
336
+ """判断是否已连接 / Check if connected
337
+
338
+ Returns:
339
+ bool: 是否已连接 / Whether connected
340
+ """
134
341
  return bool(Library.framework().MaaControllerConnected(self._handle))
135
342
 
136
343
  @property
137
344
  def uuid(self) -> str:
345
+ """获取设备 uuid / Get device uuid
346
+
347
+ Returns:
348
+ str: 设备 uuid / Device uuid
349
+
350
+ Raises:
351
+ RuntimeError: 如果获取失败
352
+ """
138
353
  buffer = StringBuffer()
139
354
  if not Library.framework().MaaControllerGetUuid(self._handle, buffer._handle):
140
355
  raise RuntimeError("Failed to get UUID.")
141
356
  return buffer.get()
142
357
 
358
+ @property
359
+ def resolution(self) -> Tuple[int, int]:
360
+ """获取设备原始(未缩放)分辨率 / Get the raw (unscaled) device resolution
361
+
362
+ Returns:
363
+ Tuple[int, int]: (宽度, 高度),获取失败时返回 (0, 0) / (width, height), returns (0, 0) on failure
364
+
365
+ Note:
366
+ 返回的是设备屏幕的实际分辨率,未经任何缩放处理。
367
+ 而通过 cached_image 获取的截图是经过缩放的,其尺寸可能与此原始分辨率不同。
368
+ 需要在首次截图后才能获取到有效值,否则返回 (0, 0)。
369
+
370
+ This returns the actual device screen resolution before any scaling.
371
+ The screenshot obtained via cached_image is scaled according to the screenshot target size settings,
372
+ so its dimensions may differ from this raw resolution.
373
+ Valid values are only available after the first screenshot is taken, otherwise returns (0, 0).
374
+ """
375
+ width = ctypes.c_int32()
376
+ height = ctypes.c_int32()
377
+ if not Library.framework().MaaControllerGetResolution(
378
+ self._handle, ctypes.byref(width), ctypes.byref(height)
379
+ ):
380
+ return (0, 0)
381
+ return (width.value, height.value)
382
+
143
383
  def set_screenshot_target_long_side(self, long_side: int) -> bool:
384
+ """设置截图缩放长边到指定长度 / Set screenshot scaling long side to specified length
385
+
386
+ Args:
387
+ long_side: 长边长度 / Long side length
388
+
389
+ Returns:
390
+ bool: 是否成功 / Whether successful
391
+ """
144
392
  cint = ctypes.c_int32(long_side)
145
393
  return bool(
146
394
  Library.framework().MaaControllerSetOption(
@@ -152,6 +400,14 @@ class Controller:
152
400
  )
153
401
 
154
402
  def set_screenshot_target_short_side(self, short_side: int) -> bool:
403
+ """设置截图缩放短边到指定长度 / Set screenshot scaling short side to specified length
404
+
405
+ Args:
406
+ short_side: 短边长度 / Short side length
407
+
408
+ Returns:
409
+ bool: 是否成功 / Whether successful
410
+ """
155
411
  cint = ctypes.c_int32(short_side)
156
412
  return bool(
157
413
  Library.framework().MaaControllerSetOption(
@@ -163,6 +419,17 @@ class Controller:
163
419
  )
164
420
 
165
421
  def set_screenshot_use_raw_size(self, enable: bool) -> bool:
422
+ """设置截图不缩放 / Set screenshot use raw size without scaling
423
+
424
+ 注意:此选项可能导致在不同分辨率的设备上坐标不正确
425
+ Note: This option may cause incorrect coordinates on devices with different resolutions
426
+
427
+ Args:
428
+ enable: 是否启用 / Whether to enable
429
+
430
+ Returns:
431
+ bool: 是否成功 / Whether successful
432
+ """
166
433
  cbool = MaaBool(enable)
167
434
  return bool(
168
435
  Library.framework().MaaControllerSetOption(
@@ -173,6 +440,41 @@ class Controller:
173
440
  )
174
441
  )
175
442
 
443
+ _sink_holder: Dict[int, "ControllerEventSink"] = {}
444
+
445
+ def add_sink(self, sink: "ControllerEventSink") -> Optional[int]:
446
+ """添加控制器事件监听器 / Add controller event listener
447
+
448
+ Args:
449
+ sink: 事件监听器 / Event sink
450
+
451
+ Returns:
452
+ Optional[int]: 监听器 id,失败返回 None / Listener id, or None if failed
453
+ """
454
+ sink_id = int(
455
+ Library.framework().MaaControllerAddSink(
456
+ self._handle, *EventSink._gen_c_param(sink)
457
+ )
458
+ )
459
+ if sink_id == MaaInvalidId:
460
+ return None
461
+
462
+ self._sink_holder[sink_id] = sink
463
+ return sink_id
464
+
465
+ def remove_sink(self, sink_id: int) -> None:
466
+ """移除控制器事件监听器 / Remove controller event listener
467
+
468
+ Args:
469
+ sink_id: 监听器 id / Listener id
470
+ """
471
+ Library.framework().MaaControllerRemoveSink(self._handle, sink_id)
472
+ self._sink_holder.pop(sink_id)
473
+
474
+ def clear_sinks(self) -> None:
475
+ """清除所有控制器事件监听器 / Clear all controller event listeners"""
476
+ Library.framework().MaaControllerClearSinks(self._handle)
477
+
176
478
  ### private ###
177
479
 
178
480
  def _status(self, maaid: int) -> MaaStatus:
@@ -184,6 +486,9 @@ class Controller:
184
486
  def _get_screencap(self, _: int) -> numpy.ndarray:
185
487
  return self.cached_image
186
488
 
489
+ def _get_shell_output(self, _: int) -> str:
490
+ return self.shell_output
491
+
187
492
  def _gen_ctrl_job(self, ctrlid: MaaCtrlId) -> Job:
188
493
  return Job(
189
494
  ctrlid,
@@ -220,6 +525,15 @@ class Controller:
220
525
  c_int32,
221
526
  ]
222
527
 
528
+ Library.framework().MaaControllerPostClickV2.restype = MaaCtrlId
529
+ Library.framework().MaaControllerPostClickV2.argtypes = [
530
+ MaaControllerHandle,
531
+ c_int32,
532
+ c_int32,
533
+ c_int32,
534
+ c_int32,
535
+ ]
536
+
223
537
  Library.framework().MaaControllerPostSwipe.restype = MaaCtrlId
224
538
  Library.framework().MaaControllerPostSwipe.argtypes = [
225
539
  MaaControllerHandle,
@@ -230,6 +544,18 @@ class Controller:
230
544
  c_int32,
231
545
  ]
232
546
 
547
+ Library.framework().MaaControllerPostSwipeV2.restype = MaaCtrlId
548
+ Library.framework().MaaControllerPostSwipeV2.argtypes = [
549
+ MaaControllerHandle,
550
+ c_int32,
551
+ c_int32,
552
+ c_int32,
553
+ c_int32,
554
+ c_int32,
555
+ c_int32,
556
+ c_int32,
557
+ ]
558
+
233
559
  Library.framework().MaaControllerPostClickKey.restype = MaaCtrlId
234
560
  Library.framework().MaaControllerPostClickKey.argtypes = [
235
561
  MaaControllerHandle,
@@ -292,6 +618,14 @@ class Controller:
292
618
  MaaControllerHandle,
293
619
  c_int32,
294
620
  ]
621
+
622
+ Library.framework().MaaControllerPostScroll.restype = MaaCtrlId
623
+ Library.framework().MaaControllerPostScroll.argtypes = [
624
+ MaaControllerHandle,
625
+ c_int32,
626
+ c_int32,
627
+ ]
628
+
295
629
  Library.framework().MaaControllerStatus.restype = MaaStatus
296
630
  Library.framework().MaaControllerStatus.argtypes = [
297
631
  MaaControllerHandle,
@@ -319,8 +653,37 @@ class Controller:
319
653
  MaaStringBufferHandle,
320
654
  ]
321
655
 
656
+ Library.framework().MaaControllerGetResolution.restype = MaaBool
657
+ Library.framework().MaaControllerGetResolution.argtypes = [
658
+ MaaControllerHandle,
659
+ ctypes.POINTER(ctypes.c_int32),
660
+ ctypes.POINTER(ctypes.c_int32),
661
+ ]
662
+
663
+ Library.framework().MaaControllerAddSink.restype = MaaSinkId
664
+ Library.framework().MaaControllerAddSink.argtypes = [
665
+ MaaControllerHandle,
666
+ MaaEventCallback,
667
+ ctypes.c_void_p,
668
+ ]
669
+
670
+ Library.framework().MaaControllerRemoveSink.restype = None
671
+ Library.framework().MaaControllerRemoveSink.argtypes = [
672
+ MaaControllerHandle,
673
+ MaaSinkId,
674
+ ]
675
+
676
+ Library.framework().MaaControllerClearSinks.restype = None
677
+ Library.framework().MaaControllerClearSinks.argtypes = [MaaControllerHandle]
678
+
322
679
 
323
680
  class AdbController(Controller):
681
+ """Adb 控制器 / Adb controller
682
+
683
+ 截图方式和输入方式会在启动时进行测速, 选择最快的方案
684
+ Screenshot and input methods will be speed tested at startup, selecting the fastest option
685
+ """
686
+
324
687
  AGENT_BINARY_PATH = os.path.join(
325
688
  os.path.dirname(__file__),
326
689
  "../MaaAgentBinary",
@@ -334,13 +697,23 @@ class AdbController(Controller):
334
697
  input_methods: int = MaaAdbInputMethodEnum.Default,
335
698
  config: Dict[str, Any] = {},
336
699
  agent_path: Union[str, Path] = AGENT_BINARY_PATH,
337
- notification_handler: Optional[NotificationHandler] = None,
338
700
  ):
701
+ """创建 Adb 控制器 / Create Adb controller
702
+
703
+ Args:
704
+ adb_path: adb 路径 / adb path
705
+ address: 连接地址 / connection address
706
+ screencap_methods: 所有可使用的截图方式 / all available screenshot methods
707
+ input_methods: 所有可使用的输入方式 / all available input methods
708
+ config: 额外配置 / extra config
709
+ agent_path: MaaAgentBinary 路径 / MaaAgentBinary path
710
+
711
+ Raises:
712
+ RuntimeError: 如果创建失败
713
+ """
339
714
  super().__init__()
340
715
  self._set_adb_api_properties()
341
716
 
342
- self._notification_handler = notification_handler
343
-
344
717
  self._handle = Library.framework().MaaAdbControllerCreate(
345
718
  str(adb_path).encode(),
346
719
  address.encode(),
@@ -348,7 +721,6 @@ class AdbController(Controller):
348
721
  MaaAdbInputMethod(input_methods),
349
722
  json.dumps(config, ensure_ascii=False).encode(),
350
723
  str(agent_path).encode(),
351
- *NotificationHandler._gen_c_param(self._notification_handler)
352
724
  )
353
725
 
354
726
  if not self._handle:
@@ -364,29 +736,38 @@ class AdbController(Controller):
364
736
  MaaAdbInputMethod,
365
737
  ctypes.c_char_p,
366
738
  ctypes.c_char_p,
367
- MaaNotificationCallback,
368
- ctypes.c_void_p,
369
739
  ]
370
740
 
371
741
 
372
742
  class Win32Controller(Controller):
743
+ """Win32 控制器 / Win32 controller"""
373
744
 
374
745
  def __init__(
375
746
  self,
376
747
  hWnd: Union[ctypes.c_void_p, int, None],
377
- screencap_method: int = MaaWin32ScreencapMethodEnum.DXGI_DesktopDup,
378
- input_method: int = MaaWin32InputMethodEnum.Seize,
379
- notification_handler: Optional[NotificationHandler] = None,
748
+ screencap_method: int = MaaWin32ScreencapMethodEnum.FramePool,
749
+ mouse_method: int = MaaWin32InputMethodEnum.Seize,
750
+ keyboard_method: int = MaaWin32InputMethodEnum.Seize,
380
751
  ):
752
+ """创建 Win32 控制器 / Create Win32 controller
753
+
754
+ Args:
755
+ hWnd: 窗口句柄 / window handle
756
+ screencap_method: 使用的截图方式 / screenshot method used
757
+ mouse_method: 使用的鼠标输入方式 / mouse input method used
758
+ keyboard_method: 使用的键盘输入方式 / keyboard input method used
759
+
760
+ Raises:
761
+ RuntimeError: 如果创建失败
762
+ """
381
763
  super().__init__()
382
764
  self._set_win32_api_properties()
383
765
 
384
- self._notification_handler = notification_handler
385
766
  self._handle = Library.framework().MaaWin32ControllerCreate(
386
767
  hWnd,
387
768
  MaaWin32ScreencapMethod(screencap_method),
388
- MaaWin32InputMethod(input_method),
389
- *NotificationHandler._gen_c_param(self._notification_handler)
769
+ MaaWin32InputMethod(mouse_method),
770
+ MaaWin32InputMethod(keyboard_method),
390
771
  )
391
772
 
392
773
  if not self._handle:
@@ -398,12 +779,52 @@ class Win32Controller(Controller):
398
779
  ctypes.c_void_p,
399
780
  MaaWin32ScreencapMethod,
400
781
  MaaWin32InputMethod,
401
- MaaNotificationCallback,
402
- ctypes.c_void_p,
782
+ MaaWin32InputMethod,
783
+ ]
784
+
785
+
786
+ class PlayCoverController(Controller):
787
+ """PlayCover 控制器 / PlayCover controller
788
+
789
+ 用于在 macOS 上控制通过 PlayCover 运行的 iOS 应用
790
+ For controlling iOS apps running via PlayCover on macOS
791
+ """
792
+
793
+ def __init__(
794
+ self,
795
+ address: str,
796
+ uuid: str,
797
+ ):
798
+ """创建 PlayCover 控制器 / Create PlayCover controller
799
+
800
+ Args:
801
+ address: PlayTools 服务地址 (host:port) / PlayTools service endpoint (host:port)
802
+ uuid: 目标应用 bundle identifier / Target app bundle identifier
803
+
804
+ Raises:
805
+ RuntimeError: 如果创建失败
806
+ """
807
+ super().__init__()
808
+ self._set_playcover_api_properties()
809
+
810
+ self._handle = Library.framework().MaaPlayCoverControllerCreate(
811
+ address.encode(),
812
+ uuid.encode(),
813
+ )
814
+
815
+ if not self._handle:
816
+ raise RuntimeError("Failed to create PlayCover controller.")
817
+
818
+ def _set_playcover_api_properties(self):
819
+ Library.framework().MaaPlayCoverControllerCreate.restype = MaaControllerHandle
820
+ Library.framework().MaaPlayCoverControllerCreate.argtypes = [
821
+ ctypes.c_char_p,
822
+ ctypes.c_char_p,
403
823
  ]
404
824
 
405
825
 
406
826
  class DbgController(Controller):
827
+ """调试控制器 / Debug controller"""
407
828
 
408
829
  def __init__(
409
830
  self,
@@ -411,18 +832,26 @@ class DbgController(Controller):
411
832
  write_path: Union[str, Path],
412
833
  dbg_type: int,
413
834
  config: Dict[str, Any] = {},
414
- notification_handler: Optional[NotificationHandler] = None,
415
835
  ):
836
+ """创建调试控制器 / Create debug controller
837
+
838
+ Args:
839
+ read_path: 输入路径, 包含通过 Recording 选项记录的操作 / Input path, includes operations recorded via Recording option
840
+ write_path: 输出路径, 包含执行结果 / Output path, includes execution results
841
+ dbg_type: 控制器模式 / Controller mode
842
+ config: 额外配置 / Extra config
843
+
844
+ Raises:
845
+ RuntimeError: 如果创建失败
846
+ """
416
847
  super().__init__()
417
848
  self._set_dbg_api_properties()
418
849
 
419
- self._notification_handler = notification_handler
420
850
  self._handle = Library.framework().MaaDbgControllerCreate(
421
851
  str(read_path).encode(),
422
852
  str(write_path).encode(),
423
853
  MaaDbgControllerType(dbg_type),
424
854
  json.dumps(config, ensure_ascii=False).encode(),
425
- *NotificationHandler._gen_c_param(self._notification_handler)
426
855
  )
427
856
 
428
857
  if not self._handle:
@@ -435,27 +864,77 @@ class DbgController(Controller):
435
864
  ctypes.c_char_p,
436
865
  MaaDbgControllerType,
437
866
  ctypes.c_char_p,
438
- MaaNotificationCallback,
439
- ctypes.c_void_p,
440
867
  ]
441
868
 
442
869
 
443
- class CustomController(Controller):
870
+ class GamepadController(Controller):
871
+ """虚拟手柄控制器 (仅 Windows) / Virtual gamepad controller (Windows only)
444
872
 
445
- _callbacks: MaaCustomControllerCallbacks
873
+ 通过 ViGEm 模拟 Xbox 360 或 DualShock 4 手柄,用于控制需要手柄输入的游戏。
874
+ Emulates Xbox 360 or DualShock 4 gamepad via ViGEm for controlling games that require gamepad input.
875
+
876
+ 需要安装 ViGEm Bus Driver: https://github.com/ViGEm/ViGEmBus/releases
877
+ Requires ViGEm Bus Driver: https://github.com/ViGEm/ViGEmBus/releases
878
+
879
+ 手柄操作映射:
880
+ - click_key/key_down/key_up: 数字按键 (使用 MaaGamepadButtonEnum)
881
+ - touch_down/touch_move/touch_up: 摇杆和扳机 (contact 使用 MaaGamepadContactEnum)
882
+ - contact 0: 左摇杆 (x, y: -32768~32767)
883
+ - contact 1: 右摇杆 (x, y: -32768~32767)
884
+ - contact 2: 左扳机 (pressure: 0~255)
885
+ - contact 3: 右扳机 (pressure: 0~255)
886
+ """
446
887
 
447
888
  def __init__(
448
889
  self,
449
- notification_handler: Optional[NotificationHandler] = None,
890
+ hWnd: Union[ctypes.c_void_p, int, None],
891
+ gamepad_type: int = MaaGamepadTypeEnum.Xbox360,
892
+ screencap_method: int = MaaWin32ScreencapMethodEnum.FramePool,
450
893
  ):
894
+ """创建虚拟手柄控制器 / Create virtual gamepad controller
895
+
896
+ Args:
897
+ hWnd: 窗口句柄,用于截图 (可为 None,不需要截图时) / Window handle for screencap (can be None if screencap not needed)
898
+ gamepad_type: 手柄类型 (MaaGamepadTypeEnum.Xbox360 或 MaaGamepadTypeEnum.DualShock4) / Gamepad type
899
+ screencap_method: 截图方式 (当 hWnd 不为 None 时使用) / Screencap method (used when hWnd is not None)
900
+
901
+ Raises:
902
+ RuntimeError: 如果创建失败
903
+ """
451
904
  super().__init__()
452
- self._set_custom_api_properties()
905
+ self._set_gamepad_api_properties()
906
+
907
+ self._handle = Library.framework().MaaGamepadControllerCreate(
908
+ hWnd,
909
+ MaaGamepadType(gamepad_type),
910
+ MaaWin32ScreencapMethod(screencap_method),
911
+ )
912
+
913
+ if not self._handle:
914
+ raise RuntimeError("Failed to create Gamepad controller.")
915
+
916
+ def _set_gamepad_api_properties(self):
917
+ Library.framework().MaaGamepadControllerCreate.restype = MaaControllerHandle
918
+ Library.framework().MaaGamepadControllerCreate.argtypes = [
919
+ ctypes.c_void_p,
920
+ MaaGamepadType,
921
+ MaaWin32ScreencapMethod,
922
+ ]
923
+
924
+
925
+ class CustomController(Controller):
926
+
927
+ _callbacks: MaaCustomControllerCallbacks
453
928
 
454
- self._notification_handler = notification_handler
929
+ def __init__(self):
930
+ super().__init__()
931
+ self._set_custom_api_properties()
455
932
 
456
933
  self._callbacks = MaaCustomControllerCallbacks(
457
934
  CustomController._c_connect_agent,
935
+ CustomController._c_connected_agent,
458
936
  CustomController._c_request_uuid_agent,
937
+ CustomController._c_get_features_agent,
459
938
  CustomController._c_start_app_agent,
460
939
  CustomController._c_stop_app_agent,
461
940
  CustomController._c_screencap_agent,
@@ -468,12 +947,12 @@ class CustomController(Controller):
468
947
  CustomController._c_input_text_agent,
469
948
  CustomController._c_key_down_agent,
470
949
  CustomController._c_key_up_agent,
950
+ CustomController._c_scroll_agent,
471
951
  )
472
952
 
473
953
  self._handle = Library.framework().MaaCustomControllerCreate(
474
954
  self.c_handle,
475
955
  self.c_arg,
476
- *NotificationHandler._gen_c_param(self._notification_handler)
477
956
  )
478
957
 
479
958
  if not self._handle:
@@ -491,10 +970,20 @@ class CustomController(Controller):
491
970
  def connect(self) -> bool:
492
971
  raise NotImplementedError
493
972
 
973
+ def connected(self) -> bool:
974
+ """检查是否已连接(可选实现,默认返回 True)"""
975
+ return True
976
+
494
977
  @abstractmethod
495
978
  def request_uuid(self) -> str:
496
979
  raise NotImplementedError
497
980
 
981
+ def get_features(self) -> int:
982
+ return (
983
+ MaaControllerFeatureEnum.UseMouseDownAndUpInsteadOfClick
984
+ | MaaControllerFeatureEnum.UseKeyboardDownAndUpInsteadOfClick
985
+ )
986
+
498
987
  @abstractmethod
499
988
  def start_app(self, intent: str) -> bool:
500
989
  raise NotImplementedError
@@ -555,6 +1044,10 @@ class CustomController(Controller):
555
1044
  def key_up(self, keycode: int) -> bool:
556
1045
  raise NotImplementedError
557
1046
 
1047
+ @abstractmethod
1048
+ def scroll(self, dx: int, dy: int) -> bool:
1049
+ raise NotImplementedError
1050
+
558
1051
  @staticmethod
559
1052
  @MaaCustomControllerCallbacks.ConnectFunc
560
1053
  def _c_connect_agent(
@@ -570,6 +1063,21 @@ class CustomController(Controller):
570
1063
 
571
1064
  return int(self.connect())
572
1065
 
1066
+ @staticmethod
1067
+ @MaaCustomControllerCallbacks.ConnectedFunc
1068
+ def _c_connected_agent(
1069
+ trans_arg: ctypes.c_void_p,
1070
+ ) -> int:
1071
+ if not trans_arg:
1072
+ return int(False)
1073
+
1074
+ self: CustomController = ctypes.cast(
1075
+ trans_arg,
1076
+ ctypes.py_object,
1077
+ ).value
1078
+
1079
+ return int(self.connected())
1080
+
573
1081
  @staticmethod
574
1082
  @MaaCustomControllerCallbacks.RequestUuidFunc
575
1083
  def _c_request_uuid_agent(
@@ -590,6 +1098,19 @@ class CustomController(Controller):
590
1098
  uuid_buffer.set(uuid)
591
1099
  return int(True)
592
1100
 
1101
+ @staticmethod
1102
+ @MaaCustomControllerCallbacks.GetFeaturesFunc
1103
+ def _c_get_features_agent(trans_arg: ctypes.c_void_p) -> int:
1104
+ if not trans_arg:
1105
+ return int(MaaControllerFeatureEnum.Null)
1106
+
1107
+ self: CustomController = ctypes.cast(
1108
+ trans_arg,
1109
+ ctypes.py_object,
1110
+ ).value
1111
+
1112
+ return int(self.get_features())
1113
+
593
1114
  @staticmethod
594
1115
  @MaaCustomControllerCallbacks.StartAppFunc
595
1116
  def _c_start_app_agent(
@@ -800,11 +1321,65 @@ class CustomController(Controller):
800
1321
 
801
1322
  return int(self.input_text(c_text.decode()))
802
1323
 
1324
+ @staticmethod
1325
+ @MaaCustomControllerCallbacks.ScrollFunc
1326
+ def _c_scroll_agent(
1327
+ c_dx: ctypes.c_int32,
1328
+ c_dy: ctypes.c_int32,
1329
+ trans_arg: ctypes.c_void_p,
1330
+ ) -> int:
1331
+ if not trans_arg:
1332
+ return int(False)
1333
+
1334
+ self: CustomController = ctypes.cast(
1335
+ trans_arg,
1336
+ ctypes.py_object,
1337
+ ).value
1338
+
1339
+ return int(self.scroll(int(c_dx), int(c_dy)))
1340
+
803
1341
  def _set_custom_api_properties(self):
804
1342
  Library.framework().MaaCustomControllerCreate.restype = MaaControllerHandle
805
1343
  Library.framework().MaaCustomControllerCreate.argtypes = [
806
1344
  ctypes.POINTER(MaaCustomControllerCallbacks),
807
1345
  ctypes.c_void_p,
808
- MaaNotificationCallback,
809
- ctypes.c_void_p,
810
1346
  ]
1347
+
1348
+
1349
+ class ControllerEventSink(EventSink):
1350
+
1351
+ @dataclass
1352
+ class ControllerActionDetail:
1353
+ ctrl_id: int
1354
+ uuid: str
1355
+ action: str
1356
+ param: dict
1357
+
1358
+ def on_controller_action(
1359
+ self,
1360
+ controller: Controller,
1361
+ noti_type: NotificationType,
1362
+ detail: ControllerActionDetail,
1363
+ ):
1364
+ pass
1365
+
1366
+ def on_raw_notification(self, controller: Controller, msg: str, details: dict):
1367
+ pass
1368
+
1369
+ def _on_raw_notification(self, handle: ctypes.c_void_p, msg: str, details: dict):
1370
+
1371
+ controller = Controller(handle=handle)
1372
+ self.on_raw_notification(controller, msg, details)
1373
+
1374
+ noti_type = EventSink._notification_type(msg)
1375
+ if msg.startswith("Controller.Action"):
1376
+ detail = self.ControllerActionDetail(
1377
+ ctrl_id=details["ctrl_id"],
1378
+ uuid=details["uuid"],
1379
+ action=details["action"],
1380
+ param=details["param"],
1381
+ )
1382
+ self.on_controller_action(controller, noti_type, detail)
1383
+
1384
+ else:
1385
+ self.on_unknown_notification(controller, msg, details)