import ctypes import os import threading import time from functools import wraps from typing import Any, Callable import numpy as np from mower.utils import config from mower.utils import typealias as tp from mower.utils.csleep import MowerExit from mower.utils.device import swipe_update from mower.utils.device.emulator import restart_emulator from mower.utils.device.exception import EmulatorError, GameError from mower.utils.device.method.mumumanager import MuMuManager from mower.utils.log import logger from mower.utils.vector import sm, va class NemuIpcIncompatible(Exception): pass def retry_mumuipc(func): @wraps(func) def retry_wrapper(self, *args, **kwargs): for _ in range(3): try: return func(self, *args, **kwargs) except MowerExit: raise except GameError as e: logger.debug_exception(e) config.device.check_current_focus() except EmulatorError as e: logger.debug_exception(e) restart_emulator() except Exception as e: logger.debug_exception(e) return retry_wrapper class MuMu12IPC: def __init__(self): self.emulator_folder = config.conf.emulator.emulator_folder self.instanse_index = int(config.conf.emulator.index) self.connection = 0 self.display_id = -1 # 加载动态链接库 dll_path = os.path.join( self.emulator_folder, "sdk", "external_renderer_ipc.dll" ) try: self.external_renderer = ctypes.CDLL(dll_path) except OSError as e: logger.error(e) # OSError: [WinError 126] 找不到指定的模块。 if not os.path.exists(dll_path): raise NemuIpcIncompatible( f"文件不存在: {dll_path}, 请检查模拟器文件夹是否正确" f"NemuIpc requires MuMu12 version >= 3.8.13, please check your version" ) else: raise NemuIpcIncompatible( f"ipc_dll={dll_path} exists, but cannot be loaded" ) # 定义函数原型 self.external_renderer.nemu_connect.argtypes = [ctypes.c_wchar_p, ctypes.c_int] self.external_renderer.nemu_connect.restype = ctypes.c_int self.external_renderer.nemu_disconnect.argtypes = [ctypes.c_int] self.external_renderer.nemu_disconnect.restype = None self.external_renderer.nemu_get_display_id.argtypes = [ ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ] self.external_renderer.nemu_get_display_id.restype = ctypes.c_int self.external_renderer.nemu_capture_display.argtypes = [ ctypes.c_int, ctypes.c_uint, ctypes.c_int, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_ubyte), ] self.external_renderer.nemu_capture_display.restype = ctypes.c_int self.external_renderer.nemu_input_event_touch_down.argtypes = [ ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ] self.external_renderer.nemu_input_event_touch_down.restype = ctypes.c_int self.external_renderer.nemu_input_event_touch_up.argtypes = [ ctypes.c_int, ctypes.c_int, ] self.external_renderer.nemu_input_event_touch_up.restype = ctypes.c_int self.external_renderer.nemu_input_event_finger_touch_down.argtypes = [ ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ] self.external_renderer.nemu_input_event_finger_touch_down.restype = ctypes.c_int self.external_renderer.nemu_input_event_finger_touch_up.argtypes = [ ctypes.c_int, ctypes.c_int, ctypes.c_int, ] self.external_renderer.nemu_input_event_finger_touch_up.restype = ctypes.c_int self.external_renderer.nemu_input_event_key_down.argtypes = [ ctypes.c_int, ctypes.c_int, ctypes.c_int, ] self.external_renderer.nemu_input_event_key_down.restype = ctypes.c_int self.external_renderer.nemu_input_event_key_up.argtypes = [ ctypes.c_int, ctypes.c_int, ctypes.c_int, ] self.external_renderer.nemu_input_event_key_up.restype = ctypes.c_int def connect(self): "连接到 emulator" if not MuMuManager().check_android_process(): raise EmulatorError("模拟器未启动,请启动模拟器") self.connection = self.external_renderer.nemu_connect( ctypes.c_wchar_p(os.path.dirname(self.emulator_folder)), self.instanse_index, ) if self.connection == 0: raise EmulatorError("连接模拟器失败,请启动模拟器") logger.info("连接模拟器成功") def get_display_id(self) -> int: config.device.check_current_focus() pkg_name = config.conf.APPNAME.encode("utf-8") # 替换为实际包名 self.display_id = self.external_renderer.nemu_get_display_id( self.connection, pkg_name, 0 ) if self.display_id < 0: logger.error(f"获取Display ID失败: {self.display_id}") raise GameError("获取Display ID失败,请先打开游戏") logger.info(f"获取Display ID成功: {self.display_id}") @retry_mumuipc def check_status(self): if self.connection == 0: self.connect() if self.display_id < 0: self.get_display_id() def capture_display(self) -> np.ndarray: self.check_status() pixels = (ctypes.c_ubyte * 8294400)() result = self.external_renderer.nemu_capture_display( self.connection, self.display_id, 8294400, ctypes.byref(ctypes.c_int(1920)), ctypes.byref(ctypes.c_int(1080)), pixels, ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.capture_display() image = np.frombuffer(pixels, dtype=np.uint8).reshape((1080, 1920, 4))[:, :, :3] image = np.flipud(image) # 翻转 return image def key_down(self, key_code: int): """按下键盘按键""" self.check_status() result = self.external_renderer.nemu_input_event_key_down( self.connection, self.display_id, key_code ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.key_down() def key_up(self, key_code: int): """释放键盘按键""" self.check_status() result = self.external_renderer.nemu_input_event_key_up( self.connection, self.display_id, key_code ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.key_up() # 单点触控 def touch_down(self, x: int, y: int): self.check_status() result = self.external_renderer.nemu_input_event_touch_down( self.connection, self.display_id, int(1080 - y), int(x) ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.touch_down() def touch_up(self): self.check_status() result = self.external_renderer.nemu_input_event_touch_up( self.connection, self.display_id ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.touch_up() # 多点触控 def finger_touch_down(self, finger_id: int, x: int, y: int): self.check_status() result = self.external_renderer.nemu_input_event_finger_touch_down( self.connection, self.display_id, finger_id, int(1080 - y), int(x) ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.finger_touch_down() def finger_touch_up(self, finger_id: int): self.check_status() result = self.external_renderer.nemu_input_event_finger_touch_up( self.connection, self.display_id, finger_id ) if result != 0: self.connection = 0 self.display_id = -1 config.device.exit() # 可能是游戏卡死 return self.finger_touch_up() def tap(self, x, y, hold_time: float = 0.07) -> None: """ Tap on screen Args: x: horizontal position y: vertical position hold_time: hold time """ self.touch_down(x, y) time.sleep(hold_time) self.touch_up() def send_keyevent(self, key_code: int, hold_time: float = 0.1): self.key_down(key_code) time.sleep(hold_time) self.key_up(key_code) def back(self): self.send_keyevent(1) def swipe( self, x0: int, y0: int, x1: int, y1: int, duration: float = 1.0, fall: bool = True, lift: bool = True, update: bool = False, interval: float = 0, func: Callable[[np.ndarray], Any] = lambda _: None, ): """ 模拟滑动 Args: x0 (int): 起点横坐标 y0 (int): 起点纵坐标 x1 (int): 终点横坐标 y1 (int): 终点纵坐标 duration (float): 滑动持续时间,单位为秒 fall (bool): 是否按下,默认 True lift (bool): 是否抬起,默认 True update (bool): 滑动前是否截图,默认 False interval (float): 滑动完成后的等待时间,单位为秒,默认 0 func (Callable[[np.ndarray], Any]): 截图后处理的函数,默认空函数 """ frame_time = 1 / 30 start_time = time.perf_counter() fall and self.touch_down(x0, y0) if update and config.recog: thread = threading.Thread(target=swipe_update.start, args=(func,)) thread.start() if (down_time := time.perf_counter()) < start_time + frame_time: time.sleep(start_time + frame_time - down_time) down_time = start_time + frame_time while (progress := (time.perf_counter() - down_time) / duration) <= 1: x, y = va(sm(1 - progress, (x0, y0)), sm(progress, (x1, y1))) start_time = time.perf_counter() self.touch_down(x, y) if (move_time := time.perf_counter() - start_time) < frame_time: time.sleep(frame_time - move_time) self.touch_down(x1, y1) lift and self.touch_up() if update and config.recog: start_time = time.perf_counter() thread.join() if (stop_time := time.perf_counter()) < start_time + interval: time.sleep(start_time + interval - stop_time) return swipe_update.result if interval > 0: time.sleep(interval) def swipe_ext( self, points: list[tp.Coordinate], durations: list[int], update: bool = False, interval: float = 0, func: Callable[[tp.Image], Any] = lambda _: None, ): total = len(durations) for idx, (S, E, D) in enumerate(zip(points[:-1], points[1:], durations)): first = idx == 0 last = idx == total - 1 result = self.swipe( x0=S[0], y0=S[1], x1=E[0], y1=E[1], duration=D / 1000, fall=first, lift=last, update=last and update, interval=interval if last else 0, func=func, ) return result