Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -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}", '
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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/Torther/DroidCast_raw/blob/DroidCast_raw/app/src/main/java/ink/mol/droidcast_raw/KtMain.kt
|
||||||
https://github.com/rayworks/DroidCast/blob/master/app/src/main/java/com/rayworks/droidcast/Main.java
|
Available APIs:
|
||||||
|
- /screenshot
|
||||||
Available APIs:
|
To get a RGB565 bitmap
|
||||||
- /screenshot
|
- /preview
|
||||||
To get JPG screenshots.
|
To get PNG screenshots.
|
||||||
- /screenshot?format=png
|
"""
|
||||||
To get PNG screenshots.
|
def droidcast_url(self, url='/preview'):
|
||||||
- /screenshot?format=webp
|
return f'http://127.0.0.1:{self._droidcast_port}{url}'
|
||||||
To get WEBP screenshots.
|
|
||||||
- /src
|
def droidcast_raw_url(self, url='/screenshot'):
|
||||||
Websocket to get JPG screenshots.
|
|
||||||
|
|
||||||
Note that /screenshot?format=jpg is unavailable.
|
|
||||||
"""
|
|
||||||
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
|
||||||
logger.attr('DroidCast', self.droidcast_url())
|
|
||||||
self.droidcast_wait_startup()
|
|
||||||
|
|
||||||
@Config.when(DROIDCAST_VERSION='DroidCast_raw')
|
if self.config.DROIDCAST_VERSION == 'DroidCast':
|
||||||
def droidcast_init(self):
|
logger.attr('DroidCast', self.droidcast_url())
|
||||||
logger.hr('Droidcast init')
|
self.droidcast_wait_startup()
|
||||||
self.resolution_check_uiautomator2()
|
elif self.config.DROIDCAST_VERSION == 'DroidCast_raw':
|
||||||
self.droidcast_stop()
|
logger.attr('DroidCast_raw', self.droidcast_raw_url())
|
||||||
|
self.droidcast_wait_startup()
|
||||||
logger.info('Pushing DroidCast apk')
|
else:
|
||||||
self.adb_push(self.config.DROIDCAST_RAW_FILEPATH_LOCAL, self.config.DROIDCAST_RAW_FILEPATH_REMOTE)
|
logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}')
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
@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():
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
6
module/device/platform/__init__.py
Normal 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
|
@ -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):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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,9 +89,13 @@ 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):
|
||||||
break
|
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
|
||||||
# Ensure character list
|
# Ensure character list
|
||||||
# Search EMPTY_SLOT to load offset
|
# Search EMPTY_SLOT to load offset
|
||||||
if not self.appear(CHARACTER_LIST) and self.appear(EMPTY_SLOT):
|
if not self.appear(CHARACTER_LIST) and self.appear(EMPTY_SLOT):
|
||||||
@ -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(
|
||||||
|