diff --git a/assets/share/combat/prepare/OCR_TRAILBLAZE_POWER.png b/assets/share/base/main_page/ROGUE_LEAVE_FOR_NOW_OE.png similarity index 50% rename from assets/share/combat/prepare/OCR_TRAILBLAZE_POWER.png rename to assets/share/base/main_page/ROGUE_LEAVE_FOR_NOW_OE.png index 8fe06b9b7..71c094417 100644 Binary files a/assets/share/combat/prepare/OCR_TRAILBLAZE_POWER.png and b/assets/share/base/main_page/ROGUE_LEAVE_FOR_NOW_OE.png differ diff --git a/assets/share/combat/fuel/RESERVED_TRAILBLAZE_POWER_ENTRANCE.png b/assets/share/base/page/MAP_EXIT_OE.png similarity index 73% rename from assets/share/combat/fuel/RESERVED_TRAILBLAZE_POWER_ENTRANCE.png rename to assets/share/base/page/MAP_EXIT_OE.png index f724a7998..6718bb2d2 100644 Binary files a/assets/share/combat/fuel/RESERVED_TRAILBLAZE_POWER_ENTRANCE.png and b/assets/share/base/page/MAP_EXIT_OE.png differ diff --git a/assets/share/combat/fuel/FUEL.SEARCH.png b/assets/share/combat/stamina/fuel/FUEL.SEARCH.png similarity index 100% rename from assets/share/combat/fuel/FUEL.SEARCH.png rename to assets/share/combat/stamina/fuel/FUEL.SEARCH.png diff --git a/assets/share/combat/fuel/FUEL.png b/assets/share/combat/stamina/fuel/FUEL.png similarity index 100% rename from assets/share/combat/fuel/FUEL.png rename to assets/share/combat/stamina/fuel/FUEL.png diff --git a/assets/share/combat/fuel/FUEL_MINUS.png b/assets/share/combat/stamina/fuel/FUEL_MINUS.png similarity index 100% rename from assets/share/combat/fuel/FUEL_MINUS.png rename to assets/share/combat/stamina/fuel/FUEL_MINUS.png diff --git a/assets/share/combat/fuel/FUEL_PLUS.png b/assets/share/combat/stamina/fuel/FUEL_PLUS.png similarity index 100% rename from assets/share/combat/fuel/FUEL_PLUS.png rename to assets/share/combat/stamina/fuel/FUEL_PLUS.png diff --git a/assets/share/combat/fuel/FUEL_SELECTED.SEARCH.png b/assets/share/combat/stamina/fuel/FUEL_SELECTED.SEARCH.png similarity index 100% rename from assets/share/combat/fuel/FUEL_SELECTED.SEARCH.png rename to assets/share/combat/stamina/fuel/FUEL_SELECTED.SEARCH.png diff --git a/assets/share/combat/fuel/FUEL_SELECTED.png b/assets/share/combat/stamina/fuel/FUEL_SELECTED.png similarity index 100% rename from assets/share/combat/fuel/FUEL_SELECTED.png rename to assets/share/combat/stamina/fuel/FUEL_SELECTED.png diff --git a/assets/share/combat/fuel/FUEL_SLIDER.png b/assets/share/combat/stamina/fuel/FUEL_SLIDER.png similarity index 100% rename from assets/share/combat/fuel/FUEL_SLIDER.png rename to assets/share/combat/stamina/fuel/FUEL_SLIDER.png diff --git a/assets/share/combat/fuel/OCR_FUEL.png b/assets/share/combat/stamina/fuel/OCR_FUEL.png similarity index 100% rename from assets/share/combat/fuel/OCR_FUEL.png rename to assets/share/combat/stamina/fuel/OCR_FUEL.png diff --git a/assets/share/combat/fuel/OCR_FUEL_COUNT.png b/assets/share/combat/stamina/fuel/OCR_FUEL_COUNT.png similarity index 100% rename from assets/share/combat/fuel/OCR_FUEL_COUNT.png rename to assets/share/combat/stamina/fuel/OCR_FUEL_COUNT.png diff --git a/assets/share/combat/fuel/USING_FUEL.png b/assets/share/combat/stamina/fuel/USING_FUEL.png similarity index 100% rename from assets/share/combat/fuel/USING_FUEL.png rename to assets/share/combat/stamina/fuel/USING_FUEL.png diff --git a/assets/share/combat/fuel/EXTRACT_RESERVED_TRAILBLAZE_POWER.png b/assets/share/combat/stamina/reserved/EXTRACT_RESERVED_TRAILBLAZE_POWER.png similarity index 100% rename from assets/share/combat/fuel/EXTRACT_RESERVED_TRAILBLAZE_POWER.png rename to assets/share/combat/stamina/reserved/EXTRACT_RESERVED_TRAILBLAZE_POWER.png diff --git a/assets/share/combat/fuel/OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT.png b/assets/share/combat/stamina/reserved/OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT.png similarity index 100% rename from assets/share/combat/fuel/OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT.png rename to assets/share/combat/stamina/reserved/OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT.png diff --git a/assets/share/combat/fuel/RESERVED_MINUS.png b/assets/share/combat/stamina/reserved/RESERVED_MINUS.png similarity index 100% rename from assets/share/combat/fuel/RESERVED_MINUS.png rename to assets/share/combat/stamina/reserved/RESERVED_MINUS.png diff --git a/assets/share/combat/fuel/RESERVED_PLUS.png b/assets/share/combat/stamina/reserved/RESERVED_PLUS.png similarity index 100% rename from assets/share/combat/fuel/RESERVED_PLUS.png rename to assets/share/combat/stamina/reserved/RESERVED_PLUS.png diff --git a/assets/share/combat/fuel/RESERVED_SLIDER.png b/assets/share/combat/stamina/reserved/RESERVED_SLIDER.png similarity index 100% rename from assets/share/combat/fuel/RESERVED_SLIDER.png rename to assets/share/combat/stamina/reserved/RESERVED_SLIDER.png diff --git a/assets/share/combat/stamina/status/ICON_SEARCH.png b/assets/share/combat/stamina/status/ICON_SEARCH.png new file mode 100644 index 000000000..38240df1a Binary files /dev/null and b/assets/share/combat/stamina/status/ICON_SEARCH.png differ diff --git a/assets/share/dungeon/stamina/ENTER_IMMERSIFIER.png b/assets/share/combat/stamina/status/IMMERSIFIER_ICON.png similarity index 100% rename from assets/share/dungeon/stamina/ENTER_IMMERSIFIER.png rename to assets/share/combat/stamina/status/IMMERSIFIER_ICON.png diff --git a/assets/share/dungeon/stamina/ENTER_IMMERSIFIER.BUTTON.png b/assets/share/combat/stamina/status/IMMERSIFIER_OCR.png similarity index 100% rename from assets/share/dungeon/stamina/ENTER_IMMERSIFIER.BUTTON.png rename to assets/share/combat/stamina/status/IMMERSIFIER_OCR.png diff --git a/assets/share/combat/fuel/OCR_RESERVED_TRAILBLAZE_POWER.png b/assets/share/combat/stamina/status/RESERVED_ICON.png similarity index 69% rename from assets/share/combat/fuel/OCR_RESERVED_TRAILBLAZE_POWER.png rename to assets/share/combat/stamina/status/RESERVED_ICON.png index 591b0d209..aa6af07e4 100644 Binary files a/assets/share/combat/fuel/OCR_RESERVED_TRAILBLAZE_POWER.png and b/assets/share/combat/stamina/status/RESERVED_ICON.png differ diff --git a/assets/share/combat/stamina/status/RESERVED_OCR.png b/assets/share/combat/stamina/status/RESERVED_OCR.png new file mode 100644 index 000000000..171c9a584 Binary files /dev/null and b/assets/share/combat/stamina/status/RESERVED_OCR.png differ diff --git a/assets/share/combat/fuel/FUEL_ENTRANCE.png b/assets/share/combat/stamina/status/STAMINA_ICON.png similarity index 74% rename from assets/share/combat/fuel/FUEL_ENTRANCE.png rename to assets/share/combat/stamina/status/STAMINA_ICON.png index fd6211b55..2ddb9dbb7 100644 Binary files a/assets/share/combat/fuel/FUEL_ENTRANCE.png and b/assets/share/combat/stamina/status/STAMINA_ICON.png differ diff --git a/assets/share/combat/stamina/status/STAMINA_OCR.png b/assets/share/combat/stamina/status/STAMINA_OCR.png new file mode 100644 index 000000000..85631fbdb Binary files /dev/null and b/assets/share/combat/stamina/status/STAMINA_OCR.png differ diff --git a/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.2.png b/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.2.png new file mode 100644 index 000000000..6db8ecab6 Binary files /dev/null and b/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.2.png differ diff --git a/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.SEARCH.png b/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.SEARCH.png index 257eff5aa..59583979e 100644 Binary files a/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.SEARCH.png and b/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.SEARCH.png differ diff --git a/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.png b/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.png index 59583979e..257eff5aa 100644 Binary files a/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.png and b/assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.png differ diff --git a/config/template.json b/config/template.json index da1b72da5..5c0c8fa09 100644 --- a/config/template.json +++ b/config/template.json @@ -107,6 +107,7 @@ }, "DungeonStorage": { "TrailblazePower": {}, + "Reserved": {}, "Immersifier": {}, "DungeonDouble": {}, "EchoOfWar": {}, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 6f256c5af..94b4738f0 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -715,6 +715,12 @@ "order": 1, "color": "#eb8efe" }, + "Reserved": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredResersed" + }, "Immersifier": { "type": "stored", "value": {}, diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 518883531..89e52636a 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -120,6 +120,8 @@ DungeonStorage: stored: StoredTrailblazePower order: 1 color: "#eb8efe" + Reserved: + stored: StoredResersed Immersifier: stored: StoredImmersifier DungeonDouble: diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json index 857b38430..0beb4eb59 100644 --- a/module/config/argument/stored.json +++ b/module/config/argument/stored.json @@ -662,6 +662,19 @@ "order": 0, "color": "#777777" }, + "Reserved": { + "name": "Reserved", + "path": "Dungeon.DungeonStorage.Reserved", + "i18n": "DungeonStorage.Reserved.name", + "stored": "StoredResersed", + "attrs": { + "time": "2020-01-01 00:00:00", + "total": 8, + "value": 0 + }, + "order": 0, + "color": "#777777" + }, "Immersifier": { "name": "Immersifier", "path": "Dungeon.DungeonStorage.Immersifier", diff --git a/module/config/config_generated.py b/module/config/config_generated.py index d20cd1636..469b09d08 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -57,6 +57,7 @@ class GeneratedConfig: # Group `DungeonStorage` DungeonStorage_TrailblazePower = {} + DungeonStorage_Reserved = {} DungeonStorage_Immersifier = {} DungeonStorage_DungeonDouble = {} DungeonStorage_EchoOfWar = {} diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 3e65f33b5..e5592fc3e 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -445,6 +445,10 @@ "name": "Power", "help": "" }, + "Reserved": { + "name": "Reserved Trailblaze Power", + "help": "" + }, "Immersifier": { "name": "Immersifier", "help": "" diff --git a/module/config/i18n/es-ES.json b/module/config/i18n/es-ES.json index 288420308..171636fa3 100644 --- a/module/config/i18n/es-ES.json +++ b/module/config/i18n/es-ES.json @@ -445,6 +445,10 @@ "name": "Poder", "help": "" }, + "Reserved": { + "name": "Trailblaze Poder Reservada", + "help": "" + }, "Immersifier": { "name": "Inmersor", "help": "" diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 5608de57a..a6176ccac 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -445,6 +445,10 @@ "name": "DungeonStorage.TrailblazePower.name", "help": "DungeonStorage.TrailblazePower.help" }, + "Reserved": { + "name": "DungeonStorage.Reserved.name", + "help": "DungeonStorage.Reserved.help" + }, "Immersifier": { "name": "DungeonStorage.Immersifier.name", "help": "DungeonStorage.Immersifier.help" diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index c3a497b1e..dd9b23529 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -445,6 +445,10 @@ "name": "开拓力", "help": "" }, + "Reserved": { + "name": "后备开拓力", + "help": "" + }, "Immersifier": { "name": "沉浸器", "help": "" diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 1addf4201..f07928363 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -445,6 +445,10 @@ "name": "開拓力", "help": "" }, + "Reserved": { + "name": "后备開拓力", + "help": "" + }, "Immersifier": { "name": "沉浸器", "help": "" diff --git a/module/config/stored/classes.py b/module/config/stored/classes.py index 8d5073e61..cbffb5382 100644 --- a/module/config/stored/classes.py +++ b/module/config/stored/classes.py @@ -206,6 +206,10 @@ class StoredTrailblazePower(StoredCounter): return value +class StoredResersed(StoredCounter): + FIXED_TOTAL = 2400 + + class StoredImmersifier(StoredCounter): FIXED_TOTAL = 8 diff --git a/module/config/stored/stored_generated.py b/module/config/stored/stored_generated.py index 25489d7b0..7a4c52f93 100644 --- a/module/config/stored/stored_generated.py +++ b/module/config/stored/stored_generated.py @@ -22,6 +22,7 @@ from module.config.stored.classes import ( StoredInt, StoredPlanner, StoredPlannerOverall, + StoredResersed, StoredSimulatedUniverse, StoredSimulatedUniverseElite, StoredTrailblazePower, @@ -85,6 +86,7 @@ class StoredGenerated: Item_Dream_Making_Engine = StoredPlanner("Dungeon.Planner.Item_Dream_Making_Engine") Item_Shards_of_Desires = StoredPlanner("Dungeon.Planner.Item_Shards_of_Desires") TrailblazePower = StoredTrailblazePower("Dungeon.DungeonStorage.TrailblazePower") + Reserved = StoredResersed("Dungeon.DungeonStorage.Reserved") Immersifier = StoredImmersifier("Dungeon.DungeonStorage.Immersifier") DungeonDouble = StoredDungeonDouble("Dungeon.DungeonStorage.DungeonDouble") EchoOfWar = StoredEchoOfWar("Dungeon.DungeonStorage.EchoOfWar") diff --git a/module/device/connection.py b/module/device/connection.py index 5779e9760..557e8e07e 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -271,6 +271,7 @@ class Connection(ConnectionAttr): return self.adb_shell(['getprop', name]).strip() @cached_property + @retry def cpu_abi(self) -> str: """ Returns: @@ -282,6 +283,7 @@ class Connection(ConnectionAttr): return abi @cached_property + @retry def sdk_ver(self) -> int: """ Android SDK/API levels, see https://apilevels.com/ @@ -295,6 +297,7 @@ class Connection(ConnectionAttr): return 0 @cached_property + @retry def is_avd(self): if get_serial_pair(self.serial)[0] is None: return False @@ -305,12 +308,35 @@ class Connection(ConnectionAttr): return False @cached_property + @retry + def is_waydroid(self): + res = self.adb_getprop('ro.product.brand') + logger.attr('ro.product.brand', res) + return 'waydroid' in res.lower() + + @cached_property + @retry def nemud_app_keep_alive(self) -> str: res = self.adb_getprop('nemud.app_keep_alive') logger.attr('nemud.app_keep_alive', res) return res + @cached_property @retry + def nemud_player_version(self) -> str: + # [nemud.player_product_version]: [3.8.27.2950] + res = self.adb_getprop('nemud.player_version') + logger.attr('nemud.player_version', res) + return res + + @cached_property + @retry + def nemud_player_engine(self) -> str: + # NEMUX or MACPRO + res = self.adb_getprop('nemud.player_engine') + logger.attr('nemud.player_engine', res) + return res + def check_mumu_app_keep_alive(self): if not self.is_mumu_family: return False @@ -340,12 +366,13 @@ class Connection(ConnectionAttr): """ if not self.is_mumu_family: return False + # >= 4.0 has no info in getprop + if self.nemud_player_version == '': + return True if self.nemud_app_keep_alive != '': return True if IS_MACINTOSH: - res = self.adb_getprop('nemud.player_engine') - logger.attr('nemud.player_engine', res) - if 'MACPRO' in res: + if 'MACPRO' in self.nemud_player_engine: return True return False @@ -665,13 +692,9 @@ class Connection(ConnectionAttr): # Brute force connect nearby ports to handle serial switches if self.is_mumu12_family: before = self.serial - for port_offset in [1, -1, 2, -2]: - port = self.port + port_offset - serial = self.serial.replace(str(self.port), str(port)) - msg = self.adb_client.connect(serial) - logger.info(msg) - if 'connected' in msg: - break + serial_list = [self.serial.replace(str(self.port), str(self.port + offset)) + for offset in [1, -1, 2, -2]] + self.adb_brute_force_connect(serial_list) self.detect_device() if self.serial != before: return True @@ -684,6 +707,25 @@ class Connection(ConnectionAttr): self.detect_device() return False + def adb_brute_force_connect(self, serial_list): + """ + Args: + serial_list (list[str]): + """ + import asyncio + ev = asyncio.new_event_loop() + + def _connect(serial): + msg = self.adb_client.connect(serial) + logger.info(msg) + return msg + + async def connect(): + tasks = [ev.run_in_executor(None, _connect, serial) for serial in serial_list] + await asyncio.gather(*tasks) + + ev.run_until_complete(connect()) + @Config.when(DEVICE_OVER_HTTP=True) def adb_connect(self): # No adb connect if over http diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index 2cb44ff5b..57fdd8468 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -163,6 +163,10 @@ class ConnectionAttr: def is_network_device(self): return bool(re.match(r'\d+\.\d+\.\d+\.\d+:\d+', self.serial)) + @cached_property + def is_local_network_device(self): + return bool(re.match(r'192\.168\.\d+\.\d+:\d+', self.serial)) + @cached_property def is_over_http(self): return bool(re.match(r"^https?://", self.serial)) diff --git a/module/device/method/adb.py b/module/device/method/adb.py index 8bfa20b8c..f699b7309 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -303,8 +303,11 @@ class Adb(Connection): logger.error(result) raise PackageNotInstalled(package_name) - ret = self.adb_shell(['am', 'start', '-a', 'android.intent.action.MAIN', '-c', - 'android.intent.category.LAUNCHER', '-n', f'{package_name}/{activity_name}']) + cmd = ['am', 'start', '-a', 'android.intent.action.MAIN', '-c', + 'android.intent.category.LAUNCHER', '-n', f'{package_name}/{activity_name}'] + if self.is_local_network_device and self.is_waydroid: + cmd += ['--windowingMode', '4'] + ret = self.adb_shell(cmd) # Invalid activity # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=... } # Error type 3 @@ -320,6 +323,24 @@ class Adb(Connection): if 'Warning: Activity not started' in ret: logger.info('App activity is already started') return True + # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.YoStarEN.AzurLane/com.manjuu.azurlane.MainActivity } + # java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.YoStarEN.AzurLane/com.manjuu.azurlane.MainActivity } from null (pid=5140, uid=2000) not exported from uid 10064 + # at android.os.Parcel.readException(Parcel.java:1692) + # at android.os.Parcel.readException(Parcel.java:1645) + # at android.app.ActivityManagerProxy.startActivityAsUser(ActivityManagerNative.java:3152) + # at com.android.commands.am.Am.runStart(Am.java:643) + # at com.android.commands.am.Am.onRun(Am.java:394) + # at com.android.internal.os.BaseCommand.run(BaseCommand.java:51) + # at com.android.commands.am.Am.main(Am.java:124) + # at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method) + # at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:290) + if 'Permission Denial' in ret: + if allow_failure: + return False + else: + logger.error(ret) + logger.error('Permission Denial while starting app, probably because activity invalid') + return False # Success # Starting: Intent... return True diff --git a/module/device/method/nemu_ipc.py b/module/device/method/nemu_ipc.py index 315c1897e..55973719f 100644 --- a/module/device/method/nemu_ipc.py +++ b/module/device/method/nemu_ipc.py @@ -486,6 +486,9 @@ class NemuIpc(Platform): def nemu_ipc_available(self) -> bool: if not self.is_mumu_family: return False + # >= 4.0 has no info in getprop + if self.nemud_player_version == '': + return True if self.nemud_app_keep_alive == '': return False try: diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index 04a55883f..9dec3f5a8 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -224,28 +224,147 @@ class Uiautomator2(Connection): return result['package'] @retry - def app_start_uiautomator2(self, package_name=None, activity_name=None): + def _app_start_u2_monkey(self, package_name=None, allow_failure=False): + """ + Args: + package_name (str): + allow_failure (bool): + + Returns: + bool: If success to start + + Raises: + PackageNotInstalled: + """ + if not package_name: + package_name = self.package + result = self.u2.shell([ + 'monkey', '-p', package_name, '-c', + 'android.intent.category.LAUNCHER', '--pct-syskeys', '0', '1' + ]) + if 'No activities found' in result.output: + # ** No activities found to run, monkey aborted. + if allow_failure: + return False + else: + logger.error(result) + raise PackageNotInstalled(package_name) + elif 'inaccessible' in result: + # /system/bin/sh: monkey: inaccessible or not found + return False + else: + # Events injected: 1 + # ## Network stats: elapsed time=4ms (0ms mobile, 0ms wifi, 4ms not connected) + return True + + @retry + def _app_start_u2_am(self, package_name=None, activity_name=None, allow_failure=False): + """ + Args: + package_name (str): + activity_name (str): + allow_failure (bool): + + Returns: + bool: If success to start + + Raises: + PackageNotInstalled: + """ + if not package_name: + package_name = self.package + if not activity_name: + try: + info = self.u2.app_info(package_name) + except u2.BaseError as e: + if allow_failure: + return False + # BaseError('package "111" not found') + elif 'not found' in str(e): + logger.error(e) + raise PackageNotInstalled(package_name) + # Unknown error + else: + raise + activity_name = info['mainActivity'] + + cmd = ['am', 'start', '-a', 'android.intent.action.MAIN', '-c', + 'android.intent.category.LAUNCHER', '-n', f'{package_name}/{activity_name}'] + if self.is_local_network_device and self.is_waydroid: + cmd += ['--windowingMode', '4'] + ret = self.u2.shell(cmd) + # Invalid activity + # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=... } + # Error type 3 + # Error: Activity class {.../...} does not exist. + if 'Error: Activity class' in ret.output: + if allow_failure: + return False + else: + logger.error(ret) + return False + # Already running + # Warning: Activity not started, intent has been delivered to currently running top-most instance. + if 'Warning: Activity not started' in ret.output: + logger.info('App activity is already started') + return True + # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.YoStarEN.AzurLane/com.manjuu.azurlane.MainActivity } + # java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.YoStarEN.AzurLane/com.manjuu.azurlane.MainActivity } from null (pid=5140, uid=2000) not exported from uid 10064 + # at android.os.Parcel.readException(Parcel.java:1692) + # at android.os.Parcel.readException(Parcel.java:1645) + # at android.app.ActivityManagerProxy.startActivityAsUser(ActivityManagerNative.java:3152) + # at com.android.commands.am.Am.runStart(Am.java:643) + # at com.android.commands.am.Am.onRun(Am.java:394) + # at com.android.internal.os.BaseCommand.run(BaseCommand.java:51) + # at com.android.commands.am.Am.main(Am.java:124) + # at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method) + # at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:290) + if 'Permission Denial' in ret.output: + if allow_failure: + return False + else: + logger.error(ret) + logger.error('Permission Denial while starting app, probably because activity invalid') + return False + # Success + # Starting: Intent... + return True + + # No @retry decorator since _app_start_adb_am and _app_start_adb_monkey have @retry already + # @retry + def app_start_uiautomator2(self, package_name=None, activity_name=None, allow_failure=False): """ Args: package_name (str): If None, to get from config activity_name (str): If None, to get from DICT_PACKAGE_TO_ACTIVITY + If still None, launch from monkey + If monkey failed, fetch activity name and launch from am + allow_failure (bool): + True for no PackageNotInstalled raising, just return False Returns: + bool: If success to start + Raises: + PackageNotInstalled: """ if not package_name: package_name = self.package if not activity_name: activity_name = DICT_PACKAGE_TO_ACTIVITY.get(package_name) - try: - self.u2.app_start(package_name, activity_name) - except u2.exceptions.BaseError as e: - # BaseError: package "com.bilibili.azurlane" not found - logger.error(e) - raise PackageNotInstalled(package_name) + if activity_name: + if self._app_start_u2_am(package_name, activity_name, allow_failure): + return True + if self._app_start_u2_monkey(package_name, allow_failure): + return True + if self._app_start_u2_am(package_name, activity_name, allow_failure): + return True + + logger.error('app_start_uiautomator2: All trials failed') + return False @retry def app_stop_uiautomator2(self, package_name=None): diff --git a/tasks/base/assets/assets_base_main_page.py b/tasks/base/assets/assets_base_main_page.py index 3a50ad9ed..00705ff87 100644 --- a/tasks/base/assets/assets_base_main_page.py +++ b/tasks/base/assets/assets_base_main_page.py @@ -23,3 +23,13 @@ ROGUE_LEAVE_FOR_NOW = ButtonWrapper( button=(729, 475, 765, 519), ), ) +ROGUE_LEAVE_FOR_NOW_OE = ButtonWrapper( + name='ROGUE_LEAVE_FOR_NOW_OE', + share=Button( + file='./assets/share/base/main_page/ROGUE_LEAVE_FOR_NOW_OE.png', + area=(730, 551, 760, 587), + search=(710, 531, 780, 607), + color=(63, 52, 40), + button=(730, 551, 760, 587), + ), +) diff --git a/tasks/base/assets/assets_base_page.py b/tasks/base/assets/assets_base_page.py index 7f8721892..5169979cc 100644 --- a/tasks/base/assets/assets_base_page.py +++ b/tasks/base/assets/assets_base_page.py @@ -297,6 +297,16 @@ MAP_EXIT = ButtonWrapper( ), ], ) +MAP_EXIT_OE = ButtonWrapper( + name='MAP_EXIT_OE', + share=Button( + file='./assets/share/base/page/MAP_EXIT_OE.png', + area=(51, 55, 68, 84), + search=(31, 35, 88, 104), + color=(141, 140, 141), + button=(51, 55, 68, 84), + ), +) MAP_GOTO_WORLD = ButtonWrapper( name='MAP_GOTO_WORLD', share=Button( diff --git a/tasks/base/ui.py b/tasks/base/ui.py index c0614c0d5..b344d5747 100644 --- a/tasks/base/ui.py +++ b/tasks/base/ui.py @@ -4,8 +4,8 @@ from module.base.timer import Timer from module.exception import GameNotRunningError, GamePageUnknownError from module.logger import logger from module.ocr.ocr import Ocr -from tasks.base.assets.assets_base_main_page import ROGUE_LEAVE_FOR_NOW -from tasks.base.assets.assets_base_page import CLOSE, MAIN_GOTO_CHARACTER, MAP_EXIT +from tasks.base.assets.assets_base_main_page import ROGUE_LEAVE_FOR_NOW, ROGUE_LEAVE_FOR_NOW_OE +from tasks.base.assets.assets_base_page import CLOSE, MAIN_GOTO_CHARACTER, MAP_EXIT, MAP_EXIT_OE from tasks.base.main_page import MainPage from tasks.base.page import Page, page_gacha, page_main from tasks.combat.assets.assets_combat_finish import COMBAT_EXIT @@ -344,7 +344,6 @@ class UI(MainPage): return appear - def is_in_map_exit(self, interval=0): self.device.stuck_record_add(MAP_EXIT) @@ -355,6 +354,9 @@ class UI(MainPage): if MAP_EXIT.match_template_luma(self.device.image): if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): appear = True + if MAP_EXIT_OE.match_template_luma(self.device.image): + if self.image_color_count(MAP_EXIT_OE, color=(235, 235, 235), threshold=221, count=50): + appear = True if appear and interval: self.interval_reset(MAP_EXIT, interval=interval) @@ -482,3 +484,6 @@ class UI(MainPage): if self.appear_then_click(ROGUE_LEAVE_FOR_NOW, interval=2): clicked = True continue + if self.appear_then_click(ROGUE_LEAVE_FOR_NOW_OE, interval=2): + clicked = True + continue diff --git a/tasks/combat/assets/assets_combat_fuel.py b/tasks/combat/assets/assets_combat_fuel.py deleted file mode 100644 index c34ebee6b..000000000 --- a/tasks/combat/assets/assets_combat_fuel.py +++ /dev/null @@ -1,165 +0,0 @@ -from module.base.button import Button, ButtonWrapper - -# This file was auto-generated, do not modify it manually. To generate: -# ``` python -m dev_tools.button_extract ``` - -EXTRACT_RESERVED_TRAILBLAZE_POWER = ButtonWrapper( - name='EXTRACT_RESERVED_TRAILBLAZE_POWER', - share=Button( - file='./assets/share/combat/fuel/EXTRACT_RESERVED_TRAILBLAZE_POWER.png', - area=(909, 506, 929, 526), - search=(889, 486, 949, 546), - color=(91, 91, 91), - button=(909, 506, 929, 526), - ), -) -FUEL = ButtonWrapper( - name='FUEL', - share=Button( - file='./assets/share/combat/fuel/FUEL.png', - area=(592, 276, 688, 366), - search=(474, 271, 811, 396), - color=(123, 96, 134), - button=(592, 276, 688, 366), - ), -) -FUEL_ENTRANCE = ButtonWrapper( - name='FUEL_ENTRANCE', - share=Button( - file='./assets/share/combat/fuel/FUEL_ENTRANCE.png', - area=(1035, 26, 1056, 48), - search=(1015, 6, 1076, 68), - color=(188, 180, 226), - button=(1035, 26, 1056, 48), - ), -) -FUEL_MINUS = ButtonWrapper( - name='FUEL_MINUS', - share=Button( - file='./assets/share/combat/fuel/FUEL_MINUS.png', - area=(472, 425, 510, 450), - search=(452, 405, 530, 470), - color=(236, 236, 236), - button=(472, 425, 510, 450), - ), -) -FUEL_PLUS = ButtonWrapper( - name='FUEL_PLUS', - share=Button( - file='./assets/share/combat/fuel/FUEL_PLUS.png', - area=(967, 426, 1005, 449), - search=(947, 406, 1025, 469), - color=(232, 232, 232), - button=(967, 426, 1005, 449), - ), -) -FUEL_SELECTED = ButtonWrapper( - name='FUEL_SELECTED', - share=Button( - file='./assets/share/combat/fuel/FUEL_SELECTED.png', - area=(587, 271, 692, 368), - search=(474, 271, 811, 396), - color=(136, 112, 144), - button=(587, 271, 692, 368), - ), -) -FUEL_SLIDER = ButtonWrapper( - name='FUEL_SLIDER', - share=Button( - file='./assets/share/combat/fuel/FUEL_SLIDER.png', - area=(561, 434, 916, 441), - search=(541, 414, 936, 461), - color=(215, 185, 154), - button=(561, 434, 916, 441), - ), -) -OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT = ButtonWrapper( - name='OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT', - share=Button( - file='./assets/share/combat/fuel/OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT.png', - area=(425, 415, 688, 436), - search=(405, 395, 708, 456), - color=(192, 192, 192), - button=(425, 415, 688, 436), - ), -) -OCR_FUEL = ButtonWrapper( - name='OCR_FUEL', - share=Button( - file='./assets/share/combat/fuel/OCR_FUEL.png', - area=(605, 369, 678, 386), - search=(585, 349, 698, 406), - color=(66, 66, 66), - button=(605, 369, 678, 386), - ), -) -OCR_FUEL_COUNT = ButtonWrapper( - name='OCR_FUEL_COUNT', - share=Button( - file='./assets/share/combat/fuel/OCR_FUEL_COUNT.png', - area=(686, 409, 881, 425), - search=(666, 389, 901, 445), - color=(205, 205, 205), - button=(686, 409, 881, 425), - ), -) -OCR_RESERVED_TRAILBLAZE_POWER = ButtonWrapper( - name='OCR_RESERVED_TRAILBLAZE_POWER', - share=Button( - file='./assets/share/combat/fuel/OCR_RESERVED_TRAILBLAZE_POWER.png', - area=(883, 29, 992, 44), - search=(863, 9, 1012, 64), - color=(51, 65, 65), - button=(883, 29, 992, 44), - ), -) -RESERVED_MINUS = ButtonWrapper( - name='RESERVED_MINUS', - share=Button( - file='./assets/share/combat/fuel/RESERVED_MINUS.png', - area=(248, 474, 281, 498), - search=(228, 454, 301, 518), - color=(238, 238, 238), - button=(248, 474, 281, 498), - ), -) -RESERVED_PLUS = ButtonWrapper( - name='RESERVED_PLUS', - share=Button( - file='./assets/share/combat/fuel/RESERVED_PLUS.png', - area=(938, 475, 974, 498), - search=(918, 455, 994, 518), - color=(232, 232, 232), - button=(938, 475, 974, 498), - ), -) -RESERVED_SLIDER = ButtonWrapper( - name='RESERVED_SLIDER', - share=Button( - file='./assets/share/combat/fuel/RESERVED_SLIDER.png', - area=(334, 483, 873, 489), - search=(314, 463, 893, 509), - color=(212, 173, 130), - button=(334, 483, 873, 489), - ), -) -RESERVED_TRAILBLAZE_POWER_ENTRANCE = ButtonWrapper( - name='RESERVED_TRAILBLAZE_POWER_ENTRANCE', - share=Button( - file='./assets/share/combat/fuel/RESERVED_TRAILBLAZE_POWER_ENTRANCE.png', - area=(895, 26, 916, 48), - search=(875, 6, 936, 68), - color=(154, 213, 214), - button=(895, 26, 916, 48), - ), -) -USING_FUEL = ButtonWrapper( - name='USING_FUEL', - share=Button( - file='./assets/share/combat/fuel/USING_FUEL.png', - area=(263, 265, 363, 365), - search=(243, 245, 383, 385), - color=(161, 116, 129), - button=(263, 265, 363, 365), - ), -) diff --git a/tasks/combat/assets/assets_combat_prepare.py b/tasks/combat/assets/assets_combat_prepare.py index 8363a4da0..3bf3b1af3 100644 --- a/tasks/combat/assets/assets_combat_prepare.py +++ b/tasks/combat/assets/assets_combat_prepare.py @@ -20,16 +20,6 @@ COMBAT_PREPARE = ButtonWrapper( button=(956, 640, 1225, 676), ), ) -OCR_TRAILBLAZE_POWER = ButtonWrapper( - name='OCR_TRAILBLAZE_POWER', - share=Button( - file='./assets/share/combat/prepare/OCR_TRAILBLAZE_POWER.png', - area=(998, 26, 1130, 48), - search=(978, 6, 1150, 68), - color=(77, 76, 87), - button=(998, 26, 1130, 48), - ), -) OCR_WAVE_COST = ButtonWrapper( name='OCR_WAVE_COST', share=Button( diff --git a/tasks/combat/assets/assets_combat_stamina_fuel.py b/tasks/combat/assets/assets_combat_stamina_fuel.py new file mode 100644 index 000000000..23abbfcae --- /dev/null +++ b/tasks/combat/assets/assets_combat_stamina_fuel.py @@ -0,0 +1,85 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +FUEL = ButtonWrapper( + name='FUEL', + share=Button( + file='./assets/share/combat/stamina/fuel/FUEL.png', + area=(592, 276, 688, 366), + search=(474, 271, 811, 396), + color=(123, 96, 134), + button=(592, 276, 688, 366), + ), +) +FUEL_MINUS = ButtonWrapper( + name='FUEL_MINUS', + share=Button( + file='./assets/share/combat/stamina/fuel/FUEL_MINUS.png', + area=(472, 425, 510, 450), + search=(452, 405, 530, 470), + color=(236, 236, 236), + button=(472, 425, 510, 450), + ), +) +FUEL_PLUS = ButtonWrapper( + name='FUEL_PLUS', + share=Button( + file='./assets/share/combat/stamina/fuel/FUEL_PLUS.png', + area=(967, 426, 1005, 449), + search=(947, 406, 1025, 469), + color=(232, 232, 232), + button=(967, 426, 1005, 449), + ), +) +FUEL_SELECTED = ButtonWrapper( + name='FUEL_SELECTED', + share=Button( + file='./assets/share/combat/stamina/fuel/FUEL_SELECTED.png', + area=(587, 271, 692, 368), + search=(474, 271, 811, 396), + color=(136, 112, 144), + button=(587, 271, 692, 368), + ), +) +FUEL_SLIDER = ButtonWrapper( + name='FUEL_SLIDER', + share=Button( + file='./assets/share/combat/stamina/fuel/FUEL_SLIDER.png', + area=(561, 434, 916, 441), + search=(541, 414, 936, 461), + color=(215, 185, 154), + button=(561, 434, 916, 441), + ), +) +OCR_FUEL = ButtonWrapper( + name='OCR_FUEL', + share=Button( + file='./assets/share/combat/stamina/fuel/OCR_FUEL.png', + area=(605, 369, 678, 386), + search=(585, 349, 698, 406), + color=(66, 66, 66), + button=(605, 369, 678, 386), + ), +) +OCR_FUEL_COUNT = ButtonWrapper( + name='OCR_FUEL_COUNT', + share=Button( + file='./assets/share/combat/stamina/fuel/OCR_FUEL_COUNT.png', + area=(686, 409, 881, 425), + search=(666, 389, 901, 445), + color=(205, 205, 205), + button=(686, 409, 881, 425), + ), +) +USING_FUEL = ButtonWrapper( + name='USING_FUEL', + share=Button( + file='./assets/share/combat/stamina/fuel/USING_FUEL.png', + area=(263, 265, 363, 365), + search=(243, 245, 383, 385), + color=(161, 116, 129), + button=(263, 265, 363, 365), + ), +) diff --git a/tasks/combat/assets/assets_combat_stamina_reserved.py b/tasks/combat/assets/assets_combat_stamina_reserved.py new file mode 100644 index 000000000..f164e18f1 --- /dev/null +++ b/tasks/combat/assets/assets_combat_stamina_reserved.py @@ -0,0 +1,55 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +EXTRACT_RESERVED_TRAILBLAZE_POWER = ButtonWrapper( + name='EXTRACT_RESERVED_TRAILBLAZE_POWER', + share=Button( + file='./assets/share/combat/stamina/reserved/EXTRACT_RESERVED_TRAILBLAZE_POWER.png', + area=(909, 506, 929, 526), + search=(889, 486, 949, 546), + color=(91, 91, 91), + button=(909, 506, 929, 526), + ), +) +OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT = ButtonWrapper( + name='OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT', + share=Button( + file='./assets/share/combat/stamina/reserved/OCR_EXTRACT_RESERVED_TRAILBLAZE_POWER_COUNT.png', + area=(425, 415, 688, 436), + search=(405, 395, 708, 456), + color=(192, 192, 192), + button=(425, 415, 688, 436), + ), +) +RESERVED_MINUS = ButtonWrapper( + name='RESERVED_MINUS', + share=Button( + file='./assets/share/combat/stamina/reserved/RESERVED_MINUS.png', + area=(248, 474, 281, 498), + search=(228, 454, 301, 518), + color=(238, 238, 238), + button=(248, 474, 281, 498), + ), +) +RESERVED_PLUS = ButtonWrapper( + name='RESERVED_PLUS', + share=Button( + file='./assets/share/combat/stamina/reserved/RESERVED_PLUS.png', + area=(938, 475, 974, 498), + search=(918, 455, 994, 518), + color=(232, 232, 232), + button=(938, 475, 974, 498), + ), +) +RESERVED_SLIDER = ButtonWrapper( + name='RESERVED_SLIDER', + share=Button( + file='./assets/share/combat/stamina/reserved/RESERVED_SLIDER.png', + area=(334, 483, 873, 489), + search=(314, 463, 893, 509), + color=(212, 173, 130), + button=(334, 483, 873, 489), + ), +) diff --git a/tasks/combat/assets/assets_combat_stamina_status.py b/tasks/combat/assets/assets_combat_stamina_status.py new file mode 100644 index 000000000..2789baa9c --- /dev/null +++ b/tasks/combat/assets/assets_combat_stamina_status.py @@ -0,0 +1,75 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +ICON_SEARCH = ButtonWrapper( + name='ICON_SEARCH', + share=Button( + file='./assets/share/combat/stamina/status/ICON_SEARCH.png', + area=(568, 8, 1265, 66), + search=(548, 0, 1280, 86), + color=(71, 70, 100), + button=(568, 8, 1265, 66), + ), +) +IMMERSIFIER_ICON = ButtonWrapper( + name='IMMERSIFIER_ICON', + share=Button( + file='./assets/share/combat/stamina/status/IMMERSIFIER_ICON.png', + area=(1047, 26, 1066, 49), + search=(1027, 6, 1086, 69), + color=(138, 127, 117), + button=(1047, 26, 1066, 49), + ), +) +IMMERSIFIER_OCR = ButtonWrapper( + name='IMMERSIFIER_OCR', + share=Button( + file='./assets/share/combat/stamina/status/IMMERSIFIER_OCR.png', + area=(1049, 26, 1151, 48), + search=(1029, 6, 1171, 68), + color=(64, 61, 61), + button=(1049, 26, 1151, 48), + ), +) +RESERVED_ICON = ButtonWrapper( + name='RESERVED_ICON', + share=Button( + file='./assets/share/combat/stamina/status/RESERVED_ICON.png', + area=(895, 26, 916, 48), + search=(875, 6, 936, 68), + color=(155, 212, 215), + button=(895, 26, 916, 48), + ), +) +RESERVED_OCR = ButtonWrapper( + name='RESERVED_OCR', + share=Button( + file='./assets/share/combat/stamina/status/RESERVED_OCR.png', + area=(895, 26, 999, 48), + search=(875, 6, 1019, 68), + color=(50, 69, 83), + button=(895, 26, 999, 48), + ), +) +STAMINA_ICON = ButtonWrapper( + name='STAMINA_ICON', + share=Button( + file='./assets/share/combat/stamina/status/STAMINA_ICON.png', + area=(873, 26, 894, 48), + search=(853, 6, 914, 68), + color=(188, 180, 226), + button=(873, 26, 894, 48), + ), +) +STAMINA_OCR = ButtonWrapper( + name='STAMINA_OCR', + share=Button( + file='./assets/share/combat/stamina/status/STAMINA_OCR.png', + area=(873, 26, 1013, 48), + search=(853, 6, 1033, 68), + color=(66, 64, 88), + button=(873, 26, 1013, 48), + ), +) diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index 538aff780..6a96ada49 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -54,7 +54,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo # Check limits if self.config.stored.TrailblazePower.value < self.combat_wave_cost: - return self._try_get_more_trablaize_power(self.config.stored.TrailblazePower.value, self.combat_wave_cost) + return self._try_get_more_trablaize_power(self.combat_wave_cost) if self.combat_waves <= 0: logger.info('Combat wave limited, cannot continue combat') return False @@ -163,6 +163,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo skip_first_screenshot = True is_executing = True self.combat_state_reset() + self.device.stuck_record_clear() + self.device.click_record_clear() self.device.screenshot_interval_set('combat') while 1: if skip_first_screenshot: @@ -196,6 +198,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo if self.handle_battle_pass_notification(): continue + self.device.stuck_record_clear() + self.device.click_record_clear() self.device.screenshot_interval_set() def _combat_can_again(self) -> bool: @@ -221,7 +225,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can run again') return True else: - return self._try_get_more_trablaize_power(current, self.combat_wave_cost * self.combat_waves) + return self._try_get_more_trablaize_power(self.combat_wave_cost * self.combat_waves) elif self.combat_wave_cost <= 0: logger.info(f'Free combat, combat costs {self.combat_wave_cost}, can not run again') return False @@ -230,20 +234,15 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can run again') return True else: - return self._try_get_more_trablaize_power(current, self.combat_wave_cost * self.combat_waves) - - def _try_get_more_trablaize_power(self, current, cost): - if self.config.TrailblazePower_ExtractReservedTrailblazePower: - logger.info('Extract reserved trailblaze power to get more trailblaze power') - if self.extract_reserved_trailblaze_power(current): - self.combat_get_trailblaze_power() - self.get_interval_timer(COMBAT_EXIT).wait() - if self.config.TrailblazePower_UseFuel: - logger.info('Use fuel to get more trailblaze power') - if self.use_fuel(current): - self.combat_get_trailblaze_power() - self.get_interval_timer(COMBAT_AGAIN).wait() + return self._try_get_more_trablaize_power(self.combat_wave_cost * self.combat_waves) + def _try_get_more_trablaize_power(self, cost): + self.extract_stamina( + update=False, + use_reserved=self.config.TrailblazePower_ExtractReservedTrailblazePower, + use_fuel=self.config.TrailblazePower_UseFuel + ) + current = self.config.stored.TrailblazePower.value if current >= cost: return True else: diff --git a/tasks/combat/fuel.py b/tasks/combat/fuel.py index 06fc08df0..461fa1196 100644 --- a/tasks/combat/fuel.py +++ b/tasks/combat/fuel.py @@ -4,14 +4,16 @@ from module.base.utils import area_offset, crop from module.logger import logger from module.ocr.ocr import Digit from tasks.base.assets.assets_base_popup import GET_REWARD, POPUP_CANCEL, POPUP_CONFIRM -from tasks.base.ui import UI from tasks.combat.assets.assets_combat_finish import COMBAT_AGAIN, COMBAT_EXIT -from tasks.combat.assets.assets_combat_fuel import * from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE +from tasks.combat.assets.assets_combat_stamina_fuel import * +from tasks.combat.assets.assets_combat_stamina_reserved import * +from tasks.combat.assets.assets_combat_stamina_status import * +from tasks.combat.stamina_status import StaminaStatus from tasks.item.slider import Slider -class Fuel(UI): +class Fuel(StaminaStatus): fuel_trailblaze_power = 60 def _use_fuel_finish(self): @@ -26,7 +28,7 @@ class Fuel(UI): return True if self.appear(COMBAT_PREPARE): if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400): - logger.info(f'Use fuel finished at COMBAT_AGAIN') + logger.info(f'Use fuel finished at COMBAT_PREPARE') return True return False @@ -87,7 +89,7 @@ class Fuel(UI): timer = self.get_interval_timer(COMBAT_EXIT, interval=5, renew=True) timer.set_current(4.4) - def extract_reserved_trailblaze_power(self, current, skip_first_screenshot=True): + def extract_reserved_trailblaze_power(self, skip_first_screenshot=True): """ Extract reserved trailblaze power from previous combat. @@ -95,11 +97,8 @@ class Fuel(UI): bool: If extracted """ logger.info('Extract reserved trailblaze power') - reserved = Digit(OCR_RESERVED_TRAILBLAZE_POWER).ocr_single_line(self.device.image) - if reserved <= 0: - logger.info('No reserved trailblaze power') - return False + RESERVED_ICON.load_search(ICON_SEARCH.area) self.interval_clear([POPUP_CONFIRM, POPUP_CANCEL, GET_REWARD]) while 1: if skip_first_screenshot: @@ -111,12 +110,14 @@ class Fuel(UI): break if self.appear_then_click(EXTRACT_RESERVED_TRAILBLAZE_POWER): continue - if self.appear_then_click(RESERVED_TRAILBLAZE_POWER_ENTRANCE): + if self.appear_then_click(RESERVED_ICON): continue - count = min(reserved, self.config.stored.TrailblazePower.FIXED_TOTAL - current) - logger.info(f'Having {reserved} reserved, going to use {count}') - self.set_reserved_trailblaze_power(count, total=reserved) + # No need, amount will be set by game client + # count = min(reserved, self.config.stored.TrailblazePower.FIXED_TOTAL - current) + # logger.info(f'Having {reserved} reserved, going to use {count}') + # self.set_reserved_trailblaze_power(count, total=reserved) + self._fuel_confirm() return True @@ -160,6 +161,7 @@ class Fuel(UI): logger.info("Use Fuel") + STAMINA_ICON.load_search(ICON_SEARCH.area) timeout = Timer(1, count=3) has_fuel = False while 1: @@ -182,7 +184,7 @@ class Fuel(UI): if self.appear_then_click(FUEL): has_fuel = True continue - if not self.appear(POPUP_CONFIRM) and self.appear_then_click(FUEL_ENTRANCE): + if not self.appear(POPUP_CONFIRM) and self.appear_then_click(STAMINA_ICON): continue offset = FUEL_SELECTED.button_offset @@ -215,3 +217,39 @@ class Fuel(UI): self.set_fuel_count(use) self._fuel_confirm() return True + + def extract_stamina(self, update=True, use_reserved=True, use_fuel=False): + """ + Args: + update: + use_reserved: + use_fuel: + + Returns: + bool: If used + """ + if not use_reserved and not use_fuel: + return False + + logger.hr('Extract stamina', level=2) + logger.info(f'Extract stamina, reserved={use_reserved}, fuel={use_fuel}') + if update: + self.update_stamina_status() + used = False + + if use_reserved: + if self.config.stored.Reserved.value <= 0: + logger.info('No reserved stamina') + else: + self.extract_reserved_trailblaze_power() + used = True + self.update_stamina_status() + self.get_interval_timer(COMBAT_AGAIN).wait() + + if use_fuel: + self.use_fuel(current=self.config.stored.TrailblazePower.value) + used = True + self.update_stamina_status() + self.get_interval_timer(COMBAT_AGAIN).wait() + + return used diff --git a/tasks/combat/prepare.py b/tasks/combat/prepare.py index 3f76ecd87..37e069731 100644 --- a/tasks/combat/prepare.py +++ b/tasks/combat/prepare.py @@ -1,35 +1,20 @@ -import re - import module.config.server as server from module.base.timer import Timer from module.base.utils import color_similar, get_color from module.logger import logger -from module.ocr.ocr import Digit, DigitCounter -from tasks.base.ui import UI +from module.ocr.ocr import Digit from tasks.combat.assets.assets_combat_prepare import ( - OCR_TRAILBLAZE_POWER, OCR_WAVE_COST, OCR_WAVE_COUNT, WAVE_MINUS, - WAVE_PLUS, WAVE_SLIDER + WAVE_PLUS, + WAVE_SLIDER ) +from tasks.combat.stamina_status import StaminaStatus from tasks.item.slider import Slider -class TrailblazePowerOcr(DigitCounter): - def after_process(self, result): - result = super().after_process(result) - # The trailblaze power icon is recognized as 买 - # OCR_TRAILBLAZE_POWER includes the icon because the length varies by value - result = re.sub(r'[买米装:()]', '', result) - # 61240 -> 6/240 - result = re.sub(r'1240$', '/240', result) - # 0*0/24 -> 0/240 - result = re.sub(r'24$', '240', result) - return result - - -class CombatPrepare(UI): +class CombatPrepare(StaminaStatus): # Current combat waves, combat_waves = 1 # Limit combat runs, 0 means no limit. @@ -78,20 +63,20 @@ class CombatPrepare(UI): else: self.device.screenshot() - current, _, total = TrailblazePowerOcr(OCR_TRAILBLAZE_POWER).ocr_single_line(self.device.image) - # Empty result - if total == 0: + data = self.update_stamina_status(image=self.device.image) + if data.stamina is None: continue # Confirm if it is > 240, sometimes just OCR errors - if current > 240 and timeout.reached(): + # if current > 240 and timeout.reached(): + # break + if expect_reduce and timeout.reached(): break - if expect_reduce and current >= before: + if expect_reduce and data.stamina >= before: continue - if current <= 240: + if data.stamina <= 240: break - self.config.stored.TrailblazePower.value = current - return current + return data.stamina def combat_get_wave_cost(self, skip_first_screenshot=True): """ diff --git a/tasks/combat/stamina_status.py b/tasks/combat/stamina_status.py new file mode 100644 index 000000000..a21ad7050 --- /dev/null +++ b/tasks/combat/stamina_status.py @@ -0,0 +1,161 @@ +import re + +from pydantic import BaseModel + +from module.base.timer import Timer +from module.base.utils import crop +from module.logger import logger +from module.ocr.ocr import Digit, DigitCounter +from tasks.base.ui import UI +from tasks.combat.assets.assets_combat_stamina_status import * + + +class StaminaOcr(DigitCounter): + def after_process(self, result): + result = super().after_process(result) + # The trailblaze power icon is recognized as 买 + # OCR_TRAILBLAZE_POWER includes the icon because the length varies by value + result = re.sub(r'[买米装来:()]', '', result) + # 61240 -> 6/240 + result = re.sub(r'1240$', '/240', result) + # 0*0/24 -> 0/240 + result = re.sub(r'24$', '240', result) + return result + + +class ReservedOcr(Digit): + pass + + +class ImmersifierOcr(DigitCounter): + pass + + +class DataStaminaStatus(BaseModel): + stamina: int | None + reserved: int | None + immersifier: int | None + + +class StaminaStatus(UI): + @staticmethod + def get_stamina_status(image) -> DataStaminaStatus: + """ + Update trailblaze power, stored trailblaze power, immersifier + + Returns: + int: Stamina, or None if stamina not displayed or error on OCR + int: Reserved stamina + int: Immersifier + """ + for button in [STAMINA_ICON, RESERVED_ICON, IMMERSIFIER_ICON]: + button.load_search(ICON_SEARCH.area) + + stamina = None + if STAMINA_ICON.match_template(image): + STAMINA_OCR.load_offset(STAMINA_ICON) + im = crop(image, STAMINA_OCR.button, copy=False) + stamina, _, total = StaminaOcr(STAMINA_OCR).ocr_single_line(im, direct_ocr=True) + if total > 240 or total == 0: + logger.warning(f'Unexpected stamina total: {total}') + stamina = None + + reserved = None + if RESERVED_ICON.match_template(image): + RESERVED_OCR.load_offset(RESERVED_ICON) + im = crop(image, RESERVED_OCR.button, copy=False) + reserved = ReservedOcr(RESERVED_OCR).ocr_single_line(im, direct_ocr=True) + if reserved > 2400: + logger.warning(f'Unexpected reserved value: {reserved}') + reserved = None + + immersifier = None + if IMMERSIFIER_ICON.match_template(image): + IMMERSIFIER_OCR.load_offset(IMMERSIFIER_ICON) + im = crop(image, IMMERSIFIER_OCR.button, copy=False) + immersifier, _, total = StaminaOcr(IMMERSIFIER_OCR).ocr_single_line(im, direct_ocr=True) + if total != 8: + logger.warning(f'Unexpected immersifier total: {total}') + immersifier = None + + return DataStaminaStatus( + stamina=stamina, + reserved=reserved, + immersifier=immersifier, + ) + + def update_stamina_status( + self, + image=None, + skip_first_screenshot=True, + expect_stamina=False, + expect_reserved=False, + expect_immersifier=False, + ) -> DataStaminaStatus: + """ + Update stamina status with retry + + Args: + image: Detect given image only, no new screenshot will be taken + and all expect_* are considered False + skip_first_screenshot: + expect_stamina: + True to expect stamina exists, retry detect if it wasn't + expect_reserved: + expect_immersifier: + + Pages: + in: page_guild, Survival_Index, Simulated_Universe + or page_rogue, LEVEL_CONFIRM + or rogue, REWARD_CLOSE + """ + timeout = Timer(1, count=2).start() + if image is None: + image = self.device.image + use_cached_image = False + else: + skip_first_screenshot = True + use_cached_image = True + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + image = self.device.image + + # Timeout + if timeout.reached(): + logger.warning('dungeon_update_stamina() timeout') + return DataStaminaStatus( + stamina=None, + reserved=None, + immersifier=None, + ) + + # Ocr + status = self.get_stamina_status(image) + valid = True + if expect_stamina and status.stamina is None: + valid = False + if expect_reserved and status.reserved is None: + valid = False + if expect_immersifier and status.immersifier is None: + valid = False + if status.stamina is None and status.reserved is None and status.immersifier is None: + logger.warning('update_stamina_status: No icon detected') + valid = False + + # Write config + with self.config.multi_set(): + if status.stamina is not None: + self.config.stored.TrailblazePower.value = status.stamina + if status.reserved is not None: + self.config.stored.Reserved.value = status.reserved + if status.immersifier is not None: + self.config.stored.Immersifier.value = status.reserved + + if use_cached_image or valid: + return status + else: + continue diff --git a/tasks/combat/support.py b/tasks/combat/support.py index 867f353c4..fe86baa69 100644 --- a/tasks/combat/support.py +++ b/tasks/combat/support.py @@ -203,6 +203,7 @@ class CombatSupport(UI): scroll.set_bottom(main=self) scroll.drag_threshold = backup scroll.set_top(main=self) + self.device.click_record_clear() logger.info("Searching support") skip_first_screenshot = True @@ -216,9 +217,11 @@ class CombatSupport(UI): if character: logger.info("Support found") if self._select_support(character): + self.device.click_record_clear() return True else: logger.warning("Support not selected") + self.device.click_record_clear() return False if not scroll.at_bottom(main=self): @@ -226,6 +229,7 @@ class CombatSupport(UI): continue else: logger.info("Support not found") + self.device.click_record_clear() return False def _select_support(self, character: SupportCharacter): diff --git a/tasks/dungeon/assets/assets_dungeon_stamina.py b/tasks/dungeon/assets/assets_dungeon_stamina.py index 290d0b3f0..78d5b12f1 100644 --- a/tasks/dungeon/assets/assets_dungeon_stamina.py +++ b/tasks/dungeon/assets/assets_dungeon_stamina.py @@ -41,16 +41,6 @@ AMOUNT_PLUS = ButtonWrapper( ), ], ) -ENTER_IMMERSIFIER = ButtonWrapper( - name='ENTER_IMMERSIFIER', - share=Button( - file='./assets/share/dungeon/stamina/ENTER_IMMERSIFIER.png', - area=(1047, 26, 1066, 49), - search=(1027, 6, 1086, 69), - color=(138, 127, 117), - button=(1049, 26, 1151, 48), - ), -) IMMERSIFIER_CHECK = ButtonWrapper( name='IMMERSIFIER_CHECK', share=Button( diff --git a/tasks/dungeon/assets/assets_dungeon_ui_rogue.py b/tasks/dungeon/assets/assets_dungeon_ui_rogue.py index c64300e06..d2e5e6db9 100644 --- a/tasks/dungeon/assets/assets_dungeon_ui_rogue.py +++ b/tasks/dungeon/assets/assets_dungeon_ui_rogue.py @@ -45,13 +45,22 @@ SIMULATED_UNIVERSE_LOADED_CLASSIC = ButtonWrapper( ) SURVIVAL_INDEX_OE_LOADED = ButtonWrapper( name='SURVIVAL_INDEX_OE_LOADED', - share=Button( - file='./assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.png', - area=(455, 208, 485, 338), - search=(460, 238, 480, 268), - color=(130, 116, 91), - button=(455, 208, 485, 338), - ), + share=[ + Button( + file='./assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.png', + area=(460, 238, 480, 268), + search=(455, 208, 485, 338), + color=(221, 192, 131), + button=(460, 238, 480, 268), + ), + Button( + file='./assets/share/dungeon/ui_rogue/SURVIVAL_INDEX_OE_LOADED.2.png', + area=(460, 238, 480, 268), + search=(455, 208, 485, 338), + color=(198, 161, 100), + button=(460, 238, 480, 268), + ), + ], ) SURVIVAL_INDEX_SU_LOADED = ButtonWrapper( name='SURVIVAL_INDEX_SU_LOADED', diff --git a/tasks/dungeon/stamina.py b/tasks/dungeon/stamina.py index 6f28729b3..41fd56354 100644 --- a/tasks/dungeon/stamina.py +++ b/tasks/dungeon/stamina.py @@ -3,6 +3,7 @@ from module.base.timer import Timer from module.logger import logger from module.ocr.ocr import Digit from tasks.base.page import page_guide +from tasks.combat.assets.assets_combat_stamina_status import ICON_SEARCH, IMMERSIFIER_ICON from tasks.dungeon.assets.assets_dungeon_stamina import * from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB from tasks.dungeon.ui import DungeonUI @@ -16,6 +17,7 @@ class DungeonStamina(DungeonUI): out: IMMERSIFIER_CHECK """ logger.info('Enter immersifier') + IMMERSIFIER_ICON.load_search(ICON_SEARCH.area) while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -24,7 +26,7 @@ class DungeonStamina(DungeonUI): if self.appear(IMMERSIFIER_CHECK): break - if self.appear_then_click(ENTER_IMMERSIFIER, interval=2): + if self.appear_then_click(IMMERSIFIER_ICON, interval=2): continue def _immersifier_exit(self, skip_first_screenshot=True): @@ -140,7 +142,7 @@ class DungeonStamina(DungeonUI): logger.hr('Immersifier store', level=2) logger.info(f'Max store: {max_store}') self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) - self.dungeon_update_stamina() + self.update_stamina_status() before = self.config.stored.Immersifier.value if self.config.stored.Immersifier.is_full(): @@ -159,7 +161,7 @@ class DungeonStamina(DungeonUI): self._immersifier_enter() self._item_amount_set(amount, ocr_button=OCR_IMMERSIFIER_AMOUNT) self._item_confirm() - self.dungeon_update_stamina() + self.update_stamina_status() diff = self.config.stored.Immersifier.value - before logger.info(f'Stored {diff} immersifiers') return diff diff --git a/tasks/dungeon/state.py b/tasks/dungeon/state.py index 6a1a042ed..814306161 100644 --- a/tasks/dungeon/state.py +++ b/tasks/dungeon/state.py @@ -1,14 +1,13 @@ from datetime import timedelta from module.base.base import ModuleBase -from module.base.timer import Timer from module.base.utils import crop from module.config.stored.classes import now from module.config.utils import DEFAULT_TIME, get_server_next_monday_update, get_server_next_update from module.logger import logger from module.ocr.ocr import DigitCounter -from tasks.base.ui import UI -from tasks.dungeon.assets.assets_dungeon_state import OCR_SIMUNI_POINT, OCR_SIMUNI_POINT_OFFSET, OCR_STAMINA +from tasks.combat.stamina_status import StaminaStatus +from tasks.dungeon.assets.assets_dungeon_state import OCR_SIMUNI_POINT, OCR_SIMUNI_POINT_OFFSET from tasks.dungeon.keywords import DungeonList @@ -19,7 +18,7 @@ class OcrSimUniPoint(DigitCounter): return result -class DungeonState(UI): +class DungeonState(StaminaStatus): def dungeon_get_simuni_point(self, image=None) -> int: """ Page: @@ -48,67 +47,6 @@ class DungeonState(UI): logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}') return 0 - def dungeon_update_stamina(self, image=None, skip_first_screenshot=True): - """ - Returns: - bool: If success - - Pages: - in: page_guild, Survival_Index, Simulated_Universe - or page_rogue, LEVEL_CONFIRM - or rogue, REWARD_CLOSE - """ - ocr = DigitCounter(OCR_STAMINA) - timeout = Timer(1, count=2).start() - if image is None: - image = self.device.image - use_cached_image = False - else: - skip_first_screenshot = True - use_cached_image = True - - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - image = self.device.image - - stamina = (0, 0, 0) - immersifier = (0, 0, 0) - - if timeout.reached(): - logger.warning('dungeon_update_stamina() timeout') - return False - - for row in ocr.detect_and_ocr(image): - if row.ocr_text.isdigit(): - continue - if row.ocr_text == '+': - continue - if not ocr.is_format_matched(row.ocr_text): - continue - data = ocr.format_result(row.ocr_text) - if data[2] == self.config.stored.TrailblazePower.FIXED_TOTAL: - stamina = data - if data[2] == self.config.stored.Immersifier.FIXED_TOTAL: - immersifier = data - - if stamina[2] > 0 and immersifier[2] > 0: - break - if use_cached_image: - logger.info('dungeon_update_stamina() ended') - return - - stamina = stamina[0] - immersifier = immersifier[0] - logger.attr('TrailblazePower', stamina) - logger.attr('Imersifier', immersifier) - with self.config.multi_set(): - self.config.stored.TrailblazePower.value = stamina - self.config.stored.Immersifier.value = immersifier - return True - def dungeon_update_simuni(self): """ Update rogue weekly points, stamina, immersifier @@ -122,8 +60,8 @@ class DungeonState(UI): def func(image): logger.info('Update thread start') with self.config.multi_set(): - self.dungeon_get_simuni_point(image) - self.dungeon_update_stamina(image) + # self.dungeon_get_simuni_point(image) + self.update_stamina_status(image) ModuleBase.worker.submit(func, self.device.image) diff --git a/tasks/dungeon/ui.py b/tasks/dungeon/ui.py index 45e09b102..e558d038d 100644 --- a/tasks/dungeon/ui.py +++ b/tasks/dungeon/ui.py @@ -578,6 +578,7 @@ class DungeonUI(DungeonState): DUNGEON_LIST.use_plane = bool(dungeon.is_Calyx_Crimson) # Insight dungeon DUNGEON_LIST.insight_row(dungeon, main=self) + self.device.click_record_clear() # Check if dungeon unlocked for entrance in DUNGEON_LIST.navigates: entrance: OcrResultButton = entrance @@ -596,6 +597,7 @@ class DungeonUI(DungeonState): DUNGEON_LIST.drag_vector = (0.2, 0.4) DUNGEON_LIST.limit_entrance = True DUNGEON_LIST.insight_row(dungeon, main=self) + self.device.click_record_clear() DUNGEON_LIST.drag_vector = DraggableList.drag_vector DUNGEON_LIST.limit_entrance = False DUNGEON_LIST.load_rows(main=self) diff --git a/tasks/ornament/combat.py b/tasks/ornament/combat.py index 2e6c6b512..638e508e9 100644 --- a/tasks/ornament/combat.py +++ b/tasks/ornament/combat.py @@ -130,7 +130,7 @@ class OrnamentCombat(Dungeon, RouteLoader, DungeonState): after = before for _ in range(3): - self.dungeon_update_stamina() + self.update_stamina_status() after = self.get_equivalent_stamina() if expect_reduce: if before > after: diff --git a/tasks/rogue/entry/entry.py b/tasks/rogue/entry/entry.py index 9286f45e0..6a94b51b9 100644 --- a/tasks/rogue/entry/entry.py +++ b/tasks/rogue/entry/entry.py @@ -261,7 +261,7 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonRogueUI if not self.image_color_count(LEVEL_CONFIRM, color=(223, 223, 225), threshold=240, count=50): self.interval_clear(LEVEL_CONFIRM) continue - self.dungeon_update_stamina() + self.update_stamina_status() self.check_stop_condition() self.device.click(LEVEL_CONFIRM) continue diff --git a/tasks/rogue/event/reward.py b/tasks/rogue/event/reward.py index 4d9390922..a280c24ad 100644 --- a/tasks/rogue/event/reward.py +++ b/tasks/rogue/event/reward.py @@ -54,7 +54,7 @@ class RogueReward(RogueUI, CombatInteract, DungeonState): confirm.reset() continue if self.appear(REWARD_CLOSE, interval=2): - self.dungeon_update_stamina() + self.update_stamina_status() if not init: initial_stamina = self.config.stored.TrailblazePower.value initial_immersifier = self.config.stored.Immersifier.value