MaaFw 4.4.0b1__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,36 +44,145 @@ 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
 
58
107
  def post_press_key(self, key: int) -> Job:
59
- ctrl_id = Library.framework().MaaControllerPostPressKey(self._handle, key)
108
+ """
109
+ Deprecated: Use post_click_key instead.
110
+ """
111
+ return self.post_click_key(key)
112
+
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
+ """
122
+ ctrl_id = Library.framework().MaaControllerPostClickKey(self._handle, key)
123
+ return self._gen_ctrl_job(ctrl_id)
124
+
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
+ """
134
+ ctrl_id = Library.framework().MaaControllerPostKeyDown(self._handle, key)
135
+ return self._gen_ctrl_job(ctrl_id)
136
+
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
+ """
146
+ ctrl_id = Library.framework().MaaControllerPostKeyUp(self._handle, key)
60
147
  return self._gen_ctrl_job(ctrl_id)
61
148
 
62
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
+ """
63
158
  ctrl_id = Library.framework().MaaControllerPostInputText(
64
159
  self._handle, text.encode()
65
160
  )
66
161
  return self._gen_ctrl_job(ctrl_id)
67
162
 
68
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
+ """
69
172
  ctrl_id = Library.framework().MaaControllerPostStartApp(
70
173
  self._handle, intent.encode()
71
174
  )
72
175
  return self._gen_ctrl_job(ctrl_id)
73
176
 
74
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
+ """
75
186
  ctrl_id = Library.framework().MaaControllerPostStopApp(
76
187
  self._handle, intent.encode()
77
188
  )
