Merge pull request #403 from LmeSzinc/dev

Bug fix
This commit is contained in:
LmeSzinc 2024-04-03 09:29:32 +08:00 committed by GitHub
commit 60b83abd86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 139 additions and 112 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -273,15 +273,20 @@ class Connection(ConnectionAttr):
return True return True
return False return False
@cached_property
def nemud_app_keep_alive(self) -> str:
res = self.adb_getprop('nemud.app_keep_alive')
return res
@retry @retry
def check_mumu_app_keep_alive(self): def check_mumu_app_keep_alive(self):
if not self.is_mumu_family: if not self.is_mumu_family:
return False return False
res = self.adb_getprop('nemud.app_keep_alive') res = self.nemud_app_keep_alive
logger.attr('nemud.app_keep_alive', res) logger.attr('nemud.app_keep_alive', res)
if res == '': if res == '':
# Empry property, might not be a mumu emulator or might be an old mumu # Empty property, probably MuMu6 or MuMu12 version < 3.5.6
return True return True
elif res == 'false': elif res == 'false':
# Disabled # Disabled
@ -850,7 +855,7 @@ class Connection(ConnectionAttr):
packages = re.findall(r'package:([^\s]+)', output) packages = re.findall(r'package:([^\s]+)', output)
return packages return packages
def list_azurlane_packages(self, show_log=True): def list_known_packages(self, show_log=True):
""" """
Args: Args:
show_log: show_log:
@ -867,7 +872,7 @@ class Connection(ConnectionAttr):
Show all possible packages with the given keyword on this device. Show all possible packages with the given keyword on this device.
""" """
logger.hr('Detect package') logger.hr('Detect package')
packages = self.list_azurlane_packages() packages = self.list_known_packages()
# Show packages # Show packages
logger.info(f'Here are the available packages in device "{self.serial}", ' logger.info(f'Here are the available packages in device "{self.serial}", '

View File

@ -127,7 +127,12 @@ class ConnectionAttr:
def is_mumu_family(self): def is_mumu_family(self):
# 127.0.0.1:7555 # 127.0.0.1:7555
# 127.0.0.1:16384 + 32*n # 127.0.0.1:16384 + 32*n
return self.serial == '127.0.0.1:7555' or self.serial.startswith('127.0.0.1:16') return self.serial == '127.0.0.1:7555' or self.is_mumu12_family
@cached_property
def is_mumu12_family(self):
# 127.0.0.1:16384 + 32*n
return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16')
@cached_property @cached_property
def is_emulator(self): def is_emulator(self):

View File

@ -1,10 +1,10 @@
import collections import collections
import itertools import itertools
import sys
from module.base.timer import Timer from module.base.timer import Timer
from module.device.app_control import AppControl from module.device.app_control import AppControl
from module.device.control import Control from module.device.control import Control
from module.device.platform import Platform
from module.device.screenshot import Screenshot from module.device.screenshot import Screenshot
from module.exception import ( from module.exception import (
EmulatorNotRunningError, EmulatorNotRunningError,
@ -15,11 +15,6 @@ from module.exception import (
) )
from module.logger import logger from module.logger import logger
if sys.platform == 'win32':
from module.device.platform.platform_windows import PlatformWindows as Platform
else:
from module.device.platform.platform_base import PlatformBase as Platform
def show_function_call(): def show_function_call():
""" """
@ -83,6 +78,10 @@ class Device(Screenshot, Control, AppControl, Platform):
) )
raise raise
# Auto-fill emulator info
if self.config.EmulatorInfo_Emulator == 'auto':
_ = self.emulator_instance
self.screenshot_interval_set() self.screenshot_interval_set()
# Auto-select the fastest screenshot method # Auto-select the fastest screenshot method

View File

@ -128,7 +128,7 @@ class Adb(Connection):
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.imdecode') raise ImageTruncated('Empty image after cv2.imdecode')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image)
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.cvtColor') raise ImageTruncated('Empty image after cv2.cvtColor')

View File

@ -165,11 +165,11 @@ class AScreenCap(Connection):
# ValueError: cannot reshape array of size 0 into shape (720,1280,4) # ValueError: cannot reshape array of size 0 into shape (720,1280,4)
raise ImageTruncated(str(e)) raise ImageTruncated(str(e))
image = cv2.flip(image, 0) cv2.flip(image, 0, dst=image)
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.flip') raise ImageTruncated('Empty image after cv2.flip')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image)
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.cvtColor') raise ImageTruncated('Empty image after cv2.cvtColor')

View File

@ -6,11 +6,11 @@ import numpy as np
import requests import requests
from adbutils.errors import AdbError from adbutils.errors import AdbError
from module.base.decorator import Config, cached_property, del_cached_property from module.base.decorator import cached_property, del_cached_property
from module.base.timer import Timer from module.base.timer import Timer
from module.device.method.uiautomator_2 import Uiautomator2, ProcessInfo from module.device.method.uiautomator_2 import ProcessInfo, Uiautomator2
from module.device.method.utils import (retry_sleep, RETRY_TRIES, handle_adb_error, from module.device.method.utils import (
ImageTruncated, PackageNotInstalled) ImageTruncated, PackageNotInstalled, RETRY_TRIES, handle_adb_error, retry_sleep)
from module.exception import RequestHumanTakeover from module.exception import RequestHumanTakeover
from module.logger import logger from module.logger import logger
@ -91,7 +91,7 @@ def retry(func):
class DroidCast(Uiautomator2): class DroidCast(Uiautomator2):
""" """
DroidCast, another screenshot method, https://github.com/rayworks/DroidCast DroidCast, another screenshot method, https://github.com/rayworks/DroidCast
DroidCast_raw, a modified version of DroidCast sending raw bitmap https://github.com/Torther/DroidCastS DroidCast_raw, a modified version of DroidCast sending raw bitmap and png, https://github.com/Torther/DroidCastS
""" """
_droidcast_port: int = 0 _droidcast_port: int = 0
@ -103,92 +103,73 @@ class DroidCast(Uiautomator2):
self._droidcast_port = self.adb_forward('tcp:53516') self._droidcast_port = self.adb_forward('tcp:53516')
return session return session
def droidcast_url(self, url='/screenshot?format=png'):
""" """
Check APIs from source code: Check APIs from source code:
https://github.com/rayworks/DroidCast/blob/master/app/src/main/java/com/rayworks/droidcast/Main.java https://github.com/Torther/DroidCast_raw/blob/DroidCast_raw/app/src/main/java/ink/mol/droidcast_raw/KtMain.kt
Available APIs: Available APIs:
- /screenshot - /screenshot
To get JPG screenshots. To get a RGB565 bitmap
- /screenshot?format=png - /preview
To get PNG screenshots. To get PNG screenshots.
- /screenshot?format=webp
To get WEBP screenshots.
- /src
Websocket to get JPG screenshots.
Note that /screenshot?format=jpg is unavailable.
""" """
def droidcast_url(self, url='/preview'):
return f'http://127.0.0.1:{self._droidcast_port}{url}'
def droidcast_raw_url(self, url='/screenshot'):
return f'http://127.0.0.1:{self._droidcast_port}{url}' return f'http://127.0.0.1:{self._droidcast_port}{url}'
@Config.when(DROIDCAST_VERSION='DroidCast')
def droidcast_init(self): def droidcast_init(self):
logger.hr('Droidcast init') logger.hr('DroidCast init')
self.droidcast_stop() self.droidcast_stop()
logger.info('Pushing DroidCast apk') logger.info('Pushing DroidCast apk')
self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE) self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE)
logger.info('Starting DroidCast apk') logger.info('Starting DroidCast apk')
# CLASSPATH=/data/local/tmp/DroidCast.apk app_process / com.rayworks.droidcast.Main > /dev/null # DroidCast_raw-release-1.0.apk
# CLASSPATH=/data/local/tmp/DroidCast_raw.apk app_process / ink.mol.droidcast_raw.Main > /dev/null
# adb shell CLASSPATH=/data/local/tmp/DroidCast_raw.apk app_process / ink.mol.droidcast_raw.Main
resp = self.u2_shell_background([ resp = self.u2_shell_background([
'CLASSPATH=/data/local/tmp/DroidCast.apk', 'CLASSPATH=/data/local/tmp/DroidCast_raw.apk',
'app_process', 'app_process',
'/', '/',
'com.rayworks.droidcast.Main', 'ink.mol.droidcast_raw.Main',
'>', '>',
'/dev/null' '/dev/null'
]) ])
logger.info(resp) logger.info(resp)
del_cached_property(self, 'droidcast_session') del_cached_property(self, 'droidcast_session')
_ = self.droidcast_session _ = self.droidcast_session
if self.config.DROIDCAST_VERSION == 'DroidCast':
logger.attr('DroidCast', self.droidcast_url()) logger.attr('DroidCast', self.droidcast_url())
self.droidcast_wait_startup() self.droidcast_wait_startup()
elif self.config.DROIDCAST_VERSION == 'DroidCast_raw':
@Config.when(DROIDCAST_VERSION='DroidCast_raw') logger.attr('DroidCast_raw', self.droidcast_raw_url())
def droidcast_init(self):
logger.hr('Droidcast init')
self.resolution_check_uiautomator2()
self.droidcast_stop()
logger.info('Pushing DroidCast apk')
self.adb_push(self.config.DROIDCAST_RAW_FILEPATH_LOCAL, self.config.DROIDCAST_RAW_FILEPATH_REMOTE)
logger.info('Starting DroidCast apk')
# DroidCastS-release-1.1.5.apk
# CLASSPATH=/data/local/tmp/DroidCastS-release-1.1.5.apk app_process / com.torther.droidcasts.Main > /dev/null
resp = self.u2_shell_background([
'CLASSPATH=/data/local/tmp/DroidCastS.apk',
'app_process',
'/',
'com.torther.droidcasts.Main',
'>',
'/dev/null'
])
logger.info(resp)
del_cached_property(self, 'droidcast_session')
_ = self.droidcast_session
logger.attr('DroidCast', self.droidcast_url())
self.droidcast_wait_startup() self.droidcast_wait_startup()
else:
logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}')
@retry @retry
def screenshot_droidcast(self): def screenshot_droidcast(self):
self.config.DROIDCAST_VERSION = 'DroidCast' self.config.DROIDCAST_VERSION = 'DroidCast'
image = self.droidcast_session.get(self.droidcast_url(), timeout=3).content resp = self.droidcast_session.get(self.droidcast_url(), timeout=3)
if resp.status_code == 404:
raise DroidCastVersionIncompatible('DroidCast server does not have /preview')
image = resp.content
image = np.frombuffer(image, np.uint8) image = np.frombuffer(image, np.uint8)
if image is None: if image is None:
raise ImageTruncated('Empty image after reading from buffer') raise ImageTruncated('Empty image after reading from buffer')
if image.shape == (1843200,): if image.shape == (1843200,):
raise DroidCastVersionIncompatible('Requesting screenshots from `DroidCast` but server is `DroidCast_raw`') raise DroidCastVersionIncompatible('Requesting screenshots from `DroidCast` but server is `DroidCast_raw`')
if image.size < 500:
logger.warning(f'Unexpected screenshot: {resp.content}')
image = cv2.imdecode(image, cv2.IMREAD_COLOR) image = cv2.imdecode(image, cv2.IMREAD_COLOR)
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.imdecode') raise ImageTruncated('Empty image after cv2.imdecode')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image)
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.cvtColor') raise ImageTruncated('Empty image after cv2.cvtColor')
@ -197,12 +178,14 @@ class DroidCast(Uiautomator2):
@retry @retry
def screenshot_droidcast_raw(self): def screenshot_droidcast_raw(self):
self.config.DROIDCAST_VERSION = 'DroidCast_raw' self.config.DROIDCAST_VERSION = 'DroidCast_raw'
image = self.droidcast_session.get(self.droidcast_url(), timeout=3).content image = self.droidcast_session.get(self.droidcast_raw_url(), timeout=3).content
# DroidCast_raw returns a RGB565 bitmap # DroidCast_raw returns a RGB565 bitmap
try: try:
arr = np.frombuffer(image, dtype=np.uint16).reshape((720, 1280)) arr = np.frombuffer(image, dtype=np.uint16).reshape((720, 1280))
except ValueError as e: except ValueError as e:
if len(image) < 500:
logger.warning(f'Unexpected screenshot: {image}')
# Try to load as `DroidCast` # Try to load as `DroidCast`
image = np.frombuffer(image, np.uint8) image = np.frombuffer(image, np.uint8)
if image is not None: if image is not None:
@ -228,14 +211,25 @@ class DroidCast(Uiautomator2):
# image = cv2.merge([r, g, b]) # image = cv2.merge([r, g, b])
# The same as the code above but costs about 5ms instead of 10ms. # The same as the code above but costs about 5ms instead of 10ms.
r = cv2.multiply(arr & 0b1111100000000000, 0.00390625).astype(np.uint8) r = cv2.bitwise_and(arr, 0b1111100000000000)
g = cv2.multiply(arr & 0b0000011111100000, 0.125).astype(np.uint8) cv2.multiply(r, 0.00390625, dst=r)
b = cv2.multiply(arr & 0b0000000000011111, 8).astype(np.uint8) r = np.uint8(r)
r = cv2.add(r, cv2.multiply(r, 0.03125)) m = cv2.multiply(r, 0.03125)
g = cv2.add(g, cv2.multiply(g, 0.015625)) cv2.add(r, m, dst=r)
b = cv2.add(b, cv2.multiply(b, 0.03125))
image = cv2.merge([r, g, b])
g = cv2.bitwise_and(arr, 0b0000011111100000)
cv2.multiply(g, 0.125, dst=g)
g = np.uint8(g)
m = cv2.multiply(g, 0.015625)
cv2.add(g, m, dst=g)
b = cv2.bitwise_and(arr, 0b0000000000011111)
cv2.multiply(b, 8, dst=b)
b = np.uint8(b)
m = cv2.multiply(b, 0.03125)
cv2.add(b, m, dst=b)
image = cv2.merge([r, g, b])
return image return image
def droidcast_wait_startup(self): def droidcast_wait_startup(self):
@ -262,13 +256,12 @@ class DroidCast(Uiautomator2):
def droidcast_uninstall(self): def droidcast_uninstall(self):
""" """
Stop all DroidCast processes and remove DroidCast APK. Stop DroidCast processes and remove DroidCast APK.
DroidCast has't been installed but a JAVA class call, uninstall is a file delete. DroidCast hasn't been installed but a JAVA class call, uninstall is a file delete.
""" """
self.droidcast_stop() self.droidcast_stop()
logger.info('Removing DroidCast') logger.info('Removing DroidCast')
self.adb_shell(["rm", self.config.DROIDCAST_FILEPATH_REMOTE]) self.adb_shell(["rm", self.config.DROIDCAST_FILEPATH_REMOTE])
self.adb_shell(["rm", self.config.DROIDCAST_RAW_FILEPATH_REMOTE])
def _iter_droidcast_proc(self) -> t.Iterable[ProcessInfo]: def _iter_droidcast_proc(self) -> t.Iterable[ProcessInfo]:
""" """
@ -280,10 +273,12 @@ class DroidCast(Uiautomator2):
yield proc yield proc
if 'com.torther.droidcasts.Main' in proc.cmdline: if 'com.torther.droidcasts.Main' in proc.cmdline:
yield proc yield proc
if 'ink.mol.droidcast_raw.Main' in proc.cmdline:
yield proc
def droidcast_stop(self): def droidcast_stop(self):
""" """
Stop all DroidCast processes. Stop DroidCast processes.
""" """
logger.info('Stopping DroidCast') logger.info('Stopping DroidCast')
for proc in self._iter_droidcast_proc(): for proc in self._iter_droidcast_proc():

