Merge pull request #377 from LmeSzinc/dev
Support cloud HSR on Android | 支持云星穹铁道安卓端
BIN
assets/share/base/daemon/DUNGEON_EXIT.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/share/base/daemon/TUTORIAL_CHECK.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/share/base/daemon/TUTORIAL_CLOSE.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/share/base/daemon/TUTORIAL_NEXT.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
assets/share/login/LOGIN_CONFIRM.2.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
assets/share/map/control/TECHNIQUE_POINT_0.SEARCH.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
assets/share/map/control/TECHNIQUE_POINT_0.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/share/map/control/TECHNIQUE_POINT_1.SEARCH.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 2.9 KiB |
@ -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": {
|
||||
|
@ -92,8 +92,9 @@ 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:
|
||||
file = os.path.join(path, file).replace('\\', '/')
|
||||
yield AssetsImage(file)
|
||||
if not file.startswith('.'):
|
||||
file = os.path.join(path, file).replace('\\', '/')
|
||||
yield AssetsImage(file)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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": {
|
||||
|
@ -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 ]
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -13,6 +13,7 @@ Alas:
|
||||
- EmulatorInfo
|
||||
- Error
|
||||
- Optimization
|
||||
- CloudStorage
|
||||
Restart:
|
||||
- Scheduler
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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`
|
||||
|
@ -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]:
|
||||
"""
|
||||
|
@ -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)",
|
||||
|
@ -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)",
|
||||
|
@ -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": "侵蝕トンネル・漂泊の路(侵蝕トンネル・漂泊の路)",
|
||||
|
@ -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": "遗器:治疗套+快枪手(漂泊之径•侵蚀隧洞)",
|
||||
|
@ -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": "遺器:治療套+快槍手(漂泊之徑•侵蝕隧洞)",
|
||||
|
@ -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,18 +51,30 @@ 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.
|
||||
"""
|
||||
for key, value in VALID_SERVER.items():
|
||||
if value == package_or_server:
|
||||
return value
|
||||
if key == package_or_server:
|
||||
return value
|
||||
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
|
||||
if key == package_or_server:
|
||||
return value
|
||||
|
||||
raise ValueError(f'Server invalid: {package_or_server}')
|
||||
|
@ -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")
|
||||
|
@ -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:
|
||||
self.hierarchy = self.dump_hierarchy_uiautomator2()
|
||||
else:
|
||||
self.hierarchy = self.dump_hierarchy_adb()
|
||||
# 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()
|
||||
return self.hierarchy
|
||||
|
||||
def xpath_to_button(self, xpath: str) -> HierarchyButton:
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
return False
|
||||
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
@ -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
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -5,13 +5,22 @@ from module.base.button import Button, ButtonWrapper
|
||||
|
||||
LOGIN_CONFIRM = ButtonWrapper(
|
||||
name='LOGIN_CONFIRM',
|
||||
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),
|
||||
),
|
||||
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
@ -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()
|
@ -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')
|
||||
self.device.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()
|
||||
self.device.app_start()
|
||||
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()
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
@ -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)
|
||||
for button in [
|
||||
TECHNIQUE_POINT_1,
|
||||
TECHNIQUE_POINT_2,
|
||||
TECHNIQUE_POINT_3,
|
||||
TECHNIQUE_POINT_4,
|
||||
TECHNIQUE_POINT_5,
|
||||
]
|
||||
]
|
||||
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
|
||||
|
@ -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
|
||||
|