diff --git a/assets/share/base/daemon/DUNGEON_EXIT.png b/assets/share/base/daemon/DUNGEON_EXIT.png new file mode 100644 index 000000000..44b396976 Binary files /dev/null and b/assets/share/base/daemon/DUNGEON_EXIT.png differ diff --git a/assets/share/base/daemon/TUTORIAL_CHECK.png b/assets/share/base/daemon/TUTORIAL_CHECK.png new file mode 100644 index 000000000..6d5698c99 Binary files /dev/null and b/assets/share/base/daemon/TUTORIAL_CHECK.png differ diff --git a/assets/share/base/daemon/TUTORIAL_CLOSE.png b/assets/share/base/daemon/TUTORIAL_CLOSE.png new file mode 100644 index 000000000..b3971bc63 Binary files /dev/null and b/assets/share/base/daemon/TUTORIAL_CLOSE.png differ diff --git a/assets/share/base/daemon/TUTORIAL_NEXT.png b/assets/share/base/daemon/TUTORIAL_NEXT.png new file mode 100644 index 000000000..80810a88b Binary files /dev/null and b/assets/share/base/daemon/TUTORIAL_NEXT.png differ diff --git a/assets/share/login/LOGIN_CONFIRM.2.png b/assets/share/login/LOGIN_CONFIRM.2.png new file mode 100644 index 000000000..0a57da31e Binary files /dev/null and b/assets/share/login/LOGIN_CONFIRM.2.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_0.SEARCH.png b/assets/share/map/control/TECHNIQUE_POINT_0.SEARCH.png new file mode 100644 index 000000000..114720cc9 Binary files /dev/null and b/assets/share/map/control/TECHNIQUE_POINT_0.SEARCH.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_0.png b/assets/share/map/control/TECHNIQUE_POINT_0.png new file mode 100644 index 000000000..d1bab5e85 Binary files /dev/null and b/assets/share/map/control/TECHNIQUE_POINT_0.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_1.SEARCH.png b/assets/share/map/control/TECHNIQUE_POINT_1.SEARCH.png new file mode 100644 index 000000000..e2c0b68e1 Binary files /dev/null and b/assets/share/map/control/TECHNIQUE_POINT_1.SEARCH.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_1.png b/assets/share/map/control/TECHNIQUE_POINT_1.png index ea6201eaa..6f98d1bf9 100644 Binary files a/assets/share/map/control/TECHNIQUE_POINT_1.png and b/assets/share/map/control/TECHNIQUE_POINT_1.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_2.png b/assets/share/map/control/TECHNIQUE_POINT_2.png index 1c8b28ae6..94f46fcc2 100644 Binary files a/assets/share/map/control/TECHNIQUE_POINT_2.png and b/assets/share/map/control/TECHNIQUE_POINT_2.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_3.png b/assets/share/map/control/TECHNIQUE_POINT_3.png index d388277ab..7214bac51 100644 Binary files a/assets/share/map/control/TECHNIQUE_POINT_3.png and b/assets/share/map/control/TECHNIQUE_POINT_3.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_4.png b/assets/share/map/control/TECHNIQUE_POINT_4.png index d6d9e5bf3..5037e1bd0 100644 Binary files a/assets/share/map/control/TECHNIQUE_POINT_4.png and b/assets/share/map/control/TECHNIQUE_POINT_4.png differ diff --git a/assets/share/map/control/TECHNIQUE_POINT_5.png b/assets/share/map/control/TECHNIQUE_POINT_5.png index f22c60117..9599b127a 100644 Binary files a/assets/share/map/control/TECHNIQUE_POINT_5.png and b/assets/share/map/control/TECHNIQUE_POINT_5.png differ diff --git a/config/template.json b/config/template.json index 72e91bb2a..584323178 100644 --- a/config/template.json +++ b/config/template.json @@ -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": { diff --git a/dev_tools/button_extract.py b/dev_tools/button_extract.py index a45325055..67a623acc 100644 --- a/dev_tools/button_extract.py +++ b/dev_tools/button_extract.py @@ -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 diff --git a/module/alas.py b/module/alas.py index 5abfd0ae7..34d1d7b53 100644 --- a/module/alas.py +++ b/module/alas.py @@ -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): diff --git a/module/base/base.py b/module/base/base.py index 7ecfff3c0..9a1dc41cb 100644 --- a/module/base/base.py +++ b/module/base/base.py @@ -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) diff --git a/module/config/argument/args.json b/module/config/argument/args.json index c85879ceb..50b6b5f83 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -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": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 871437c39..2f976f2ef 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -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 ] diff --git a/module/config/argument/override.yaml b/module/config/argument/override.yaml index bb458f39e..b38bca2a5 100644 --- a/module/config/argument/override.yaml +++ b/module/config/argument/override.yaml @@ -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: diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json index 55599ae90..f984b47e4 100644 --- a/module/config/argument/stored.json +++ b/module/config/argument/stored.json @@ -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", diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index 34a1a102c..5e10c85ea 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -13,6 +13,7 @@ Alas: - EmulatorInfo - Error - Optimization + - CloudStorage Restart: - Scheduler diff --git a/module/config/config.py b/module/config/config.py index 8a276ff90..6ba4e6d5a 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -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() diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 8af4d7616..fcae8e7bf 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -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` diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 0f10f4d41..b5e9c8e13 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -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]: """ diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index b6dad37bd..40cda96b4 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -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)", diff --git a/module/config/i18n/es-ES.json b/module/config/i18n/es-ES.json index e9a468347..a6215168d 100644 --- a/module/config/i18n/es-ES.json +++ b/module/config/i18n/es-ES.json @@ -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)", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index a09ba1f54..a24a28e16 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -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": "侵蝕トンネル・漂泊の路(侵蝕トンネル・漂泊の路)", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 4f8a58c57..429a7974f 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -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": "遗器:治疗套+快枪手(漂泊之径•侵蚀隧洞)", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index b77f7b05f..69fe1752d 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -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": "遺器:治療套+快槍手(漂泊之徑•侵蝕隧洞)", diff --git a/module/config/server.py b/module/config/server.py index 2049e3c47..ee3fc23f0 100644 --- a/module/config/server.py +++ b/module/config/server.py @@ -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}') diff --git a/module/config/stored/stored_generated.py b/module/config/stored/stored_generated.py index 1280f1e19..bc8f2c526 100644 --- a/module/config/stored/stored_generated.py +++ b/module/config/stored/stored_generated.py @@ -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") diff --git a/module/device/app_control.py b/module/device/app_control.py index 390c50516..cf6c3a529 100644 --- a/module/device/app_control.py +++ b/module/device/app_control.py @@ -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: diff --git a/module/device/connection.py b/module/device/connection.py index 4b2094815..f62149b4c 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -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) diff --git a/module/device/method/utils.py b/module/device/method/utils.py index 67bd65451..36d6bbd49 100644 --- a/module/device/method/utils.py +++ b/module/device/method/utils.py @@ -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 diff --git a/src.py b/src.py index 5cb317b84..3027bd28e 100644 --- a/src.py +++ b/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 diff --git a/tasks/base/assets/assets_base_daemon.py b/tasks/base/assets/assets_base_daemon.py index 700ea2eb5..71732ebec 100644 --- a/tasks/base/assets/assets_base_daemon.py +++ b/tasks/base/assets/assets_base_daemon.py @@ -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), + ), +) diff --git a/tasks/base/daemon.py b/tasks/base/daemon.py index 5e5ebaff9..deb483fa4 100644 --- a/tasks/base/daemon.py +++ b/tasks/base/daemon.py @@ -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 diff --git a/tasks/combat/state.py b/tasks/combat/state.py index 3884a94a9..cac86c67c 100644 --- a/tasks/combat/state.py +++ b/tasks/combat/state.py @@ -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] diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index 6c7e5a60a..8bf32a05b 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -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 diff --git a/tasks/login/assets/assets_login.py b/tasks/login/assets/assets_login.py index 5352a9c6a..e7f36c8f0 100644 --- a/tasks/login/assets/assets_login.py +++ b/tasks/login/assets/assets_login.py @@ -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', diff --git a/tasks/login/cloud.py b/tasks/login/cloud.py new file mode 100644 index 000000000..6f520e31c --- /dev/null +++ b/tasks/login/cloud.py @@ -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() diff --git a/tasks/login/login.py b/tasks/login/login.py index 967f956ae..7bc6b0905 100644 --- a/tasks/login/login.py +++ b/tasks/login/login.py @@ -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() diff --git a/tasks/map/assets/assets_map_control.py b/tasks/map/assets/assets_map_control.py index 6aaa78af9..659f100a7 100644 --- a/tasks/map/assets/assets_map_control.py +++ b/tasks/map/assets/assets_map_control.py @@ -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), ), ) diff --git a/tasks/map/control/joystick.py b/tasks/map/control/joystick.py index 86480a934..1d1372e07 100644 --- a/tasks/map/control/joystick.py +++ b/tasks/map/control/joystick.py @@ -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 diff --git a/tasks/rogue/route/base.py b/tasks/rogue/route/base.py index b68a4f793..b7017991e 100644 --- a/tasks/rogue/route/base.py +++ b/tasks/rogue/route/base.py @@ -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