View File

@ -122,7 +122,7 @@ class Uiautomator2(Connection):
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.imdecode') raise ImageTruncated('Empty image after cv2.imdecode')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image)
if image is None: if image is None:
raise ImageTruncated('Empty image after cv2.cvtColor') raise ImageTruncated('Empty image after cv2.cvtColor')

View File

@ -0,0 +1,6 @@
import sys
if sys.platform == 'win32':
from module.device.platform.platform_windows import PlatformWindows as Platform
else:
from module.device.platform.platform_base import PlatformBase as Platform

View File

@ -54,7 +54,7 @@ class EmulatorInstanceBase:
Returns: Returns:
str: Emulator type, such as Emulator.NoxPlayer str: Emulator type, such as Emulator.NoxPlayer
""" """
return EmulatorBase.path_to_type(self.path) return self.emulator.type
@cached_property @cached_property
def emulator(self): def emulator(self):

View File

@ -8,8 +8,8 @@ from dataclasses import dataclass
# module/device/platform/emulator_base.py # module/device/platform/emulator_base.py
# module/device/platform/emulator_windows.py # module/device/platform/emulator_windows.py
# Will be used in Alas Easy Install, they shouldn't import any Alas modules. # Will be used in Alas Easy Install, they shouldn't import any Alas modules.
from module.device.platform.utils import cached_property, iter_folder
from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase
from module.device.platform.utils import cached_property, iter_folder
@dataclass @dataclass
@ -56,14 +56,6 @@ def abspath(path):
class EmulatorInstance(EmulatorInstanceBase): class EmulatorInstance(EmulatorInstanceBase):
@cached_property
def type(self) -> str:
"""
Returns:
str: Emulator type, such as Emulator.NoxPlayer
"""
return Emulator.path_to_type(self.path)
@cached_property @cached_property
def emulator(self): def emulator(self):
""" """

