Merge pull request #377 from LmeSzinc/dev

Support cloud HSR on Android | 支持云星穹铁道安卓端
This commit is contained in:
LmeSzinc 2024-03-20 19:13:55 +08:00 committed by GitHub
commit 5b710eff47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 995 additions and 99 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -2,6 +2,7 @@
"Alas": {
"Emulator": {
"Serial": "auto",
"GameClient": "android",
"PackageName": "auto",
"GameLanguage": "auto",
"ScreenshotMethod": "scrcpy",
@ -23,6 +24,11 @@
"ScreenshotInterval": 0.2,
"CombatScreenshotInterval": 1.0,
"WhenTaskQueueEmpty": "goto_main"
},
"CloudStorage": {
"CloudRemainSeasonPass": {},
"CloudRemainPaid": {},
"CloudRemainFree": {}
}
},
"Restart": {

View File

@ -92,6 +92,7 @@ def iter_images():
for server in ASSET_SERVER:
for path, folders, files in os.walk(os.path.join(AzurLaneConfig.ASSETS_FOLDER, server)):
for file in files:
if not file.startswith('.'):
file = os.path.join(path, file).replace('\\', '/')
yield AssetsImage(file)

View File

@ -62,6 +62,18 @@ class AzurLaneAutoScript:
logger.exception(e)
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):
try:
self.device.screenshot()
@ -211,7 +223,7 @@ class AzurLaneAutoScript:
method = self.config.Optimization_WhenTaskQueueEmpty
if method == 'close_game':
logger.info('Close game during wait')
self.device.app_stop()
self.run('stop')
release_resources()
self.device.release_during_wait()
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.config.config import AzurLaneConfig
from module.device.device import Device
from module.device.method.utils import HierarchyButton
from module.logger import logger
from module.webui.setting import cached_class_property
@ -132,9 +133,56 @@ class ModuleBase:
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):
button = self.xpath(button)
appear = self.appear(button, interval=interval, similarity=similarity)
if appear:
self.device.click(button)

View File