@@ -80,6 +191,17 @@ class Controller:
80
191
  def post_touch_down(
81
192
  self, x: int, y: int, contact: int = 0, pressure: int = 1
82
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
+ """
83
205
  ctrl_id = Library.framework().MaaControllerPostTouchDown(
84
206
  self._handle, contact, x, y, pressure
85
207
  )
@@ -88,16 +210,57 @@ class Controller:
88
210
  def post_touch_move(
89
211
  self, x: int, y: int, contact: int = 0, pressure: int = 1
90
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
+ """
91
224
  ctrl_id = Library.framework().MaaControllerPostTouchMove(
92
225
  self._handle, contact, x, y, pressure
93
226
  )
94
227
  return self._gen_ctrl_job(ctrl_id)
95
228
 
96
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
+ """
97
238
  ctrl_id = Library.framework().MaaControllerPostTouchUp(self._handle, contact)
98
239
  return self._gen_ctrl_job(ctrl_id)
99
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
+
100
258
  def post_screencap(self) -> JobWithResult:
259
+ """截图 / Screenshot
260
+
261
+ Returns:
262
+ JobWithResult: 作业对象,可通过 result 获取截图 / Job object, can get screenshot via result
263
+ """
101
264
  ctrl_id = Library.framework().MaaControllerPostScreencap(self._handle)
102
265
  return JobWithResult(
103
266
  ctrl_id,
@@ -108,6 +271,22 @@ class Controller:
108
271
 
109
272
  @property
110
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
+ """
111
290
  image_buffer = ImageBuffer()
112
291
  if not Library.framework().MaaControllerCachedImage(
113
292
  self._handle, image_buffer._handle
@@ -115,18 +294,101 @@ class Controller:
115
294
  raise RuntimeError("Failed to get cached image.")
116
295
  return image_buffer.get()
117
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
+
118
334
  @property
119
335
  def connected(self) -> bool:
336
+ """判断是否已连接 / Check if connected
337
+
338
+ Returns:
339
+ bool: 是否已连接 / Whether connected
340
+ """
120
341
  return bool(Library.framework().MaaControllerConnected(self._handle))
121
342
 
122
343
  @property
123
344
  def uuid(self) -> str:
345
+ """获取设备 uuid / Get device uuid
346
+
347
+ Returns:
348
+ str: 设备 uuid / Device uuid
349
+
350
+ Raises:
351
+ RuntimeError: 如果获取失败
352
+ """
124
353
  buffer = StringBuffer()
125
354
  if not Library.framework().MaaControllerGetUuid(self._handle, buffer._handle):
126
355
  raise RuntimeError("Failed to get UUID.")
127
356
  return buffer.get()
128
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
+
129
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
+ """
130
392
  cint = ctypes.c_int32(long_side)
131
393
  return bool(
132
394
  Library.framework().MaaControllerSetOption(
@@ -138,6 +400,14 @@ class Controller:
138
400
  )
139
401
 
140
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
+ """
141
411
  cint = ctypes.c_int32(short_side)
142
412
  return bool(
143
413
  Library.framework().MaaControllerSetOption(
@@ -149,6 +419,17 @@ class Controller:
149
419
  )
150
420
 
151
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
+ """
152
433
  cbool = MaaBool(enable)
153
434
  return bool(
154
435
  Library.framework().MaaControllerSetOption(
@@ -159,6 +440,41 @@ class Controller:
159
440
  )
160
441
  )
161
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
+
162
478
  ### private ###
163
479
 
164
480
  def _status(self, maaid: int) -> MaaStatus:
@@ -170,6 +486,9 @@ class Controller:
170
486
  def _get_screencap(self, _: int) -> numpy.ndarray:
171
487
  return self.cached_image
172
488
 
489
+ def _get_shell_output(self, _: int) -> str:
490
+ return self.shell_output
491
+
173
492
  def _gen_ctrl_job(self, ctrlid: MaaCtrlId) -> Job:
174
493
  return Job(
175
494
  ctrlid,
@@ -206,6 +525,15 @@ class Controller:
206
525
  c_int32,
207
526
  ]
208
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
+
209
537
  Library.framework().MaaControllerPostSwipe.restype = MaaCtrlId
210
538
  Library.framework().MaaControllerPostSwipe.argtypes = [
211
539
  MaaControllerHandle,
@@ -216,12 +544,34 @@ class Controller:
216
544
  c_int32,
217
545
  ]
218
546
 
219
- Library.framework().MaaControllerPostPressKey.restype = MaaCtrlId
220
- Library.framework().MaaControllerPostPressKey.argtypes = [
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
+
559
+ Library.framework().MaaControllerPostClickKey.restype = MaaCtrlId
560
+ Library.framework().MaaControllerPostClickKey.argtypes = [
221
561
  MaaControllerHandle,
222
562
  c_int32,
223
563
  ]
224
564
 
565
+ Library.framework().MaaControllerPostKeyDown.restype = MaaCtrlId
566
+ Library.framework().MaaControllerPostKeyDown.argtypes = [
567
+ MaaControllerHandle,
568
+ c_int32,
569
+ ]
570
+ Library.framework().MaaControllerPostKeyUp.restype = MaaCtrlId
571
+ Library.framework().MaaControllerPostKeyUp.argtypes = [
572
+ MaaControllerHandle,
573
+ c_int32,
574
+ ]
225
575
  Library.framework().MaaControllerPostInputText.restype = MaaCtrlId
226
576
  Library.framework().MaaControllerPostInputText.argtypes = [
227
577
  MaaControllerHandle,
@@ -268,6 +618,14 @@ class Controller:
268
618
  MaaControllerHandle,
269
619
  c_int32,
270
620
  ]
621
+
622
+ Library.framework().MaaControllerPostScroll.restype = MaaCtrlId
623
+ Library.framework().MaaControllerPostScroll.argtypes = [
624
+ MaaControllerHandle,
625
+ c_int32,
626
+ c_int32,
627
+ ]
628
+
271
629
  Library.framework().MaaControllerStatus.restype = MaaStatus
272
630
  Library.framework().MaaControllerStatus.argtypes = [
273
631
  MaaControllerHandle,
@@ -295,8 +653,37 @@ class Controller:
295
653
  MaaStringBufferHandle,
296
654
  ]
297
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
+
298
679
 
299
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
+
300
687
  AGENT_BINARY_PATH = os.path.join(
301
688
  os.path.dirname(__file__),
302
689
  "../MaaAgentBinary",
@@ -310,13 +697,23 @@ class AdbController(Controller):
310
697
  input_methods: int = MaaAdbInputMethodEnum.Default,
311
698
  config: Dict[str, Any] = {},
312
699
  agent_path: Union[str, Path] = AGENT_BINARY_PATH,
313
- notification_handler: Optional[NotificationHandler] = None,
314
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
+ """
315
714
  super().__init__()
316
715
  self._set_adb_api_properties()
317
716
 
318
- self._notification_handler = notification_handler
319
-
320
717
  self._handle = Library.framework().MaaAdbControllerCreate(
321
718
  str(adb_path).encode(),
322
719
  address.encode(),
@@ -324,7 +721,6 @@ class AdbController(Controller):
324
721
  MaaAdbInputMethod(input_methods),
325
722
  json.dumps(config, ensure_ascii=False).encode(),
326
723
  str(agent_path).encode(),
327
- *NotificationHandler._gen_c_param(self._notification_handler)
328
724
  )
329
725
 
330
726
  if not self._handle:
@@ -340,29 +736,38 @@ class AdbController(Controller):
340
736
  MaaAdbInputMethod,
341
737
  ctypes.c_char_p,
342
738
  ctypes.c_char_p,
343
- MaaNotificationCallback,
344
- ctypes.c_void_p,
345
739
  ]
346
740
 
347
741
 
348
742
  class Win32Controller(Controller):
743
+ """Win32 控制器 / Win32 controller"""
349
744
 
350
745
  def __init__(
351
746
  self,
352
747
  hWnd: Union[ctypes.c_void_p, int, None],
353
- screencap_method: int = MaaWin32ScreencapMethodEnum.DXGI_DesktopDup,
354
- input_method: int = MaaWin32InputMethodEnum.Seize,
355
- notification_handler: Optional[NotificationHandler] = None,
748
+ screencap_method: int = MaaWin32ScreencapMethodEnum.FramePool,
749
+ mouse_method: int = MaaWin32InputMethodEnum.Seize,
750
+ keyboard_method: int = MaaWin32InputMethodEnum.Seize,
356
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
+ """
357
763
  super().__init__()
358
764
  self._set_win32_api_properties()
359
765
 
360
- self._notification_handler = notification_handler
361
766
  self._handle = Library.framework().MaaWin32ControllerCreate(
362
767
  hWnd,
363
768
  MaaWin32ScreencapMethod(screencap_method),
364
- MaaWin32InputMethod(input_method),
365
- *NotificationHandler._gen_c_param(self._notification_handler)
769
+ MaaWin32InputMethod(mouse_method),
770
+ MaaWin32InputMethod(keyboard_method),
366
771
  )
367
772
 
368
773
  if not self._handle:
@@ -374,12 +779,52 @@ class Win32Controller(Controller):
374
779
  ctypes.c_void_p,
375
780
  MaaWin32ScreencapMethod,
376
781
  MaaWin32InputMethod,
377
- MaaNotificationCallback,
378
- 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,
379
823
  ]
380
824
 
381
825
 
382
826
  class DbgController(Controller):
827
+ """调试控制器 / Debug controller"""
383
828
 
384
829
  def __init__(
385
830
  self,
@@ -387,18 +832,26 @@ class DbgController(Controller):
387
832
  write_path: Union[str, Path],
388
833
  dbg_type: int,
389
834
  config: Dict[str, Any] = {},
390
- notification_handler: Optional[NotificationHandler] = None,
391
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
+ """
392
847
  super().__init__()
393
848
  self._set_dbg_api_properties()
394
849
 
395
- self._notification_handler = notification_handler
396
850
  self._handle = Library.framework().MaaDbgControllerCreate(
397
851
  str(read_path).encode(),
398
852
  str(write_path).encode(),
399
853
  MaaDbgControllerType(dbg_type),
400
854
  json.dumps(config, ensure_ascii=False).encode(),
401
- *NotificationHandler._gen_c_param(self._notification_handler)
402
855
  )
403
856
 
404
857
  if not self._handle:
@@ -411,27 +864,77 @@ class DbgController(Controller):
411
864
  ctypes.c_char_p,
412
865
  MaaDbgControllerType,
413
866
  ctypes.c_char_p,
414
- MaaNotificationCallback,
415
- ctypes.c_void_p,
416
867
  ]
417
868
 
418
869
 
419
- class CustomController(Controller):
870
+ class GamepadController(Controller):
871
+ """虚拟手柄控制器 (仅 Windows) / Virtual gamepad controller (Windows only)
420
872
 
421
- _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
+ """
422
887
 
423
888
  def __init__(
424
889
  self,
425
- 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,
426
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
+ """
427
904
  super().__init__()
428
- 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
+ )
429
912
 
430
- self._notification_handler = notification_handler
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
928
+
929
+ def __init__(self):
930
+ super().__init__()
931
+ self._set_custom_api_properties()
431
932
 
432
933
  self._callbacks = MaaCustomControllerCallbacks(
433
934
  CustomController._c_connect_agent,
935
+ CustomController._c_connected_agent,
434
936
  CustomController._c_request_uuid_agent,
937
+ CustomController._c_get_features_agent,
435
938
  CustomController._c_start_app_agent,
436
939
  CustomController._c_stop_app_agent,
437
940
  CustomController._c_screencap_agent,
@@ -440,14 +943,16 @@ class CustomController(Controller):
440
943
  CustomController._c_touch_down_agent,
441
944
  CustomController._c_touch_move_agent,
442
945
  CustomController._c_touch_up_agent,
443
- CustomController._c_press_key_agent,
946
+ CustomController._c_click_key_agent,
444
947
  CustomController._c_input_text_agent,
948
+ CustomController._c_key_down_agent,
949
+ CustomController._c_key_up_agent,
950
+ CustomController._c_scroll_agent,
445
951
  )
446
952
 
447
953
  self._handle = Library.framework().MaaCustomControllerCreate(
448
954
  self.c_handle,
449
955
  self.c_arg,
450
- *NotificationHandler._gen_c_param(self._notification_handler)
451
956
  )
452
957
 
453
958
  if not self._handle:
@@ -465,10 +970,20 @@ class CustomController(Controller):
465
970
  def connect(self) -> bool:
466
971
  raise NotImplementedError
467
972
 
973
+ def connected(self) -> bool:
974
+ """检查是否已连接(可选实现,默认返回 True)"""
975
+ return True
976
+
468
977
  @abstractmethod
469
978
  def request_uuid(self) -> str:
470
979
  raise NotImplementedError
471
980
 
981
+ def get_features(self) -> int:
982
+ return (
983
+ MaaControllerFeatureEnum.UseMouseDownAndUpInsteadOfClick
984
+ | MaaControllerFeatureEnum.UseKeyboardDownAndUpInsteadOfClick
985
+ )
986
+
472
987
  @abstractmethod
473
988
  def start_app(self, intent: str) -> bool:
474
989
  raise NotImplementedError
@@ -514,13 +1029,25 @@ class CustomController(Controller):
514
1029
  raise NotImplementedError
515
1030
 
516
1031
  @abstractmethod
517
- def press_key(self, keycode: int) -> bool:
1032
+ def click_key(self, keycode: int) -> bool:
518
1033
  raise NotImplementedError
519
1034
 
520
1035
  @abstractmethod
521
1036
  def input_text(self, text: str) -> bool:
522
1037
  raise NotImplementedError
523
1038
 
1039
+ @abstractmethod
1040
+ def key_down(self, keycode: int) -> bool:
1041
+ raise NotImplementedError
1042
+
1043
+ @abstractmethod
1044
+ def key_up(self, keycode: int) -> bool:
1045
+ raise NotImplementedError
1046
+
1047
+ @abstractmethod
1048
+ def scroll(self, dx: int, dy: int) -> bool:
1049
+ raise NotImplementedError
1050
+
524
1051
  @staticmethod
525
1052
  @MaaCustomControllerCallbacks.ConnectFunc
526
1053
  def _c_connect_agent(
@@ -536,6 +1063,21 @@ class CustomController(Controller):
536
1063
 
537
1064
  return int(self.connect())
538
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
+
539
1081
  @staticmethod
540
1082
  @MaaCustomControllerCallbacks.RequestUuidFunc
541
1083
  def _c_request_uuid_agent(
@@ -556,6 +1098,19 @@ class CustomController(Controller):
556
1098
  uuid_buffer.set(uuid)
557
1099
  return int(True)
558
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
+
559
1114
  @staticmethod
560
1115
  @MaaCustomControllerCallbacks.StartAppFunc
561
1116
  def _c_start_app_agent(
@@ -703,8 +1258,8 @@ class CustomController(Controller):
703
1258
  return int(self.touch_up(int(c_contact)))
704
1259
 
705
1260
  @staticmethod
706
- @MaaCustomControllerCallbacks.PressKeyFunc
707
- def _c_press_key_agent(
1261
+ @MaaCustomControllerCallbacks.ClickKeyFunc
1262
+ def _c_click_key_agent(
708
1263
  c_keycode: ctypes.c_int32,
709
1264
  trans_arg: ctypes.c_void_p,
710
1265
  ) -> int:
@@ -716,7 +1271,39 @@ class CustomController(Controller):
716
1271
  ctypes.py_object,
717
1272
  ).value
718
1273
 
719
- return int(self.press_key(int(c_keycode)))
1274
+ return int(self.click_key(int(c_keycode)))
1275
+
1276
+ @staticmethod
1277
+ @MaaCustomControllerCallbacks.KeyDownFunc
1278
+ def _c_key_down_agent(
1279
+ c_keycode: ctypes.c_int32,
1280
+ trans_arg: ctypes.c_void_p,
1281
+ ) -> int:
1282
+ if not trans_arg:
1283
+ return int(False)
1284
+
1285
+ self: CustomController = ctypes.cast(
1286
+ trans_arg,
1287
+ ctypes.py_object,
1288
+ ).value
1289
+
1290
+ return int(self.key_down(int(c_keycode)))
1291
+
1292
+ @staticmethod
1293
+ @MaaCustomControllerCallbacks.KeyUpFunc
1294
+ def _c_key_up_agent(
1295
+ c_keycode: ctypes.c_int32,
1296
+ trans_arg: ctypes.c_void_p,
1297
+ ) -> int:
1298
+ if not trans_arg:
1299
+ return int(False)
1300
+
1301
+ self: CustomController = ctypes.cast(
1302
+ trans_arg,
1303
+ ctypes.py_object,
1304
+ ).value
1305
+
1306
+ return int(self.key_up(int(c_keycode)))
720
1307
 
721
1308
  @staticmethod
722
1309
  @MaaCustomControllerCallbacks.InputTextFunc
@@ -734,11 +1321,65 @@ class CustomController(Controller):
734
1321
 
735
1322
  return int(self.input_text(c_text.decode()))
736
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
+
737
1341
  def _set_custom_api_properties(self):
738
1342
  Library.framework().MaaCustomControllerCreate.restype = MaaControllerHandle
739
1343
  Library.framework().MaaCustomControllerCreate.argtypes = [
740
1344
  ctypes.POINTER(MaaCustomControllerCallbacks),
741
1345
  ctypes.c_void_p,
742
- MaaNotificationCallback,
743
- ctypes.c_void_p,
744
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)