View File

@ -1,14 +1,13 @@
import sys import sys
import typing as t import typing as t
import yaml from pydantic import BaseModel
from pydantic import BaseModel, SecretStr
from module.base.decorator import cached_property, del_cached_property from module.base.decorator import cached_property, del_cached_property
from module.base.utils import SelectedGrids
from module.device.connection import Connection from module.device.connection import Connection
from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase
from module.logger import logger from module.logger import logger
from module.base.utils import SelectedGrids
class EmulatorInfo(BaseModel): class EmulatorInfo(BaseModel):

View File

@ -267,7 +267,7 @@ class PlatformWindows(PlatformBase, EmulatorManager):
show_ping(pong) show_ping(pong)
# Check azuelane package # Check azuelane package
packages = self.list_azurlane_packages(show_log=False) packages = self.list_known_packages(show_log=False)
if len(packages): if len(packages):
pass pass
else: else:
@ -317,4 +317,5 @@ class PlatformWindows(PlatformBase, EmulatorManager):
if __name__ == '__main__': if __name__ == '__main__':
self = PlatformWindows('alas') self = PlatformWindows('alas')
self.emulator_start() d = self.emulator_instance
print(d)

View File

@ -128,17 +128,17 @@ CONFIRM_ASSIGNMENT = ButtonWrapper(
name='CONFIRM_ASSIGNMENT', name='CONFIRM_ASSIGNMENT',
cn=Button( cn=Button(
file='./assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png', file='./assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png',
area=(1024, 653, 1104, 672), area=(1007, 660, 1085, 678),
search=(1004, 633, 1124, 692), search=(987, 640, 1105, 698),
color=(154, 154, 153), color=(148, 148, 147),
button=(920, 645, 1208, 682), button=(909, 651, 1184, 686),
), ),
en=Button( en=Button(
file='./assets/en/assignment/dispatch/CONFIRM_ASSIGNMENT.png', file='./assets/en/assignment/dispatch/CONFIRM_ASSIGNMENT.png',
area=(964, 655, 1164, 671), area=(946, 661, 1146, 677),
search=(944, 635, 1184, 691), search=(926, 641, 1166, 697),
color=(161, 160, 160), color=(162, 162, 161),
button=(928, 645, 1201, 681), button=(909, 652, 1182, 687),
), ),
) )
DURATION_12 = ButtonWrapper( DURATION_12 = ButtonWrapper(

View File

@ -77,8 +77,9 @@ class AssignmentDispatch(AssignmentUI):
""" """
Pages: Pages:
in: EMPTY_SLOT in: EMPTY_SLOT
out: CHARACTER_LIST out: EMPTY_SLOT
""" """
logger.info('Select characters')
skip_first_screenshot = True skip_first_screenshot = True
self.interval_clear( self.interval_clear(
(CHARACTER_LIST, CHARACTER_1_SELECTED, CHARACTER_2_SELECTED), interval=2) (CHARACTER_LIST, CHARACTER_1_SELECTED, CHARACTER_2_SELECTED), interval=2)
@ -88,8 +89,12 @@ class AssignmentDispatch(AssignmentUI):
else: else:
self.device.screenshot() self.device.screenshot()
# End # End
if self.match_template_color(CONFIRM_ASSIGNMENT): if self.appear(CONFIRM_ASSIGNMENT):
logger.info('Characters are all selected') if self.image_color_count(CONFIRM_ASSIGNMENT.button, color=(227, 227, 227), count=1000):
logger.info('Characters are all selected (light button)')
break
if self.image_color_count(CONFIRM_ASSIGNMENT.button, color=(144, 144, 144), count=1000):
logger.info('Characters are all selected (gray button)')
break break
# Ensure character list # Ensure character list
# Search EMPTY_SLOT to load offset # Search EMPTY_SLOT to load offset
@ -108,6 +113,26 @@ class AssignmentDispatch(AssignmentUI):
self.device.click(CHARACTER_2) self.device.click(CHARACTER_2)
self.interval_reset(CHARACTER_2_SELECTED, interval=2) self.interval_reset(CHARACTER_2_SELECTED, interval=2)
# CHARACTER_LIST -> CONFIRM_ASSIGNMENT
logger.info('Close character list')
self.interval_clear([CHARACTER_LIST, EMPTY_SLOT], interval=2)
skip_first_screenshot = True
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
if self.appear(CONFIRM_ASSIGNMENT):
if self.image_color_count(CONFIRM_ASSIGNMENT.button, color=(227, 227, 227), count=1000):
logger.info('Characters are all selected (light button)')
break
if self.appear(CHARACTER_LIST, interval=2):
# EMPTY_SLOT appeared above
self.device.click(EMPTY_SLOT)
continue
def _select_support(self): def _select_support(self):
skip_first_screenshot = True skip_first_screenshot = True
self.interval_clear( self.interval_clear(