kotonebot 0.2.0__tar.gz → 0.3.0__tar.gz

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 (76) hide show
  1. {kotonebot-0.2.0/kotonebot.egg-info → kotonebot-0.3.0}/PKG-INFO +1 -1
  2. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/ocr.py +20 -2
  3. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/mumu12_host.py +157 -44
  4. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +5 -0
  5. {kotonebot-0.2.0 → kotonebot-0.3.0/kotonebot.egg-info}/PKG-INFO +1 -1
  6. {kotonebot-0.2.0 → kotonebot-0.3.0}/pyproject.toml +1 -1
  7. {kotonebot-0.2.0 → kotonebot-0.3.0}/LICENSE +0 -0
  8. {kotonebot-0.2.0 → kotonebot-0.3.0}/MANIFEST.in +0 -0
  9. {kotonebot-0.2.0 → kotonebot-0.3.0}/README.md +0 -0
  10. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/__init__.py +0 -0
  11. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/__init__.py +0 -0
  12. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/bot.py +0 -0
  13. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/color.py +0 -0
  14. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/context/__init__.py +0 -0
  15. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/context/context.py +0 -0
  16. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/context/task_action.py +0 -0
  17. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/core.py +0 -0
  18. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/__init__.py +0 -0
  19. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/entry.py +0 -0
  20. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/mock.py +0 -0
  21. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/server.py +0 -0
  22. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/vars.py +0 -0
  23. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/dispatch.py +0 -0
  24. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/flow_controller.py +0 -0
  25. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/image.py +0 -0
  26. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/loop.py +0 -0
  27. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/preprocessor.py +0 -0
  28. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/__init__.py +0 -0
  29. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/device.py +0 -0
  30. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/fast_screenshot.py +0 -0
  31. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/__init__.py +0 -0
  32. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/adb_common.py +0 -0
  33. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/custom.py +0 -0
  34. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/leidian_host.py +0 -0
  35. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/protocol.py +0 -0
  36. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/windows_common.py +0 -0
  37. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/__init__.py +0 -0
  38. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/adb.py +0 -0
  39. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/adb_raw.py +0 -0
  40. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/__init__.py +0 -0
  41. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/nemu_ipc.py +0 -0
  42. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/remote_windows.py +0 -0
  43. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/uiautomator2.py +0 -0
  44. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/windows.py +0 -0
  45. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/protocol.py +0 -0
  46. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/registration.py +0 -0
  47. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/config/__init__.py +0 -0
  48. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/config/base_config.py +0 -0
  49. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/config/manager.py +0 -0
  50. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/errors.py +0 -0
  51. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/__init__.py +0 -0
  52. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/message_box.py +0 -0
  53. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/reg.py +0 -0
  54. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/shortcut.py +0 -0
  55. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/task_dialog.py +0 -0
  56. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/logging/__init__.py +0 -0
  57. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/logging/log.py +0 -0
  58. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/primitives/__init__.py +0 -0
  59. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/primitives/geometry.py +0 -0
  60. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/primitives/visual.py +0 -0
  61. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/tools/__init__.py +0 -0
  62. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/tools/mirror.py +0 -0
  63. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/__init__.py +0 -0
  64. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/file_host/sensio.py +0 -0
  65. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/file_host/tmp_send.py +0 -0
  66. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/__init__.py +0 -0
  67. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/image_host.py +0 -0
  68. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/protocol.py +0 -0
  69. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/wxpusher.py +0 -0
  70. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/user.py +0 -0
  71. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/util.py +0 -0
  72. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/SOURCES.txt +0 -0
  73. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/dependency_links.txt +0 -0
  74. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/requires.txt +0 -0
  75. {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/top_level.txt +0 -0
  76. {kotonebot-0.2.0 → kotonebot-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kotonebot
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Kotonebot is game/app automation library based on computer vision technology, works for Windows and Android.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -24,6 +24,25 @@ logger = logging.getLogger(__name__)
24
24
  StringMatchFunction = Callable[[str], bool]
25
25
  REGEX_NUMBERS = re.compile(r'\d+')
26
26
 
27
+ global_character_mapping: dict[str, str] = {
28
+ 'ó': '6',
29
+ 'ą': 'a',
30
+ }
31
+ """
32
+ 全局字符映射表。某些字符可能在某些情况下被错误地识别,此时可以在这里添加映射。
33
+ """
34
+
35
+ def sanitize_text(text: str) -> str:
36
+ """
37
+ 对识别结果进行清理。此函数将被所有 OCR 引擎调用。
38
+
39
+ 默认使用 `global_character_mapping` 中的映射数据进行清理。
40
+ 可以重写此函数以实现自定义的清理逻辑。
41
+ """
42
+ for k, v in global_character_mapping.items():
43
+ text = text.replace(k, v)
44
+ return text
45
+
27
46
  @dataclass
28
47
  class OcrResult:
29
48
  text: str
@@ -330,8 +349,7 @@ class Ocr:
330
349
  return OcrResultList()
331
350
  ret = []
332
351
  for r in result:
333
- # HACK: 识别结果中包含奇怪的符号,暂时替换掉
334
- text = unicodedata.normalize('NFKC', r[1]).replace('ą', 'a')
352
+ text = sanitize_text(r[1])
335
353
  # r[0] = [左上, 右上, 右下, 左下]
336
354
  # 这里有个坑,返回的点不一定是矩形,只能保证是四边形
337
355
  # 所以这里需要计算出四个点的外接矩形
@@ -3,7 +3,7 @@ import os
3
3
  import json
4
4
  import subprocess
5
5
  from functools import lru_cache
6
- from typing import Any, Literal, overload
6
+ from typing import Any, Literal, overload, TYPE_CHECKING
7
7
  from typing_extensions import override
8
8
 
9
9
  from kotonebot import logging
@@ -22,6 +22,10 @@ else:
22
22
  """Stub for read_reg on non-Windows platforms."""
23
23
  return default
24
24
 
25
+ # Forward declarations for type hints
26
+ if TYPE_CHECKING:
27
+ from typing import Type
28
+
25
29
  logger = logging.getLogger(__name__)
26
30
  MuMu12Recipes = AdbRecipes | Literal['nemu_ipc']
27
31
 
@@ -35,24 +39,137 @@ class MuMu12HostConfig(AdbHostConfig):
35
39
  app_index: int = 0
36
40
  """多开应用索引,传给 get_display_id 方法。"""
37
41
 
42
+ class Mumu12Host(HostProtocol[MuMu12Recipes]):
43
+ InstanceClass: 'Type[Mumu12Instance]'
44
+
45
+ @staticmethod
46
+ @lru_cache(maxsize=1)
47
+ def _read_install_path() -> str | None:
48
+ r"""
49
+ 从注册表中读取 MuMu Player 12 的安装路径。
50
+
51
+ 返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
52
+
53
+ :return: 若找到,则返回安装路径;否则返回 None。
54
+ """
55
+ if os.name != 'nt':
56
+ return None
57
+
58
+ uninstall_subkeys = [
59
+ r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer-12.0',
60
+ # TODO: 支持国际版 MuMu
61
+ # r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayerGlobal-12.0'
62
+ ]
63
+
64
+ for subkey in uninstall_subkeys:
65
+ icon_path = read_reg('HKLM', subkey, 'DisplayIcon', default=None)
66
+ if icon_path and isinstance(icon_path, str):
67
+ icon_path = icon_path.replace('"', '')
68
+ path = os.path.dirname(icon_path)
69
+ logger.debug('MuMu Player 12 installation path: %s', path)
70
+ # 返回根目录(去掉 shell 子目录)
71
+ if os.path.basename(path).lower() == 'shell':
72
+ path = os.path.dirname(path)
73
+ return path
74
+ return None
75
+
76
+ @classmethod
77
+ def _invoke_manager(cls,args: list[str]) -> str:
78
+ """
79
+ 调用 MuMuManager.exe。
80
+
81
+ :param args: 命令行参数列表。
82
+ :return: 命令执行的输出。
83
+ """
84
+ install_path = cls._read_install_path()
85
+ if install_path is None:
86
+ raise RuntimeError('MuMu Player 12 is not installed.')
87
+ manager_path = os.path.join(install_path, 'shell', 'MuMuManager.exe')
88
+ logger.debug('MuMuManager execute: %s', repr(args))
89
+ output = subprocess.run(
90
+ [manager_path] + args,
91
+ capture_output=True,
92
+ text=True,
93
+ encoding='utf-8',
94
+ # https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
95
+ creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
96
+ )
97
+ if output.returncode != 0:
98
+ # raise RuntimeError(f'Failed to invoke MuMuManager: {output.stderr}')
99
+ logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
100
+ return output.stdout
101
+
102
+ @staticmethod
103
+ def installed() -> bool:
104
+ return Mumu12Host._read_install_path() is not None
105
+
106
+ @classmethod
107
+ def list(cls) -> list[Instance]:
108
+ nemu_path = cls._read_install_path()
109
+ if nemu_path is None:
110
+ raise RuntimeError("Nemu path not found.")
111
+ output = cls._invoke_manager(['info', '-v', 'all'])
112
+ logger.debug('MuMuManager.exe output: %s', output)
113
+
114
+ try:
115
+ data: dict[str, dict[str, Any]] = json.loads(output)
116
+ if 'name' in data.keys():
117
+ # 这里有个坑:
118
+ # 如果只有一个实例,返回的 JSON 结构是单个对象而不是数组
119
+ data = { '0': data }
120
+ instances = []
121
+ for index, instance_data in data.items():
122
+ instance = cls.InstanceClass(
123
+ id=index,
124
+ name=instance_data['name'],
125
+ adb_port=instance_data.get('adb_port'),
126
+ adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
127
+ adb_name=None,
128
+ )
129
+ instance.nemu_path = nemu_path
130
+ instance.index = int(index)
131
+ instance.is_android_started = instance_data.get('is_android_started', False)
132
+ logger.debug('Mumu12 instance: %s', repr(instance))
133
+ instances.append(instance)
134
+ return instances
135
+ except json.JSONDecodeError as e:
136
+ raise RuntimeError(f'Failed to parse output: {e}')
137
+
138
+ @classmethod
139
+ def query(cls, *, id: str) -> Instance | None:
140
+ instances = cls.list()
141
+ for instance in instances:
142
+ if instance.id == id:
143
+ return instance
144
+ return None
145
+
146
+ @staticmethod
147
+ def recipes() -> 'list[MuMu12Recipes]':
148
+ return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
38
149
 
39
150
  class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
151
+ HostClass: 'type[Mumu12Host]' = Mumu12Host
152
+
40
153
  @copy_type(Instance.__init__)
41
154
  def __init__(self, *args, **kwargs):
155
+ if not hasattr(self.HostClass, 'InstanceClass'):
156
+ raise RuntimeError(f"{self.HostClass.__name__}.InstanceClass not initialized")
157
+
42
158
  super().__init__(*args, **kwargs)
43
159
  self._args = args
44
160
  self.index: int | None = None
45
161
  self.is_android_started: bool = False
162
+ self.nemu_path: str | None = None
46
163
 
47
164
  @override
48
165
  def refresh(self):
49
- ins = Mumu12Host.query(id=self.id)
50
- assert isinstance(ins, Mumu12Instance), f'Expected Mumu12Instance, got {type(ins)}'
51
- if ins is not None:
166
+ ins = self.HostClass.query(id=self.id)
167
+ if ins is not None and isinstance(ins, self.__class__):
52
168
  self.adb_port = ins.adb_port
53
169
  self.adb_ip = ins.adb_ip
54
170
  self.adb_name = ins.adb_name
55
171
  self.is_android_started = ins.is_android_started
172
+ self.nemu_path = ins.nemu_path
56
173
  logger.debug('Refreshed MuMu12 instance: %s', repr(ins))
57
174
 
58
175
  @override
@@ -61,7 +178,7 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
61
178
  logger.warning('Instance is already running.')
62
179
  return
63
180
  logger.info('Starting MuMu12 instance %s', self)
64
- Mumu12Host._invoke_manager(['control', '-v', self.id, 'launch'])
181
+ self.HostClass._invoke_manager(['control', '-v', self.id, 'launch'])
65
182
  self.refresh()
66
183
 
67
184
  @override
@@ -70,7 +187,7 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
70
187
  logger.warning('Instance is not running.')
71
188
  return
72
189
  logger.info('Stopping MuMu12 instance id=%s name=%s...', self.id, self.name)
73
- Mumu12Host._invoke_manager(['control', '-v', self.id, 'shutdown'])
190
+ self.HostClass._invoke_manager(['control', '-v', self.id, 'shutdown'])
74
191
  self.refresh()
75
192
 
76
193
  @override
@@ -100,11 +217,10 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
100
217
 
101
218
  if recipe == 'nemu_ipc' and isinstance(host_config, MuMu12HostConfig):
102
219
  # NemuImpl
103
- nemu_path = Mumu12Host._read_install_path()
104
- if not nemu_path:
105
- raise RuntimeError("无法找到 MuMu12 的安装路径。")
220
+ if self.nemu_path is None:
221
+ raise RuntimeError("Nemu path is not set.")
106
222
  nemu_config = NemuIpcImplConfig(
107
- nemu_folder=nemu_path,
223
+ nemu_folder=self.nemu_path,
108
224
  instance_id=int(self.id),
109
225
  display_id=host_config.display_id,
110
226
  target_package_name=host_config.target_package_name,
@@ -129,12 +245,14 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
129
245
  else:
130
246
  raise ValueError(f'Unknown recipe: {recipe}')
131
247
 
132
- class Mumu12Host(HostProtocol[MuMu12Recipes]):
133
- @staticmethod
248
+ class Mumu12V5Host(Mumu12Host):
249
+ InstanceClass: 'Type[Mumu12V5Instance]'
250
+
251
+ @classmethod
134
252
  @lru_cache(maxsize=1)
135
- def _read_install_path() -> str | None:
253
+ def _read_install_path(cls) -> str | None:
136
254
  r"""
137
- 从注册表中读取 MuMu Player 12 的安装路径。
255
+ 从注册表中读取 MuMu Player 12 v5.x 的安装路径。
138
256
 
139
257
  返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
140
258
 
@@ -144,9 +262,7 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
144
262
  return None
145
263
 
146
264
  uninstall_subkeys = [
147
- r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer-12.0',
148
- # TODO: 支持国际版 MuMu
149
- # r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayerGlobal-12.0'
265
+ r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer',
150
266
  ]
151
267
 
152
268
  for subkey in uninstall_subkeys:
@@ -156,23 +272,23 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
156
272
  path = os.path.dirname(icon_path)
157
273
  logger.debug('MuMu Player 12 installation path: %s', path)
158
274
  # 返回根目录(去掉 shell 子目录)
159
- if os.path.basename(path).lower() == 'shell':
275
+ if os.path.basename(path).lower() == 'nx_main':
160
276
  path = os.path.dirname(path)
161
277
  return path
162
278
  return None
163
279
 
164
- @staticmethod
165
- def _invoke_manager(args: list[str]) -> str:
280
+ @classmethod
281
+ def _invoke_manager(cls, args: list[str]) -> str:
166
282
  """
167
283
  调用 MuMuManager.exe。
168
284
 
169
285
  :param args: 命令行参数列表。
170
286
  :return: 命令执行的输出。
171
287
  """
172
- install_path = Mumu12Host._read_install_path()
288
+ install_path = cls._read_install_path()
173
289
  if install_path is None:
174
- raise RuntimeError('MuMu Player 12 is not installed.')
175
- manager_path = os.path.join(install_path, 'shell', 'MuMuManager.exe')
290
+ raise RuntimeError('MuMu Player 12 v5.x is not installed.')
291
+ manager_path = os.path.join(install_path, 'nx_main', 'MuMuManager.exe')
176
292
  logger.debug('MuMuManager execute: %s', repr(args))
177
293
  output = subprocess.run(
178
294
  [manager_path] + args,
@@ -187,13 +303,13 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
187
303
  logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
188
304
  return output.stdout
189
305
 
190
- @staticmethod
191
- def installed() -> bool:
192
- return Mumu12Host._read_install_path() is not None
306
+ @classmethod
307
+ def installed(cls) -> bool:
308
+ return cls._read_install_path() is not None
193
309
 
194
- @staticmethod
195
- def list() -> list[Instance]:
196
- output = Mumu12Host._invoke_manager(['info', '-v', 'all'])
310
+ @classmethod
311
+ def list(cls) -> list[Instance]:
312
+ output = cls._invoke_manager(['info', '-v', 'all'])
197
313
  logger.debug('MuMuManager.exe output: %s', output)
198
314
 
199
315
  try:
@@ -204,40 +320,37 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
204
320
  data = { '0': data }
205
321
  instances = []
206
322
  for index, instance_data in data.items():
207
- instance = Mumu12Instance(
323
+ instance = cls.InstanceClass(
208
324
  id=index,
209
325
  name=instance_data['name'],
210
326
  adb_port=instance_data.get('adb_port'),
211
327
  adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
212
328
  adb_name=None
213
329
  )
330
+ instance.nemu_path = cls._read_install_path()
214
331
  instance.index = int(index)
215
332
  instance.is_android_started = instance_data.get('is_android_started', False)
216
- logger.debug('Mumu12 instance: %s', repr(instance))
333
+ logger.debug('Mumu12 v5.x instance: %s', repr(instance))
217
334
  instances.append(instance)
218
335
  return instances
219
336
  except json.JSONDecodeError as e:
220
337
  raise RuntimeError(f'Failed to parse output: {e}')
221
-
222
- @staticmethod
223
- def query(*, id: str) -> Instance | None:
224
- instances = Mumu12Host.list()
225
- for instance in instances:
226
- if instance.id == id:
227
- return instance
228
- return None
229
338
 
230
- @staticmethod
231
- def recipes() -> 'list[MuMu12Recipes]':
232
- return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
233
-
339
+ class Mumu12V5Instance(Mumu12Instance):
340
+ HostClass: 'type[Mumu12V5Host]' = Mumu12V5Host
341
+
342
+ # 延迟初始化 InstanceClass 变量
343
+ Mumu12Host.InstanceClass = Mumu12Instance
344
+ Mumu12V5Host.InstanceClass = Mumu12V5Instance
345
+
346
+
234
347
  if __name__ == '__main__':
235
348
  logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
236
349
  print(Mumu12Host._read_install_path())
237
350
  print(Mumu12Host.installed())
238
351
  print(Mumu12Host.list())
239
352
  print(ins:=Mumu12Host.query(id='2'))
240
- assert isinstance(ins, Mumu12Instance)
353
+ assert isinstance(ins, Mumu12Host.InstanceClass)
241
354
  ins.start()
242
355
  ins.wait_available()
243
356
  print('status', ins.running(), ins.adb_port, ins.adb_ip)
@@ -204,6 +204,7 @@ class ExternalRendererIpc:
204
204
  def __load_dll(self, mumu_root_folder: str) -> ctypes.CDLL:
205
205
  """尝试多条路径加载 DLL。传入为 MuMu 根目录。"""
206
206
  candidate_paths = [
207
+ # <= 4.x
207
208
  os.path.join(mumu_root_folder, "shell", "sdk", "external_renderer_ipc.dll"),
208
209
  os.path.join(
209
210
  mumu_root_folder,
@@ -213,6 +214,10 @@ class ExternalRendererIpc:
213
214
  "sdk",
214
215
  "external_renderer_ipc.dll",
215
216
  ),
217
+ # >= 5.x
218
+ os.path.join(
219
+ mumu_root_folder, "nx_device", "12.0", "shell", "sdk", "external_renderer_ipc.dll"
220
+ ),
216
221
  ]
217
222
  for p in candidate_paths:
218
223
  if not os.path.exists(p):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kotonebot
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Kotonebot is game/app automation library based on computer vision technology, works for Windows and Android.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kotonebot"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Kotonebot is game/app automation library based on computer vision technology, works for Windows and Android."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes