StarRailCopilot/module/device/method/maatouch.py

314 lines
10 KiB
Python

import socket
import threading
from functools import wraps
from adbutils.errors import AdbError
from module.base.decorator import cached_property, del_cached_property, has_cached_property
from module.base.timer import Timer
from module.base.utils import *
from module.device.connection import Connection
from module.device.method.minitouch import CommandBuilder, insert_swipe
from module.device.method.utils import RETRY_TRIES, handle_adb_error, handle_unknown_host_service, retry_sleep
from module.exception import RequestHumanTakeover
from module.logger import logger
def retry(func):
@wraps(func)
def retry_wrapper(self, *args, **kwargs):
"""
Args:
self (MaaTouch):
"""
init = None
for _ in range(RETRY_TRIES):
try:
if callable(init):
retry_sleep(_)
init()
return func(self, *args, **kwargs)
# Can't handle
except RequestHumanTakeover:
break
# When adb server was killed
except ConnectionResetError as e:
logger.error(e)
def init():
self.adb_reconnect()
del_cached_property(self, '_maatouch_builder')
# Emulator closed
except ConnectionAbortedError as e:
logger.error(e)
def init():
self.adb_reconnect()
del_cached_property(self, '_maatouch_builder')
# AdbError
except AdbError as e:
if handle_adb_error(e):
def init():
self.adb_reconnect()
del_cached_property(self, '_maatouch_builder')
elif handle_unknown_host_service(e):
def init():
self.adb_start_server()
self.adb_reconnect()
del_cached_property(self, '_maatouch_builder')
else:
break
# MaaTouchNotInstalledError: Received "Aborted" from MaaTouch
except MaaTouchNotInstalledError as e:
logger.error(e)
def init():
self.maatouch_install()
del_cached_property(self, '_maatouch_builder')
except BrokenPipeError as e:
logger.error(e)
def init():
del_cached_property(self, '_maatouch_builder')
# Unknown, probably a trucked image
except Exception as e:
logger.exception(e)
def init():
pass
logger.critical(f'Retry {func.__name__}() failed')
raise RequestHumanTakeover
return retry_wrapper
class MaatouchBuilder(CommandBuilder):
def __init__(self, device, contact=0, handle_orientation=False):
"""
Args:
device (MaaTouch):
"""
super().__init__(device, contact, handle_orientation)
def send(self):
return self.device.maatouch_send(builder=self)
class MaaTouchNotInstalledError(Exception):
pass
class MaaTouch(Connection):
"""
Control method that implements the same as scrcpy and has an interface similar to minitouch.
https://github.com/MaaAssistantArknights/MaaTouch
"""
max_x: int
max_y: int
_maatouch_stream: socket.socket = None
_maatouch_stream_storage = None
_maatouch_init_thread = None
_maatouch_orientation: int = None
@cached_property
@retry
def _maatouch_builder(self):
self.maatouch_init()
return MaatouchBuilder(self)
@property
def maatouch_builder(self):
# Wait init thread
if self._maatouch_init_thread is not None:
self._maatouch_init_thread.join()
del self._maatouch_init_thread
self._maatouch_init_thread = None
return self._maatouch_builder
def early_maatouch_init(self):
"""
Start a thread to init maatouch connection while the Alas instance just starting to take screenshots
This would speed up the first click 0.2 ~ 0.4s.
"""
if has_cached_property(self, '_maatouch_builder'):
return
def early_maatouch_init_func():
_ = self._maatouch_builder
thread = threading.Thread(target=early_maatouch_init_func, daemon=True)
self._maatouch_init_thread = thread
thread.start()
def on_orientation_change_maatouch(self):
"""
MaaTouch caches devices orientation at its startup
A restart is required when orientation changed
"""
if self._maatouch_orientation is None:
return
if self.orientation == self._maatouch_orientation:
return
logger.info(f'Orientation changed {self._maatouch_orientation} => {self.orientation}, re-init MaaTouch')
del_cached_property(self, '_maatouch_builder')
self.early_maatouch_init()
def maatouch_init(self):
logger.hr('MaaTouch init')
max_x, max_y = 1280, 720
max_contacts = 2
max_pressure = 50
# Try to close existing stream
if self._maatouch_stream is not None:
try:
self._maatouch_stream.close()
except Exception as e:
logger.error(e)
del self._maatouch_stream
if self._maatouch_stream_storage is not None:
del self._maatouch_stream_storage
# MaaTouch caches devices orientation at its startup
super(MaaTouch, self).get_orientation()
self._maatouch_orientation = self.orientation
# CLASSPATH=/data/local/tmp/maatouch app_process / com.shxyke.MaaTouch.App
stream = self.adb_shell(
['CLASSPATH=/data/local/tmp/maatouch', 'app_process', '/', 'com.shxyke.MaaTouch.App'],
stream=True,
recvall=False
)
# Prevent shell stream from being deleted causing socket close
self._maatouch_stream_storage = stream
stream = stream.conn
stream.settimeout(10)
self._maatouch_stream = stream
retry_timeout = Timer(5).start()
while 1:
# v <version>
# protocol version, usually it is 1. needn't use this
# get maatouch server info
socket_out = stream.makefile()
# ^ <max-contacts> <max-x> <max-y> <max-pressure>
out = socket_out.readline().replace("\n", "").replace("\r", "")
logger.info(out)
if out.strip() == 'Aborted':
stream.close()
raise MaaTouchNotInstalledError(
'Received "Aborted" MaaTouch, '
'probably because MaaTouch is not installed'
)
try:
_, max_contacts, max_x, max_y, max_pressure = out.split(" ")
break
except ValueError:
stream.close()
if retry_timeout.reached():
raise MaaTouchNotInstalledError(
'Received empty data from MaaTouch, '
'probably because MaaTouch is not installed'
)
else:
# maatouch may not start that fast
self.sleep(1)
continue
# self.max_contacts = max_contacts
self.max_x = int(max_x)
self.max_y = int(max_y)
# self.max_pressure = max_pressure
# $ <pid>
out = socket_out.readline().replace("\n", "").replace("\r", "")
logger.info(out)
# _, pid = out.split(" ")
# self._maatouch_pid = pid
logger.info(
"MaaTouch stream connected"
)
logger.info(
"max_contact: {}; max_x: {}; max_y: {}; max_pressure: {}".format(
max_contacts, max_x, max_y, max_pressure
)
)
def maatouch_send(self, builder: MaatouchBuilder):
content = builder.to_minitouch()
# logger.info("send operation: {}".format(content.replace("\n", "\\n")))
byte_content = content.encode('utf-8')
self._maatouch_stream.sendall(byte_content)
self._maatouch_stream.recv(0)
self.sleep(self.maatouch_builder.delay / 1000 + builder.DEFAULT_DELAY)
builder.clear()
def maatouch_install(self):
logger.hr('MaaTouch install')
self.adb_push(self.config.MAATOUCH_FILEPATH_LOCAL, self.config.MAATOUCH_FILEPATH_REMOTE)
def maatouch_uninstall(self):
logger.hr('MaaTouch uninstall')
self.adb_shell(["rm", self.config.MAATOUCH_FILEPATH_REMOTE])
@retry
def click_maatouch(self, x, y):
builder = self.maatouch_builder
builder.down(x, y).commit()
builder.up().commit()
builder.send()
@retry
def long_click_maatouch(self, x, y, duration=1.0):
duration = int(duration * 1000)
builder = self.maatouch_builder
builder.down(x, y).commit().wait(duration)
builder.up().commit()
builder.send()
@retry
def swipe_maatouch(self, p1, p2):
points = insert_swipe(p0=p1, p3=p2)
builder = self.maatouch_builder
builder.down(*points[0]).commit()
builder.send()
for point in points[1:]:
builder.move(*point).commit().wait(10)
builder.send()
builder.up().commit()
builder.send()
@retry
def drag_maatouch(self, p1, p2, point_random=(-10, -10, 10, 10)):
p1 = np.array(p1) - random_rectangle_point(point_random)
p2 = np.array(p2) - random_rectangle_point(point_random)
points = insert_swipe(p0=p1, p3=p2, speed=20)
builder = self.maatouch_builder
builder.down(*points[0]).commit()
builder.send()
for point in points[1:]:
builder.move(*point).commit().wait(10)
builder.send()
builder.move(*p2).commit().wait(140)
builder.move(*p2).commit().wait(140)
builder.send()
builder.up().commit()
builder.send()
if __name__ == '__main__':
self = MaaTouch('src')
self.maatouch_uninstall()