Add: Cloud game support

This commit is contained in:
LmeSzinc 2024-01-15 02:34:26 +08:00
parent 5b7b226066
commit c493292181
9 changed files with 367 additions and 21 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -62,6 +62,18 @@ class AzurLaneAutoScript:
logger.exception(e) logger.exception(e)
exit(1) exit(1)
def restart(self):
raise NotImplemented
def start(self):
raise NotImplemented
def stop(self):
raise NotImplemented
def goto_main(self):
raise NotImplemented
def run(self, command): def run(self, command):
try: try:
self.device.screenshot() self.device.screenshot()
@ -211,7 +223,7 @@ class AzurLaneAutoScript:
method = self.config.Optimization_WhenTaskQueueEmpty method = self.config.Optimization_WhenTaskQueueEmpty
if method == 'close_game': if method == 'close_game':
logger.info('Close game during wait') logger.info('Close game during wait')
self.device.app_stop() self.run('stop')
release_resources() release_resources()
self.device.release_during_wait() self.device.release_during_wait()
if not self.wait_until(task.next_run): if not self.wait_until(task.next_run):

View File

@ -6,6 +6,7 @@ from module.base.timer import Timer
from module.base.utils import * from module.base.utils import *
from module.config.config import AzurLaneConfig from module.config.config import AzurLaneConfig
from module.device.device import Device from module.device.device import Device
from module.device.method.utils import HierarchyButton
from module.logger import logger from module.logger import logger
from module.webui.setting import cached_class_property from module.webui.setting import cached_class_property
@ -132,9 +133,56 @@ class ModuleBase:
return appear return appear
appear = match_template def xpath(self, xpath) -> HierarchyButton:
if isinstance(xpath, str):
return HierarchyButton(self.device.hierarchy, xpath)
else:
return xpath
def xpath_appear(self, xpath: str, interval=0):
button = self.xpath(xpath)
self.device.stuck_record_add(button)
if interval and not self.interval_is_reached(button, interval=interval):
return False
appear = bool(button)
if appear and interval:
self.interval_reset(button, interval=interval)
return appear
def appear(self, button, interval=0, similarity=0.85):
"""
Args:
button (Button, ButtonWrapper, HierarchyButton, str):
interval (int, float): interval between two active events.
Returns:
bool:
Examples:
Template match:
```
self.device.screenshot()
self.appear(POPUP_CONFIRM)
```
Hierarchy detection (detect elements with xpath):
```
self.device.dump_hierarchy()
self.appear('//*[@resource-id="..."]')
```
"""
if isinstance(button, (HierarchyButton, str)):
return self.xpath_appear(button, interval=interval)
else:
return self.match_template(button, interval=interval, similarity=similarity)
def appear_then_click(self, button, interval=5, similarity=0.85): def appear_then_click(self, button, interval=5, similarity=0.85):
button = self.xpath(button)
appear = self.appear(button, interval=interval, similarity=similarity) appear = self.appear(button, interval=interval, similarity=similarity)
if appear: if appear:
self.device.click(button) self.device.click(button)

View File

@ -49,11 +49,14 @@ class AppControl(Adb, WSA, Uiautomator2):
Returns: Returns:
etree._Element: Select elements with `self.hierarchy.xpath('//*[@text="Hermit"]')` for example. etree._Element: Select elements with `self.hierarchy.xpath('//*[@text="Hermit"]')` for example.
""" """
method = self.config.Emulator_ControlMethod # method = self.config.Emulator_ControlMethod
if method in AppControl._app_u2_family: # if method in AppControl._app_u2_family:
# self.hierarchy = self.dump_hierarchy_uiautomator2()
# else:
# self.hierarchy = self.dump_hierarchy_adb()
# Using uiautomator2
self.hierarchy = self.dump_hierarchy_uiautomator2() self.hierarchy = self.dump_hierarchy_uiautomator2()
else:
self.hierarchy = self.dump_hierarchy_adb()
return self.hierarchy return self.hierarchy
def xpath_to_button(self, xpath: str) -> HierarchyButton: def xpath_to_button(self, xpath: str) -> HierarchyButton:

View File

@ -18,9 +18,11 @@ except ImportError:
# We expect `screencap | nc 192.168.0.1 20298` instead of `screencap '|' nc 192.168.80.1 20298` # We expect `screencap | nc 192.168.0.1 20298` instead of `screencap '|' nc 192.168.80.1 20298`
import adbutils import adbutils
import subprocess import subprocess
adbutils._utils.list2cmdline = subprocess.list2cmdline adbutils._utils.list2cmdline = subprocess.list2cmdline
adbutils._device.list2cmdline = subprocess.list2cmdline adbutils._device.list2cmdline = subprocess.list2cmdline
# BaseDevice.shell() is missing a check_okay() call before reading output, # BaseDevice.shell() is missing a check_okay() call before reading output,
# resulting in an `OKAY` prefix in output. # resulting in an `OKAY` prefix in output.
def shell(self, def shell(self,
@ -40,6 +42,7 @@ except ImportError:
output = c.read_until_close() output = c.read_until_close()
return output.rstrip() if rstrip else output return output.rstrip() if rstrip else output
adbutils._device.BaseDevice.shell = shell adbutils._device.BaseDevice.shell = shell
from module.base.decorator import cached_property from module.base.decorator import cached_property
@ -323,7 +326,7 @@ class HierarchyButton:
if res: if res:
return res[0] return res[0]
else: else:
return 'HierarchyButton' return self.xpath
@cached_property @cached_property
def count(self): def count(self):
@ -333,10 +336,17 @@ class HierarchyButton:
def exist(self): def exist(self):
return self.count == 1 return self.count == 1
@cached_property
def attrib(self):
if self.exist:
return self.nodes[0].attrib
else:
return {}
@cached_property @cached_property
def area(self): def area(self):
if self.exist: if self.exist:
bounds = self.nodes[0].attrib.get("bounds") bounds = self.attrib.get("bounds")
lx, ly, rx, ry = map(int, re.findall(r"\d+", bounds)) lx, ly, rx, ry = map(int, re.findall(r"\d+", bounds))
return lx, ly, rx, ry return lx, ly, rx, ry
else: else:
@ -355,6 +365,28 @@ class HierarchyButton:
@cached_property @cached_property
def focused(self): def focused(self):
if self.exist: if self.exist:
return self.nodes[0].attrib.get("focused").lower() == 'true' return self.attrib.get("focused").lower() == 'true'
else: else:
return False return False
@cached_property
def text(self):
if self.exist:
return self.attrib.get("text").strip()
else:
return ""
class AreaButton:
def __init__(self, area, name='AREA_BUTTON'):
self.area = area
self.color = ()
self.name = name
self.button = area
def __str__(self):
return self.name
def __bool__(self):
# Cannot appear
return False

4
src.py
View File

@ -11,6 +11,10 @@ class StarRailCopilot(AzurLaneAutoScript):
from tasks.login.login import Login from tasks.login.login import Login
Login(self.config, device=self.device).app_start() Login(self.config, device=self.device).app_start()
def stop(self):
from tasks.login.login import Login
Login(self.config, device=self.device).app_stop()
def goto_main(self): def goto_main(self):
from tasks.login.login import Login from tasks.login.login import Login
from tasks.base.ui import UI from tasks.base.ui import UI

View File

@ -5,13 +5,22 @@ from module.base.button import Button, ButtonWrapper
LOGIN_CONFIRM = ButtonWrapper( LOGIN_CONFIRM = ButtonWrapper(
name='LOGIN_CONFIRM', name='LOGIN_CONFIRM',
share=Button( share=[
Button(
file='./assets/share/login/LOGIN_CONFIRM.png', file='./assets/share/login/LOGIN_CONFIRM.png',
area=(1188, 44, 1220, 74), area=(1188, 44, 1220, 74),
search=(1168, 24, 1240, 94), search=(1168, 24, 1240, 94),
color=(140, 124, 144), color=(140, 124, 144),
button=(683, 327, 1143, 620), button=(683, 327, 1143, 620),
), ),
Button(
file='./assets/share/login/LOGIN_CONFIRM.2.png',
area=(1109, 48, 1139, 73),
search=(1089, 28, 1159, 93),
color=(149, 145, 164),
button=(1109, 48, 1139, 73),
),
],
) )
LOGIN_LOADING = ButtonWrapper( LOGIN_LOADING = ButtonWrapper(
name='LOGIN_LOADING', name='LOGIN_LOADING',

216
tasks/login/cloud.py Normal file
View File

@ -0,0 +1,216 @@
import re
from module.base.base import ModuleBase
from module.base.timer import Timer
from module.base.utils import area_offset
from module.device.method.utils import AreaButton
from module.exception import GameNotRunningError, RequestHumanTakeover
from module.logger import logger
class XPath:
# 帐号登录界面的进入游戏按钮,有这按钮说明帐号没登录
ACCOUNT_LOGIN = '//*[@text="进入游戏"]'
# 登录后的弹窗,获得免费时长
GET_REWARD = '//*[@text="点击空白区域关闭"]'
# 补丁资源已更新,重启游戏可活动更好的游玩体验
# - 下次再说 - 关闭游戏
POPUP_TITLE = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/titleTv"]'
POPUP_CONFIRM = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/confirmTv"]'
POPUP_CANCEL = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/cancelTv"]'
# 畅玩卡的剩余时间
REMAIN_SEASON_PASS = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tvCardStatus"]'
# 星云币时长0 分钟
REMAIN_PAID = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tvMiCoinDuration"]'
# 免费时长: 600 分钟
REMAIN_FREE = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tvRemainingFreeTime"]'
# 主界面的开始游戏按钮
START_GAME = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/btnLauncher"]'
# 悬浮窗
FLOAT_WINDOW = '//*[@class="android.widget.ImageView"]'
# 悬浮窗内的延迟
# 将这个区域向右偏移作为退出悬浮窗的按钮
FLOAT_DELAY = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tv_delay"]'
'//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/ivPingIcon"]'
'//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tv_ping"]'
class LoginAndroidCloud(ModuleBase):
def _cloud_start(self, skip_first=False):
"""
Pages:
out: START_GAME
"""
logger.hr('Cloud start')
update_checker = Timer(2)
while 1:
if skip_first:
skip_first = False
else:
self.device.dump_hierarchy()
# End
if self.appear(XPath.START_GAME):
logger.info('Login to cloud main page')
break
if self.appear(XPath.ACCOUNT_LOGIN):
logger.critical('Account not login, you must have login once before running')
raise RequestHumanTakeover
if update_checker.started() and update_checker.reached():
if not self.device.app_is_running():
logger.error('Detected hot fixes from game server, game died')
raise GameNotRunningError('Game not running')
update_checker.clear()
# Click
if self.appear_then_click(XPath.GET_REWARD):
continue
if self.appear_then_click(XPath.POPUP_CONFIRM):
update_checker.start()
continue
def _cloud_get_remain(self):
"""
Pages:
in: START_GAME
"""
regex = re.compile(r'(\d+)')
text = self.xpath(XPath.REMAIN_SEASON_PASS).text
logger.info(f'Remain season pass: {text}')
if res := regex.search(text):
season_pass = int(res.group(1))
else:
season_pass = 0
text = self.xpath(XPath.REMAIN_PAID).text
logger.info(f'Remain paid: {text}')
if res := regex.search(text):
paid = int(res.group(1))
else:
paid = 0
text = self.xpath(XPath.REMAIN_FREE).text
logger.info(f'Remain free: {text}')
if res := regex.search(text):
free = int(res.group(1))
else:
free = 0
logger.info(f'Cloud remain: season pass {season_pass} days, {paid} min paid, {free} min free')
with self.config.multi_set():
self.config.stored.CloudRemainSeasonPass = season_pass
self.config.stored.CloudRemainPaid = paid
self.config.stored.CloudRemainFree = free
def _cloud_enter(self, skip_first=False):
"""
Pages:
out: START_GAME
"""
logger.hr('Cloud enter')
while 1:
if skip_first:
skip_first = False
else:
self.device.dump_hierarchy()
# End
if self.appear(XPath.FLOAT_WINDOW):
logger.info('Cloud game entered')
break
# Click
if self.appear_then_click(XPath.START_GAME):
continue
if self.appear(XPath.POPUP_CONFIRM, interval=5):
# 计费提示
# 本次游戏将使用畅玩卡无限畅玩
# - 进入游戏(9s) - 退出游戏
title = self.xpath(XPath.POPUP_TITLE).text
logger.info(f'Popup: {title}')
if title == '计费提示':
self.device.click(self.xpath(XPath.POPUP_CONFIRM))
continue
# 连接中断
# 因为您长时间未操作游戏,已中断连接,错误码: -1022
# - 退出游戏
if title == '连接中断':
self.device.click(self.xpath(XPath.POPUP_CONFIRM))
continue
def _cloud_setting_enter(self, skip_first=True):
while 1:
if skip_first:
skip_first = False
else:
self.device.dump_hierarchy()
if self.appear(XPath.FLOAT_DELAY):
break
if self.appear_then_click(XPath.FLOAT_WINDOW, interval=3):
continue
def _cloud_setting_exit(self, skip_first=True):
while 1:
if skip_first:
skip_first = False
else:
self.device.dump_hierarchy()
if self.appear(XPath.FLOAT_WINDOW):
break
if self.appear(XPath.FLOAT_DELAY, interval=3):
area = self.xpath(XPath.FLOAT_DELAY).area
area = area_offset(area, offset=(150, 0))
button = AreaButton(area=area, name='CLOUD_SETTING_EXIT')
self.device.click(button)
continue
def cloud_ensure_ingame(self):
logger.hr('Cloud ensure ingame', level=1)
for _ in range(3):
if self.device.app_is_running():
logger.info('Cloud game is already running')
self.device.dump_hierarchy()
if self.appear(XPath.START_GAME):
logger.info('Cloud game is in main page')
self._cloud_get_remain()
self._cloud_enter()
return True
elif self.appear(XPath.FLOAT_WINDOW):
logger.info('Cloud game is in game')
return True
elif self.appear(XPath.FLOAT_DELAY):
logger.info('Cloud game is in game with float window expanded')
self._cloud_setting_exit()
return True
elif self.appear(XPath.POPUP_CONFIRM):
logger.info('Cloud game have a popup')
self._cloud_enter()
return True
else:
try:
self._cloud_start()
except GameNotRunningError:
continue
self._cloud_get_remain()
self._cloud_enter()
return True
else:
logger.info('Cloud game is not running')
self.device.app_start()
try:
self._cloud_start()
except GameNotRunningError:
continue
self._cloud_get_remain()
self._cloud_enter()
return True
logger.error('Failed to enter cloud game after 3 trials')
return False

View File

@ -3,10 +3,11 @@ from module.exception import GameNotRunningError
from module.logger import logger from module.logger import logger
from tasks.base.page import page_main from tasks.base.page import page_main
from tasks.base.ui import UI from tasks.base.ui import UI
from tasks.login.assets.assets_login import LOGIN_CONFIRM, USER_AGREEMENT_ACCEPT, LOGIN_LOADING from tasks.login.assets.assets_login import LOGIN_CONFIRM, LOGIN_LOADING, USER_AGREEMENT_ACCEPT
from tasks.login.cloud import LoginAndroidCloud
class Login(UI): class Login(UI, LoginAndroidCloud):
def _handle_app_login(self): def _handle_app_login(self):
""" """
Pages: Pages:
@ -86,12 +87,33 @@ class Login(UI):
def app_start(self): def app_start(self):
logger.hr('App start') logger.hr('App start')
if self.config.is_cloud_game:
self.cloud_ensure_ingame()
else:
self.device.app_start() self.device.app_start()
self.handle_app_login() self.handle_app_login()
def app_restart(self): def app_restart(self):
logger.hr('App restart') logger.hr('App restart')
self.device.app_stop() self.device.app_stop()
if self.config.is_cloud_game:
self.cloud_ensure_ingame()
else:
self.device.app_start() self.device.app_start()
self.handle_app_login() self.handle_app_login()
self.config.task_delay(server_update=True) self.config.task_delay(server_update=True)
def cloud_start(self):
if not self.config.is_cloud_game:
return
logger.hr('Cloud start')
self.cloud_ensure_ingame()
self.handle_app_login()
def cloud_stop(self):
if not self.config.is_cloud_game:
return
logger.hr('Cloud stop')
self.app_stop()