@ -6,6 +6,14 @@
"value": "auto",
"valuetype": "str"
},
"GameClient": {
"type": "select",
"value": "android",
"option": [
"android",
"cloud_android"
]
},
"PackageName": {
"type": "select",
"value": "auto",
@ -131,6 +139,26 @@
"close_game"
]
}
},
"CloudStorage": {
"CloudRemainSeasonPass": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredInt"
},
"CloudRemainPaid": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredInt"
},
"CloudRemainFree": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredInt"
}
}
},
"Restart": {
@ -165,11 +193,13 @@
"Dungeon": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"type": "state",
"value": true,
"option": [
true,
false
true
],
"option_bold": [
true
]
},
"NextRun": {
@ -243,7 +273,6 @@
"type": "select",
"value": "Calyx_Golden_Treasures",
"option": [
"do_not_participate",
"Calyx_Golden_Memories_Jarilo_VI",
"Calyx_Golden_Memories_The_Xianzhou_Luofu",
"Calyx_Golden_Memories_Penacony",
@ -269,7 +298,6 @@
"type": "select",
"value": "Cavern_of_Corrosion_Path_of_Providence",
"option": [
"do_not_participate",
"Cavern_of_Corrosion_Path_of_Gelid_Wind",
"Cavern_of_Corrosion_Path_of_Jabbing_Punch",
"Cavern_of_Corrosion_Path_of_Drifting",
@ -476,11 +504,13 @@
"DailyQuest": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"type": "state",
"value": true,
"option": [
true,
false
true
],
"option_bold": [
true
]
},
"NextRun": {
@ -911,11 +941,13 @@
"BattlePass": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"type": "state",
"value": true,
"option": [
true,
false
true
],
"option_bold": [
true
]
},
"NextRun": {
@ -996,11 +1028,13 @@
"Assignment": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"type": "state",
"value": true,
"option": [
true,
false
true
],
"option_bold": [
true
]
},
"NextRun": {

View File

@ -18,6 +18,9 @@ Emulator:
Serial:
value: auto
valuetype: str
GameClient:
value: android
option: [ android, cloud_android ]
PackageName:
value: auto
option: [ auto, ]
@ -72,6 +75,13 @@ Optimization:
WhenTaskQueueEmpty:
value: goto_main
option: [ stay_there, goto_main, close_game ]
CloudStorage:
CloudRemainSeasonPass:
stored: StoredInt
CloudRemainPaid:
stored: StoredInt
CloudRemainFree:
stored: StoredInt
# ==================== Daily ====================
@ -82,10 +92,10 @@ Dungeon:
option: [ ]
NameAtDoubleCalyx:
value: Calyx_Golden_Treasures
option: [ do_not_participate, ]
option: [ ]
NameAtDoubleRelic:
value: Cavern_of_Corrosion_Path_of_Providence
option: [ do_not_participate, ]
option: [ ]
Team:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

View File

@ -23,6 +23,34 @@ Restart:
# ==================== Daily ====================
Dungeon:
Scheduler:
Enable:
type: state
value: true
option: [ true, ]
option_bold: [ true, ]
DailyQuest:
Scheduler:
Enable:
type: state
value: true
option: [ true, ]
option_bold: [ true, ]
BattlePass:
Scheduler:
Enable:
type: state
value: true
option: [ true, ]
option_bold: [ true, ]
Assignment:
Scheduler:
Enable:
type: state
value: true
option: [ true, ]
option_bold: [ true, ]
DataUpdate:
Scheduler:
Enable:

View File

@ -101,6 +101,42 @@
"order": 8,
"color": "#fc8f8b"
},
"CloudRemainSeasonPass": {
"name": "CloudRemainSeasonPass",
"path": "Alas.CloudStorage.CloudRemainSeasonPass",
"i18n": "CloudStorage.CloudRemainSeasonPass.name",
"stored": "StoredInt",
"attrs": {
"time": "2020-01-01 00:00:00",
"value": 0
},
"order": 0,
"color": "#777777"
},
"CloudRemainPaid": {
"name": "CloudRemainPaid",
"path": "Alas.CloudStorage.CloudRemainPaid",
"i18n": "CloudStorage.CloudRemainPaid.name",
"stored": "StoredInt",
"attrs": {
"time": "2020-01-01 00:00:00",
"value": 0
},
"order": 0,
"color": "#777777"
},
"CloudRemainFree": {
"name": "CloudRemainFree",
"path": "Alas.CloudStorage.CloudRemainFree",
"i18n": "CloudStorage.CloudRemainFree.name",
"stored": "StoredInt",
"attrs": {
"time": "2020-01-01 00:00:00",
"value": 0
},
"order": 0,
"color": "#777777"
},
"Immersifier": {
"name": "Immersifier",
"path": "Dungeon.DungeonStorage.Immersifier",

View File

@ -13,6 +13,7 @@ Alas:
- EmulatorInfo
- Error
- Optimization
- CloudStorage
Restart:
- Scheduler

View File

@ -176,6 +176,12 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher
self.data, keys="Alas.Optimization.CloseGameDuringWait", default=False
)
@property
def is_cloud_game(self):
return deep_get(
self.data, keys="Alas.Emulator.GameClient"
) == 'cloud_android'
@cached_property
def stored(self) -> StoredGenerated:
stored = StoredGenerated()

View File

@ -17,6 +17,7 @@ class GeneratedConfig:
# Group `Emulator`
Emulator_Serial = 'auto'
Emulator_GameClient = 'android' # android, cloud_android
Emulator_PackageName = 'auto' # auto, CN-Official, CN-Bilibili, OVERSEA-America, OVERSEA-Asia, OVERSEA-Europe, OVERSEA-TWHKMO
Emulator_GameLanguage = 'auto' # auto, cn, en
Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy
@ -39,10 +40,15 @@ class GeneratedConfig:
Optimization_CombatScreenshotInterval = 1.0
Optimization_WhenTaskQueueEmpty = 'goto_main' # stay_there, goto_main, close_game
# Group `CloudStorage`
CloudStorage_CloudRemainSeasonPass = {}
CloudStorage_CloudRemainPaid = {}
CloudStorage_CloudRemainFree = {}
# Group `Dungeon`
Dungeon_Name = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Rime, Stagnant_Shadow_Icicle, Stagnant_Shadow_Nectar, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Doom, Stagnant_Shadow_Gust, Stagnant_Shadow_Celestial, Stagnant_Shadow_Quanta, Stagnant_Shadow_Abomination, Stagnant_Shadow_Roast, Stagnant_Shadow_Mirage, Stagnant_Shadow_Puppetry, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
Dungeon_NameAtDoubleCalyx = 'Calyx_Golden_Treasures' # do_not_participate, Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission
Dungeon_NameAtDoubleRelic = 'Cavern_of_Corrosion_Path_of_Providence' # do_not_participate, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
Dungeon_NameAtDoubleCalyx = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission
Dungeon_NameAtDoubleRelic = 'Cavern_of_Corrosion_Path_of_Providence' # Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
Dungeon_Team = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
# Group `DungeonDaily`

View File

@ -795,6 +795,9 @@ class ConfigUpdater:
# Store immersifier in dungeon task
if deep_get(data, keys='Rogue.RogueWorld.UseImmersifier') is True:
deep_set(data, keys='Dungeon.Scheduler.Enable', value=True)
# Cloud settings
if deep_get(data, keys='Alas.Emulator.GameClient') == 'cloud_android':
deep_set(data, keys='Alas.Emulator.PackageName', value='CN-Official')
return data
@ -842,6 +845,9 @@ class ConfigUpdater:
yield 'Rogue.RogueWorld.UseImmersifier', True
elif key == 'Rogue.RogueWorld.DoubleEvent' and value is True:
yield 'Rogue.RogueWorld.UseImmersifier', True
elif key == 'Alas.Emulator.GameClient' and value == 'cloud_android':
yield 'Alas.Emulator.PackageName', 'CN-Official'
yield 'Alas.Optimization.WhenTaskQueueEmpty', 'close_game'
def iter_hidden_args(self, data) -> t.Iterator[str]:
"""

View File

@ -96,6 +96,12 @@
"name": "Serial",
"help": "Common emulator Serial can be queried in the list below\nUse \"auto\" to auto-detect emulators, but if multiple emulators are running or use emulators that do not support auto-detect, \"auto\" cannot be used and serial must be filled in manually\nDefault serial for select emulators:\n- BlueStacks 127.0.0.1:5555\n- BlueStacks4 Hyper-V use \"bluestacks4-hyperv\", \"bluestacks4-hyperv-2\" for multi instance, and so on\n- BlueStacks5 Hyper-V use \"bluestacks5-hyperv\", \"bluestacks5-hyperv-1\" for multi instance, and so on\n- NoxPlayer 127.0.0.1:62001\n- NoxPlayer64bit 127.0.0.1:59865\n- MuMuPlayer/MuMuPlayer X 127.0.0.1:7555\n- MuMuPlayer12 127.0.0.1:16384\n- MemuPlayer 127.0.0.1:21503\n- LDPlayer emulator-5554 or 127.0.0.1:5555\n- WSA use \"wsa-0\" to make the game run in the background, which needs to be controlled or closed by third-party software\nIf there are multiple emulator instances running, the default is reserved for one of them and the others will use different serials to avoid conflicts\nOpen console.bat and run `adb devices` to find them or follow the emulator's official tutorial"
},
"GameClient": {
"name": "Game Client",
"help": "When using cloud, you will be automatically queued to log in. Cloud game is available in CN only",
"android": "Android client",
"cloud_android": "Cloud game android client"
},
"PackageName": {
"name": "Game Server",
"help": "Can't distinguish different regions of oversea servers, please select the server manually.",
@ -215,6 +221,24 @@
"close_game": "Close Game"
}
},
"CloudStorage": {
"_info": {
"name": "CloudStorage._info.name",
"help": "CloudStorage._info.help"
},
"CloudRemainSeasonPass": {
"name": "CloudStorage.CloudRemainSeasonPass.name",
"help": "CloudStorage.CloudRemainSeasonPass.help"
},
"CloudRemainPaid": {
"name": "CloudStorage.CloudRemainPaid.name",
"help": "CloudStorage.CloudRemainPaid.help"
},
"CloudRemainFree": {
"name": "CloudStorage.CloudRemainFree.name",
"help": "CloudStorage.CloudRemainFree.help"
}
},
"Dungeon": {
"_info": {
"name": "Dungeon Settings",
@ -271,7 +295,6 @@
"NameAtDoubleCalyx": {
"name": "At Double Calyx Event, choose dungeon",
"help": "Return to the default dungeon settings after double times exhausted",
"do_not_participate": "Dont participate in event",
"Calyx_Golden_Memories_Jarilo_VI": "Material: Character EXP (Bud of Memories (Jarilo-Ⅵ))",
"Calyx_Golden_Memories_The_Xianzhou_Luofu": "Material: Character EXP (Bud of Memories (The Xianzhou Luofu))",
"Calyx_Golden_Memories_Penacony": "Material: Character EXP (Bud of Memories (Penacony))",
@ -295,7 +318,6 @@
"NameAtDoubleRelic": {
"name": "At Double Relic Event, choose dungeon",
"help": "Return to the default dungeon settings after double times exhausted",
"do_not_participate": "Dont participate in event",
"Cavern_of_Corrosion_Path_of_Gelid_Wind": "Relics: Ice Set & Wind Set (Path of Gelid Wind)",
"Cavern_of_Corrosion_Path_of_Jabbing_Punch": "Relics: Physical Set & Break Effect Set (Path of Jabbing Punch)",
"Cavern_of_Corrosion_Path_of_Drifting": "Relics: Healing Set & Musketeer Set (Path of Drifting)",

View File

@ -96,6 +96,12 @@
"name": "Serial",
"help": "Los serial de emuladores comunes pueden ser consultados aquí:\nUsa \"auto\" para detectar automáticamente el emulador, pero si hay varios ejecutando a la vez o no se puede detectar de forma automática, \"auto\" no podrá ser usado y tendrás que ajustarlo manualmente.\nSerial comunes de algunos emuladores:\n- BlueStacks 127.0.0.1:5555\n- BlueStacks4 Hyper-V usa \"bluestacks4-hyperv\", \"bluestacks4-hyperv-2\" para multi-instancia, etc...\n- BlueStacks5 Hyper-V usa \"bluestacks5-hyperv\", \"bluestacks5-hyperv-1\" para multi-instancia, etc...\n- NoxPlayer 127.0.0.1:62001\n- NoxPlayer64bit 127.0.0.1:59865\n- MuMuPlayer/MuMuPlayer X 127.0.0.1:7555\n- MuMuPlayer12 127.0.0.1:16384\n- MemuPlayer 127.0.0.1:21503\n- LDPlayer emulator-5554 or 127.0.0.1:5555\n- WSA usa \"wsa-0\" para ejecutar el juego en segundo plano, que será cerrado/controlado por software externo.\nSi hay varias instancias ejecutándose a la vez, se usará la predeterminada para la primera y las demás se reajustarán.\nAbre console.bat y ejecuta `adb devices` para encontrarlas o usa la guía oficial del emulador."
},
"GameClient": {
"name": "Cliente del juego",
"help": "Al usar la nube, automáticamente se pondrá en cola para iniciar sesión. El juego en la nube está disponible solo en CN",
"android": "Cliente Android",
"cloud_android": "Cliente de juego en la nube para Android"
},
"PackageName": {
"name": "Región del juego",
"help": "No es posible detectar regiones que no sean de China, por tanto, ajústalo manualmente si se da la situación.",
@ -215,6 +221,24 @@
"close_game": "Cerrar el juego"
}
},
"CloudStorage": {
"_info": {
"name": "CloudStorage._info.name",
"help": "CloudStorage._info.help"
},
"CloudRemainSeasonPass": {
"name": "CloudStorage.CloudRemainSeasonPass.name",
"help": "CloudStorage.CloudRemainSeasonPass.help"
},
"CloudRemainPaid": {
"name": "CloudStorage.CloudRemainPaid.name",
"help": "CloudStorage.CloudRemainPaid.help"
},
"CloudRemainFree": {
"name": "CloudStorage.CloudRemainFree.name",
"help": "CloudStorage.CloudRemainFree.help"
}
},
"Dungeon": {
"_info": {
"name": "Ajustes de Mazmorra",
@ -271,7 +295,6 @@
"NameAtDoubleCalyx": {
"name": "En los eventos de x2 de Cáliz",
"help": "Se volverán a los ajustes predeterminados una vez se acaben los intentos del evento",
"do_not_participate": "No participar en el evento",
"Calyx_Golden_Memories_Jarilo_VI": "Material: EXP de personaje (Flor de los recuerdos (Jarilo-Ⅵ))",
"Calyx_Golden_Memories_The_Xianzhou_Luofu": "Material: EXP de personaje (Flor de los recuerdos (El Luofu de Xianzhou))",
"Calyx_Golden_Memories_Penacony": "Material: EXP de personaje (Flor de los recuerdos (Colonipenal))",
@ -295,7 +318,6 @@
"NameAtDoubleRelic": {
"name": "En los eventos de x2 de Caverna de la corrosión",
"help": "Se volverán a los ajustes predeterminados una vez se acaben los intentos del evento",
"do_not_participate": "No participar en el evento",
"Cavern_of_Corrosion_Path_of_Gelid_Wind": "Artefactos: Hielo y Viento (Senda del viento gélido)",
"Cavern_of_Corrosion_Path_of_Jabbing_Punch": "Artefactos: Físico y Efecto de Ruptura (Senda de los puños rápidos)",
"Cavern_of_Corrosion_Path_of_Drifting": "Artefactos: Curación y Pistolera de la espiga silvestre (Senda de la deriva)",

View File

@ -96,6 +96,12 @@
"name": "Emulator.Serial.name",
"help": "Emulator.Serial.help"
},
"GameClient": {
"name": "Emulator.GameClient.name",
"help": "Emulator.GameClient.help",
"android": "android",
"cloud_android": "cloud_android"
},
"PackageName": {
"name": "Emulator.PackageName.name",
"help": "Emulator.PackageName.help",
@ -215,6 +221,24 @@
"close_game": "close_game"
}
},
"CloudStorage": {
"_info": {
"name": "CloudStorage._info.name",
"help": "CloudStorage._info.help"
},
"CloudRemainSeasonPass": {
"name": "CloudStorage.CloudRemainSeasonPass.name",
"help": "CloudStorage.CloudRemainSeasonPass.help"
},
"CloudRemainPaid": {
"name": "CloudStorage.CloudRemainPaid.name",
"help": "CloudStorage.CloudRemainPaid.help"
},
"CloudRemainFree": {
"name": "CloudStorage.CloudRemainFree.name",
"help": "CloudStorage.CloudRemainFree.help"
}
},
"Dungeon": {
"_info": {
"name": "Dungeon._info.name",
@ -271,7 +295,6 @@
"NameAtDoubleCalyx": {
"name": "Dungeon.NameAtDoubleCalyx.name",
"help": "Dungeon.NameAtDoubleCalyx.help",
"do_not_participate": "do_not_participate",
"Calyx_Golden_Memories_Jarilo_VI": "素材:役割経験(回憶の蕾・ヤリーロ-Ⅵ):",
"Calyx_Golden_Memories_The_Xianzhou_Luofu": "素材:役割経験(回憶の蕾・仙舟羅浮):",
"Calyx_Golden_Memories_Penacony": "素材:役割経験(回憶の蕾・ピノコニー):",
@ -295,7 +318,6 @@
"NameAtDoubleRelic": {
"name": "Dungeon.NameAtDoubleRelic.name",
"help": "Dungeon.NameAtDoubleRelic.help",
"do_not_participate": "do_not_participate",
"Cavern_of_Corrosion_Path_of_Gelid_Wind": "侵蝕トンネル・霜風の路(侵蝕トンネル・霜風の路)",
"Cavern_of_Corrosion_Path_of_Jabbing_Punch": "侵蝕トンネル・迅拳の路(侵蝕トンネル・迅拳の路)",
"Cavern_of_Corrosion_Path_of_Drifting": "侵蝕トンネル・漂泊の路(侵蝕トンネル・漂泊の路)",

View File

@ -96,6 +96,12 @@
"name": "模拟器 Serial",
"help": "常见的模拟器 Serial 可以查询下方列表\n填 \"auto\" 自动检测模拟器,多个模拟器正在运行或使用不支持自动检测的模拟器时无法使用 \"auto\",必须手动填写\n\n模拟器默认 Serial\n- 蓝叠模拟器 127.0.0.1:5555\n- 蓝叠模拟器4 Hyper-v版填\"bluestacks4-hyperv\"自动连接,多开填\"bluestacks4-hyperv-2\"以此类推\n- 蓝叠模拟器5 Hyper-v版填\"bluestacks5-hyperv\"自动连接,多开填\"bluestacks5-hyperv-1\"以此类推\n- 夜神模拟器 127.0.0.1:62001\n- 夜神模拟器64位 127.0.0.1:59865\n- MuMu模拟器/MuMu模拟器X 127.0.0.1:7555\n- MuMu模拟器12 127.0.0.1:16384\n- 逍遥模拟器 127.0.0.1:21503\n- 雷电模拟器 emulator-5554 或 127.0.0.1:5555\n- WSA填\"wsa-0\"使游戏在后台运行需要使用第三方软件操控或关闭建议使用scrcpy操控\n如果你使用了模拟器的多开功能它们的 Serial 将不是默认的,可以在 console.bat 中执行 `adb devices` 查询,或根据模拟器官方的教程填写"
},
"GameClient": {
"name": "游戏客户端",
"help": "选择云游戏时,将自动排队登录,云游戏目前仅有国服",
"android": "安卓端",
"cloud_android": "云游戏安卓端"
},
"PackageName": {
"name": "游戏服务器",
"help": "无法区分国际服的不同地区,请手动选择服务器",
@ -215,6 +221,24 @@
"close_game": "关闭游戏"
}
},
"CloudStorage": {
"_info": {
"name": "",
"help": ""
},
"CloudRemainSeasonPass": {
"name": "畅玩卡剩余 X 天",
"help": ""
},
"CloudRemainPaid": {
"name": "星云币剩余 X 分钟",
"help": ""
},
"CloudRemainFree": {
"name": "免费时长剩余 X 分钟",
"help": ""
}
},
"Dungeon": {
"_info": {
"name": "每日副本设置",
@ -271,7 +295,6 @@
"NameAtDoubleCalyx": {
"name": "有双倍花活动时,选择副本",
"help": "次数耗尽后回退到默认打本设置",
"do_not_participate": "不参与活动",
"Calyx_Golden_Memories_Jarilo_VI": "材料:角色经验(回忆之蕾•雅利洛-Ⅵ)",
"Calyx_Golden_Memories_The_Xianzhou_Luofu": "材料:角色经验(回忆之蕾•仙舟罗浮)",
"Calyx_Golden_Memories_Penacony": "材料:角色经验(回忆之蕾•匹诺康尼)",
@ -295,7 +318,6 @@
"NameAtDoubleRelic": {
"name": "有遗器活动时,选择副本",
"help": "次数耗尽后回退到默认打本设置",
"do_not_participate": "不参与活动",
"Cavern_of_Corrosion_Path_of_Gelid_Wind": "遗器:冰套+风套(霜风之径•侵蚀隧洞)",
"Cavern_of_Corrosion_Path_of_Jabbing_Punch": "遗器:物理套+击破套(迅拳之径•侵蚀隧洞)",
"Cavern_of_Corrosion_Path_of_Drifting": "遗器:治疗套+快枪手(漂泊之径•侵蚀隧洞)",

View File

@ -96,6 +96,12 @@
"name": "模擬器 Serial",
"help": "常見的模擬器 Serial 可以查詢下方列表\n填 \"auto\" 自動檢測模擬器,多個模擬器正在運行或使用不支援自動檢測的模擬器時無法使用 \"auto\",必須手動填寫\n模擬器預設 Serial\n- 藍疊模擬器 127.0.0.1:5555\n- 藍疊模擬器4 Hyper-v版填\"bluestacks4-hyperv\"自動連接,多開填\"bluestacks4-hyperv-2\"以此類推\n- 藍疊模擬器5 Hyper-v版填\"bluestacks5-hyperv\"自動連接,多開填\"bluestacks5-hyperv-1\"以此類推\n- 夜神模擬器 127.0.0.1:62001\n- 夜神模擬器64位元 127.0.0.1:59865\n- MuMu模擬器/MuMu模擬器X 127.0.0.1:7555\n- MuMu模擬器12 127.0.0.1:16384\n- 逍遙模擬器 127.0.0.1:21503\n- 雷電模擬器 emulator-5554 或 127.0.0.1:5555\n- WSA填\"wsa-0\"使遊戲在後臺運行,需要使用第三方軟件操控或關閉\n如果你使用了模擬器的多開功能他們的 Serial 將不是預設的,可以在 console.bat 中執行 `adb devices` 查詢,或根據模擬器官方的教程填寫"
},
"GameClient": {
"name": "遊戲客戶端",
"help": "選擇雲端遊戲時,將自動排隊登入,雲端遊戲目前僅有國服",
"android": "安卓端",
"cloud_android": "雲端遊戲安卓端"
},
"PackageName": {
"name": "遊戲伺服器",
"help": "無法區分國際服的不同地區,請手動選擇伺服器",
@ -215,6 +221,24 @@
"close_game": "關閉遊戲"
}
},
"CloudStorage": {
"_info": {
"name": "CloudStorage._info.name",
"help": "CloudStorage._info.help"
},
"CloudRemainSeasonPass": {
"name": "CloudStorage.CloudRemainSeasonPass.name",
"help": "CloudStorage.CloudRemainSeasonPass.help"
},
"CloudRemainPaid": {
"name": "CloudStorage.CloudRemainPaid.name",
"help": "CloudStorage.CloudRemainPaid.help"
},
"CloudRemainFree": {
"name": "CloudStorage.CloudRemainFree.name",
"help": "CloudStorage.CloudRemainFree.help"
}
},
"Dungeon": {
"_info": {
"name": "每日副本設定",
@ -271,7 +295,6 @@
"NameAtDoubleCalyx": {
"name": "有雙倍花活動時,選擇副本",
"help": "次數耗儘後回退到默認打本設定",
"do_not_participate": "不參與活動",
"Calyx_Golden_Memories_Jarilo_VI": "材料:角色經驗(回憶之蕾•雅利洛-Ⅵ)",
"Calyx_Golden_Memories_The_Xianzhou_Luofu": "材料:角色經驗(回憶之蕾•仙舟羅浮)",
"Calyx_Golden_Memories_Penacony": "材料:角色經驗(回憶之蕾•匹諾康尼)",
@ -295,7 +318,6 @@
"NameAtDoubleRelic": {
"name": "有遺器活動時,選擇副本",
"help": "次數耗儘後回退到默認打本設定",
"do_not_participate": "不參與活動",
"Cavern_of_Corrosion_Path_of_Gelid_Wind": "遺器:冰套+風套(霜風之徑•侵蝕隧洞)",
"Cavern_of_Corrosion_Path_of_Jabbing_Punch": "遺器:物理套+擊破套(迅拳之徑•侵蝕隧洞)",
"Cavern_of_Corrosion_Path_of_Drifting": "遺器:治療套+快槍手(漂泊之徑•侵蝕隧洞)",

View File

@ -15,6 +15,10 @@ VALID_SERVER = {
'OVERSEA-TWHKMO': 'com.HoYoverse.hkrpgoversea',
}
VALID_PACKAGE = set(list(VALID_SERVER.values()))
VALID_CLOUD_SERVER = {
'CN-Official': 'com.miHoYo.cloudgames.hkrpg',
}
VALID_CLOUD_PACKAGE = set(list(VALID_SERVER.values()))
def set_lang(lang_: str):
@ -47,14 +51,26 @@ def to_server(package_or_server: str) -> str:
return key
if key == package_or_server:
return key
for key, value in VALID_CLOUD_SERVER.items():
if value == package_or_server:
return key
if key == package_or_server:
return key
raise ValueError(f'Package invalid: {package_or_server}')
def to_package(package_or_server: str) -> str:
def to_package(package_or_server: str, is_cloud=False) -> str:
"""
Convert package/server to package.
"""
if is_cloud:
for key, value in VALID_CLOUD_SERVER.items():
if value == package_or_server:
return value
if key == package_or_server:
return value
else:
for key, value in VALID_SERVER.items():
if value == package_or_server:
return value

View File

@ -28,6 +28,9 @@ from module.config.stored.classes import (
# ``` python -m module/config/config_updater.py ```
class StoredGenerated:
CloudRemainSeasonPass = StoredInt("Alas.CloudStorage.CloudRemainSeasonPass")
CloudRemainPaid = StoredInt("Alas.CloudStorage.CloudRemainPaid")
CloudRemainFree = StoredInt("Alas.CloudStorage.CloudRemainFree")
TrailblazePower = StoredTrailblazePower("Dungeon.DungeonStorage.TrailblazePower")
Immersifier = StoredImmersifier("Dungeon.DungeonStorage.Immersifier")
DungeonDouble = StoredDungeonDouble("Dungeon.DungeonStorage.DungeonDouble")

View File

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

View File

@ -100,7 +100,9 @@ class Connection(ConnectionAttr):
logger.attr('AdbDevice', self.adb)
# Package
if self.config.Emulator_PackageName == 'auto':
if self.config.is_cloud_game:
self.package = server_.to_package(self.config.Emulator_PackageName, is_cloud=True)
elif self.config.Emulator_PackageName == 'auto':
self.detect_package()
else:
self.package = server_.to_package(self.config.Emulator_PackageName)

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`
import adbutils
import subprocess
adbutils._utils.list2cmdline = subprocess.list2cmdline
adbutils._device.list2cmdline = subprocess.list2cmdline
# BaseDevice.shell() is missing a check_okay() call before reading output,
# resulting in an `OKAY` prefix in output.
def shell(self,
@ -40,6 +42,7 @@ except ImportError:
output = c.read_until_close()
return output.rstrip() if rstrip else output
adbutils._device.BaseDevice.shell = shell
from module.base.decorator import cached_property
@ -323,7 +326,7 @@ class HierarchyButton:
if res:
return res[0]
else:
return 'HierarchyButton'
return self.xpath
@cached_property
def count(self):
@ -333,15 +336,30 @@ class HierarchyButton:
def exist(self):
return self.count == 1
@cached_property
def attrib(self):
if self.exist:
return self.nodes[0].attrib
else:
return {}
@cached_property
def area(self):
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))
return lx, ly, rx, ry
else:
return None
@cached_property
def size(self):
if self.area is not None:
lx, ly, rx, ry = self.area
return rx - lx, ry - ly
else:
return None
@cached_property
def button(self):
return self.area
@ -352,9 +370,82 @@ class HierarchyButton:
def __str__(self):
return self.name
"""
Element props
"""
def _get_bool_prop(self, prop: str) -> bool:
return self.attrib.get(prop, "").lower() == 'true'
@cached_property
def focused(self):
if self.exist:
return self.nodes[0].attrib.get("focused").lower() == 'true'
else:
def index(self) -> int:
try:
return int(self.attrib.get("index", 0))
except IndexError:
return 0
@cached_property
def text(self) -> str:
return self.attrib.get("text", "").strip()
@cached_property
def resourceId(self) -> str:
return self.attrib.get("resourceId", "").strip()
@cached_property
def package(self) -> str:
return self.attrib.get("resourceId", "").strip()
@cached_property
def description(self) -> str:
return self.attrib.get("resourceId", "").strip()
@cached_property
def checkable(self) -> bool:
return self._get_bool_prop('checkable')
@cached_property
def clickable(self) -> bool:
return self._get_bool_prop('clickable')
@cached_property
def enabled(self) -> bool:
return self._get_bool_prop('enabled')
@cached_property
def fucusable(self) -> bool:
return self._get_bool_prop('fucusable')
@cached_property
def focused(self) -> bool:
return self._get_bool_prop('focused')
@cached_property
def scrollable(self) -> bool:
return self._get_bool_prop('scrollable')
@cached_property
def longClickable(self) -> bool:
return self._get_bool_prop('longClickable')
@cached_property
def password(self) -> bool:
return self._get_bool_prop('password')
@cached_property
def selected(self) -> bool:
return self._get_bool_prop('selected')
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
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):
from tasks.login.login import Login
from tasks.base.ui import UI

View File

@ -23,6 +23,16 @@ CHAT_OPTION = ButtonWrapper(
button=(649, 496, 1129, 525),
),
)
DUNGEON_EXIT = ButtonWrapper(
name='DUNGEON_EXIT',
share=Button(
file='./assets/share/base/daemon/DUNGEON_EXIT.png',
area=(582, 598, 606, 622),
search=(562, 578, 626, 642),
color=(106, 99, 89),
button=(582, 598, 606, 622),
),
)
INTERACT_COLLECT = ButtonWrapper(
name='INTERACT_COLLECT',
share=Button(
@ -73,3 +83,33 @@ STORY_OPTION = ButtonWrapper(
button=(813, 453, 1069, 488),
),
)
TUTORIAL_CHECK = ButtonWrapper(
name='TUTORIAL_CHECK',
share=Button(
file='./assets/share/base/daemon/TUTORIAL_CHECK.png',
area=(628, 43, 653, 65),
search=(608, 23, 673, 85),
color=(90, 155, 145),
button=(628, 43, 653, 65),
),
)
TUTORIAL_CLOSE = ButtonWrapper(
name='TUTORIAL_CLOSE',
share=Button(
file='./assets/share/base/daemon/TUTORIAL_CLOSE.png',
area=(579, 634, 700, 669),
search=(559, 614, 720, 689),
color=(215, 213, 215),
button=(579, 634, 700, 669),
),
)
TUTORIAL_NEXT = ButtonWrapper(
name='TUTORIAL_NEXT',
share=Button(
file='./assets/share/base/daemon/TUTORIAL_NEXT.png',
area=(1190, 303, 1240, 365),
search=(1170, 283, 1260, 385),
color=(45, 45, 49),
button=(1190, 303, 1240, 365),
),
)

View File

@ -107,6 +107,16 @@ class Daemon(RouteBase, DaemonBase, AimDetectorMixin):
continue
if self.handle_ui_close(PICTURE_TAKEN, interval=1):
continue
if self.appear_then_click(DUNGEON_EXIT, interval=1.5):
continue
# Tutorial popup
if self.appear(TUTORIAL_CHECK, interval=0.2):
if self.image_color_count(TUTORIAL_CLOSE, color=(255, 255, 255), threshold=180, count=400):
self.device.click(TUTORIAL_CLOSE)
continue
if self.image_color_count(TUTORIAL_NEXT, color=(255, 255, 255), threshold=180, count=50):
self.device.click(TUTORIAL_NEXT)
continue
# Rogue
if self.handle_blessing():
continue

View File

@ -2,7 +2,7 @@ import cv2
from scipy import signal
from module.base.timer import Timer
from module.base.utils import rgb2gray
from module.base.utils import rgb2luma
from module.logger import logger
from tasks.base.ui import UI
from tasks.combat.assets.assets_combat_state import COMBAT_AUTO, COMBAT_PAUSE, COMBAT_SPEED_2X
@ -22,7 +22,7 @@ class CombatState(UI):
return False
def _is_combat_button_active(self, button):
image = rgb2gray(self.image_crop(button))
image = rgb2luma(self.image_crop(button))
lines = cv2.reduce(image, 1, cv2.REDUCE_AVG).flatten()
# [122 122 122 182 141 127 139 135 130 135 136 141 147 149 149 150 147 145
# 148 150 150 150 150 150 144 138 134 141 136 133 173 183 130 128 127 126]

View File

@ -226,8 +226,7 @@ class Dungeon(DungeonStamina, DungeonEvent, Combat):
ran_calyx_crimson = False
ran_cavern_of_corrosion = False
# Double calyx
if self.config.Dungeon_NameAtDoubleCalyx != 'do_not_participate' \
and self.config.stored.DungeonDouble.calyx > 0:
if self.config.stored.DungeonDouble.calyx > 0:
logger.info('Run double calyx')
dungeon = DungeonList.find(self.config.Dungeon_NameAtDoubleCalyx)
self.running_double = True
@ -237,8 +236,7 @@ class Dungeon(DungeonStamina, DungeonEvent, Combat):
if dungeon.is_Calyx_Crimson:
ran_calyx_crimson = True
# Double relic
if self.config.Dungeon_NameAtDoubleRelic != 'do_not_participate' \
and self.config.stored.DungeonDouble.relic > 0:
if self.config.stored.DungeonDouble.relic > 0:
logger.info('Run double relic')
dungeon = DungeonList.find(self.config.Dungeon_NameAtDoubleRelic)
self.running_double = True

View File

@ -5,13 +5,22 @@ from module.base.button import Button, ButtonWrapper
LOGIN_CONFIRM = ButtonWrapper(
name='LOGIN_CONFIRM',
share=Button(
share=[
Button(
file='./assets/share/login/LOGIN_CONFIRM.png',
area=(1188, 44, 1220, 74),
search=(1168, 24, 1240, 94),
color=(140, 124, 144),
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=(683, 327, 1143, 620),
),
],
)
LOGIN_LOADING = ButtonWrapper(
name='LOGIN_LOADING',

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

@ -0,0 +1,361 @@
import re
from module.base.base import ModuleBase
from module.base.timer import Timer
from module.base.utils import area_offset, random_rectangle_vector_opted
from module.device.method.utils import AreaButton
from module.exception import GameNotRunningError, RequestHumanTakeover
from module.logger import logger
class XPath:
"""
xpath 元素元素可通过 uiautomator2 内的 weditor.exe 查找
"""
"""
登录界面元素
"""
# 帐号登录界面的进入游戏按钮,有这按钮说明帐号没登录
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"]'
# 排队剩余时间
QUEUE_REMAIN = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tvQueueInfoWaitTimeContent"]'
"""
游戏界面元素
"""
# 网络状态 简洁
FLOAT_STATE_SIMPLE = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tvSimpleNetStateMode"]'
# 网络状态 详细
FLOAT_STATE_DETAIL = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tv_ping_value"]'
"""
悬浮窗及侧边栏元素
"""
# 悬浮窗
FLOAT_WINDOW = '//*[@class="android.widget.ImageView"]'
# 弹出侧边栏的 节点信息
# 将这个区域向右偏移作为退出悬浮窗的按钮
FLOAT_DELAY = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tv_node_region"]'
# 弹出侧边栏的滚动区域
SCROLL_VIEW = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/innerScrollView"]'
# 画质选择 超高清
# 选中时selected=True
SETTING_BITRATE_UHD = '//*[@text="超高清"]'
# 网络状态 开关
SETTING_NET_STATE_TOGGLE = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/sw_net_state"]'
SETTING_NET_STATE_SIMPLE = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/mTvTitleOfSimpleMode"]'
SETTING_NET_STATE_DETAIL = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/mTvTitleOfDetailMode"]'
# 问题反馈
SETTING_PROBLEM = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tv_problem"]'
# 下载游戏
SETTING_DOWNLOAD = '//*[@resource-id="com.miHoYo.cloudgames.hkrpg:id/tv_downloadGame"]'
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:
in: START_GAME
out: page_main
"""
logger.hr('Cloud enter')
while 1:
if skip_first:
skip_first = False
else:
self.device.dump_hierarchy()
# End
button = self.xpath(XPath.FLOAT_WINDOW)
if self.appear(button):
# Confirm float window size
width, height = button.size
if (width < 120 and height < 120) and (width / height < 0.6 or height / width < 0.6):
logger.info('Cloud game entered')
break
# Queue daemon
button = self.xpath(XPath.QUEUE_REMAIN)
if self.appear(button):
remain = button.text
logger.info(f'Queue remain: {remain}')
self.device.stuck_record_clear()
# Click
if self.appear_then_click(XPath.START_GAME):
continue
if self.appear(XPath.POPUP_CONFIRM, interval=5):
title = self.xpath(XPath.POPUP_TITLE).text
logger.info(f'Popup: {title}')
# 计费提示
# 本次游戏将使用畅玩卡无限畅玩
# - 进入游戏(9s) - 退出游戏
if title == '计费提示':
self.device.click(self.xpath(XPath.POPUP_CONFIRM))
continue
# 是否使用星云币时长进入游戏
# 使用后可优先排队进入游戏,本次游戏仅可使用星云币时长,无法消耗免费时长
# - 确认使用 - 暂不使用
if title == '是否使用星云币时长进入游戏':
self.device.click(self.xpath(XPath.POPUP_CONFIRM))
continue
# 连接中断
# 因为您长时间未操作游戏,已中断连接,错误码: -1022
# - 退出游戏
if title == '连接中断':
self.device.click(self.xpath(XPath.POPUP_CONFIRM))
continue
# Disable net state display
if self._cloud_net_state_appear():
self._cloud_setting_disable_net_state()
# Login to game
from tasks.login.login import Login
Login(config=self.config, device=self.device).handle_app_login()
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_setting_disable_net_state(self, skip_first=True):
"""
Pages:
in: page_main
out: page_main
"""
self._cloud_setting_enter(skip_first=skip_first)
skip_first = True
while 1:
if skip_first:
skip_first = False
else:
self.device.dump_hierarchy()
button = self.xpath(XPath.SETTING_BITRATE_UHD)
if self.appear(button, interval=3):
if not button.selected:
logger.info('Set bitrate to UHD')
self.device.click(button)
continue
if self.appear(XPath.SETTING_NET_STATE_TOGGLE):
if self.appear(XPath.SETTING_NET_STATE_SIMPLE) or self.appear(XPath.SETTING_NET_STATE_DETAIL):
logger.info('Set net state to disabled')
self.appear_then_click(XPath.SETTING_NET_STATE_TOGGLE, interval=3)
continue
else:
logger.info('Net state display disabled')
break
# Scroll down
if not self.appear(XPath.SETTING_PROBLEM):
area = self.xpath(XPath.SCROLL_VIEW).area
# An area safe to swipe
area = (area[0], area[1], area[0] + 25, area[3])
p1, p2 = random_rectangle_vector_opted(
(0, -450), box=area, random_range=(-10, -30, 10, 30), padding=2)
self.device.swipe(p1, p2, name='SETTING_SCROLL')
continue
self._cloud_setting_exit(skip_first=True)
def _cloud_net_state_appear(self):
"""
Returns:
bool: True if net state display is enabled
"""
if self.appear(XPath.FLOAT_STATE_SIMPLE):
logger.attr('Net state', 'FLOAT_STATE_SIMPLE')
return True
if self.appear(XPath.FLOAT_STATE_DETAIL):
logger.attr('Net state', 'FLOAT_STATE_DETAIL')
return True
logger.attr('Net state', None)
return False
def cloud_ensure_ingame(self):
"""
Pages:
in: Any
out: page_main
"""
logger.hr('Cloud ensure ingame', level=1)
with self.config.multi_set():
if self.config.Emulator_GameClient != 'cloud_android':
self.config.Emulator_GameClient = 'cloud_android'
if self.config.Emulator_PackageName != 'CN-Official':
self.config.Emulator_PackageName = 'CN-Official'
if self.config.Optimization_WhenTaskQueueEmpty != 'close_game':
self.config.Optimization_WhenTaskQueueEmpty = 'close_game'
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
def cloud_keep_alive(self):
"""
Randomly do something to prevent being kicked
WARNING:
this may cause extra fee
"""
logger.hr('cloud_keep_alive', level=2)
while 1:
self.device.sleep((45, 60))
logger.info('cloud_keep_alive')
self._cloud_setting_enter(skip_first=False)
self._cloud_setting_exit(skip_first=True)
if __name__ == '__main__':
self = LoginAndroidCloud('src')
self.cloud_ensure_ingame()
self.cloud_keep_alive()

View File

@ -3,10 +3,11 @@ from module.exception import GameNotRunningError
from module.logger import logger
from tasks.base.page import page_main
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):
"""
Pages:
@ -86,12 +87,33 @@ class Login(UI):
def app_start(self):
logger.hr('App start')
if self.config.is_cloud_game:
self.cloud_ensure_ingame()
else:
self.device.app_start()
self.handle_app_login()
def app_restart(self):
logger.hr('App restart')
self.device.app_stop()
if self.config.is_cloud_game:
self.cloud_ensure_ingame()
else:
self.device.app_start()
self.handle_app_login()
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()

View File

@ -53,53 +53,63 @@ RUN_BUTTON = ButtonWrapper(
button=(1147, 591, 1195, 639),
),
)
TECHNIQUE_POINT_0 = ButtonWrapper(
name='TECHNIQUE_POINT_0',
share=Button(
file='./assets/share/map/control/TECHNIQUE_POINT_0.png',
area=(884, 597, 891, 604),
search=(831, 589, 944, 612),
color=(56, 56, 56),
button=(884, 597, 891, 604),
),
)
TECHNIQUE_POINT_1 = ButtonWrapper(
name='TECHNIQUE_POINT_1',
share=Button(
file='./assets/share/map/control/TECHNIQUE_POINT_1.png',
area=(881, 594, 894, 607),
search=(861, 574, 914, 627),
color=(149, 141, 186),
button=(881, 594, 894, 607),
area=(884, 597, 891, 604),
search=(831, 589, 944, 612),
color=(222, 213, 253),
button=(884, 597, 891, 604),
),
)
TECHNIQUE_POINT_2 = ButtonWrapper(
name='TECHNIQUE_POINT_2',
share=Button(
file='./assets/share/map/control/TECHNIQUE_POINT_2.png',
area=(889, 578, 903, 592),
search=(869, 558, 923, 612),
color=(139, 132, 174),
button=(889, 578, 903, 592),
area=(892, 581, 900, 589),
search=(872, 561, 920, 609),
color=(213, 203, 249),
button=(892, 581, 900, 589),
),
)
TECHNIQUE_POINT_3 = ButtonWrapper(
name='TECHNIQUE_POINT_3',
share=Button(
file='./assets/share/map/control/TECHNIQUE_POINT_3.png',
area=(902, 566, 916, 580),
search=(882, 546, 936, 600),
color=(138, 130, 173),
button=(902, 566, 916, 580),
area=(905, 569, 913, 577),
search=(885, 549, 933, 597),
color=(207, 195, 249),
button=(905, 569, 913, 577),
),
)
TECHNIQUE_POINT_4 = ButtonWrapper(
name='TECHNIQUE_POINT_4',
share=Button(
file='./assets/share/map/control/TECHNIQUE_POINT_4.png',
area=(918, 559, 932, 573),
search=(898, 539, 952, 593),
color=(138, 130, 173),
button=(918, 559, 932, 573),
area=(921, 562, 929, 570),
search=(901, 542, 949, 590),
color=(210, 198, 248),
button=(921, 562, 929, 570),
),
)
TECHNIQUE_POINT_5 = ButtonWrapper(
name='TECHNIQUE_POINT_5',
share=Button(
file='./assets/share/map/control/TECHNIQUE_POINT_5.png',
area=(935, 559, 948, 573),
search=(915, 539, 968, 593),
color=(71, 72, 77),
button=(935, 559, 948, 573),
area=(938, 562, 945, 570),
search=(918, 542, 965, 590),
color=(215, 203, 250),
button=(938, 562, 945, 570),
),
)

View File

@ -5,6 +5,7 @@ import cv2
import numpy as np
from module.base.timer import Timer
from module.base.utils import area_offset
from module.device.method.maatouch import MaatouchBuilder
from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution
from module.exception import ScriptError
@ -195,18 +196,35 @@ class MapControlJoystick(UI):
def map_get_technique_points(self):
"""
Returns:
int: 0 to 5.
int: 0 to 5
"""
points = [
self.image_color_count(button, color=(255, 255, 255), threshold=221, count=20)
confirm = Timer(3, count=0).start()
while 1:
matched = TECHNIQUE_POINT_1.match_template(self.device.image)
if matched:
matched_button = TECHNIQUE_POINT_1
break
matched = TECHNIQUE_POINT_0.match_template(self.device.image)
if matched:
matched_button = TECHNIQUE_POINT_0
break
if confirm.reached():
logger.warning('Can not match technique points.')
return 0
else:
self.device.screenshot()
points = []
for button in [
TECHNIQUE_POINT_1,
TECHNIQUE_POINT_2,
TECHNIQUE_POINT_3,
TECHNIQUE_POINT_4,
TECHNIQUE_POINT_5,
]
]
]:
if matched_button is not None:
button.load_offset(matched_button)
points.append(self.image_color_count(area_offset(button.area, button.button_offset), color=(255, 255, 255),
threshold=221, count=20))
count = sum(points)
logger.attr('TechniquePoints', count)
return count

View File

@ -293,6 +293,11 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward):
"""
logger.hr('Domain single exit', level=1)
waypoints = ensure_waypoints(waypoints)
for point in waypoints:
if 'item' not in point.expected_enroute:
point.expected_enroute.append('item')
end_point = waypoints[-1]
end_point.min_speed = 'run'
end_point.interact_radius = 5