diff --git a/assets/character/Aventurine.png b/assets/character/Aventurine.png new file mode 100644 index 000000000..2e1c1de45 Binary files /dev/null and b/assets/character/Aventurine.png differ diff --git a/assets/character/Robin.png b/assets/character/Robin.png new file mode 100644 index 000000000..17046c663 Binary files /dev/null and b/assets/character/Robin.png differ diff --git a/assets/cn/assignment/ui/CHARACTER_MATERIALS_CHECK.png b/assets/cn/assignment/ui/CHARACTER_MATERIALS_CHECK.png index d35f8d96d..d88e9ea6b 100644 Binary files a/assets/cn/assignment/ui/CHARACTER_MATERIALS_CHECK.png and b/assets/cn/assignment/ui/CHARACTER_MATERIALS_CHECK.png differ diff --git a/assets/cn/assignment/ui/CHARACTER_MATERIALS_CLICK.png b/assets/cn/assignment/ui/CHARACTER_MATERIALS_CLICK.png index 41c3a87de..542ef36bb 100644 Binary files a/assets/cn/assignment/ui/CHARACTER_MATERIALS_CLICK.png and b/assets/cn/assignment/ui/CHARACTER_MATERIALS_CLICK.png differ diff --git a/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CHECK.png b/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CHECK.png index 19245e7cf..bf97a78c9 100644 Binary files a/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CHECK.png and b/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CHECK.png differ diff --git a/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CLICK.png b/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CLICK.png index 05b9782ab..11f5ac020 100644 Binary files a/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CLICK.png and b/assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CLICK.png differ diff --git a/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CHECK.png b/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CHECK.png index 26999de56..cdd4856ae 100644 Binary files a/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CHECK.png and b/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CHECK.png differ diff --git a/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CLICK.png b/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CLICK.png index 4031171e0..b7a0ebbda 100644 Binary files a/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CLICK.png and b/assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CLICK.png differ diff --git a/assets/share/combat/team/TEAM_5_CLICK.png b/assets/cn/base/page/ASSIGNMENT_CHECK.png similarity index 73% rename from assets/share/combat/team/TEAM_5_CLICK.png rename to assets/cn/base/page/ASSIGNMENT_CHECK.png index deb5e49f8..a063e299c 100644 Binary files a/assets/share/combat/team/TEAM_5_CLICK.png and b/assets/cn/base/page/ASSIGNMENT_CHECK.png differ diff --git a/assets/share/combat/team/TEAM_1_CLICK.png b/assets/cn/rogue/ui/CURIO_FIXED.png similarity index 63% rename from assets/share/combat/team/TEAM_1_CLICK.png rename to assets/cn/rogue/ui/CURIO_FIXED.png index 3f8f592d8..8d6adc00d 100644 Binary files a/assets/share/combat/team/TEAM_1_CLICK.png and b/assets/cn/rogue/ui/CURIO_FIXED.png differ diff --git a/assets/share/combat/team/TEAM_7_CLICK.png b/assets/en/base/page/ASSIGNMENT_CHECK.png similarity index 83% rename from assets/share/combat/team/TEAM_7_CLICK.png rename to assets/en/base/page/ASSIGNMENT_CHECK.png index 9bdb478c9..cc84e201c 100644 Binary files a/assets/share/combat/team/TEAM_7_CLICK.png and b/assets/en/base/page/ASSIGNMENT_CHECK.png differ diff --git a/assets/gui/css/alas.css b/assets/gui/css/alas.css index 91c5649fa..5c7610226 100644 --- a/assets/gui/css/alas.css +++ b/assets/gui/css/alas.css @@ -441,10 +441,13 @@ pre.rich-traceback-code { [id^="pywebio-scope-dashboard-value-"] { display: flex; - align-items: flex-end; + align-items: baseline; height: 1.5rem; } +[id^="pywebio-scope-arg_stored-stored-value-"] p { + margin-bottom: 0; +} #pywebio-scope-log { line-height: 1.2; diff --git a/assets/share/assignment/dispatch/CHARACTER_1.png b/assets/share/assignment/dispatch/CHARACTER_1.png index a64234004..91d01ca2b 100644 Binary files a/assets/share/assignment/dispatch/CHARACTER_1.png and b/assets/share/assignment/dispatch/CHARACTER_1.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png b/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png index 2cf101dc3..954517450 100644 Binary files a/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png and b/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_2.png b/assets/share/assignment/dispatch/CHARACTER_2.png index 96ca4fe2a..c68a4daa4 100644 Binary files a/assets/share/assignment/dispatch/CHARACTER_2.png and b/assets/share/assignment/dispatch/CHARACTER_2.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png b/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png index 0980134cc..1bfd1964a 100644 Binary files a/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png and b/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png differ diff --git a/assets/share/base/page/ASSIGNMENT_CHECK.png b/assets/share/base/page/ASSIGNMENT_CHECK.png deleted file mode 100644 index c9869cf7c..000000000 Binary files a/assets/share/base/page/ASSIGNMENT_CHECK.png and /dev/null differ diff --git a/assets/share/combat/team/TEAM_4_CLICK.png b/assets/share/base/page/MAP_EXIT.2.png similarity index 79% rename from assets/share/combat/team/TEAM_4_CLICK.png rename to assets/share/base/page/MAP_EXIT.2.png index 1ce323438..6a4dcb099 100644 Binary files a/assets/share/combat/team/TEAM_4_CLICK.png and b/assets/share/base/page/MAP_EXIT.2.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_LIST.png b/assets/share/combat/support/COMBAT_SUPPORT_LIST.png index 9917bea76..f728912e2 100644 Binary files a/assets/share/combat/support/COMBAT_SUPPORT_LIST.png and b/assets/share/combat/support/COMBAT_SUPPORT_LIST.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png b/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png index a9839d371..d9d6f7597 100644 Binary files a/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png and b/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png b/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png index 1e4779af3..8e7040fec 100644 Binary files a/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png and b/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png b/assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png deleted file mode 100644 index 5ea0ab7bc..000000000 Binary files a/assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png and /dev/null differ diff --git a/assets/share/combat/support/SUPPORT_SELECTED.SEARCH.png b/assets/share/combat/support/SUPPORT_SELECTED.SEARCH.png index 10a1f29b5..b7966bf7c 100644 Binary files a/assets/share/combat/support/SUPPORT_SELECTED.SEARCH.png and b/assets/share/combat/support/SUPPORT_SELECTED.SEARCH.png differ diff --git a/assets/share/combat/team/TEAM_8_CLICK.png b/assets/share/combat/team/TEAM_8_CLICK.png deleted file mode 100644 index 8e2dbcf65..000000000 Binary files a/assets/share/combat/team/TEAM_8_CLICK.png and /dev/null differ diff --git a/assets/share/combat/team/TEAM_9_CLICK.png b/assets/share/combat/team/TEAM_9_CLICK.png deleted file mode 100644 index 018a0efa7..000000000 Binary files a/assets/share/combat/team/TEAM_9_CLICK.png and /dev/null differ diff --git a/assets/share/combat/team/TEAM_3_CLICK.png b/assets/share/combat/team/TEAM_NEXT.png similarity index 56% rename from assets/share/combat/team/TEAM_3_CLICK.png rename to assets/share/combat/team/TEAM_NEXT.png index 1d4a6130d..52ee873a1 100644 Binary files a/assets/share/combat/team/TEAM_3_CLICK.png and b/assets/share/combat/team/TEAM_NEXT.png differ diff --git a/assets/share/combat/team/TEAM_2_CLICK.png b/assets/share/combat/team/TEAM_PREV.png similarity index 52% rename from assets/share/combat/team/TEAM_2_CLICK.png rename to assets/share/combat/team/TEAM_PREV.png index 7a20b4e62..9f37c8752 100644 Binary files a/assets/share/combat/team/TEAM_2_CLICK.png and b/assets/share/combat/team/TEAM_PREV.png differ diff --git a/assets/share/freebies/support_reward/CAN_GET_REWARD.png b/assets/share/freebies/support_reward/CAN_GET_REWARD.png index afaf00938..a7c64e380 100644 Binary files a/assets/share/freebies/support_reward/CAN_GET_REWARD.png and b/assets/share/freebies/support_reward/CAN_GET_REWARD.png differ diff --git a/assets/share/freebies/support_reward/IN_PROFILE.png b/assets/share/freebies/support_reward/IN_PROFILE.png index b6392d7fd..a6f257eea 100644 Binary files a/assets/share/freebies/support_reward/IN_PROFILE.png and b/assets/share/freebies/support_reward/IN_PROFILE.png differ diff --git a/assets/share/combat/team/TEAM_6_CLICK.png b/assets/share/freebies/support_reward/REWARD_POPUP.png similarity index 67% rename from assets/share/combat/team/TEAM_6_CLICK.png rename to assets/share/freebies/support_reward/REWARD_POPUP.png index 4fcd4b8c9..a4a0edcc9 100644 Binary files a/assets/share/combat/team/TEAM_6_CLICK.png and b/assets/share/freebies/support_reward/REWARD_POPUP.png differ diff --git a/assets/share/rogue/path/PATH_LOADED_CHECK.png b/assets/share/rogue/path/PATH_LOADED_CHECK.png new file mode 100644 index 000000000..14e7eae56 Binary files /dev/null and b/assets/share/rogue/path/PATH_LOADED_CHECK.png differ diff --git a/bin/MaaTouch/maatouch b/bin/MaaTouch/maatouch index 3673f3ff0..e164cdb89 100644 Binary files a/bin/MaaTouch/maatouch and b/bin/MaaTouch/maatouch differ diff --git a/config/template.json b/config/template.json index 584323178..9d666a02e 100644 --- a/config/template.json +++ b/config/template.json @@ -199,7 +199,9 @@ "DomainStrategy": "combat", "UseImmersifier": true, "DoubleEvent": true, - "UseStamina": false + "WeeklyFarming": false, + "UseStamina": false, + "SimulatedUniverseFarm": {} }, "RogueBlessing": { "PresetBlessingFilter": "preset", diff --git a/deploy/Windows/config.py b/deploy/Windows/config.py index adf91ee67..5c10ab0a8 100644 --- a/deploy/Windows/config.py +++ b/deploy/Windows/config.py @@ -1,6 +1,7 @@ import copy import os import subprocess +import sys from typing import Optional, Union from deploy.Windows.logger import logger @@ -80,12 +81,6 @@ class DeployConfig(ConfigModel): self.config_template = {} self.read() - # Bypass webui.config.DeployConfig.__setattr__() - # Don't write these into deploy.yaml - super().__setattr__('GitOverCdn', self.Repository in ['cn']) - if self.Repository in ['global', 'cn']: - super().__setattr__('Repository', 'https://github.com/LmeSzinc/StarRailCopilot') - self.write() self.show_config() @@ -109,9 +104,21 @@ class DeployConfig(ConfigModel): if hasattr(self, key): super().__setattr__(key, value) + self.config_redirect() + def write(self): poor_yaml_write(self.config, self.file) + def config_redirect(self): + """ + Redirect deploy config, must be called after each `read()` + """ + # Bypass webui.config.DeployConfig.__setattr__() + # Don't write these into deploy.yaml + super().__setattr__('GitOverCdn', self.Repository in ['cn']) + if self.Repository in ['global', 'cn']: + super().__setattr__('Repository', 'https://github.com/LmeSzinc/StarRailCopilot') + def filepath(self, path): """ Args: @@ -143,7 +150,7 @@ class DeployConfig(ConfigModel): if os.path.exists(exe): return exe - logger.warning(f'AdbExecutable: {exe} does not exists, use `adb` instead') + logger.warning(f'AdbExecutable: {exe} does not exist, use `adb` instead') return 'adb' @cached_property @@ -152,12 +159,18 @@ class DeployConfig(ConfigModel): if os.path.exists(exe): return exe - logger.warning(f'GitExecutable: {exe} does not exists, use `git` instead') + logger.warning(f'GitExecutable: {exe} does not exist, use `git` instead') return 'git' @cached_property def python(self) -> str: - return self.filepath(self.PythonExecutable) + exe = self.filepath(self.PythonExecutable) + if os.path.exists(exe): + return exe + + current = sys.executable.replace("\\", "/") + logger.warning(f'PythonExecutable: {exe} does not exist, use current python instead: {current}') + return current @cached_property def requirements_file(self) -> str: diff --git a/dev_tools/keywords/assignment.py b/dev_tools/keywords/assignment.py index 30dadcb25..25d53a3e3 100644 --- a/dev_tools/keywords/assignment.py +++ b/dev_tools/keywords/assignment.py @@ -5,6 +5,22 @@ from dev_tools.keywords.base import UI_LANGUAGES, GenerateKeyword from module.config.utils import deep_get +def resort(dic: dict): + # Poor assigment sort for 2.2 + order = [ + 1008, 1007, 1006, 1005, 1004, 1003, 1002, 1001, + 3001, 2001, 4001, + 5008, 5006, 5005, 5003, 5002, 5007, 5004, 5001, + ] + out = {} + for index in order: + value = dic.pop(index) + out[index] = value + for k, v, in dic.items(): + out[k] = v + return out + + @cache def get_assignment_entry_data(): """ @@ -16,6 +32,9 @@ def get_assignment_entry_data(): deep_get(expedition, 'Name.Hash'): deep_get(expedition, 'ExpeditionID') for expedition in GenerateKeyword.read_file('./ExcelOutput/ExpeditionData.json').values() } + rev = {v: k for k, v in expedition_namehash_to_id.items()} + rev = resort(rev) + expedition_namehash_to_id = {v: k for k, v in rev.items()} expedition_id_to_reward_id = { deep_get(expedition, '4.2.ExpeditionID'): deep_get(expedition, '4.2.RewardID') for expedition in GenerateKeyword.read_file('./ExcelOutput/ExpeditionReward.json').values() @@ -119,5 +138,6 @@ class GenerateAssignmentEventEntry(GenerateKeyword): if __name__ == "__main__": from dev_tools.keywords.base import TextMap + TextMap.DATA_FOLDER = '../StarRailData' GenerateAssignment()() diff --git a/dev_tools/keywords/base.py b/dev_tools/keywords/base.py index e44f0ac61..f9c147b3f 100644 --- a/dev_tools/keywords/base.py +++ b/dev_tools/keywords/base.py @@ -63,7 +63,8 @@ class TextMap: def text_to_variable(text): text = re.sub("'s |s' ", '_', text) - text = re.sub(r'[ \-—:\'/•.]+', '_', text) + text = re.sub(r'[ \-—–:\'/•.™]+', '_', text) + text = re.sub(r'[█]+', '', text) text = re.sub(r'[(),#"?!&%*]|', '', text) # text = re.sub(r'[#_]?\d+(_times?)?', '', text) text = re.sub(r'', '', text) diff --git a/dev_tools/keywords/dungeon_list.py b/dev_tools/keywords/dungeon_list.py index d18451619..35221f9da 100644 --- a/dev_tools/keywords/dungeon_list.py +++ b/dev_tools/keywords/dungeon_list.py @@ -16,6 +16,8 @@ def dungeon_name(name: str) -> str: name = f'Echo_of_War_{name}' if name in ['The_Swarm_Disaster', 'Gold_and_Gears']: name = f'Simulated_Universe_{name}' + name = name.replace('Stagnant_Shadow_Stagnant_Shadow', 'Stagnant_Shadow') + name = name.replace('Cavern_of_Corrosion_Cavern_of_Corrosion', 'Cavern_of_Corrosion') return name @@ -70,7 +72,10 @@ class GenerateDungeonList(GenerateKeyword): if text.startswith('Calyx_Crimson'): plane = MapPlane.find_plane_id(keyword['plane_id']) - text = f'{text}_{plane.name}' + if plane is not None: + text = f'{text}_{plane.name}' + else: + text = f'{text}_unknown_plane' return text def convert_keyword(self, text: str, lang: str) -> str: diff --git a/dev_tools/keywords/map_plane.py b/dev_tools/keywords/map_plane.py index dec382cff..0d365dc50 100644 --- a/dev_tools/keywords/map_plane.py +++ b/dev_tools/keywords/map_plane.py @@ -68,3 +68,7 @@ class GenerateMapPlane(GenerateKeyword): return f'Special_{text}' else: return f'{world.short_name}_{text}' + + def convert_keyword(self, text: str, lang: str) -> str: + text = text.replace('™', '') + return super().convert_keyword(text, lang=lang) diff --git a/module/base/base.py b/module/base/base.py index 9a1dc41cb..f7687d214 100644 --- a/module/base/base.py +++ b/module/base/base.py @@ -1,5 +1,3 @@ -from concurrent.futures import ThreadPoolExecutor - import module.config.server as server_ from module.base.button import Button, ButtonWrapper, ClickButton, match_template from module.base.timer import Timer @@ -50,11 +48,22 @@ class ModuleBase: self.interval_timer = {} @cached_class_property - def worker(self) -> ThreadPoolExecutor: + def worker(self): """ A thread pool to run things at background + + Examples: + ``` + def func(image): + logger.info('Update thread start') + with self.config.multi_set(): + self.dungeon_get_simuni_point(image) + self.dungeon_update_stamina(image) + ModuleBase.worker.submit(func, self.device.image) + ``` """ logger.hr('Creating worker') + from concurrent.futures import ThreadPoolExecutor pool = ThreadPoolExecutor(1) return pool diff --git a/module/base/button.py b/module/base/button.py index b0e057ee5..153df61df 100644 --- a/module/base/button.py +++ b/module/base/button.py @@ -34,12 +34,34 @@ class Button(Resource): def clear_offset(self): self._button_offset = (0, 0) + def is_offset_in(self, x=0, y=0): + """ + Args: + x: + y: + + Returns: + bool: If _button_offset is in (-x, -y, x, y) + """ + if x: + if self._button_offset[0] < -x or self._button_offset[0] > x: + return False + if y: + if self._button_offset[1] < -y or self._button_offset[1] > y: + return False + return True + @cached_property def image(self): return load_image(self.file, self.area) + @cached_property + def image_binary(self): + return rgb2gray(self.image) + def resource_release(self): del_cached_property(self, 'image') + del_cached_property(self, 'image_binary') self.clear_offset() def __str__(self): @@ -96,6 +118,29 @@ class Button(Resource): self._button_offset = np.array(point) + self.search[:2] - self.area[:2] return sim > similarity + def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool: + """ + Detects assets by template matching. + + To Some buttons, its location may not be static, `_button_offset` will be set. + + Args: + image: Screenshot. + similarity (float): 0-1. + direct_match: True to ignore `self.search` + + Returns: + bool. + """ + if not direct_match: + image = crop(image, self.search, copy=False) + image = rgb2gray(image) + res = cv2.matchTemplate(self.image_binary, image, cv2.TM_CCOEFF_NORMED) + _, sim, _, point = cv2.minMaxLoc(res) + + self._button_offset = np.array(point) + self.search[:2] - self.area[:2] + return sim > similarity + def match_multi_template(self, image, similarity=0.85, direct_match=False): """ Detects assets by template matching, return multiple reults @@ -208,6 +253,13 @@ class ButtonWrapper(Resource): return True return False + def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool: + for assets in self.buttons: + if assets.match_template_binary(image, similarity=similarity, direct_match=direct_match): + self._matched_button = assets + return True + return False + def match_multi_template(self, image, similarity=0.85, threshold=5, direct_match=False): """ Detects assets by template matching, return multiple results @@ -295,6 +347,17 @@ class ButtonWrapper(Resource): for b in self.iter_buttons(): b.clear_offset() + def is_offset_in(self, x=0, y=0): + """ + Args: + x: + y: + + Returns: + bool: If _button_offset is in (-x, -y, x, y) + """ + return self.matched_button.is_offset_in(x=x, y=y) + def load_search(self, area): """ Set `search` attribute. diff --git a/module/base/utils/utils.py b/module/base/utils/utils.py index 4f2abea70..c3d919136 100644 --- a/module/base/utils/utils.py +++ b/module/base/utils/utils.py @@ -625,17 +625,29 @@ def image_paste(image, background, origin): def rgb2gray(image): """ + gray = ( MAX(r, g, b) + MIN(r, g, b)) / 2 + Args: image (np.ndarray): Shape (height, width, channel) Returns: np.ndarray: Shape (height, width) """ + # r, g, b = cv2.split(image) + # return cv2.add( + # cv2.multiply(cv2.max(cv2.max(r, g), b), 0.5), + # cv2.multiply(cv2.min(cv2.min(r, g), b), 0.5) + # ) r, g, b = cv2.split(image) - return cv2.add( - cv2.multiply(cv2.max(cv2.max(r, g), b), 0.5), - cv2.multiply(cv2.min(cv2.min(r, g), b), 0.5) - ) + maximum = cv2.max(r, g) + cv2.max(maximum, b, dst=maximum) + cv2.convertScaleAbs(maximum, alpha=0.5, dst=maximum) + cv2.min(r, g, dst=r) + cv2.min(r, b, dst=r) + cv2.convertScaleAbs(r, alpha=0.5, dst=r) + # minimum = r + cv2.add(maximum, r, dst=maximum) + return maximum def rgb2hsv(image): @@ -791,11 +803,24 @@ def color_similarity_2d(image, color): Returns: np.ndarray: uint8 """ - r, g, b = cv2.split(cv2.subtract(image, (*color, 0))) - positive = cv2.max(cv2.max(r, g), b) - r, g, b = cv2.split(cv2.subtract((*color, 0), image)) - negative = cv2.max(cv2.max(r, g), b) - return cv2.subtract(255, cv2.add(positive, negative)) + # r, g, b = cv2.split(cv2.subtract(image, (*color, 0))) + # positive = cv2.max(cv2.max(r, g), b) + # r, g, b = cv2.split(cv2.subtract((*color, 0), image)) + # negative = cv2.max(cv2.max(r, g), b) + # return cv2.subtract(255, cv2.add(positive, negative)) + diff = cv2.subtract(image, (*color, 0)) + r, g, b = cv2.split(diff) + cv2.max(r, g, dst=r) + cv2.max(r, b, dst=r) + positive = r + cv2.subtract((*color, 0), image, dst=diff) + r, g, b = cv2.split(diff) + cv2.max(r, g, dst=r) + cv2.max(r, b, dst=r) + negative = r + cv2.add(positive, negative, dst=positive) + cv2.subtract(255, positive, dst=positive) + return positive def extract_letters(image, letter=(255, 255, 255), threshold=128): @@ -809,11 +834,24 @@ def extract_letters(image, letter=(255, 255, 255), threshold=128): Returns: np.ndarray: Shape (height, width) """ - r, g, b = cv2.split(cv2.subtract(image, (*letter, 0))) - positive = cv2.max(cv2.max(r, g), b) - r, g, b = cv2.split(cv2.subtract((*letter, 0), image)) - negative = cv2.max(cv2.max(r, g), b) - return cv2.multiply(cv2.add(positive, negative), 255.0 / threshold) + # r, g, b = cv2.split(cv2.subtract(image, (*letter, 0))) + # positive = cv2.max(cv2.max(r, g), b) + # r, g, b = cv2.split(cv2.subtract((*letter, 0), image)) + # negative = cv2.max(cv2.max(r, g), b) + # return cv2.multiply(cv2.add(positive, negative), 255.0 / threshold) + diff = cv2.subtract(image, (*letter, 0)) + r, g, b = cv2.split(diff) + cv2.max(r, g, dst=r) + cv2.max(r, b, dst=r) + positive = r + cv2.subtract((*letter, 0), image, dst=diff) + r, g, b = cv2.split(diff) + cv2.max(r, g, dst=r) + cv2.max(r, b, dst=r) + negative = r + cv2.add(positive, negative, dst=positive) + cv2.convertScaleAbs(positive, alpha=255.0 / threshold, dst=positive) + return positive def extract_white_letters(image, threshold=128): @@ -827,10 +865,21 @@ def extract_white_letters(image, threshold=128): Returns: np.ndarray: Shape (height, width) """ + # minimum = cv2.min(cv2.min(r, g), b) + # maximum = cv2.max(cv2.max(r, g), b) + # return cv2.multiply(cv2.add(maximum, cv2.subtract(maximum, minimum)), 255.0 / threshold) r, g, b = cv2.split(cv2.subtract((255, 255, 255, 0), image)) - minimum = cv2.min(cv2.min(r, g), b) - maximum = cv2.max(cv2.max(r, g), b) - return cv2.multiply(cv2.add(maximum, cv2.subtract(maximum, minimum)), 255.0 / threshold) + maximum = cv2.max(r, g) + cv2.max(maximum, b, dst=maximum) + cv2.convertScaleAbs(maximum, alpha=0.5, dst=maximum) + cv2.min(r, g, dst=r) + cv2.min(r, b, dst=r) + cv2.convertScaleAbs(r, alpha=0.5, dst=r) + minimum = r + cv2.subtract(maximum, minimum, dst=minimum) + cv2.add(maximum, minimum, dst=maximum) + cv2.convertScaleAbs(maximum, alpha=255.0 / threshold, dst=maximum) + return maximum def color_mapping(image, max_multiply=2): @@ -849,7 +898,9 @@ def color_mapping(image, max_multiply=2): low, high = np.min(image), np.max(image) multiply = min(255 / (high - low), max_multiply) add = (255 - multiply * (low + high)) / 2 - image = cv2.add(cv2.multiply(image, multiply), add) + # image = cv2.add(cv2.multiply(image, multiply), add) + cv2.multiply(image, multiply, dst=image) + cv2.add(image, add, dst=image) image[image > 255] = 255 image[image < 0] = 0 return image.astype(np.uint8) @@ -909,7 +960,7 @@ def color_bar_percentage(image, area, prev_color, reverse=False, starter=0, thre Returns: float: 0 to 1. """ - image = crop(image, area) + image = crop(image, area, copy=False) image = image[:, ::-1, :] if reverse else image length = image.shape[1] prev_index = starter diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 75c4cba04..64f0ee68d 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -48,7 +48,8 @@ "aScreenCap_nc", "DroidCast", "DroidCast_raw", - "scrcpy" + "scrcpy", + "nemu_ipc" ], "display": "hide" }, @@ -237,6 +238,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", "Calyx_Crimson_Erudition_Jarilo_RivetTown", @@ -246,6 +248,7 @@ "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", "Stagnant_Shadow_Spike", "Stagnant_Shadow_Perdition", + "Stagnant_Shadow_Duty", "Stagnant_Shadow_Blaze", "Stagnant_Shadow_Scorch", "Stagnant_Shadow_Ire", @@ -290,6 +293,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", "Calyx_Crimson_Erudition_Jarilo_RivetTown", @@ -357,6 +361,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", "Calyx_Crimson_Erudition_Jarilo_RivetTown", @@ -373,6 +378,7 @@ "do_not_achieve", "Stagnant_Shadow_Spike", "Stagnant_Shadow_Perdition", + "Stagnant_Shadow_Duty", "Stagnant_Shadow_Blaze", "Stagnant_Shadow_Scorch", "Stagnant_Shadow_Ire", @@ -426,6 +432,7 @@ "Argenti", "Arlan", "Asta", + "Aventurine", "Bailu", "BlackSwan", "Blade", @@ -454,6 +461,7 @@ "Natasha", "Pela", "Qingque", + "Robin", "RuanMei", "Sampo", "Seele", @@ -1068,100 +1076,100 @@ "type": "select", "value": "Nameless_Land_Nameless_People", "option": [ - "Nine_Billion_Names", - "Destruction_of_the_Destroyer", - "Winter_Soldiers", - "Born_to_Obey", - "Root_Out_the_Turpitude", - "Fire_Lord_Inflames_Blades_of_War", - "A_Startling_Night_Terror", "Tranquility_of_Vimala_bhumi", - "Nameless_Land_Nameless_People", + "A_Startling_Night_Terror", + "Fire_Lord_Inflames_Blades_of_War", + "Root_Out_the_Turpitude", + "Born_to_Obey", + "Winter_Soldiers", + "Destruction_of_the_Destroyer", + "Nine_Billion_Names", "Akashic_Records", + "Nameless_Land_Nameless_People", "The_Invisible_Hand", - "Abandoned_and_Insulted", - "Spring_of_Life", - "The_Land_of_Gold", - "The_Blossom_in_the_Storm", - "Legend_of_the_Puppet_Master", + "Scalpel_and_Screwdriver", "The_Wages_of_Humanity", + "Legend_of_the_Puppet_Master", + "The_Land_of_Gold", + "Spring_of_Life", "Fragments_of_Illusory_Dreams", - "Scalpel_and_Screwdriver" + "The_Blossom_in_the_Storm", + "Abandoned_and_Insulted" ] }, "Name_2": { "type": "select", "value": "Akashic_Records", "option": [ - "Nine_Billion_Names", - "Destruction_of_the_Destroyer", - "Winter_Soldiers", - "Born_to_Obey", - "Root_Out_the_Turpitude", - "Fire_Lord_Inflames_Blades_of_War", - "A_Startling_Night_Terror", "Tranquility_of_Vimala_bhumi", - "Nameless_Land_Nameless_People", + "A_Startling_Night_Terror", + "Fire_Lord_Inflames_Blades_of_War", + "Root_Out_the_Turpitude", + "Born_to_Obey", + "Winter_Soldiers", + "Destruction_of_the_Destroyer", + "Nine_Billion_Names", "Akashic_Records", + "Nameless_Land_Nameless_People", "The_Invisible_Hand", - "Abandoned_and_Insulted", - "Spring_of_Life", - "The_Land_of_Gold", - "The_Blossom_in_the_Storm", - "Legend_of_the_Puppet_Master", + "Scalpel_and_Screwdriver", "The_Wages_of_Humanity", + "Legend_of_the_Puppet_Master", + "The_Land_of_Gold", + "Spring_of_Life", "Fragments_of_Illusory_Dreams", - "Scalpel_and_Screwdriver" + "The_Blossom_in_the_Storm", + "Abandoned_and_Insulted" ] }, "Name_3": { "type": "select", "value": "The_Invisible_Hand", "option": [ - "Nine_Billion_Names", - "Destruction_of_the_Destroyer", - "Winter_Soldiers", - "Born_to_Obey", - "Root_Out_the_Turpitude", - "Fire_Lord_Inflames_Blades_of_War", - "A_Startling_Night_Terror", "Tranquility_of_Vimala_bhumi", - "Nameless_Land_Nameless_People", + "A_Startling_Night_Terror", + "Fire_Lord_Inflames_Blades_of_War", + "Root_Out_the_Turpitude", + "Born_to_Obey", + "Winter_Soldiers", + "Destruction_of_the_Destroyer", + "Nine_Billion_Names", "Akashic_Records", + "Nameless_Land_Nameless_People", "The_Invisible_Hand", - "Abandoned_and_Insulted", - "Spring_of_Life", - "The_Land_of_Gold", - "The_Blossom_in_the_Storm", - "Legend_of_the_Puppet_Master", + "Scalpel_and_Screwdriver", "The_Wages_of_Humanity", + "Legend_of_the_Puppet_Master", + "The_Land_of_Gold", + "Spring_of_Life", "Fragments_of_Illusory_Dreams", - "Scalpel_and_Screwdriver" + "The_Blossom_in_the_Storm", + "Abandoned_and_Insulted" ] }, "Name_4": { "type": "select", "value": "Nine_Billion_Names", "option": [ - "Nine_Billion_Names", - "Destruction_of_the_Destroyer", - "Winter_Soldiers", - "Born_to_Obey", - "Root_Out_the_Turpitude", - "Fire_Lord_Inflames_Blades_of_War", - "A_Startling_Night_Terror", "Tranquility_of_Vimala_bhumi", - "Nameless_Land_Nameless_People", + "A_Startling_Night_Terror", + "Fire_Lord_Inflames_Blades_of_War", + "Root_Out_the_Turpitude", + "Born_to_Obey", + "Winter_Soldiers", + "Destruction_of_the_Destroyer", + "Nine_Billion_Names", "Akashic_Records", + "Nameless_Land_Nameless_People", "The_Invisible_Hand", - "Abandoned_and_Insulted", - "Spring_of_Life", - "The_Land_of_Gold", - "The_Blossom_in_the_Storm", - "Legend_of_the_Puppet_Master", + "Scalpel_and_Screwdriver", "The_Wages_of_Humanity", + "Legend_of_the_Puppet_Master", + "The_Land_of_Gold", + "Spring_of_Life", "Fragments_of_Illusory_Dreams", - "Scalpel_and_Screwdriver" + "The_Blossom_in_the_Storm", + "Abandoned_and_Insulted" ] }, "Duration": { @@ -1303,7 +1311,8 @@ "Echo_of_War_Destruction_Beginning", "Echo_of_War_End_of_the_Eternal_Freeze", "Echo_of_War_Divine_Seed", - "Echo_of_War_Borehole_Planet_Old_Crater" + "Echo_of_War_Borehole_Planet_Old_Crater", + "Echo_of_War_Salutations_of_Ashen_Dreams" ] }, "Team": { @@ -1340,6 +1349,7 @@ "Argenti", "Arlan", "Asta", + "Aventurine", "Bailu", "BlackSwan", "Blade", @@ -1368,6 +1378,7 @@ "Natasha", "Pela", "Qingque", + "Robin", "RuanMei", "Sampo", "Seele", @@ -1466,9 +1477,19 @@ "type": "checkbox", "value": true }, + "WeeklyFarming": { + "type": "checkbox", + "value": false + }, "UseStamina": { "type": "checkbox", "value": false + }, + "SimulatedUniverseFarm": { + "type": "stored", + "value": {}, + "display": "disabled", + "stored": "StoredSimulatedUniverseElite" } }, "RogueBlessing": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 2f976f2ef..0dc814a8f 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -29,7 +29,18 @@ Emulator: option: [ auto, cn, en ] ScreenshotMethod: value: auto - option: [ auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy ] + option: [ + auto, + ADB, + ADB_nc, + uiautomator2, + aScreenCap, + aScreenCap_nc, + DroidCast, + DroidCast_raw, + scrcpy, + nemu_ipc, + ] ControlMethod: value: MaaTouch option: [ minitouch, MaaTouch ] @@ -246,7 +257,11 @@ RogueWorld: option: [ combat, occurrence ] UseImmersifier: true DoubleEvent: true + WeeklyFarming: false UseStamina: false + SimulatedUniverseFarm: + stored: StoredSimulatedUniverseElite + display: disabled RogueBlessing: PresetBlessingFilter: diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json index f984b47e4..b9c86336a 100644 --- a/module/config/argument/stored.json +++ b/module/config/argument/stored.json @@ -293,5 +293,18 @@ }, "order": 0, "color": "#777777" + }, + "SimulatedUniverseFarm": { + "name": "SimulatedUniverseFarm", + "path": "Rogue.RogueWorld.SimulatedUniverseFarm", + "i18n": "RogueWorld.SimulatedUniverseFarm.name", + "stored": "StoredSimulatedUniverseElite", + "attrs": { + "time": "2020-01-01 00:00:00", + "total": 100, + "value": 0 + }, + "order": 0, + "color": "#777777" } } \ No newline at end of file diff --git a/module/config/config.py b/module/config/config.py index 6ba4e6d5a..26e54e287 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -176,6 +176,10 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher self.data, keys="Alas.Optimization.CloseGameDuringWait", default=False ) + @property + def is_actual_task(self): + return self.task.command.lower() not in ['alas', 'template'] + @property def is_cloud_game(self): return deep_get( diff --git a/module/config/config_generated.py b/module/config/config_generated.py index bd79039f3..efb301bda 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -20,7 +20,7 @@ class GeneratedConfig: 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 + Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc Emulator_ControlMethod = 'MaaTouch' # minitouch, MaaTouch Emulator_AdbRestart = False @@ -46,20 +46,20 @@ class GeneratedConfig: 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_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, 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_Ire, 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' # 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_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, 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_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_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, 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_Duty, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, 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' # 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_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, 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` DungeonDaily_CalyxGolden = 'Calyx_Golden_Treasures_Jarilo_VI' # do_not_achieve, 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 - DungeonDaily_CalyxCrimson = 'Calyx_Crimson_Destruction_Herta_StorageZone' # do_not_achieve, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, 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 - DungeonDaily_StagnantShadow = 'Stagnant_Shadow_Quanta' # do_not_achieve, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, 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 + DungeonDaily_CalyxCrimson = 'Calyx_Crimson_Destruction_Herta_StorageZone' # do_not_achieve, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, 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 + DungeonDaily_StagnantShadow = 'Stagnant_Shadow_Quanta' # do_not_achieve, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Duty, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, 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 DungeonDaily_CavernOfCorrosion = 'Cavern_of_Corrosion_Path_of_Providence' # do_not_achieve, 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 # Group `DungeonSupport` DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use - DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Acheron, Argenti, Arlan, Asta, Bailu, BlackSwan, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, DrRatio, FuXuan, Gallagher, Gepard, Guinaifen, Hanya, Herta, Himeko, Hook, Huohuo, JingYuan, Jingliu, Kafka, Luka, Luocha, Lynx, March7th, Misha, Natasha, Pela, Qingque, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong + DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Acheron, Argenti, Arlan, Asta, Aventurine, Bailu, BlackSwan, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, DrRatio, FuXuan, Gallagher, Gepard, Guinaifen, Hanya, Herta, Himeko, Hook, Huohuo, JingYuan, Jingliu, Kafka, Luka, Luocha, Lynx, March7th, Misha, Natasha, Pela, Qingque, Robin, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong # Group `DungeonStorage` DungeonStorage_TrailblazePower = {} @@ -72,7 +72,7 @@ class GeneratedConfig: SupportReward_Collect = True # Group `Weekly` - Weekly_Name = 'Echo_of_War_Divine_Seed' # Echo_of_War_Destruction_Beginning, Echo_of_War_End_of_the_Eternal_Freeze, Echo_of_War_Divine_Seed, Echo_of_War_Borehole_Planet_Old_Crater + Weekly_Name = 'Echo_of_War_Divine_Seed' # Echo_of_War_Destruction_Beginning, Echo_of_War_End_of_the_Eternal_Freeze, Echo_of_War_Divine_Seed, Echo_of_War_Borehole_Planet_Old_Crater, Echo_of_War_Salutations_of_Ashen_Dreams Weekly_Team = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9 # Group `AchievableQuest` @@ -119,10 +119,10 @@ class GeneratedConfig: BattlePassStorage_BattlePassQuestTrailblazePower = {} # Group `Assignment` - Assignment_Name_1 = 'Nameless_Land_Nameless_People' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver - Assignment_Name_2 = 'Akashic_Records' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver - Assignment_Name_3 = 'The_Invisible_Hand' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver - Assignment_Name_4 = 'Nine_Billion_Names' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver + Assignment_Name_1 = 'Nameless_Land_Nameless_People' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted + Assignment_Name_2 = 'Akashic_Records' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted + Assignment_Name_3 = 'The_Invisible_Hand' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted + Assignment_Name_4 = 'Nine_Billion_Names' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted Assignment_Duration = 20 # 4, 8, 12, 20 Assignment_Event = True Assignment_Assignment = {} @@ -138,7 +138,9 @@ class GeneratedConfig: RogueWorld_DomainStrategy = 'combat' # combat, occurrence RogueWorld_UseImmersifier = True RogueWorld_DoubleEvent = True + RogueWorld_WeeklyFarming = False RogueWorld_UseStamina = False + RogueWorld_SimulatedUniverseFarm = {} # Group `RogueBlessing` RogueBlessing_PresetBlessingFilter = 'preset' # preset, custom diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 3b83ed950..1a1f06ed6 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -100,7 +100,7 @@ class ConfigGenerator: options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Echo_of_War]) # Insert characters from tasks.character.keywords import CharacterList - unsupported_characters = ['Aventurine'] + unsupported_characters = ["Boothill", "TrailblazerHarmony"] characters = [character.name for character in CharacterList.instances.values() if character.name not in unsupported_characters] option_add(keys='DungeonSupport.Character.option', options=characters) @@ -430,7 +430,7 @@ class ConfigGenerator: value=i18n_crimson[ingame_lang].format(path=path, plane=plane)) if dungeon.is_Cavern_of_Corrosion: value = deep_get(new, keys=['Dungeon', 'Name', dungeon.name], default='') - suffix = i18n_relic[ingame_lang].format(dungeon=dungeon_name) + suffix = i18n_relic[ingame_lang].format(dungeon=dungeon_name).replace('Cavern of Corrosion: ', '') if not value.endswith(suffix): deep_set(new, keys=['Dungeon', 'Name', dungeon.name], value=f'{value}{suffix}') @@ -489,7 +489,7 @@ class ConfigGenerator: for dungeon in dungeons: world = dungeon.plane.world world_name = world.__getattribute__(ingame_lang) - dungeon_name = dungeon.__getattribute__(ingame_lang) + dungeon_name = dungeon.__getattribute__(ingame_lang).replace('Echo of War: ', '') value = f'{dungeon_name} ({world_name})' deep_set(new, keys=['Weekly', 'Name', dungeon.name], value=value) # Rogue worlds @@ -653,6 +653,7 @@ class ConfigUpdater: ('Dungeon.Dungeon.NameAtDoubleCalyx', 'Dungeon.Dungeon.NameAtDoubleCalyx', convert_20_dungeon), ('Dungeon.DungeonDaily.CalyxGolden', 'Dungeon.DungeonDaily.CalyxGolden', convert_20_dungeon), ('Dungeon.DungeonDaily.CalyxCrimson', 'Dungeon.DungeonDaily.CalyxCrimson', convert_20_dungeon), + ('Rogue.RogueWorld.SimulatedUniverseElite', 'Rogue.RogueWorld.SimulatedUniverseFarm', convert_rogue_farm), ] @cached_property @@ -863,6 +864,8 @@ class ConfigUpdater: yield 'Rogue.RogueBlessing.CustomResonanceFilter' if deep_get(data, 'Rogue.RogueBlessing.PresetCurioFilter') != 'custom': yield 'Rogue.RogueBlessing.CustomCurioFilter' + if deep_get(data, 'Rogue.RogueWorld.WeeklyFarming', default=False) is False: + yield 'Rogue.RogueWorld.SimulatedUniverseFarm' def get_hidden_args(self, data) -> t.Set[str]: """ diff --git a/module/config/convert.py b/module/config/convert.py index aadc72df4..799a5ac03 100644 --- a/module/config/convert.py +++ b/module/config/convert.py @@ -30,3 +30,10 @@ def convert_20_dungeon(value): return 'Calyx_Crimson_Abundance_Jarilo_BackwaterPass' return value + + +def convert_rogue_farm(value): + if isinstance(value, dict) and 'value' in value.keys(): + value['value'] = 100 - value['value'] + value['total'] = 100 + return value diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 70e7ebb8e..2c6f0adc4 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -131,7 +131,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Control Method", @@ -261,6 +262,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "Trace: Preservation (Supply Zone)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Trace: Preservation (Clock Studios Theme Park)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Trace: The Hunt (Outlying Snow Plains)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Trace: The Hunt (SoulGlad Scorchsand Audition Venue)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Trace: Abundance (Backwater Pass)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Trace: Abundance (Fyxestroll Garden)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "Trace: Erudition (Rivet Town)", @@ -270,6 +272,7 @@ "Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "Trace: Nihility (Alchemy Commission)", "Stagnant_Shadow_Spike": "Ascension: Physical (Natasha / Clara / Luka / Sushang)", "Stagnant_Shadow_Perdition": "Ascension: Physical (Hanya / Argenti)", + "Stagnant_Shadow_Duty": "Ascension: Physical (Boothill / Robin)", "Stagnant_Shadow_Blaze": "Ascension: Fire (Himeko / Asta / Hook)", "Stagnant_Shadow_Scorch": "Ascension: Fire (Guinaifen / Topaz & Numby)", "Stagnant_Shadow_Ire": "Ascension: Fire (Gallagher)", @@ -312,6 +315,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "Trace: Preservation (Supply Zone)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Trace: Preservation (Clock Studios Theme Park)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Trace: The Hunt (Outlying Snow Plains)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Trace: The Hunt (SoulGlad Scorchsand Audition Venue)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Trace: Abundance (Backwater Pass)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Trace: Abundance (Fyxestroll Garden)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "Trace: Erudition (Rivet Town)", @@ -375,6 +379,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "Trace: Preservation (Supply Zone)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Trace: Preservation (Clock Studios Theme Park)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Trace: The Hunt (Outlying Snow Plains)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Trace: The Hunt (SoulGlad Scorchsand Audition Venue)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Trace: Abundance (Backwater Pass)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Trace: Abundance (Fyxestroll Garden)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "Trace: Erudition (Rivet Town)", @@ -389,6 +394,7 @@ "do_not_achieve": "Don't Do This Quest", "Stagnant_Shadow_Spike": "Ascension: Physical (Natasha / Clara / Luka / Sushang)", "Stagnant_Shadow_Perdition": "Ascension: Physical (Hanya / Argenti)", + "Stagnant_Shadow_Duty": "Ascension: Physical (Boothill / Robin)", "Stagnant_Shadow_Blaze": "Ascension: Fire (Himeko / Asta / Hook)", "Stagnant_Shadow_Scorch": "Ascension: Fire (Guinaifen / Topaz & Numby)", "Stagnant_Shadow_Ire": "Ascension: Fire (Gallagher)", @@ -440,6 +446,7 @@ "Argenti": "Argenti", "Arlan": "Arlan", "Asta": "Asta", + "Aventurine": "Aventurine", "Bailu": "Bailu", "BlackSwan": "Black Swan", "Blade": "Blade", @@ -468,6 +475,7 @@ "Natasha": "Natasha", "Pela": "Pela", "Qingque": "Qingque", + "Robin": "Robin", "RuanMei": "Ruan Mei", "Sampo": "Sampo", "Seele": "Seele", @@ -532,7 +540,8 @@ "Echo_of_War_Destruction_Beginning": "Destruction's Beginning (Herta Space Station)", "Echo_of_War_End_of_the_Eternal_Freeze": "End of the Eternal Freeze (Jarilo-VI)", "Echo_of_War_Divine_Seed": "Divine Seed (The Xianzhou Luofu)", - "Echo_of_War_Borehole_Planet_Old_Crater": "Borehole Planet's Old Crater (Herta Space Station)" + "Echo_of_War_Borehole_Planet_Old_Crater": "Borehole Planet's Old Crater (Herta Space Station)", + "Echo_of_War_Salutations_of_Ashen_Dreams": "Salutations of Ashen Dreams (Penacony)" }, "Team": { "name": "Dungeon Team", @@ -800,94 +809,94 @@ "Name_1": { "name": "Assignment 1 Preference", "help": "", - "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", - "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", - "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", - "Born_to_Obey": "Ancient Part (Born to Obey)", - "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", - "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", - "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", "Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)", - "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", "The_Invisible_Hand": "Credit (The Invisible Hand)", - "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", - "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", - "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", - "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", - "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)", "The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)", + "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", + "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", "Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)", - "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)" + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)" }, "Name_2": { "name": "Assignment 2 Preference", "help": "", - "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", - "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", - "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", - "Born_to_Obey": "Ancient Part (Born to Obey)", - "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", - "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", - "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", "Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)", - "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", "The_Invisible_Hand": "Credit (The Invisible Hand)", - "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", - "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", - "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", - "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", - "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)", "The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)", + "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", + "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", "Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)", - "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)" + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)" }, "Name_3": { "name": "Assignment 3 Preference", "help": "", - "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", - "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", - "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", - "Born_to_Obey": "Ancient Part (Born to Obey)", - "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", - "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", - "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", "Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)", - "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", "The_Invisible_Hand": "Credit (The Invisible Hand)", - "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", - "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", - "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", - "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", - "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)", "The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)", + "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", + "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", "Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)", - "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)" + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)" }, "Name_4": { "name": "Assignment 4 Preference", "help": "", - "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", - "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", - "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", - "Born_to_Obey": "Ancient Part (Born to Obey)", - "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", - "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", - "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", "Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)", - "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", "The_Invisible_Hand": "Credit (The Invisible Hand)", - "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", - "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", - "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", - "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", - "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)", "The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)", + "Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)", + "The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)", + "Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)", "Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)", - "Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)" + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)" }, "Duration": { "name": "Dispatch Duration", @@ -969,9 +978,17 @@ "name": "Participate in Double Planer Event", "help": "" }, + "WeeklyFarming": { + "name": "Farm 100 Elites Weekly", + "help": "" + }, "UseStamina": { "name": "Farm Planers Using Trailblase Power", "help": "Task \"Dungeon\" will no longer run, and all trailblaze power will be used first to claim immersion rewards, except for double events." + }, + "SimulatedUniverseFarm": { + "name": "Progress of elite boss farmed", + "help": "" } }, "RogueBlessing": { diff --git a/module/config/i18n/es-ES.json b/module/config/i18n/es-ES.json index 5357226e1..058d97844 100644 --- a/module/config/i18n/es-ES.json +++ b/module/config/i18n/es-ES.json @@ -131,7 +131,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Método de control", @@ -261,6 +262,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "Rastros: Conservación (Zona de suministros)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Rastros: Conservación (Parque temático de los Estudios Reloj)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Rastros: Cacería (Llanuras nevadas de las afueras)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Rastros: Cacería (Recinto de las Audiciones FelizAlma en la Arena Ardiente)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Rastros: Abundancia (Paso del Remanso)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Rastros: Abundancia (Jardín del Sosiego)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "Rastros: Erudición (Villarremache)", @@ -270,6 +272,7 @@ "Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "Rastros: Nihilidad (Comisión de Alquimia)", "Stagnant_Shadow_Spike": "Ascension: Físico (Natasha / Clara / Luka / Sushang)", "Stagnant_Shadow_Perdition": "Ascension: Físico (Hanya / Argenti)", + "Stagnant_Shadow_Duty": "Ascension: Físico (Boothill / Robin)", "Stagnant_Shadow_Blaze": "Ascension: Fuego (Himeko / Asta / Hook)", "Stagnant_Shadow_Scorch": "Ascension: Fuego (Guinaifen / Topaz y Conti)", "Stagnant_Shadow_Ire": "Ascension: Fuego (Gallagher)", @@ -312,6 +315,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "Rastros: Conservación (Zona de suministros)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Rastros: Conservación (Parque temático de los Estudios Reloj)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Rastros: Cacería (Llanuras nevadas de las afueras)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Rastros: Cacería (Recinto de las Audiciones FelizAlma en la Arena Ardiente)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Rastros: Abundancia (Paso del Remanso)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Rastros: Abundancia (Jardín del Sosiego)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "Rastros: Erudición (Villarremache)", @@ -375,6 +379,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "Rastros: Conservación (Zona de suministros)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Rastros: Conservación (Parque temático de los Estudios Reloj)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Rastros: Cacería (Llanuras nevadas de las afueras)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Rastros: Cacería (Recinto de las Audiciones FelizAlma en la Arena Ardiente)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Rastros: Abundancia (Paso del Remanso)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Rastros: Abundancia (Jardín del Sosiego)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "Rastros: Erudición (Villarremache)", @@ -389,6 +394,7 @@ "do_not_achieve": "No hacer esta misión", "Stagnant_Shadow_Spike": "Ascension: Físico (Natasha / Clara / Luka / Sushang)", "Stagnant_Shadow_Perdition": "Ascension: Físico (Hanya / Argenti)", + "Stagnant_Shadow_Duty": "Ascension: Físico (Boothill / Robin)", "Stagnant_Shadow_Blaze": "Ascension: Fuego (Himeko / Asta / Hook)", "Stagnant_Shadow_Scorch": "Ascension: Fuego (Guinaifen / Topaz y Conti)", "Stagnant_Shadow_Ire": "Ascension: Fuego (Gallagher)", @@ -440,6 +446,7 @@ "Argenti": "Argenti", "Arlan": "Arlan", "Asta": "Asta", + "Aventurine": "Aventurino", "Bailu": "Bailu", "BlackSwan": "Cisne Negro", "Blade": "Blade", @@ -468,6 +475,7 @@ "Natasha": "Natasha", "Pela": "Pela", "Qingque": "Qingque", + "Robin": "Robin", "RuanMei": "Ruan Mei", "Sampo": "Sampo", "Seele": "Seele", @@ -532,7 +540,8 @@ "Echo_of_War_Destruction_Beginning": "El principio de la Destrucción (Estación Espacial Herta)", "Echo_of_War_End_of_the_Eternal_Freeze": "El fin del Hielo Eterno (Jarilo-VI)", "Echo_of_War_Divine_Seed": "Semilla divina (El Luofu de Xianzhou)", - "Echo_of_War_Borehole_Planet_Old_Crater": "Cráter del planeta devorado (Estación Espacial Herta)" + "Echo_of_War_Borehole_Planet_Old_Crater": "Cráter del planeta devorado (Estación Espacial Herta)", + "Echo_of_War_Salutations_of_Ashen_Dreams": "Ecos de la guerra: Tributo del sueño ceniciento (Colonipenal)" }, "Team": { "name": "Equipo de mazmorra", @@ -800,94 +809,94 @@ "Name_1": { "name": "Preferencia de Encargo 1", "help": "", - "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", - "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", - "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", - "Born_to_Obey": "Componente antiguo (Creados para obedecer)", - "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", - "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", - "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", "Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)", - "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", + "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", + "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", + "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", + "Born_to_Obey": "Componente antiguo (Creados para obedecer)", + "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", + "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", + "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", "Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)", + "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", "The_Invisible_Hand": "Crédito (La mano invisible)", - "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)", - "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", - "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", - "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", - "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)", "The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)", + "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", + "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", "Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)", - "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)" + "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", + "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)" }, "Name_2": { "name": "Preferencia de Encargo 2", "help": "", - "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", - "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", - "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", - "Born_to_Obey": "Componente antiguo (Creados para obedecer)", - "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", - "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", - "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", "Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)", - "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", + "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", + "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", + "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", + "Born_to_Obey": "Componente antiguo (Creados para obedecer)", + "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", + "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", + "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", "Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)", + "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", "The_Invisible_Hand": "Crédito (La mano invisible)", - "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)", - "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", - "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", - "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", - "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)", "The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)", + "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", + "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", "Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)", - "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)" + "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", + "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)" }, "Name_3": { "name": "Preferencia de Encargo 3", "help": "", - "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", - "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", - "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", - "Born_to_Obey": "Componente antiguo (Creados para obedecer)", - "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", - "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", - "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", "Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)", - "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", + "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", + "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", + "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", + "Born_to_Obey": "Componente antiguo (Creados para obedecer)", + "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", + "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", + "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", "Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)", + "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", "The_Invisible_Hand": "Crédito (La mano invisible)", - "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)", - "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", - "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", - "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", - "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)", "The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)", + "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", + "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", "Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)", - "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)" + "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", + "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)" }, "Name_4": { "name": "Preferencia de Encargo 4", "help": "", - "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", - "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", - "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", - "Born_to_Obey": "Componente antiguo (Creados para obedecer)", - "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", - "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", - "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", "Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)", - "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", + "A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)", + "Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)", + "Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)", + "Born_to_Obey": "Componente antiguo (Creados para obedecer)", + "Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)", + "Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)", + "Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)", "Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)", + "Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)", "The_Invisible_Hand": "Crédito (La mano invisible)", - "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)", - "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", - "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", - "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", - "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)", "The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)", + "Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)", + "The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)", + "Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)", "Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)", - "Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)" + "The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)", + "Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)" }, "Duration": { "name": "Duración del encargo", @@ -969,9 +978,17 @@ "name": "Participa en doble planer evento", "help": "" }, + "WeeklyFarming": { + "name": "Granja 100 élites semanalmente", + "help": "" + }, "UseStamina": { "name": "Reclamar de planers mediante poder trazacaminos", "help": "La tarea de mazmorra ya no se ejecutará y todo el poder trazacaminos se usará primero para reclamar recompensas de inmersión, excepto para eventos dobles" + }, + "SimulatedUniverseFarm": { + "name": "Progreso de élites derrotadas", + "help": "" } }, "RogueBlessing": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 0a40b8d2f..436e36592 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -131,7 +131,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "Emulator.ControlMethod.name", @@ -261,6 +262,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "軌跡素材:存護(サポート部分)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "軌跡素材:存護(クラークフィルムランド)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "軌跡素材:巡狩(郊外雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "軌跡素材:巡狩(スラーダ熱砂オーディション会場)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "軌跡素材:豊穣(外縁通路)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "軌跡素材:豊穣(綏園)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "軌跡素材:知恵(リベットタウン)", @@ -270,6 +272,7 @@ "Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "軌跡素材:虚無(丹鼎司)", "Stagnant_Shadow_Spike": "キャラクター昇格素材:物理(ナターシャ / クラーラ / ルカ / 素裳)", "Stagnant_Shadow_Perdition": "キャラクター昇格素材:物理(寒鴉 / アルジェンティ)", + "Stagnant_Shadow_Duty": "キャラクター昇格素材:物理(ブートヒル / ロビン)", "Stagnant_Shadow_Blaze": "キャラクター昇格素材:炎(姫子 / アスター / フック)", "Stagnant_Shadow_Scorch": "キャラクター昇格素材:炎(桂乃芬 / トパーズ&カブ)", "Stagnant_Shadow_Ire": "キャラクター昇格素材:炎(ギャラガー)", @@ -312,6 +315,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "軌跡素材:存護(サポート部分)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "軌跡素材:存護(クラークフィルムランド)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "軌跡素材:巡狩(郊外雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "軌跡素材:巡狩(スラーダ熱砂オーディション会場)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "軌跡素材:豊穣(外縁通路)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "軌跡素材:豊穣(綏園)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "軌跡素材:知恵(リベットタウン)", @@ -375,6 +379,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "軌跡素材:存護(サポート部分)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "軌跡素材:存護(クラークフィルムランド)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "軌跡素材:巡狩(郊外雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "軌跡素材:巡狩(スラーダ熱砂オーディション会場)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "軌跡素材:豊穣(外縁通路)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "軌跡素材:豊穣(綏園)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "軌跡素材:知恵(リベットタウン)", @@ -389,6 +394,7 @@ "do_not_achieve": "do_not_achieve", "Stagnant_Shadow_Spike": "キャラクター昇格素材:物理(ナターシャ / クラーラ / ルカ / 素裳)", "Stagnant_Shadow_Perdition": "キャラクター昇格素材:物理(寒鴉 / アルジェンティ)", + "Stagnant_Shadow_Duty": "キャラクター昇格素材:物理(ブートヒル / ロビン)", "Stagnant_Shadow_Blaze": "キャラクター昇格素材:炎(姫子 / アスター / フック)", "Stagnant_Shadow_Scorch": "キャラクター昇格素材:炎(桂乃芬 / トパーズ&カブ)", "Stagnant_Shadow_Ire": "キャラクター昇格素材:炎(ギャラガー)", @@ -440,6 +446,7 @@ "Argenti": "アルジェンティ", "Arlan": "アーラン", "Asta": "アスター", + "Aventurine": "アベンチュリン", "Bailu": "白露", "BlackSwan": "ブラックスワン", "Blade": "刃", @@ -468,6 +475,7 @@ "Natasha": "ナターシャ", "Pela": "ペラ", "Qingque": "青雀", + "Robin": "ロビン", "RuanMei": "ルアン・メェイ", "Sampo": "サンポ", "Seele": "ゼーレ", @@ -532,7 +540,8 @@ "Echo_of_War_Destruction_Beginning": "歴戦余韻・壊滅の始まり (宇宙ステーション「ヘルタ」)", "Echo_of_War_End_of_the_Eternal_Freeze": "歴戦余韻・寒波の幕切れ (ヤリーロ-VI)", "Echo_of_War_Divine_Seed": "歴戦余韻・不死の神実 (仙舟「羅浮」)", - "Echo_of_War_Borehole_Planet_Old_Crater": "歴戦余韻・星を蝕む往日の面影 (宇宙ステーション「ヘルタ」)" + "Echo_of_War_Borehole_Planet_Old_Crater": "歴戦余韻・星を蝕む往日の面影 (宇宙ステーション「ヘルタ」)", + "Echo_of_War_Salutations_of_Ashen_Dreams": "歴戦余韻・現世の夢の礼賛 (ピノコニー)" }, "Team": { "name": "Weekly.Team.name", @@ -800,94 +809,94 @@ "Name_1": { "name": "依頼 1", "help": "", - "Nine_Billion_Names": "消滅した原核(九十億の御名)", - "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", - "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", - "Born_to_Obey": "古代パーツ(生まれながらに服従する)", - "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", - "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", - "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", "Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)", - "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", + "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", "Akashic_Records": "光円錐経験値素材(アーカーシャの記録)", + "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", "The_Invisible_Hand": "信用ポイント(見えざる手)", - "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)", - "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", - "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", - "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", - "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)", "The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)", + "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", + "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", "Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)", - "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)" + "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", + "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)" }, "Name_2": { "name": "依頼 2", "help": "", - "Nine_Billion_Names": "消滅した原核(九十億の御名)", - "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", - "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", - "Born_to_Obey": "古代パーツ(生まれながらに服従する)", - "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", - "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", - "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", "Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)", - "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", + "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", "Akashic_Records": "光円錐経験値素材(アーカーシャの記録)", + "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", "The_Invisible_Hand": "信用ポイント(見えざる手)", - "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)", - "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", - "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", - "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", - "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)", "The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)", + "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", + "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", "Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)", - "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)" + "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", + "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)" }, "Name_3": { "name": "依頼 3", "help": "", - "Nine_Billion_Names": "消滅した原核(九十億の御名)", - "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", - "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", - "Born_to_Obey": "古代パーツ(生まれながらに服従する)", - "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", - "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", - "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", "Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)", - "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", + "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", "Akashic_Records": "光円錐経験値素材(アーカーシャの記録)", + "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", "The_Invisible_Hand": "信用ポイント(見えざる手)", - "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)", - "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", - "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", - "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", - "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)", "The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)", + "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", + "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", "Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)", - "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)" + "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", + "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)" }, "Name_4": { "name": "依頼 4", "help": "", - "Nine_Billion_Names": "消滅した原核(九十億の御名)", - "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", - "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", - "Born_to_Obey": "古代パーツ(生まれながらに服従する)", - "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", - "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", - "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", "Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)", - "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", + "A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", "Akashic_Records": "光円錐経験値素材(アーカーシャの記録)", + "Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)", "The_Invisible_Hand": "信用ポイント(見えざる手)", - "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)", - "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", - "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", - "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", - "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)", "The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)", + "Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)", + "The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)", + "Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)", "Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)", - "Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)" + "The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)", + "Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)" }, "Duration": { "name": "派遣時間", @@ -969,9 +978,17 @@ "name": "RogueWorld.DoubleEvent.name", "help": "RogueWorld.DoubleEvent.help" }, + "WeeklyFarming": { + "name": "RogueWorld.WeeklyFarming.name", + "help": "RogueWorld.WeeklyFarming.help" + }, "UseStamina": { "name": "RogueWorld.UseStamina.name", "help": "RogueWorld.UseStamina.help" + }, + "SimulatedUniverseFarm": { + "name": "RogueWorld.SimulatedUniverseFarm.name", + "help": "RogueWorld.SimulatedUniverseFarm.help" } }, "RogueBlessing": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index a6e7e78c4..a0a857a7a 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -131,7 +131,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "模拟器控制方案", @@ -261,6 +262,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "行迹材料:存护(支援舱段)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行迹材料:存护(克劳克影视乐园)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行迹材料:巡猎(城郊雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行迹材料:巡猎(苏乐达热砂海选会场)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行迹材料:丰饶(边缘通路)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行迹材料:丰饶(绥园)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "行迹材料:智识(铆钉镇)", @@ -270,6 +272,7 @@ "Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "行迹材料:虚无(丹鼎司)", "Stagnant_Shadow_Spike": "角色晋阶材料:物理(娜塔莎 / 克拉拉 / 卢卡 / 素裳)", "Stagnant_Shadow_Perdition": "角色晋阶材料:物理(寒鸦 / 银枝)", + "Stagnant_Shadow_Duty": "角色晋阶材料:物理(波提欧 / 知更鸟)", "Stagnant_Shadow_Blaze": "角色晋阶材料:火(姬子 / 艾丝妲 / 虎克)", "Stagnant_Shadow_Scorch": "角色晋阶材料:火(桂乃芬 / 托帕&账账)", "Stagnant_Shadow_Ire": "角色晋阶材料:火(加拉赫)", @@ -312,6 +315,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "行迹材料:存护(支援舱段)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行迹材料:存护(克劳克影视乐园)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行迹材料:巡猎(城郊雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行迹材料:巡猎(苏乐达热砂海选会场)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行迹材料:丰饶(边缘通路)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行迹材料:丰饶(绥园)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "行迹材料:智识(铆钉镇)", @@ -375,6 +379,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "行迹材料:存护(支援舱段)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行迹材料:存护(克劳克影视乐园)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行迹材料:巡猎(城郊雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行迹材料:巡猎(苏乐达热砂海选会场)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行迹材料:丰饶(边缘通路)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行迹材料:丰饶(绥园)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "行迹材料:智识(铆钉镇)", @@ -389,6 +394,7 @@ "do_not_achieve": "不完成这个任务", "Stagnant_Shadow_Spike": "角色晋阶材料:物理(娜塔莎 / 克拉拉 / 卢卡 / 素裳)", "Stagnant_Shadow_Perdition": "角色晋阶材料:物理(寒鸦 / 银枝)", + "Stagnant_Shadow_Duty": "角色晋阶材料:物理(波提欧 / 知更鸟)", "Stagnant_Shadow_Blaze": "角色晋阶材料:火(姬子 / 艾丝妲 / 虎克)", "Stagnant_Shadow_Scorch": "角色晋阶材料:火(桂乃芬 / 托帕&账账)", "Stagnant_Shadow_Ire": "角色晋阶材料:火(加拉赫)", @@ -440,6 +446,7 @@ "Argenti": "银枝", "Arlan": "阿兰", "Asta": "艾丝妲", + "Aventurine": "砂金", "Bailu": "白露", "BlackSwan": "黑天鹅", "Blade": "刃", @@ -468,6 +475,7 @@ "Natasha": "娜塔莎", "Pela": "佩拉", "Qingque": "青雀", + "Robin": "知更鸟", "RuanMei": "阮•梅", "Sampo": "桑博", "Seele": "希儿", @@ -532,7 +540,8 @@ "Echo_of_War_Destruction_Beginning": "毁灭的开端•历战余响 (空间站「黑塔」)", "Echo_of_War_End_of_the_Eternal_Freeze": "寒潮的落幕•历战余响 (雅利洛-Ⅵ)", "Echo_of_War_Divine_Seed": "不死的神实•历战余响 (仙舟「罗浮」)", - "Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的旧靥•历战余响 (空间站「黑塔」)" + "Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的旧靥•历战余响 (空间站「黑塔」)", + "Echo_of_War_Salutations_of_Ashen_Dreams": "尘梦的赞礼•历战余响 (匹诺康尼)" }, "Team": { "name": "打本队伍", @@ -800,94 +809,94 @@ "Name_1": { "name": "第1个委托选择", "help": "", - "Nine_Billion_Names": "熄灭原核(九十亿个名字)", - "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", - "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", - "Born_to_Obey": "古代零件(生而服从)", - "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", - "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", "Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)", - "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Born_to_Obey": "古代零件(生而服从)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", "Akashic_Records": "光锥经验材料(阿卡夏记录)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", "The_Invisible_Hand": "信用点(看不见的手)", - "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)", - "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", - "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", - "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)", "The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)", + "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", + "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)", - "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)" + "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)" }, "Name_2": { "name": "第2个委托选择", "help": "", - "Nine_Billion_Names": "熄灭原核(九十亿个名字)", - "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", - "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", - "Born_to_Obey": "古代零件(生而服从)", - "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", - "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", "Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)", - "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Born_to_Obey": "古代零件(生而服从)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", "Akashic_Records": "光锥经验材料(阿卡夏记录)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", "The_Invisible_Hand": "信用点(看不见的手)", - "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)", - "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", - "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", - "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)", "The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)", + "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", + "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)", - "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)" + "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)" }, "Name_3": { "name": "第3个委托选择", "help": "", - "Nine_Billion_Names": "熄灭原核(九十亿个名字)", - "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", - "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", - "Born_to_Obey": "古代零件(生而服从)", - "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", - "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", "Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)", - "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Born_to_Obey": "古代零件(生而服从)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", "Akashic_Records": "光锥经验材料(阿卡夏记录)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", "The_Invisible_Hand": "信用点(看不见的手)", - "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)", - "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", - "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", - "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)", "The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)", + "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", + "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)", - "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)" + "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)" }, "Name_4": { "name": "第4个委托选择", "help": "", - "Nine_Billion_Names": "熄灭原核(九十亿个名字)", - "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", - "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", - "Born_to_Obey": "古代零件(生而服从)", - "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", - "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", "Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)", - "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Born_to_Obey": "古代零件(生而服从)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", "Akashic_Records": "光锥经验材料(阿卡夏记录)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", "The_Invisible_Hand": "信用点(看不见的手)", - "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)", - "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", - "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", - "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)", "The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)", + "Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)", + "Spring_of_Life": "固态净水 & 虚粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)", - "Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)" + "The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)" }, "Duration": { "name": "派遣时长", @@ -969,9 +978,17 @@ "name": "参与双倍内圈仪器活动", "help": "" }, + "WeeklyFarming": { + "name": "每周刷100精英怪", + "help": "" + }, "UseStamina": { "name": "使用开拓力刷内圈遗器", "help": "每日副本任务将不再打本,所有开拓力将优先被用于领取浸器奖励,双倍活动时除外" + }, + "SimulatedUniverseFarm": { + "name": "刷精英怪进度", + "help": "" } }, "RogueBlessing": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index a3f21c18e..f3eb18c55 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -131,7 +131,8 @@ "aScreenCap_nc": "aScreenCap_nc", "DroidCast": "DroidCast", "DroidCast_raw": "DroidCast_raw", - "scrcpy": "scrcpy" + "scrcpy": "scrcpy", + "nemu_ipc": "nemu_ipc" }, "ControlMethod": { "name": "模擬器控制方案", @@ -261,6 +262,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "行跡材料:存護(支援艙段)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行跡材料:存護(克勞克影視樂園)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行跡材料:巡獵(城郊雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行跡材料:巡獵(蘇樂達熱砂海選會場)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行跡材料:豐饒(邊緣通道)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行跡材料:豐饒(綏園)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "行跡材料:智識(鉚釘鎮)", @@ -270,6 +272,7 @@ "Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "行跡材料:虛無(丹鼎司)", "Stagnant_Shadow_Spike": "角色晉階材料:物理(娜塔莎 / 克拉拉 / 盧卡 / 素裳)", "Stagnant_Shadow_Perdition": "角色晉階材料:物理(寒鴉 / 銀枝)", + "Stagnant_Shadow_Duty": "角色晉階材料:物理(波提歐 / 知更鳥)", "Stagnant_Shadow_Blaze": "角色晉階材料:火(姬子 / 艾絲妲 / 虎克)", "Stagnant_Shadow_Scorch": "角色晉階材料:火(桂乃芬 / 托帕&帳帳)", "Stagnant_Shadow_Ire": "角色晉階材料:火(加拉赫)", @@ -312,6 +315,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "行跡材料:存護(支援艙段)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行跡材料:存護(克勞克影視樂園)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行跡材料:巡獵(城郊雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行跡材料:巡獵(蘇樂達熱砂海選會場)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行跡材料:豐饒(邊緣通道)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行跡材料:豐饒(綏園)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "行跡材料:智識(鉚釘鎮)", @@ -375,6 +379,7 @@ "Calyx_Crimson_Preservation_Herta_SupplyZone": "行跡材料:存護(支援艙段)", "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行跡材料:存護(克勞克影視樂園)", "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行跡材料:巡獵(城郊雪原)", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行跡材料:巡獵(蘇樂達熱砂海選會場)", "Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行跡材料:豐饒(邊緣通道)", "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行跡材料:豐饒(綏園)", "Calyx_Crimson_Erudition_Jarilo_RivetTown": "行跡材料:智識(鉚釘鎮)", @@ -389,6 +394,7 @@ "do_not_achieve": "不完成這個任務", "Stagnant_Shadow_Spike": "角色晉階材料:物理(娜塔莎 / 克拉拉 / 盧卡 / 素裳)", "Stagnant_Shadow_Perdition": "角色晉階材料:物理(寒鴉 / 銀枝)", + "Stagnant_Shadow_Duty": "角色晉階材料:物理(波提歐 / 知更鳥)", "Stagnant_Shadow_Blaze": "角色晉階材料:火(姬子 / 艾絲妲 / 虎克)", "Stagnant_Shadow_Scorch": "角色晉階材料:火(桂乃芬 / 托帕&帳帳)", "Stagnant_Shadow_Ire": "角色晉階材料:火(加拉赫)", @@ -440,6 +446,7 @@ "Argenti": "銀枝", "Arlan": "阿蘭", "Asta": "艾絲妲", + "Aventurine": "砂金", "Bailu": "白露", "BlackSwan": "黑天鵝", "Blade": "刃", @@ -468,6 +475,7 @@ "Natasha": "娜塔莎", "Pela": "佩拉", "Qingque": "青雀", + "Robin": "知更鳥", "RuanMei": "阮•梅", "Sampo": "桑博", "Seele": "希兒", @@ -532,7 +540,8 @@ "Echo_of_War_Destruction_Beginning": "毀滅的開端•歷戰餘響 (太空站「黑塔」)", "Echo_of_War_End_of_the_Eternal_Freeze": "寒潮的落幕•歷戰餘響 (雅利洛-Ⅵ)", "Echo_of_War_Divine_Seed": "不死的神實•歷戰餘響 (仙舟「羅浮」)", - "Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的舊靨•歷戰餘響 (太空站「黑塔」)" + "Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的舊靨•歷戰餘響 (太空站「黑塔」)", + "Echo_of_War_Salutations_of_Ashen_Dreams": "塵夢的讚禮•歷戰餘響 (匹諾康尼)" }, "Team": { "name": "打本隊伍", @@ -800,94 +809,94 @@ "Name_1": { "name": "第1個委託選擇", "help": "", - "Nine_Billion_Names": "熄滅原核(九十億個名字)", - "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", - "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", - "Born_to_Obey": "古代零件(生而服從)", - "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", - "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", "Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)", - "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", + "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Born_to_Obey": "古代零件(生而服從)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", "Akashic_Records": "光錐經驗素材(阿卡夏紀錄)", + "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", "The_Invisible_Hand": "信用點(看不見的手)", - "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)", - "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", - "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", - "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)", "The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)", + "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", + "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)", - "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)" + "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)" }, "Name_2": { "name": "第2個委託選擇", "help": "", - "Nine_Billion_Names": "熄滅原核(九十億個名字)", - "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", - "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", - "Born_to_Obey": "古代零件(生而服從)", - "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", - "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", "Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)", - "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", + "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Born_to_Obey": "古代零件(生而服從)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", "Akashic_Records": "光錐經驗素材(阿卡夏紀錄)", + "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", "The_Invisible_Hand": "信用點(看不見的手)", - "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)", - "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", - "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", - "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)", "The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)", + "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", + "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)", - "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)" + "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)" }, "Name_3": { "name": "第3個委託選擇", "help": "", - "Nine_Billion_Names": "熄滅原核(九十億個名字)", - "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", - "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", - "Born_to_Obey": "古代零件(生而服從)", - "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", - "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", "Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)", - "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", + "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Born_to_Obey": "古代零件(生而服從)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", "Akashic_Records": "光錐經驗素材(阿卡夏紀錄)", + "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", "The_Invisible_Hand": "信用點(看不見的手)", - "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)", - "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", - "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", - "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)", "The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)", + "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", + "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)", - "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)" + "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)" }, "Name_4": { "name": "第4個委託選擇", "help": "", - "Nine_Billion_Names": "熄滅原核(九十億個名字)", - "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", - "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", - "Born_to_Obey": "古代零件(生而服從)", - "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", - "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", - "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", "Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)", - "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", + "A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Born_to_Obey": "古代零件(生而服從)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", "Akashic_Records": "光錐經驗素材(阿卡夏紀錄)", + "Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)", "The_Invisible_Hand": "信用點(看不見的手)", - "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)", - "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", - "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", - "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", - "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)", "The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)", + "Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)", + "The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)", + "Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)", "Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)", - "Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)" + "The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)", + "Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)" }, "Duration": { "name": "派遣時間", @@ -969,9 +978,17 @@ "name": "參與雙倍內圈儀器活動", "help": "" }, + "WeeklyFarming": { + "name": "每週農100精英怪", + "help": "" + }, "UseStamina": { "name": "用開拓力農遺器", "help": "每日副本任務將不再打本,所有開拓力將優先被用於領取浸器獎勵,雙倍活動時除外" + }, + "SimulatedUniverseFarm": { + "name": "農精英怪進度", + "help": "" } }, "RogueBlessing": { diff --git a/module/config/server.py b/module/config/server.py index ee3fc23f0..899284329 100644 --- a/module/config/server.py +++ b/module/config/server.py @@ -18,7 +18,7 @@ VALID_PACKAGE = set(list(VALID_SERVER.values())) VALID_CLOUD_SERVER = { 'CN-Official': 'com.miHoYo.cloudgames.hkrpg', } -VALID_CLOUD_PACKAGE = set(list(VALID_SERVER.values())) +VALID_CLOUD_PACKAGE = set(list(VALID_CLOUD_SERVER.values())) def set_lang(lang_: str): diff --git a/module/config/stored/classes.py b/module/config/stored/classes.py index 44e818826..e66cb299a 100644 --- a/module/config/stored/classes.py +++ b/module/config/stored/classes.py @@ -208,6 +208,15 @@ class StoredSimulatedUniverse(StoredCounter, StoredExpiredAtMonday0400): pass +class StoredSimulatedUniverseElite(StoredCounter, StoredExpiredAtMonday0400): + # These variables are used in Rogue Farming feature. + + # FIXED_TOTAL --- Times of boss drop chance per week. In current version of StarRail, this value is 100. + FIXED_TOTAL = 100 + + # value --- Times left to farm. Resets to 100 every Monday 04:00, and decreases each time the elite boss is cleared. + + class StoredAssignment(StoredCounter): pass diff --git a/module/config/stored/stored_generated.py b/module/config/stored/stored_generated.py index bc8f2c526..bcb853b5c 100644 --- a/module/config/stored/stored_generated.py +++ b/module/config/stored/stored_generated.py @@ -20,6 +20,7 @@ from module.config.stored.classes import ( StoredImmersifier, StoredInt, StoredSimulatedUniverse, + StoredSimulatedUniverseElite, StoredTrailblazePower, ) @@ -50,3 +51,4 @@ class StoredGenerated: Assignment = StoredAssignment("Assignment.Assignment.Assignment") Credit = StoredInt("DataUpdate.ItemStorage.Credit") StallerJade = StoredInt("DataUpdate.ItemStorage.StallerJade") + SimulatedUniverseFarm = StoredSimulatedUniverseElite("Rogue.RogueWorld.SimulatedUniverseFarm") diff --git a/module/device/connection.py b/module/device/connection.py index 2bc838e70..1fd5eb911 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -1,9 +1,9 @@ import ipaddress import logging -import platform import re import socket import subprocess +import sys import time from functools import wraps @@ -12,7 +12,8 @@ from adbutils import AdbClient, AdbDevice, AdbTimeout, ForwardItem, ReverseItem from adbutils.errors import AdbError import module.config.server as server_ -from module.base.decorator import Config, cached_property, del_cached_property +import platform +from module.base.decorator import Config, cached_property, del_cached_property, run_once from module.base.utils import SelectedGrids, ensure_time from module.device.connection_attr import ConnectionAttr from module.device.method.utils import ( @@ -84,10 +85,17 @@ class AdbDeviceWithStatus(AdbDevice): def __bool__(self): return True + @cached_property + def port(self) -> int: + try: + return int(self.serial.split(':')[1]) + except (IndexError, ValueError): + return 0 + @cached_property def may_mumu12_family(self): # 127.0.0.1:16XXX - return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16') + return 16384 <= self.port <= 17408 class Connection(ConnectionAttr): @@ -276,6 +284,7 @@ class Connection(ConnectionAttr): @cached_property def nemud_app_keep_alive(self) -> str: res = self.adb_getprop('nemud.app_keep_alive') + logger.attr('nemud.app_keep_alive', res) return res @retry @@ -284,7 +293,6 @@ class Connection(ConnectionAttr): return False res = self.nemud_app_keep_alive - logger.attr('nemud.app_keep_alive', res) if res == '': # Empty property, probably MuMu6 or MuMu12 version < 3.5.6 return True @@ -299,6 +307,15 @@ class Connection(ConnectionAttr): logger.warning(f'Invalid nemud.app_keep_alive value: {res}') return False + @cached_property + def is_mumu_over_version_356(self) -> bool: + """ + Returns: + bool: If MuMu12 version >= 3.5.6, + which has nemud.app_keep_alive and always be a vertical device + """ + return self.nemud_app_keep_alive != '' + @cached_property def _nc_server_host_port(self): """ @@ -496,30 +513,51 @@ class Connection(ConnectionAttr): def adb_forward_remove(self, local): """ Equivalent to `adb -s forward --remove ` + No error raised when removing a non-existent forward + More about the commands send to ADB server, see: https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SERVICES.TXT Args: local (str): Such as 'tcp:2437' """ - with self.adb_client._connect() as c: - list_cmd = f"host-serial:{self.serial}:killforward:{local}" - c.send_command(list_cmd) - c.check_okay() + try: + with self.adb_client._connect() as c: + list_cmd = f"host-serial:{self.serial}:killforward:{local}" + c.send_command(list_cmd) + c.check_okay() + except AdbError as e: + # No error raised when removing a non-existed forward + # adbutils.errors.AdbError: listener 'tcp:8888' not found + msg = str(e) + if re.search(r'listener .*? not found', msg): + logger.warning(f'{type(e).__name__}: {msg}') + else: + raise def adb_reverse_remove(self, local): """ Equivalent to `adb -s reverse --remove ` + No error raised when removing a non-existent reverse Args: local (str): Such as 'tcp:2437' """ - with self.adb_client._connect() as c: - c.send_command(f"host:transport:{self.serial}") - c.check_okay() - list_cmd = f"reverse:killforward:{local}" - c.send_command(list_cmd) - c.check_okay() + try: + with self.adb_client._connect() as c: + c.send_command(f"host:transport:{self.serial}") + c.check_okay() + list_cmd = f"reverse:killforward:{local}" + c.send_command(list_cmd) + c.check_okay() + except AdbError as e: + # No error raised when removing a non-existed forward + # adbutils.errors.AdbError: listener 'tcp:8888' not found + msg = str(e) + if re.search(r'listener .*? not found', msg): + logger.warning(f'{type(e).__name__}: {msg}') + else: + raise def adb_push(self, local, remote): """ @@ -549,14 +587,14 @@ class Connection(ConnectionAttr): # Disconnect offline device before connecting for device in self.list_device(): if device.status == 'offline': - logger.warning(f'Device {serial} is offline, disconnect it before connecting') - self.adb_disconnect(serial) + logger.warning(f'Device {device.serial} is offline, disconnect it before connecting') + self.adb_disconnect(device.serial) elif device.status == 'unauthorized': - logger.error(f'Device {serial} is unauthorized, please accept ADB debugging on your device') + logger.error(f'Device {device.serial} is unauthorized, please accept ADB debugging on your device') elif device.status == 'device': pass else: - logger.warning(f'Device {serial} is is having a unknown status: {device.status}') + logger.warning(f'Device {device.serial} is is having a unknown status: {device.status}') # Skip for emulator-5554 if 'emulator-' in serial: @@ -764,23 +802,45 @@ class Connection(ConnectionAttr): If serial=='auto' and only 1 device detected, use it """ logger.hr('Detect device') - logger.info('Here are the available devices, ' - 'copy to Alas.Emulator.Serial to use it or set Alas.Emulator.Serial="auto"') - devices = self.list_device() + available = SelectedGrids([]) + devices = SelectedGrids([]) - # Show available devices - available = devices.select(status='device') - for device in available: - logger.info(device.serial) - if not len(available): - logger.info('No available devices') + @run_once + def brute_force_connect(): + logger.info('Brute force connect') + from deploy.Windows.emulator import EmulatorManager + manager = EmulatorManager() + manager.brute_force_connect() - # Show unavailable devices if having any - unavailable = devices.delete(available) - if len(unavailable): - logger.info('Here are the devices detected but unavailable') - for device in unavailable: - logger.info(f'{device.serial} ({device.status})') + for _ in range(2): + logger.info('Here are the available devices, ' + 'copy to Alas.Emulator.Serial to use it or set Alas.Emulator.Serial="auto"') + devices = self.list_device() + + # Show available devices + available = devices.select(status='device') + for device in available: + logger.info(device.serial) + if not len(available): + logger.info('No available devices') + + # Show unavailable devices if having any + unavailable = devices.delete(available) + if len(unavailable): + logger.info('Here are the devices detected but unavailable') + for device in unavailable: + logger.info(f'{device.serial} ({device.status})') + + # brute_force_connect + if self.config.Emulator_Serial == 'auto' and available.count == 0: + logger.warning(f'No available device found') + if sys.platform == 'win32': + brute_force_connect() + continue + else: + break + else: + break # Auto device detection if self.config.Emulator_Serial == 'auto': @@ -790,7 +850,7 @@ class Connection(ConnectionAttr): raise RequestHumanTakeover elif available.count == 1: logger.info(f'Auto device detection found only one device, using it') - self.serial = available[0].serial + self.config.Emulator_Serial = self.serial = available[0].serial del_cached_property(self, 'adb') elif available.count == 2 \ and available.select(serial='127.0.0.1:7555') \ @@ -799,7 +859,7 @@ class Connection(ConnectionAttr): # For MuMu12 serials like 127.0.0.1:7555 and 127.0.0.1:16384 # ignore 7555 use 16384 remain = available.select(may_mumu12_family=True).first_or_none() - self.serial = remain.serial + self.config.Emulator_Serial = self.serial = remain.serial del_cached_property(self, 'adb') else: logger.critical('Multiple devices found, auto device detection cannot decide which to choose, ' @@ -808,6 +868,7 @@ class Connection(ConnectionAttr): # Handle LDPlayer # LDPlayer serial jumps between `127.0.0.1:5555+{X}` and `emulator-5554+{X}` + # No config write since it's dynamic port_serial, emu_serial = get_serial_pair(self.serial) if port_serial and emu_serial: # Might be LDPlayer, check connected devices @@ -834,6 +895,57 @@ class Connection(ConnectionAttr): f'Using serial: {emu_serial}') self.serial = emu_serial + # Redirect MuMu12 from 127.0.0.1:7555 to 127.0.0.1:16xxx + if self.serial == '127.0.0.1:7555': + for _ in range(2): + mumu12 = available.select(may_mumu12_family=True) + if mumu12.count == 1: + emu_serial = mumu12.first_or_none().serial + logger.warning(f'Redirect MuMu12 {self.serial} to {emu_serial}') + self.config.Emulator_Serial = self.serial = emu_serial + break + elif mumu12.count >= 2: + logger.warning(f'Multiple MuMu12 serial found, cannot redirect') + break + else: + # Only 127.0.0.1:7555 + if self.is_mumu_over_version_356: + # is_mumu_over_version_356 and nemud_app_keep_alive was cached + # Acceptable since it's the same device + logger.warning(f'Device {self.serial} is MuMu12 but corresponding port not found') + brute_force_connect() + devices = self.list_device() + # Show available devices + available = devices.select(status='device') + for device in available: + logger.info(device.serial) + if not len(available): + logger.info('No available devices') + continue + else: + # MuMu6 + break + + # MuMu12 uses 127.0.0.1:16385 if port 16384 is occupied, auto redirect + # No config write since it's dynamic + if self.is_mumu12_family: + matched = False + for device in available.select(may_mumu12_family=True): + if device.port == self.port: + # Exact match + matched = True + break + if not matched: + for device in available.select(may_mumu12_family=True): + if -2 <= device.port - self.port <= 2: + # Port switched + logger.info(f'MuMu12 port switches from {self.serial} to {device.serial}') + del_cached_property(self, 'port') + del_cached_property(self, 'is_mumu12_family') + del_cached_property(self, 'is_mumu_family') + self.serial = device.serial + break + @retry def list_package(self, show_log=True): """ @@ -864,7 +976,7 @@ class Connection(ConnectionAttr): list[str]: List of package names """ packages = self.list_package(show_log=show_log) - packages = [p for p in packages if p in server_.VALID_PACKAGE] + packages = [p for p in packages if p in server_.VALID_PACKAGE or p in server_.VALID_CLOUD_PACKAGE] return packages def detect_package(self, set_config=True): diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index 622020960..97c914dbe 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -7,7 +7,6 @@ from adbutils import AdbClient, AdbDevice from module.base.decorator import cached_property from module.config.config import AzurLaneConfig -from module.config.utils import deep_iter from module.exception import RequestHumanTakeover from module.logger import logger @@ -49,7 +48,6 @@ class ConnectionAttr: self.serial_check() self.config.DEVICE_OVER_HTTP = self.is_over_http - @staticmethod def revise_serial(serial): serial = serial.replace(' ', '') @@ -123,6 +121,18 @@ class ConnectionAttr: def is_wsa(self): return bool(re.match(r'^wsa', self.serial)) + @cached_property + def port(self) -> int: + try: + return int(self.serial.split(':')[1]) + except (IndexError, ValueError): + return 0 + + @cached_property + def is_mumu12_family(self): + # 127.0.0.1:16XXX + return 16384 <= self.port <= 17408 + @cached_property def is_mumu_family(self): # 127.0.0.1:7555 @@ -130,9 +140,8 @@ class ConnectionAttr: return self.serial == '127.0.0.1:7555' or self.is_mumu12_family @cached_property - def is_mumu12_family(self): - # 127.0.0.1:16384 + 32*n - return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16') + def is_nox_family(self): + return 62001 <= self.port <= 63025 @cached_property def is_emulator(self): @@ -178,7 +187,8 @@ class ConnectionAttr: rf"SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config") as key: port = QueryValueEx(key, "BstAdbPort")[0] except FileNotFoundError: - logger.error(rf'Unable to find registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config') + logger.error( + rf'Unable to find registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config') logger.error('Please confirm that your are using BlueStack 4 hyper-v and not regular BlueStacks 4') logger.error(r'Please check if there is any other emulator instances under ' r'registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests') diff --git a/module/device/control.py b/module/device/control.py index cd9eb1865..b8f61260e 100644 --- a/module/device/control.py +++ b/module/device/control.py @@ -5,11 +5,12 @@ from module.base.utils import * from module.device.method.hermit import Hermit from module.device.method.maatouch import MaaTouch from module.device.method.minitouch import Minitouch +from module.device.method.nemu_ipc import NemuIpc from module.device.method.scrcpy import Scrcpy from module.logger import logger -class Control(Hermit, Minitouch, Scrcpy, MaaTouch): +class Control(Hermit, Minitouch, Scrcpy, MaaTouch, NemuIpc): def handle_control_check(self, button): # Will be overridden in Device pass @@ -22,6 +23,7 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): 'minitouch': self.click_minitouch, 'Hermit': self.click_hermit, 'MaaTouch': self.click_maatouch, + 'nemu_ipc': self.click_nemu_ipc, } def click(self, button, control_check=True): @@ -78,6 +80,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): self.long_click_scrcpy(x, y, duration) elif method == 'MaaTouch': self.long_click_maatouch(x, y, duration) + elif method == 'nemu_ipc': + self.long_click_nemu_ipc(x, y, duration) else: self.swipe_adb((x, y), (x, y), duration) @@ -86,13 +90,9 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): p1, p2 = ensure_int(p1, p2) duration = ensure_time(duration) method = self.config.Emulator_ControlMethod - if method == 'minitouch': - logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2))) - elif method == 'uiautomator2': + if method == 'uiautomator2': logger.info('Swipe %s -> %s, %s' % (point2str(*p1), point2str(*p2), duration)) - elif method == 'scrcpy': - logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2))) - elif method == 'MaaTouch': + elif method in ['minitouch', 'MaaTouch', 'scrcpy', 'nemu_ipc']: logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2))) else: # ADB needs to be slow, or swipe doesn't work @@ -114,6 +114,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): self.swipe_scrcpy(p1, p2) elif method == 'MaaTouch': self.swipe_maatouch(p1, p2) + elif method == 'nemu_ipc': + self.swipe_nemu_ipc(p1, p2) else: self.swipe_adb(p1, p2, duration=duration) @@ -163,6 +165,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch): self.drag_scrcpy(p1, p2, point_random=point_random) elif method == 'MaaTouch': self.drag_maatouch(p1, p2, point_random=point_random) + elif method == 'nemu_ipc': + self.drag_nemu_ipc(p1, p2, point_random=point_random) else: logger.warning(f'Control method {method} does not support drag well, ' f'falling back to ADB swipe may cause unexpected behaviour') diff --git a/module/device/device.py b/module/device/device.py index cbc45be7c..494914176 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -1,10 +1,15 @@ import collections import itertools +# Patch pkg_resources before importing adbutils and uiautomator2 +from module.device.pkg_resources import get_distribution + +# Just avoid being removed by import optimization +_ = get_distribution + from module.base.timer import Timer from module.device.app_control import AppControl from module.device.control import Control -from module.device.platform import Platform from module.device.screenshot import Screenshot from module.exception import ( EmulatorNotRunningError, @@ -56,7 +61,7 @@ def show_function_call(): logger.info('Function calls:' + ''.join(func_list)) -class Device(Screenshot, Control, AppControl, Platform): +class Device(Screenshot, Control, AppControl): _screen_size_checked = False detect_record = set() click_record = collections.deque(maxlen=30) @@ -82,12 +87,26 @@ class Device(Screenshot, Control, AppControl, Platform): if self.config.EmulatorInfo_Emulator == 'auto': _ = self.emulator_instance + # SRC only, use nemu_ipc if available + available = self.nemu_ipc_available() + logger.attr('nemu_ipc_available', available) + if available: + self.config.override(Emulator_ScreenshotMethod='nemu_ipc') + self.screenshot_interval_set() + self.method_check() # Auto-select the fastest screenshot method if not self.config.is_template_config and self.config.Emulator_ScreenshotMethod == 'auto': self.run_simple_screenshot_benchmark() + # Early init + if self.config.is_actual_task: + if self.config.Emulator_ControlMethod == 'MaaTouch': + self.early_maatouch_init() + if self.config.Emulator_ControlMethod == 'minitouch': + self.early_minitouch_init() + def run_simple_screenshot_benchmark(self): """ Perform a screenshot method benchmark, test 3 times on each method. @@ -101,7 +120,23 @@ class Device(Screenshot, Control, AppControl, Platform): bench = Benchmark(config=self.config, device=self) method = bench.run_simple_screenshot_benchmark() # Set - self.config.Emulator_ScreenshotMethod = method + with self.config.multi_set(): + self.config.Emulator_ScreenshotMethod = method + # if method == 'nemu_ipc': + # self.config.Emulator_ControlMethod = 'nemu_ipc' + + def method_check(self): + """ + Check combinations of screenshot method and control methods + """ + # nemu_ipc should be together + # if self.config.Emulator_ScreenshotMethod == 'nemu_ipc' and self.config.Emulator_ControlMethod != 'nemu_ipc': + # logger.warning('When using nemu_ipc, both screenshot and control should use nemu_ipc') + # self.config.Emulator_ControlMethod = 'nemu_ipc' + # if self.config.Emulator_ScreenshotMethod != 'nemu_ipc' and self.config.Emulator_ControlMethod == 'nemu_ipc': + # logger.warning('When not using nemu_ipc, both screenshot and control should not use nemu_ipc') + # self.config.Emulator_ControlMethod = 'minitouch' + pass def screenshot(self): """ @@ -127,6 +162,18 @@ class Device(Screenshot, Control, AppControl, Platform): # stop it during wait if self.config.Emulator_ScreenshotMethod == 'scrcpy': self._scrcpy_server_stop() + if self.config.Emulator_ScreenshotMethod == 'nemu_ipc': + self.nemu_ipc_release() + + def get_orientation(self): + """ + Callbacks when orientation changed. + """ + o = super().get_orientation() + + self.on_orientation_change_maatouch() + + return o def stuck_record_add(self, button): self.detect_record.add(str(button)) diff --git a/module/device/method/adb.py b/module/device/method/adb.py index 48324fd5d..bd3397edf 100644 --- a/module/device/method/adb.py +++ b/module/device/method/adb.py @@ -128,7 +128,7 @@ class Adb(Connection): if image is None: raise ImageTruncated('Empty image after cv2.imdecode') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) + cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image) if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') diff --git a/module/device/method/droidcast.py b/module/device/method/droidcast.py index 1a5e71f62..73c0f22b9 100644 --- a/module/device/method/droidcast.py +++ b/module/device/method/droidcast.py @@ -95,6 +95,8 @@ class DroidCast(Uiautomator2): """ _droidcast_port: int = 0 + droidcast_width: int = 0 + droidcast_height: int = 0 @cached_property def droidcast_session(self): @@ -112,15 +114,37 @@ class DroidCast(Uiautomator2): - /preview To get PNG screenshots. """ + def droidcast_url(self, url='/preview'): + if self.is_mumu_over_version_356: + w, h = self.droidcast_width, self.droidcast_height + if self.orientation == 0: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={w}&height={h}' + elif self.orientation == 1: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={h}&height={w}' + else: + # logger.warning('DroidCast receives invalid device orientation') + pass + return f'http://127.0.0.1:{self._droidcast_port}{url}' def droidcast_raw_url(self, url='/screenshot'): + if self.is_mumu_over_version_356: + w, h = self.droidcast_width, self.droidcast_height + if self.orientation == 0: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={w}&height={h}' + elif self.orientation == 1: + return f'http://127.0.0.1:{self._droidcast_port}{url}?width={h}&height={w}' + else: + # logger.warning('DroidCast receives invalid device orientation') + pass + return f'http://127.0.0.1:{self._droidcast_port}{url}' def droidcast_init(self): logger.hr('DroidCast init') self.droidcast_stop() + self._droidcast_update_resolution() logger.info('Pushing DroidCast apk') self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE) @@ -150,10 +174,25 @@ class DroidCast(Uiautomator2): else: logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}') + def _droidcast_update_resolution(self): + if self.is_mumu_over_version_356: + logger.info('Update droidcast resolution') + w, h = self.resolution_uiautomator2(cal_rotation=False) + self.get_orientation() + # 720, 1280 + # mumu12 > 3.5.6 is always a vertical device + self.droidcast_width, self.droidcast_height = w, h + logger.info(f'Droicast resolution: {(w, h)}') + @retry def screenshot_droidcast(self): self.config.DROIDCAST_VERSION = 'DroidCast' + if self.is_mumu_over_version_356: + if not self.droidcast_width or not self.droidcast_height: + self._droidcast_update_resolution() + resp = self.droidcast_session.get(self.droidcast_url(), timeout=3) + if resp.status_code == 404: raise DroidCastVersionIncompatible('DroidCast server does not have /preview') image = resp.content @@ -173,16 +212,37 @@ class DroidCast(Uiautomator2): if image is None: raise ImageTruncated('Empty image after cv2.cvtColor') + if self.is_mumu_over_version_356: + if self.orientation == 1: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + return image @retry def screenshot_droidcast_raw(self): self.config.DROIDCAST_VERSION = 'DroidCast_raw' + shape = (720, 1280) + if self.is_mumu_over_version_356: + if not self.droidcast_width or not self.droidcast_height: + self._droidcast_update_resolution() + if self.droidcast_height and self.droidcast_width: + shape = (self.droidcast_height, self.droidcast_width) + + rotate = self.is_mumu_over_version_356 and self.orientation == 1 + image = self.droidcast_session.get(self.droidcast_raw_url(), timeout=3).content # DroidCast_raw returns a RGB565 bitmap try: - arr = np.frombuffer(image, dtype=np.uint16).reshape((720, 1280)) + arr = np.frombuffer(image, dtype=np.uint16) + if rotate: + arr = arr.reshape(shape) + # arr = cv2.rotate(arr, cv2.ROTATE_90_CLOCKWISE) + # A little bit faster? + arr = cv2.transpose(arr) + cv2.flip(arr, 1, dst=arr) + else: + arr = arr.reshape(shape) except ValueError as e: if len(image) < 500: logger.warning(f'Unexpected screenshot: {image}') @@ -210,26 +270,26 @@ class DroidCast(Uiautomator2): # b = b.astype(np.uint8) # image = cv2.merge([r, g, b]) - # The same as the code above but costs about 5ms instead of 10ms. + # The same as the code above but costs about 3~4ms instead of 10ms. + # Note that cv2.convertScaleAbs is 5x fast as cv2.multiply, cv2.add is 8x fast as cv2.convertScaleAbs + # Note that cv2.convertScaleAbs includes rounding r = cv2.bitwise_and(arr, 0b1111100000000000) - cv2.multiply(r, 0.00390625, dst=r) - r = np.uint8(r) - m = cv2.multiply(r, 0.03125) + r = cv2.convertScaleAbs(r, alpha=0.00390625) + m = cv2.convertScaleAbs(r, alpha=0.03125) cv2.add(r, m, dst=r) g = cv2.bitwise_and(arr, 0b0000011111100000) - cv2.multiply(g, 0.125, dst=g) - g = np.uint8(g) - m = cv2.multiply(g, 0.015625) + g = cv2.convertScaleAbs(g, alpha=0.125) + m = cv2.convertScaleAbs(g, alpha=0.015625, dst=m) cv2.add(g, m, dst=g) b = cv2.bitwise_and(arr, 0b0000000000011111) - cv2.multiply(b, 8, dst=b) - b = np.uint8(b) - m = cv2.multiply(b, 0.03125) + b = cv2.convertScaleAbs(b, alpha=8) + m = cv2.convertScaleAbs(b, alpha=0.03125, dst=m) cv2.add(b, m, dst=b) image = cv2.merge([r, g, b]) + return image def droidcast_wait_startup(self): diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index 8122505bc..c61e82d24 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -1,14 +1,15 @@ import socket +import threading from functools import wraps from adbutils.errors import AdbError -from module.base.decorator import cached_property, del_cached_property +from module.base.decorator import cached_property, del_cached_property, has_cached_property from module.base.timer import Timer from module.base.utils import * from module.device.connection import Connection from module.device.method.minitouch import CommandBuilder, insert_swipe -from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error +from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep from module.exception import RequestHumanTakeover from module.logger import logger @@ -36,20 +37,20 @@ def retry(func): def init(): self.adb_reconnect() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') # Emulator closed except ConnectionAbortedError as e: logger.error(e) def init(): self.adb_reconnect() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_reconnect() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') else: break # MaaTouchNotInstalledError: Received "Aborted" from MaaTouch @@ -58,12 +59,12 @@ def retry(func): def init(): self.maatouch_install() - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') except BrokenPipeError as e: logger.error(e) def init(): - del_cached_property(self, 'maatouch_builder') + del_cached_property(self, '_maatouch_builder') # Unknown, probably a trucked image except Exception as e: logger.exception(e) @@ -101,20 +102,76 @@ class MaaTouch(Connection): """ max_x: int max_y: int - _maatouch_stream = socket.socket + _maatouch_stream: socket.socket = None _maatouch_stream_storage = None + _maatouch_init_thread = None + _maatouch_orientation: int = None @cached_property - def maatouch_builder(self): + @retry + def _maatouch_builder(self): self.maatouch_init() return MaatouchBuilder(self) + @property + def maatouch_builder(self): + # Wait init thread + if self._maatouch_init_thread is not None: + self._maatouch_init_thread.join() + del self._maatouch_init_thread + self._maatouch_init_thread = None + + return self._maatouch_builder + + def early_maatouch_init(self): + """ + Start a thread to init maatouch connection while the Alas instance just starting to take screenshots + This would speed up the first click 0.2 ~ 0.4s. + """ + if has_cached_property(self, '_maatouch_builder'): + return + + def early_maatouch_init_func(): + _ = self._maatouch_builder + + thread = threading.Thread(target=early_maatouch_init_func, daemon=True) + self._maatouch_init_thread = thread + thread.start() + + def on_orientation_change_maatouch(self): + """ + MaaTouch caches devices orientation at its startup + A restart is required when orientation changed + """ + if self._maatouch_orientation is None: + return + if self.orientation == self._maatouch_orientation: + return + + logger.info(f'Orientation changed {self._maatouch_orientation} => {self.orientation}, re-init MaaTouch') + del_cached_property(self, '_maatouch_builder') + self.early_maatouch_init() + def maatouch_init(self): logger.hr('MaaTouch init') max_x, max_y = 1280, 720 max_contacts = 2 max_pressure = 50 + # Try to close existing stream + if self._maatouch_stream is not None: + try: + self._maatouch_stream.close() + except Exception as e: + logger.error(e) + del self._maatouch_stream + if self._maatouch_stream_storage is not None: + del self._maatouch_stream_storage + + # MaaTouch caches devices orientation at its startup + super(MaaTouch, self).get_orientation() + self._maatouch_orientation = self.orientation + # CLASSPATH=/data/local/tmp/maatouch app_process / com.shxyke.MaaTouch.App stream = self.adb_shell( ['CLASSPATH=/data/local/tmp/maatouch', 'app_process', '/', 'com.shxyke.MaaTouch.App'], @@ -245,3 +302,8 @@ class MaaTouch(Connection): builder.up().commit() builder.send() + + +if __name__ == '__main__': + self = MaaTouch('src') + self.maatouch_uninstall() \ No newline at end of file diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index 405bc0299..356976c21 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -1,7 +1,7 @@ import asyncio import json -import re import socket +import threading import time from functools import wraps from typing import List @@ -10,11 +10,11 @@ import websockets from adbutils.errors import AdbError from uiautomator2 import _Service -from module.base.decorator import Config, cached_property, del_cached_property +from module.base.decorator import Config, cached_property, del_cached_property, has_cached_property from module.base.timer import Timer from module.base.utils import * from module.device.connection import Connection -from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error +from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger @@ -86,6 +86,8 @@ def insert_swipe(p0, p3, speed=15, min_distance=10): distance = np.linalg.norm(np.subtract(points[1:], points[0]), axis=1) mask = np.append(True, distance > min_distance) points = np.array(points)[mask].tolist() + if len(points) <= 1: + points = [p0, p3] else: points = [p0, p3] @@ -314,12 +316,18 @@ def retry(func): def init(): self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') # Emulator closed except ConnectionAbortedError as e: logger.error(e) def init(): self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') # MinitouchNotInstalledError: Received empty data from minitouch except MinitouchNotInstalledError as e: logger.error(e) @@ -328,7 +336,7 @@ def retry(func): self.install_uiautomator2() if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') # MinitouchOccupiedError: Timeout when connecting to minitouch except MinitouchOccupiedError as e: logger.error(e) @@ -337,19 +345,22 @@ def retry(func): self.restart_atx() if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_reconnect() + if self._minitouch_port: + self.adb_forward_remove(f'tcp:{self._minitouch_port}') + del_cached_property(self, '_minitouch_builder') else: break except BrokenPipeError as e: logger.error(e) def init(): - del_cached_property(self, 'minitouch_builder') + del_cached_property(self, '_minitouch_builder') # Unknown, probably a trucked image except Exception as e: logger.exception(e) @@ -365,23 +376,59 @@ def retry(func): class Minitouch(Connection): _minitouch_port: int = 0 - _minitouch_client: socket.socket + _minitouch_client: socket.socket = None _minitouch_pid: int _minitouch_ws: websockets.WebSocketClientProtocol max_x: int max_y: int + _minitouch_init_thread = None @cached_property - def minitouch_builder(self): + @retry + def _minitouch_builder(self): self.minitouch_init() return CommandBuilder(self) + @property + def minitouch_builder(self): + # Wait init thread + if self._minitouch_init_thread is not None: + self._minitouch_init_thread.join() + del self._minitouch_init_thread + self._minitouch_init_thread = None + + return self._minitouch_builder + + def early_minitouch_init(self): + """ + Start a thread to init minitouch connection while the Alas instance just starting to take screenshots + This would speed up the first click 0.05s. + """ + if has_cached_property(self, '_minitouch_builder'): + return + + def early_minitouch_init_func(): + _ = self._minitouch_builder + + thread = threading.Thread(target=early_minitouch_init_func, daemon=True) + self._minitouch_init_thread = thread + thread.start() + @Config.when(DEVICE_OVER_HTTP=False) def minitouch_init(self): logger.hr('MiniTouch init') max_x, max_y = 1280, 720 max_contacts = 2 max_pressure = 50 + + # Try to close existing stream + if self._minitouch_client is not None: + try: + self._minitouch_client.close() + except Exception as e: + logger.error(e) + del self._minitouch_client + self.get_orientation() self._minitouch_port = self.adb_forward("localabstract:minitouch") diff --git a/module/device/method/nemu_ipc.py b/module/device/method/nemu_ipc.py new file mode 100644 index 000000000..b79e1c322 --- /dev/null +++ b/module/device/method/nemu_ipc.py @@ -0,0 +1,541 @@ +import asyncio +import ctypes +import os +import sys +from functools import partial, wraps + +import cv2 +import numpy as np + +from module.base.decorator import cached_property, del_cached_property, has_cached_property +from module.base.utils import ensure_time +from module.device.method.minitouch import insert_swipe, random_rectangle_point +from module.device.method.utils import RETRY_TRIES, retry_sleep +from module.device.platform import Platform +from module.exception import RequestHumanTakeover +from module.logger import logger + + +class NemuIpcIncompatible(Exception): + pass + + +class NemuIpcError(Exception): + pass + + +class CaptureStd: + """ + Capture stdout and stderr from both python and C library + https://stackoverflow.com/questions/5081657/how-do-i-prevent-a-c-shared-library-to-print-on-stdout-in-python/17954769 + + ``` + with CaptureStd() as capture: + # String wasn't printed + print('whatever') + # But captured in ``capture.stdout`` + print(f'Got stdout: "{capture.stdout}"') + print(f'Got stderr: "{capture.stderr}"') + ``` + """ + + def __init__(self): + self.stdout = b'' + self.stderr = b'' + + def _redirect_stdout(self, to): + sys.stdout.close() + os.dup2(to, self.fdout) + sys.stdout = os.fdopen(self.fdout, 'w') + + def _redirect_stderr(self, to): + sys.stderr.close() + os.dup2(to, self.fderr) + sys.stderr = os.fdopen(self.fderr, 'w') + + def __enter__(self): + self.fdout = sys.stdout.fileno() + self.fderr = sys.stderr.fileno() + self.reader_out, self.writer_out = os.pipe() + self.reader_err, self.writer_err = os.pipe() + self.old_stdout = os.dup(self.fdout) + self.old_stderr = os.dup(self.fderr) + + file_out = os.fdopen(self.writer_out, 'w') + file_err = os.fdopen(self.writer_err, 'w') + self._redirect_stdout(to=file_out.fileno()) + self._redirect_stderr(to=file_err.fileno()) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._redirect_stdout(to=self.old_stdout) + self._redirect_stderr(to=self.old_stderr) + os.close(self.old_stdout) + os.close(self.old_stderr) + + self.stdout = self.recvall(self.reader_out) + self.stderr = self.recvall(self.reader_err) + os.close(self.reader_out) + os.close(self.reader_err) + + @staticmethod + def recvall(reader, length=1024) -> bytes: + fragments = [] + while 1: + chunk = os.read(reader, length) + if chunk: + fragments.append(chunk) + else: + break + output = b''.join(fragments) + return output + + +class CaptureNemuIpc(CaptureStd): + instance = None + + def is_capturing(self): + """ + Only capture at the topmost wrapper to avoid nested capturing + If a capture is ongoing, this instance does nothing + """ + cls = self.__class__ + return isinstance(cls.instance, cls) and cls.instance != self + + def __enter__(self): + if self.is_capturing(): + return self + + super().__enter__() + CaptureNemuIpc.instance = self + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.is_capturing(): + return + + CaptureNemuIpc.instance = None + super().__exit__(exc_type, exc_val, exc_tb) + + self.check_stdout() + self.check_stderr() + + def check_stdout(self): + if not self.stdout: + return + logger.info(f'NemuIpc stdout: {self.stdout}') + + def check_stderr(self): + if not self.stderr: + return + logger.error(f'NemuIpc stderr: {self.stderr}') + + # Calling an old MuMu12 player + # Tested on 3.4.0 + # b'nemu_capture_display rpc error: 1783\r\n' + # Tested on 3.7.3 + # b'nemu_capture_display rpc error: 1745\r\n' + if b'error: 1783' in self.stderr or b'error: 1745' in self.stderr: + raise NemuIpcIncompatible( + f'NemuIpc requires MuMu12 version >= 3.8.13, please check your version') + # contact_id incorrect + # b'nemu_capture_display cannot find rpc connection\r\n' + if b'cannot find rpc connection' in self.stderr: + raise NemuIpcError(self.stderr) + # Emulator died + # b'nemu_capture_display rpc error: 1722\r\n' + # MuMuVMMSVC.exe died + # b'nemu_capture_display rpc error: 1726\r\n' + # No idea how to handle yet + if b'error: 1722' in self.stderr or b'error: 1726' in self.stderr: + raise NemuIpcError('Emulator instance is probably dead') + + +def retry(func): + @wraps(func) + def retry_wrapper(self, *args, **kwargs): + """ + Args: + self (NemuIpcImpl): + """ + init = None + for _ in range(RETRY_TRIES): + try: + if callable(init): + retry_sleep(_) + init() + return func(self, *args, **kwargs) + # Can't handle + except RequestHumanTakeover: + break + # Can't handle + except NemuIpcIncompatible as e: + logger.error(e) + break + # Function call timeout + except asyncio.TimeoutError: + logger.warning(f'Func {func.__name__}() call timeout, retrying: {_}') + + def init(): + self.reconnect() + # NemuIpcError + except NemuIpcError as e: + logger.error(e) + + def init(): + self.reconnect() + # Unknown, probably a trucked image + except Exception as e: + logger.exception(e) + + def init(): + pass + + logger.critical(f'Retry {func.__name__}() failed') + raise RequestHumanTakeover + + return retry_wrapper + + +class NemuIpcImpl: + def __init__(self, nemu_folder: str, instance_id: int, display_id: int = 0): + """ + Args: + nemu_folder: Installation path of MuMu12, e.g. E:/ProgramFiles/MuMuPlayer-12.0 + instance_id: Emulator instance ID, starting from 0 + display_id: Always 0 if keep app alive was disabled + """ + self.nemu_folder: str = nemu_folder + self.instance_id: int = instance_id + self.display_id: int = display_id + + ipc_dll = os.path.abspath(os.path.join(nemu_folder, './shell/sdk/external_renderer_ipc.dll')) + logger.info( + f'NemuIpcImpl init, ' + f'nemu_folder={nemu_folder}, ' + f'ipc_dll={ipc_dll}, ' + f'instance_id={instance_id}, ' + f'display_id={display_id}' + ) + + try: + self.lib = ctypes.CDLL(ipc_dll) + except OSError as e: + logger.error(e) + # OSError: [WinError 126] 找不到指定的模块。 + if not os.path.exists(ipc_dll): + raise NemuIpcIncompatible( + f'ipc_dll={ipc_dll} does not exist, ' + f'NemuIpc requires MuMu12 version >= 3.8.13, please check your version') + else: + raise NemuIpcIncompatible( + f'ipc_dll={ipc_dll} exists, but cannot be loaded') + self.connect_id: int = 0 + self.width = 0 + self.height = 0 + + def connect(self): + if self.connect_id > 0: + return + + connect_id = self.ev_run_sync( + self.lib.nemu_connect, + self.nemu_folder, self.instance_id + ) + if connect_id == 0: + raise NemuIpcError( + 'Connection failed, please check if nemu_folder is correct and emulator is running' + ) + + self.connect_id = connect_id + # logger.info(f'NemuIpc connected: {self.connect_id}') + + def disconnect(self): + if self.connect_id == 0: + return + + self.ev_run_sync( + self.lib.nemu_disconnect, + self.connect_id + ) + + # logger.info(f'NemuIpc disconnected: {self.connect_id}') + self.connect_id = 0 + + def reconnect(self): + self.disconnect() + self.connect() + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + + @cached_property + def _ev(self): + return asyncio.new_event_loop() + + async def ev_run_async(self, func, *args, **kwargs): + """ + Args: + func: Sync function to call + *args: + **kwargs: + + Raises: + asyncio.TimeoutError: If function call timeout + """ + func_wrapped = partial(func, *args, **kwargs) + # Increased timeout for slow PCs + # Default screenshot interval is 0.2s, so a 0.15s timeout would have a fast retry without extra time costs + result = await asyncio.wait_for(self._ev.run_in_executor(None, func_wrapped), timeout=0.15) + return result + + def ev_run_sync(self, func, *args, **kwargs): + """ + Args: + func: Sync function to call + *args: + **kwargs: + + Raises: + asyncio.TimeoutError: If function call timeout + NemuIpcIncompatible: + NemuIpcError + """ + result = self._ev.run_until_complete(self.ev_run_async(func, *args, **kwargs)) + + err = False + if func.__name__ == 'nemu_connect': + if result == 0: + err = True + else: + if result > 0: + err = True + # Get to actual error message printed in std + if err: + logger.warning(f'Failed to call {func.__name__}, result={result}') + with CaptureNemuIpc(): + result = self._ev.run_until_complete(self.ev_run_async(func, *args, **kwargs)) + + return result + + def get_resolution(self): + """ + Get emulator resolution, `self.width` and `self.height` will be set + """ + if self.connect_id == 0: + self.connect() + + width_ptr = ctypes.pointer(ctypes.c_int(0)) + height_ptr = ctypes.pointer(ctypes.c_int(0)) + nullptr = ctypes.POINTER(ctypes.c_int)() + + ret = self.ev_run_sync( + self.lib.nemu_capture_display, + self.connect_id, self.display_id, 0, width_ptr, height_ptr, nullptr + ) + if ret > 0: + raise NemuIpcError('nemu_capture_display failed during get_resolution()') + self.width = width_ptr.contents.value + self.height = height_ptr.contents.value + + @retry + def screenshot(self): + """ + Returns: + np.ndarray: Image array in RGBA color space + Note that image is upside down + """ + if self.connect_id == 0: + self.connect() + + self.get_resolution() + + width_ptr = ctypes.pointer(ctypes.c_int(self.width)) + height_ptr = ctypes.pointer(ctypes.c_int(self.height)) + length = self.width * self.height * 4 + pixels_pointer = ctypes.pointer((ctypes.c_ubyte * length)()) + + ret = self.ev_run_sync( + self.lib.nemu_capture_display, + self.connect_id, self.display_id, length, width_ptr, height_ptr, pixels_pointer + ) + if ret > 0: + raise NemuIpcError('nemu_capture_display failed during screenshot()') + + # image = np.ctypeslib.as_array(pixels_pointer, shape=(self.height, self.width, 4)) + image = np.ctypeslib.as_array(pixels_pointer.contents).reshape((self.height, self.width, 4)) + return image + + def convert_xy(self, x, y): + """ + Convert classic ADB coordinates to Nemu's + `self.height` must be updated before calling this method + + Returns: + int, int + """ + x, y = int(x), int(y) + x, y = self.height - y, x + return x, y + + @retry + def down(self, x, y): + """ + Contact down, continuous contact down will be considered as swipe + """ + if self.connect_id == 0: + self.connect() + if self.height == 0: + self.get_resolution() + + x, y = self.convert_xy(x, y) + + ret = self.ev_run_sync( + self.lib.nemu_input_event_touch_down, + self.connect_id, self.display_id, x, y + ) + if ret > 0: + raise NemuIpcError('nemu_input_event_touch_down failed') + + @retry + def up(self): + """ + Contact up + """ + if self.connect_id == 0: + self.connect() + + ret = self.ev_run_sync( + self.lib.nemu_input_event_touch_up, + self.connect_id, self.display_id + ) + if ret > 0: + raise NemuIpcError('nemu_input_event_touch_up failed') + + +def serial_to_id(serial: str): + """ + Predict instance ID from serial + E.g. + "127.0.0.1:16384" -> 0 + "127.0.0.1:16416" -> 1 + + Returns: + int: instance_id, or None if failed to predict + """ + try: + port = int(serial.split(':')[1]) + except (IndexError, ValueError): + return None + index, offset = divmod(port - 16384, 32) + if 0 <= index < 32 and offset in [0, 1, 2]: + return index + else: + return None + + +class NemuIpc(Platform): + @cached_property + def nemu_ipc(self) -> NemuIpcImpl: + """ + Initialize a nemu ipc implementation + """ + # Try existing settings first + if self.config.EmulatorInfo_path: + folder = os.path.abspath(os.path.join(self.config.EmulatorInfo_path, '../../')) + index = serial_to_id(self.serial) + if index is not None: + try: + return NemuIpcImpl( + nemu_folder=folder, + instance_id=index, + display_id=0 + ).__enter__() + except (NemuIpcIncompatible, NemuIpcError) as e: + logger.error(e) + logger.error('Emulator info incorrect') + + # Search emulator instance + # with E:\ProgramFiles\MuMuPlayer-12.0\shell\MuMuPlayer.exe + # installation path is E:\ProgramFiles\MuMuPlayer-12.0 + if self.emulator_instance is None: + logger.error('Unable to use NemuIpc because emulator instance not found') + raise RequestHumanTakeover + try: + return NemuIpcImpl( + nemu_folder=self.emulator_instance.emulator.abspath('../'), + instance_id=self.emulator_instance.MuMuPlayer12_id, + display_id=0 + ).__enter__() + except (NemuIpcIncompatible, NemuIpcError) as e: + logger.error(e) + logger.error('Unable to initialize NemuIpc') + raise RequestHumanTakeover + + def nemu_ipc_available(self) -> bool: + if not self.is_mumu_family: + return False + if self.nemud_app_keep_alive == '': + return False + try: + _ = self.nemu_ipc + except RequestHumanTakeover: + return False + return True + + def nemu_ipc_release(self): + if has_cached_property(self, 'nemu_ipc'): + self.nemu_ipc.disconnect() + del_cached_property(self, 'nemu_ipc') + logger.info('nemu_ipc released') + + def screenshot_nemu_ipc(self): + image = self.nemu_ipc.screenshot() + + image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR) + cv2.flip(image, 0, dst=image) + return image + + def click_nemu_ipc(self, x, y): + down = ensure_time((0.010, 0.020)) + self.nemu_ipc.down(x, y) + self.sleep(down) + self.nemu_ipc.up() + self.sleep(0.050 - down) + + def long_click_nemu_ipc(self, x, y, duration=1.0): + self.nemu_ipc.down(x, y) + self.sleep(duration) + self.nemu_ipc.up() + self.sleep(0.050) + + def swipe_nemu_ipc(self, p1, p2): + points = insert_swipe(p0=p1, p3=p2) + + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) + + self.nemu_ipc.up() + self.sleep(0.050) + + def drag_nemu_ipc(self, p1, p2, point_random=(-10, -10, 10, 10)): + p1 = np.array(p1) - random_rectangle_point(point_random) + p2 = np.array(p2) - random_rectangle_point(point_random) + points = insert_swipe(p0=p1, p3=p2, speed=20) + + for point in points: + self.nemu_ipc.down(*point) + self.sleep(0.010) + + self.nemu_ipc.down(*p2) + self.sleep(0.140) + self.nemu_ipc.down(*p2) + self.sleep(0.140) + + self.nemu_ipc.up() + self.sleep(0.050) diff --git a/module/device/method/scrcpy/core.py b/module/device/method/scrcpy/core.py index aadb49bbd..499338b36 100644 --- a/module/device/method/scrcpy/core.py +++ b/module/device/method/scrcpy/core.py @@ -65,6 +65,8 @@ class ScrcpyCore(Connection): Raises: ScrcpyError: + adbutils.AdbTimeout: + socket.timeout: """ logger.hr('Scrcpy server start') commands = ScrcpyOptions.command_v120(jar_path=self.config.SCRCPY_FILEPATH_REMOTE) @@ -72,6 +74,7 @@ class ScrcpyCore(Connection): commands, stream=True, ) + self._scrcpy_server_stream.conn.settimeout(3) logger.info('Create server stream') ret = self._scrcpy_server_stream.read(10) @@ -104,6 +107,7 @@ class ScrcpyCore(Connection): self._scrcpy_video_socket = self.adb.create_connection( Network.LOCAL_ABSTRACT, "scrcpy" ) + self._scrcpy_video_socket.settimeout(3) break except AdbError: sleep(0.1) @@ -115,6 +119,7 @@ class ScrcpyCore(Connection): self._scrcpy_control_socket = self.adb.create_connection( Network.LOCAL_ABSTRACT, "scrcpy" ) + self._scrcpy_control_socket.settimeout(3) logger.info('Fetch device info') device_name = self._scrcpy_video_socket.recv(64).decode("utf-8").rstrip("\x00") @@ -151,23 +156,35 @@ class ScrcpyCore(Connection): # logger.error(err) self._scrcpy_alive = False - if self._scrcpy_server_stream is not None: - try: - self._scrcpy_server_stream.close() - except Exception: - pass + + if self._scrcpy_stream_loop_thread is not None: + self._scrcpy_stream_loop_thread.join(1) + del self._scrcpy_stream_loop_thread + self._scrcpy_stream_loop_thread = None if self._scrcpy_control_socket is not None: try: self._scrcpy_control_socket.close() - except Exception: - pass + except Exception as e: + logger.error(e) + del self._scrcpy_control_socket + self._scrcpy_control_socket = None if self._scrcpy_video_socket is not None: try: self._scrcpy_video_socket.close() - except Exception: - pass + except Exception as e: + logger.error(e) + del self._scrcpy_video_socket + self._scrcpy_video_socket = None + + if self._scrcpy_server_stream is not None: + try: + self._scrcpy_server_stream.close() + except Exception as e: + logger.error(e) + del self._scrcpy_server_stream + self._scrcpy_server_stream = None logger.info('Scrcpy server stopped') @@ -195,7 +212,8 @@ class ScrcpyCore(Connection): try: raw_h264 = self._scrcpy_video_socket.recv(0x10000) if raw_h264 == b"": - raise ScrcpyError("Video stream is disconnected") + if self._scrcpy_alive: + raise ScrcpyError("_scrcpy_stream_loop_thread: Video stream disconnected") packets = codec.parse(raw_h264) for packet in packets: frames = codec.decode(packet) @@ -212,5 +230,8 @@ class ScrcpyCore(Connection): if self._scrcpy_alive: logger.error(f'_scrcpy_stream_loop_thread: {repr(e)}') raise + except Exception as e: + logger.error(f'_scrcpy_stream_loop_thread exception: {repr(e)}') + raise raise ScrcpyError('_scrcpy_stream_loop stopped') diff --git a/module/device/method/scrcpy/scrcpy.py b/module/device/method/scrcpy/scrcpy.py index 50eff74f8..0730e9bac 100644 --- a/module/device/method/scrcpy/scrcpy.py +++ b/module/device/method/scrcpy/scrcpy.py @@ -1,15 +1,16 @@ +import socket import time from functools import wraps import numpy as np -from adbutils.errors import AdbError +from adbutils.errors import AdbError, AdbTimeout import module.device.method.scrcpy.const as const from module.base.utils import random_rectangle_point from module.device.method.minitouch import insert_swipe from module.device.method.scrcpy.core import ScrcpyCore, ScrcpyError from module.device.method.uiautomator_2 import Uiautomator2 -from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error +from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep from module.exception import RequestHumanTakeover from module.logger import logger @@ -19,7 +20,7 @@ def retry(func): def retry_wrapper(self, *args, **kwargs): """ Args: - self (Minitouch): + self (ScrcpyCore): """ init = None for _ in range(RETRY_TRIES): @@ -47,6 +48,13 @@ def retry(func): except ScrcpyError as e: logger.error(e) + def init(): + self.scrcpy_init() + # AdbTimeout + # socket.timeout + except (AdbTimeout, socket.timeout) as e: + logger.error(e) + def init(): self.scrcpy_init() # AdbError @@ -85,7 +93,8 @@ class Scrcpy(ScrcpyCore, Uiautomator2): now = time.time() while 1: time.sleep(0.001) - if self._scrcpy_stream_loop_thread is None or not self._scrcpy_stream_loop_thread.is_alive(): + thread = self._scrcpy_stream_loop_thread + if thread is None or not thread.is_alive(): raise ScrcpyError('_scrcpy_stream_loop_thread died') if self._scrcpy_last_frame_time > now: screenshot = self._scrcpy_last_frame.copy() diff --git a/module/device/method/uiautomator_2.py b/module/device/method/uiautomator_2.py index 69520f0a2..c116bce94 100644 --- a/module/device/method/uiautomator_2.py +++ b/module/device/method/uiautomator_2.py @@ -242,8 +242,19 @@ class Uiautomator2(Connection): hierarchy = etree.fromstring(content.encode('utf-8')) return hierarchy + def uninstall_uiautomator2(self): + logger.info('Removing uiautomator2') + for file in [ + 'app-uiautomator.apk', + 'app-uiautomator-test.apk', + 'minitouch', + 'minitouch.so', + 'atx-agent', + ]: + self.adb_shell(["rm", f"/data/local/tmp/{file}"]) + @retry - def resolution_uiautomator2(self) -> t.Tuple[int, int]: + def resolution_uiautomator2(self, cal_rotation=True) -> t.Tuple[int, int]: """ Faster u2.window_size(), cause that calls `dumpsys display` twice. @@ -252,9 +263,10 @@ class Uiautomator2(Connection): """ info = self.u2.http.get('/info').json() w, h = info['display']['width'], info['display']['height'] - rotation = self.get_orientation() - if (w > h) != (rotation % 2 == 1): - w, h = h, w + if cal_rotation: + rotation = self.get_orientation() + if (w > h) != (rotation % 2 == 1): + w, h = h, w return w, h def resolution_check_uiautomator2(self): diff --git a/module/device/method/utils.py b/module/device/method/utils.py index 36d6bbd49..352d4361b 100644 --- a/module/device/method/utils.py +++ b/module/device/method/utils.py @@ -1,3 +1,4 @@ +import os import random import re import socket @@ -5,6 +6,7 @@ import time import typing as t import uiautomator2 as u2 +import uiautomator2cache from adbutils import AdbTimeout from lxml import etree @@ -51,6 +53,25 @@ from module.logger import logger RETRY_TRIES = 5 RETRY_DELAY = 3 +# Patch uiautomator2 appdir +u2.init.appdir = os.path.dirname(uiautomator2cache.__file__) + +# Patch uiautomator2 logger +u2_logger = u2.logger +u2_logger.debug = logger.info +u2_logger.info = logger.info +u2_logger.warning = logger.warning +u2_logger.error = logger.error +u2_logger.critical = logger.critical + + +def setup_logger(*args, **kwargs): + return u2_logger + + +u2.setup_logger = setup_logger +u2.init.setup_logger = setup_logger + def is_port_using(port_num): """ if port is using by others, return True. else return False """ @@ -253,7 +274,7 @@ def remove_suffix(s, suffix): Returns: str, bytes: """ - return s[:len(suffix)] if s.endswith(suffix) else s + return s[:-len(suffix)] if s.endswith(suffix) else s def remove_shell_warning(s): diff --git a/module/device/pkg_resources/__init__.py b/module/device/pkg_resources/__init__.py new file mode 100644 index 000000000..b685a6d9a --- /dev/null +++ b/module/device/pkg_resources/__init__.py @@ -0,0 +1,109 @@ +import os +import re +import sys + +from module.base.decorator import cached_property +from module.logger import logger + +""" +Importing pkg_resources is so slow, like 0.4 ~ 1.0s, just google it you will find it indeed really slow. +Since it was some kind of standard library there is no way to modify it or speed it up. +So here's a poor but fast implementation of pkg_resources returning the things in need. + +To patch: +``` +# Patch pkg_resources before importing adbutils and uiautomator2 +from module.device.pkg_resources import get_distribution +# Just avoid being removed by import optimization +_ = get_distribution +``` +""" +# Inject sys.modules, pretend we have pkg_resources imported +try: + sys.modules['pkg_resources'] = sys.modules['module.device.pkg_resources'] +except KeyError: + logger.error('Patch pkg_resources failed, patch module does not exists') + + +def remove_suffix(s, suffix): + """ + Remove suffix of a string or bytes like `string.removesuffix(suffix)`, which is on Python3.9+ + + Args: + s (str, bytes): + suffix (str, bytes): + + Returns: + str, bytes: + """ + return s[:-len(suffix)] if s.endswith(suffix) else s + + +class FakeDistributionObject: + def __init__(self, dist, version): + self.dist = dist + self.version = version + + def __str__(self): + return f'{self.__class__.__name__}({self.dist}={self.version})' + + __repr__ = __str__ + + +class PackageCache: + @cached_property + def site_packages(self): + # Just whatever library to locate the `site-packages` directory + import requests + path = os.path.abspath(os.path.join(requests.__file__, '../../')) + return path + + @cached_property + def dict_installed_packages(self): + """ + Returns: + dict: Key: str, package name + Value: FakeDistributionObject + """ + dic = {} + for file in os.listdir(self.site_packages): + # mxnet_cu101-1.6.0.dist-info + # adbutils-0.11.0-py3.7.egg-info + res = re.match(r'^([a-zA-Z0-9._]+)-([a-zA-Z0-9._]+)-', file) + if res: + version = remove_suffix(res.group(2), '.dist') + # version = res.group(2) + obj = FakeDistributionObject( + dist=res.group(1), + version=version, + ) + dic[obj.dist] = obj + + return dic + + +PACKAGE_CACHE = PackageCache() + + +def resource_filename(*args): + if args == ("adbutils", "binaries"): + path = os.path.abspath(os.path.join(PACKAGE_CACHE.site_packages, *args)) + return path + + +def get_distribution(dist): + """Return a current distribution object for a Requirement or string""" + if dist == 'adbutils': + return PACKAGE_CACHE.dict_installed_packages.get( + 'adbutils', + FakeDistributionObject('adbutils', '0.11.0'), + ) + if dist == 'uiautomator2': + return PACKAGE_CACHE.dict_installed_packages.get( + 'uiautomator2', + FakeDistributionObject('uiautomator2', '2.16.17'), + ) + + +class DistributionNotFound(Exception): + pass diff --git a/module/device/platform/emulator_base.py b/module/device/platform/emulator_base.py index ecd026ba1..394becc24 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -36,6 +36,21 @@ def get_serial_pair(serial): return None, None +def remove_duplicated_path(paths): + """ + Args: + paths (list[str]): + + Returns: + list[str]: + """ + paths = sorted(set(paths)) + dic = {} + for path in paths: + dic.setdefault(path.lower(), path) + return list(dic.values()) + + @dataclass class EmulatorInstanceBase: # Serial for adb connection @@ -90,7 +105,7 @@ class EmulatorInstanceBase: Returns: int: Instance ID, or None if this is not a MuMu 12 instance """ - res = re.search(r'MuMuPlayer-12.0-(\d+)', self.name) + res = re.search(r'MuMuPlayer(?:Global)?-12.0-(\d+)', self.name) if res: return int(res.group(1)) res = re.search(r'YXArkNights-12.0-(\d+)', self.name) @@ -205,6 +220,14 @@ class EmulatorBase: class EmulatorManagerBase: + @staticmethod + def iter_running_emulator(): + """ + Yields: + str: Path to emulator executables, may contains duplicate values + """ + return + @cached_property def all_emulators(self) -> t.List[EmulatorBase]: """ diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index 142ad3e30..f7a5e54bc 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -8,7 +8,8 @@ from dataclasses import dataclass # module/device/platform/emulator_base.py # module/device/platform/emulator_windows.py # Will be used in Alas Easy Install, they shouldn't import any Alas modules. -from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase +from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase, \ + remove_duplicated_path from module.device.platform.utils import cached_property, iter_folder @@ -70,7 +71,7 @@ class Emulator(EmulatorBase): def path_to_type(cls, path: str) -> str: """ Args: - path: Path to .exe file + path: Path to .exe file, case insensitive Returns: str: Emulator type, such as Emulator.NoxPlayer @@ -78,46 +79,49 @@ class Emulator(EmulatorBase): folder, exe = os.path.split(path) folder, dir1 = os.path.split(folder) folder, dir2 = os.path.split(folder) - if exe == 'Nox.exe': - if dir2 == 'Nox': + exe = exe.lower() + dir1 = dir1.lower() + dir2 = dir2.lower() + if exe == 'nox.exe': + if dir2 == 'nox': return cls.NoxPlayer - elif dir2 == 'Nox64': + elif dir2 == 'nox64': return cls.NoxPlayer64 else: return cls.NoxPlayer - if exe == 'Bluestacks.exe': - if dir1 in ['BlueStacks', 'BlueStacks_cn']: + if exe == 'bluestacks.exe': + if dir1 in ['bluestacks', 'bluestacks_cn']: return cls.BlueStacks4 - elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']: + elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 else: return cls.BlueStacks4 - if exe == 'HD-Player.exe': - if dir1 in ['BlueStacks', 'BlueStacks_cn']: + if exe == 'hd-player.exe': + if dir1 in ['bluestacks', 'bluestacks_cn']: return cls.BlueStacks4 - elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']: + elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 else: return cls.BlueStacks5 if exe == 'dnplayer.exe': - if dir1 == 'LDPlayer': + if dir1 == 'ldplayer': return cls.LDPlayer3 - elif dir1 == 'LDPlayer4': + elif dir1 == 'ldplayer4': return cls.LDPlayer4 - elif dir1 == 'LDPlayer9': + elif dir1 == 'ldplayer9': return cls.LDPlayer9 else: return cls.LDPlayer3 - if exe == 'NemuPlayer.exe': + if exe == 'nemuplayer.exe': if dir2 == 'nemu': return cls.MuMuPlayer elif dir2 == 'nemu9': return cls.MuMuPlayerX else: return cls.MuMuPlayer - if exe == 'MuMuPlayer.exe': + if exe == 'mumuplayer.exe': return cls.MuMuPlayer12 - if exe == 'MEmu.exe': + if exe == 'memu.exe': return cls.MEmuPlayer return '' @@ -143,7 +147,9 @@ class Emulator(EmulatorBase): elif 'NemuMultiPlayer.exe' in exe: yield exe.replace('NemuMultiPlayer.exe', 'NemuPlayer.exe') elif 'MuMuMultiPlayer.exe' in exe: - yield exe.replace('MuMuMultiPlayer.exe', 'MuMuManager.exe') + yield exe.replace('MuMuMultiPlayer.exe', 'MuMuPlayer.exe') + elif 'MuMuManager.exe' in exe: + yield exe.replace('MuMuManager.exe', 'MuMuPlayer.exe') elif 'MEmuConsole.exe' in exe: yield exe.replace('MEmuConsole.exe', 'MEmu.exe') else: @@ -316,7 +322,7 @@ class EmulatorManager(EmulatorManagerBase): Get recently executed programs in UserAssist https://github.com/forensicmatt/MonitorUserAssist - Returns: + Yields: str: Path to emulator executables, may contains duplicate values """ path = r'Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist' @@ -447,6 +453,31 @@ class EmulatorManager(EmulatorManagerBase): uninstall = res.group(1) if res else uninstall yield uninstall + @staticmethod + def iter_running_emulator(): + """ + Yields: + str: Path to emulator executables, may contains duplicate values + """ + try: + import psutil + except ModuleNotFoundError: + return + # Since this is a one-time-usage, we access psutil._psplatform.Process directly + # to bypass the call of psutil.Process.is_running(). + # This only costs about 0.017s. + for pid in psutil.pids(): + proc = psutil._psplatform.Process(pid) + try: + exe = proc.cmdline() + exe = exe[0].replace(r'\\', '/').replace('\\', '/') + except (psutil.AccessDenied, IndexError): + # psutil.AccessDenied + continue + + if Emulator.is_emulator(exe): + yield exe + @cached_property def all_emulators(self) -> t.List[Emulator]: """ @@ -474,7 +505,7 @@ class EmulatorManager(EmulatorManagerBase): exe.add(ld) # Uninstall registry - for uninstall in self.iter_uninstall_registry(): + for uninstall in EmulatorManager.iter_uninstall_registry(): # Find emulator executable from uninstaller for file in iter_folder(abspath(os.path.dirname(uninstall)), ext='.exe'): if Emulator.is_emulator(file) and os.path.exists(file): @@ -488,9 +519,14 @@ class EmulatorManager(EmulatorManagerBase): if Emulator.is_emulator(file) and os.path.exists(file): exe.add(file) + # Running + for file in EmulatorManager.iter_running_emulator(): + if os.path.exists(file): + exe.add(file) + + # De-redundancy exe = [Emulator(path).path for path in exe if Emulator.is_emulator(path)] - exe = sorted(set(exe)) - exe = [Emulator(path) for path in exe] + exe = [Emulator(path) for path in remove_duplicated_path(exe)] return exe @cached_property diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index e48da238c..02aba6698 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -6,7 +6,8 @@ from pydantic import BaseModel from module.base.decorator import cached_property, del_cached_property from module.base.utils import SelectedGrids from module.device.connection import Connection -from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase +from module.device.method.utils import get_serial_pair +from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase, remove_duplicated_path from module.logger import logger @@ -47,8 +48,20 @@ class PlatformBase(Connection, EmulatorManagerBase): @cached_property def emulator_info(self) -> EmulatorInfo: emulator = self.config.EmulatorInfo_Emulator - name = str(self.config.EmulatorInfo_name).strip().replace('\n', '') - path = str(self.config.EmulatorInfo_path).strip().replace('\n', '') + if emulator == 'auto': + emulator = '' + + def parse_info(value): + if isinstance(value, str): + value = value.strip().replace('\n', '') + if value in ['None', 'False', 'True']: + value = '' + return value + else: + return '' + + name = parse_info(self.config.EmulatorInfo_name) + path = parse_info(self.config.EmulatorInfo_path) return EmulatorInfo( emulator=emulator, @@ -68,8 +81,14 @@ class PlatformBase(Connection, EmulatorManagerBase): path=data.path, name=data.name, ) + # Redirect emulator-5554 to 127.0.0.1:5555 + serial = self.serial + port_serial, _ = get_serial_pair(self.serial) + if port_serial is not None: + serial = port_serial + instance = self.find_emulator_instance( - serial=str(self.config.Emulator_Serial).strip(), + serial=serial, name=data.name, path=data.path, emulator=data.emulator, @@ -117,7 +136,7 @@ class PlatformBase(Connection, EmulatorManagerBase): # Search by serial select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instance with {search_args}') + logger.warning(f'No emulator instance with {search_args}, serial invalid') return None if select.count == 1: instance = select[0] @@ -130,9 +149,9 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['name'] = name select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, name invalid') + search_args.pop('name') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -143,9 +162,9 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['path'] = path select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, path invalid') + search_args.pop('path') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -156,9 +175,28 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['type'] = emulator select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, type invalid') + search_args.pop('type') + elif select.count == 1: + instance = select[0] + logger.hr('Emulator instance', level=2) + logger.info(f'Found emulator instance: {instance}') + return instance + + # Still too many instances, search from running emulators + running = remove_duplicated_path(list(self.iter_running_emulator())) + logger.info('Running emulators') + for exe in running: + logger.info(exe) + if len(running) == 1: + logger.info('Only one running emulator') + # Same as searching path + search_args['path'] = running[0] + select = instances.select(**search_args) + if select.count == 0: + logger.warning(f'No emulator instances with {search_args}, path invalid') + search_args.pop('path') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -167,9 +205,3 @@ class PlatformBase(Connection, EmulatorManagerBase): # Still too many instances logger.warning(f'Found multiple emulator instances with {search_args}') return None - - -if __name__ == '__main__': - self = PlatformBase('alas') - d = self.emulator_instance - print(d) diff --git a/module/device/screenshot.py b/module/device/screenshot.py index 9101a1d3f..2dcd6fe88 100644 --- a/module/device/screenshot.py +++ b/module/device/screenshot.py @@ -13,13 +13,14 @@ from module.base.utils import get_color, image_size, limit_in, save_image from module.device.method.adb import Adb from module.device.method.ascreencap import AScreenCap from module.device.method.droidcast import DroidCast +from module.device.method.nemu_ipc import NemuIpc from module.device.method.scrcpy import Scrcpy from module.device.method.wsa import WSA from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger -class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): +class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy, NemuIpc): _screen_size_checked = False _screen_black_checked = False _minicap_uninstalled = False @@ -38,6 +39,7 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): 'DroidCast': self.screenshot_droidcast, 'DroidCast_raw': self.screenshot_droidcast_raw, 'scrcpy': self.screenshot_scrcpy, + 'nemu_ipc': self.screenshot_nemu_ipc, } def screenshot(self): @@ -70,6 +72,10 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): return self.image + @property + def has_cached_image(self): + return hasattr(self, 'image') and self.image is not None + def _handle_orientated_image(self, image): """ Args: @@ -159,6 +165,9 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy): if interval != origin: logger.warning(f'Optimization.ScreenshotInterval {origin} is revised to {interval}') self.config.Optimization_ScreenshotInterval = interval + # Allow nemu_ipc to have a lower default + if self.config.Emulator_ScreenshotMethod == 'nemu_ipc': + interval = limit_in(origin, 0.1, 0.2) elif interval == 'combat': origin = self.config.Optimization_CombatScreenshotInterval interval = limit_in(origin, 0.3, 1.0) diff --git a/module/notify.py b/module/notify.py deleted file mode 100644 index 4f8b470ac..000000000 --- a/module/notify.py +++ /dev/null @@ -1,4 +0,0 @@ -from module.logger import logger - -def handle_notify(*args, **kwargs): - logger.error('Error notify is not supported yet') diff --git a/module/notify/__init__.py b/module/notify/__init__.py new file mode 100644 index 000000000..58df2006e --- /dev/null +++ b/module/notify/__init__.py @@ -0,0 +1,4 @@ +def handle_notify(*args, **kwargs): + # Lazy import onepush + from module.notify.notify import handle_notify + return handle_notify(*args, **kwargs) diff --git a/module/notify/notify.py b/module/notify/notify.py new file mode 100644 index 000000000..9186e5c4a --- /dev/null +++ b/module/notify/notify.py @@ -0,0 +1,75 @@ +import onepush.core +import yaml +from onepush import get_notifier +from onepush.core import Provider +from onepush.exceptions import OnePushException +from onepush.providers.custom import Custom +from requests import Response + +from module.logger import logger + +onepush.core.log = logger + + +def handle_notify(_config: str, **kwargs) -> bool: + try: + config = {} + for item in yaml.safe_load_all(_config): + config.update(item) + except Exception: + logger.error("Fail to load onepush config, skip sending") + return False + try: + provider_name: str = config.pop("provider", None) + if provider_name is None: + logger.info("No provider specified, skip sending") + return False + notifier: Provider = get_notifier(provider_name) + required: list[str] = notifier.params["required"] + config.update(kwargs) + + # pre check + for key in required: + if key not in config: + logger.warning( + f"Notifier {notifier.name} require param '{key}' but not provided" + ) + + if isinstance(notifier, Custom): + if "method" not in config or config["method"] == "post": + config["datatype"] = "json" + if not ("data" in config or isinstance(config["data"], dict)): + config["data"] = {} + if "title" in kwargs: + config["data"]["title"] = kwargs["title"] + if "content" in kwargs: + config["data"]["content"] = kwargs["content"] + + if provider_name.lower() == "gocqhttp": + access_token = config.get("access_token") + if access_token: + config["token"] = access_token + + resp = notifier.notify(**config) + if isinstance(resp, Response): + if resp.status_code != 200: + logger.warning("Push notify failed!") + logger.warning(f"HTTP Code:{resp.status_code}") + return False + else: + if provider_name.lower() == "gocqhttp": + return_data: dict = resp.json() + if return_data["status"] == "failed": + logger.warning("Push notify failed!") + logger.warning( + f"Return message:{return_data['wording']}") + return False + except OnePushException: + logger.exception("Push notify failed") + return False + except Exception as e: + logger.exception(e) + return False + + logger.info("Push notify success") + return True \ No newline at end of file diff --git a/module/ocr/keyword.py b/module/ocr/keyword.py index 3593b1d46..31bb8cdc3 100644 --- a/module/ocr/keyword.py +++ b/module/ocr/keyword.py @@ -7,7 +7,7 @@ import module.config.server as server from module.exception import ScriptError # ord('.') = 65294 -REGEX_PUNCTUATION = re.compile(r'[ ,..\'"“”,。…::;;!!??·・•●〇°*※\-—-/\\\n\t()\[\]()「」『』【】《》[]]') +REGEX_PUNCTUATION = re.compile(r'[ ,..\'"“”,。…::;;!!??·・•●〇°*※\-—–-/\\|丨\n\t()\[\]()「」『』【】《》[]]') def parse_name(n): diff --git a/module/ocr/ocr.py b/module/ocr/ocr.py index 82fb32d39..2fa29aa03 100644 --- a/module/ocr/ocr.py +++ b/module/ocr/ocr.py @@ -422,9 +422,12 @@ class Duration(Ocr): class OcrWhiteLetterOnComplexBackground(Ocr): + white_preprocess = True + def pre_process(self, image): - image = extract_white_letters(image, threshold=255) - image = cv2.merge([image, image, image]) + if self.white_preprocess: + image = extract_white_letters(image, threshold=255) + image = cv2.merge([image, image, image]) return image def detect_and_ocr(self, *args, **kwargs): diff --git a/module/ui/draggable_list.py b/module/ui/draggable_list.py index 7f8f46c92..b35616eae 100644 --- a/module/ui/draggable_list.py +++ b/module/ui/draggable_list.py @@ -160,6 +160,7 @@ class DraggableList: logger.info(f'Insight row: {row}, index={row_index}') last_buttons: set[OcrResultButton] = None + bottom_check = Timer(3, count=5).start() while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -183,8 +184,11 @@ class DraggableList: 0, count=0), timeout=Timer(1.5, count=5)) skip_first_screenshot = True if self.cur_buttons and last_buttons == set(self.cur_buttons): - logger.warning(f'No more rows in {self}') - return False + if bottom_check.reached(): + logger.warning(f'No more rows in {self}') + return False + else: + bottom_check.reset() last_buttons = set(self.cur_buttons) return True diff --git a/module/webui/config.py b/module/webui/config.py index fd4d6ef12..0df5f0527 100644 --- a/module/webui/config.py +++ b/module/webui/config.py @@ -37,6 +37,8 @@ class DeployConfig(_DeployConfig): if hasattr(self, key): super().__setattr__(key, value) + self.config_redirect() + def write(self): """ Write `self.config` into deploy config. diff --git a/module/webui/widgets.py b/module/webui/widgets.py index ef729a882..97fe7aad4 100644 --- a/module/webui/widgets.py +++ b/module/webui/widgets.py @@ -325,11 +325,17 @@ def put_arg_input(kwargs: T_Output_Kwargs) -> Output: ) -def product_stored_row(kwargs: T_Output_Kwargs, key, value): - kwargs = copy.copy(kwargs) - kwargs["name"] += f'_{key}' - kwargs["value"] = value - return put_input(**kwargs).style("--input--") +def product_stored_row(key, value): + if key[-1].isdigit(): + # quest1, quest2, quest3 + return [put_text(value).style("--dashboard-time--")] + else: + # calyx, relic + # 3 (relic) + return [ + put_text(value).style("--dashboard-value--"), + put_text(f" ({key})").style("--dashboard-time--"), + ] def put_arg_stored(kwargs: T_Output_Kwargs) -> Output: @@ -337,11 +343,32 @@ def put_arg_stored(kwargs: T_Output_Kwargs) -> Output: kwargs["disabled"] = True values = kwargs.pop("value", {}) + value = values.pop("value", "") + total = values.pop("total", "") time_ = values.pop("time", "") - rows = [product_stored_row(kwargs, key, value) for key, value in values.items() if value] + if value != "" and total != "": + rows = [put_scope(f"dashboard-value-{name}", [ + put_text(value).style("--dashboard-value--"), + put_text(f" / {total}").style("--dashboard-time--"), + ])] + elif value != "": + rows = [put_scope(f"dashboard-value-{name}", [ + put_text(value).style("--dashboard-value--") + ])] + else: + rows = [] + if values: + rows += [ + put_scope(f"dashboard-value-{name}-{key}", product_stored_row(key, value)) + for key, value in values.items() if value != "" + ] + if time_: - rows += [product_stored_row(kwargs, "time", time_)] + rows.append( + put_text(time_).style("--dashboard-time--") + ) + return put_scope( f"arg_container-stored-{name}", [ diff --git a/requirements-in.txt b/requirements-in.txt index a518216e4..656bef808 100644 --- a/requirements-in.txt +++ b/requirements-in.txt @@ -21,6 +21,7 @@ pyyaml inflection prettytable==2.2.1 pydantic>=2.4 +onepush==1.3.0 # OCR pponnxcr==2.0 diff --git a/requirements.txt b/requirements.txt index 21042b646..0c79b502b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,6 +37,7 @@ markdown-it-py==2.2.0 # via rich mdurl==0.1.2 # via markdown-it-py mpmath==1.3.0 # via sympy numpy==1.24.3 # via -r requirements-in.txt, onnxruntime, opencv-python, pponnxcr, scipy, shapely +onepush==1.3.0 # via -r requirements-in.txt onnxruntime==1.14.1 # via pponnxcr opencv-python==4.7.0.72 # via -r requirements-in.txt, pponnxcr packaging==20.9 # via deprecation, onnxruntime, uiautomator2 @@ -48,6 +49,7 @@ protobuf==4.23.0 # via onnxruntime psutil==5.9.3 # via -r requirements-in.txt py==1.11.0 # via retry pyclipper==1.3.0.post4 # via pponnxcr +pycryptodome==3.20.0 # via onepush pydantic==2.4.2 # via -r requirements-in.txt pydantic-core==2.10.1 # via pydantic pyelftools==0.29 # via apkutils2 @@ -58,7 +60,7 @@ pyreadline3==3.4.1 # via humanfriendly python-dotenv==1.0.0 # via uvicorn pywebio==1.8.3 # via -r requirements-in.txt pyyaml==6.0 # via -r requirements-in.txt, uvicorn -requests==2.30.0 # via adbutils, uiautomator2 +requests==2.30.0 # via adbutils, onepush, uiautomator2 retry==0.9.2 # via adbutils, uiautomator2 rich==13.3.5 # via -r requirements-in.txt scipy==1.10.1 # via -r requirements-in.txt diff --git a/route/daily/HimekoTrial.py b/route/daily/HimekoTrial.py index d5f77b433..8aee25967 100644 --- a/route/daily/HimekoTrial.py +++ b/route/daily/HimekoTrial.py @@ -16,7 +16,13 @@ class Route(RouteBase, Combat, CharacterTrial): def wait_next_skill(self, expected_end=None, skip_first_screenshot=True): # Ended at START_TRIAL def combat_end(): - return self.match_template_color(START_TRIAL) + if self.match_template_color(START_TRIAL): + logger.info('Trial ended at START_TRIAL') + return True + if self.is_in_main(): + logger.warning('Trial ended at is_in_main()') + return True + return False return super().wait_next_skill(expected_end=combat_end, skip_first_screenshot=skip_first_screenshot) diff --git a/route/rogue/Combat/Herta_SupplyZone_F2.py b/route/rogue/Combat/Herta_SupplyZone_F2.py index 95a9e606e..46357df1f 100644 --- a/route/rogue/Combat/Herta_SupplyZone_F2.py +++ b/route/rogue/Combat/Herta_SupplyZone_F2.py @@ -15,6 +15,7 @@ class Route(RouteBase): | enemy1 | Waypoint((46.2, 328.2)), | 12.6 | 8 | | item2 | Waypoint((42.4, 299.0)), | 352.8 | 348 | | door2 | Waypoint((46.4, 284.5)), | 4.2 | 1 | + | door2end | Waypoint((47.2, 274.8)), | 11.1 | 4 | | enemy2left | Waypoint((31.2, 248.8)), | 183.8 | 84 | | enemy2right | Waypoint((55.2, 247.2)), | 96.7 | 91 | | item3 | Waypoint((68.5, 226.5)), | 30.2 | 29 | @@ -32,6 +33,7 @@ class Route(RouteBase): enemy1 = Waypoint((46.2, 328.2)) item2 = Waypoint((42.4, 299.0)) door2 = Waypoint((46.4, 284.5)) + door2end = Waypoint((47.2, 274.8)) enemy2left = Waypoint((31.2, 248.8)) enemy2right = Waypoint((55.2, 247.2)) item3 = Waypoint((68.5, 226.5)) @@ -47,6 +49,7 @@ class Route(RouteBase): # self.clear_item(item2) self.clear_enemy( door2.set_threshold(3), + door2end.set_threshold(3), # Go through door enemy2left, enemy2right.straight_run(), @@ -58,6 +61,7 @@ class Route(RouteBase): self.clear_enemy( enemy3.straight_run(), ) + # ('Combat_Herta_SupplyZone_F2_X45Y369', 0.243, (57.2, 351.6)) def Herta_SupplyZone_F2_X397Y233(self): """ diff --git a/route/rogue/Combat/Jarilo_BackwaterPass_F1.py b/route/rogue/Combat/Jarilo_BackwaterPass_F1.py index 12db93cf2..a3af0a87d 100644 --- a/route/rogue/Combat/Jarilo_BackwaterPass_F1.py +++ b/route/rogue/Combat/Jarilo_BackwaterPass_F1.py @@ -63,7 +63,7 @@ class Route(RouteBase): ) # 2 self.clear_enemy( - node2, + node2.set_threshold(3), enemy2.straight_run(), ) # 3 @@ -116,6 +116,43 @@ class Route(RouteBase): self.clear_enemy(enemy2left.straight_run()) self.clear_enemy(enemy3.straight_run()) + def Jarilo_BackwaterPass_F1_X503Y736(self): + """ + | Waypoint | Position | Direction | Rotation | + | ---------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((507.2, 733.7)), | 6.7 | 4 | + | enemy1 | Waypoint((507.0, 644.0)), | 12.6 | 6 | + | enemy2left | Waypoint((536.0, 630.5)), | 48.1 | 43 | + | enemy3 | Waypoint((557.0, 585.2)), | 114.1 | 6 | + | exit_ | Waypoint((557.0, 585.2)), | 114.1 | 6 | + | exit1 | Waypoint((549.5, 575.4)), | 356.2 | 354 | + | exit2 | Waypoint((565.4, 575.6)), | 4.1 | 359 | + """ + self.map_init(plane=Jarilo_BackwaterPass, floor="F1", position=(503.2, 736.9)) + self.register_domain_exit( + Waypoint((557.0, 585.2)), end_rotation=6, + left_door=Waypoint((549.5, 575.4)), right_door=Waypoint((565.4, 575.6))) + enemy1 = Waypoint((507.0, 644.0)) + enemy2left = Waypoint((536.0, 630.5)) + enemy3 = Waypoint((557.0, 585.2)) + # ===== End of generated waypoints ===== + + self.clear_enemy(enemy1) + self.clear_enemy(enemy2left.straight_run()) + self.clear_enemy(enemy3.straight_run()) + + """ + Notes + Herta_SupplyZone_F2_X397Y239 is the same as Herta_SupplyZone_F2_X397Y233 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Combat_Jarilo_BackwaterPass_F1_X507Y733', 0.26, (503.2, 736.9)), + # ('Combat_Luofu_ArtisanshipCommission_F1_X41Y640', 0.18, (50.7, 644.4)), + # ('Combat_Luofu_DivinationCommission_F1_X737Y372', 0.174, (717.2, 355.4)), + # ('Combat_Herta_SupplyZone_F2_X45Y369', 0.168, (46.5, 370.0)) + # ] + def Jarilo_BackwaterPass_F1_X555Y643(self): """ | Waypoint | Position | Direction | Rotation | diff --git a/route/rogue/Combat/Jarilo_CorridorofFadingEchoes_F1.py b/route/rogue/Combat/Jarilo_CorridorofFadingEchoes_F1.py index 562f5c057..080668a9e 100644 --- a/route/rogue/Combat/Jarilo_CorridorofFadingEchoes_F1.py +++ b/route/rogue/Combat/Jarilo_CorridorofFadingEchoes_F1.py @@ -1,3 +1,4 @@ +from module.logger import logger from tasks.map.control.waypoint import Waypoint from tasks.map.keywords.plane import Jarilo_CorridorofFadingEchoes from tasks.rogue.route.base import RouteBase @@ -30,6 +31,7 @@ class Route(RouteBase): | ----------- | -------------------------- | --------- | -------- | | spawn | Waypoint((201.2, 1071.4)), | 6.7 | 4 | | enemy1right | Waypoint((200.3, 1032.4)), | 342.0 | 343 | + | node1 | Waypoint((194.6, 1023.4)), | 109.3 | 294 | | enemy1left | Waypoint((168.6, 1022.3)), | 279.8 | 89 | | node2 | Waypoint((118.4, 1019.0)), | 282.9 | 285 | | enemy2left | Waypoint((105.2, 1012.0)), | 317.9 | 315 | @@ -45,6 +47,7 @@ class Route(RouteBase): Waypoint((103.4, 919.2)), end_rotation=4, left_door=Waypoint((98.8, 908.9)), right_door=Waypoint((111.4, 909.8))) enemy1right = Waypoint((200.3, 1032.4)) + node1 = Waypoint((194.6, 1023.4)) enemy1left = Waypoint((168.6, 1022.3)) node2 = Waypoint((118.4, 1019.0)) enemy2left = Waypoint((105.2, 1012.0)) @@ -56,20 +59,22 @@ class Route(RouteBase): # 1 self.rotation_set(315) self.clear_enemy( - enemy1right.set_threshold(5), - enemy1left.set_threshold(5), + enemy1right.set_threshold(3), + node1.set_threshold(3), + enemy1left.set_threshold(3), ) # 2 self.clear_enemy( - enemy1left.set_threshold(5), + enemy1left.set_threshold(3), node2.set_threshold(5), enemy2left, enemy2right, ) # 3 + self.rotation_set(0) self.clear_enemy( - node3.set_threshold(5), - enemy3.straight_run(), + node3.set_threshold(3), + enemy3, ) def Jarilo_CorridorofFadingEchoes_F1_X266Y457(self): @@ -167,12 +172,23 @@ class Route(RouteBase): enemy2right.straight_run(), enemy2left.straight_run().set_threshold(5), ) - self.clear_enemy( - enemy2left.set_threshold(5), - node3.straight_run(), - node4.set_threshold(3).straight_run(), - enemy4.straight_run(), - ) + if self.minimap.is_position_near(enemy2left.position, threshold=30): + logger.info('Near enemy2right') + self.clear_enemy( + enemy2left.set_threshold(5), + node3.straight_run(), + node4.set_threshold(3).straight_run(), + enemy4.straight_run(), + ) + else: + logger.info('Not near enemy2right') + self.clear_enemy( + enemy2right.set_threshold(5), + enemy2left.set_threshold(5), + node3.straight_run(), + node4.set_threshold(3).straight_run(), + enemy4.straight_run(), + ) def Jarilo_CorridorofFadingEchoes_F1_X437Y122(self): """ diff --git a/route/rogue/Combat/Luofu_ArtisanshipCommission_F1.py b/route/rogue/Combat/Luofu_ArtisanshipCommission_F1.py index 54ee2e7f3..1be60db11 100644 --- a/route/rogue/Combat/Luofu_ArtisanshipCommission_F1.py +++ b/route/rogue/Combat/Luofu_ArtisanshipCommission_F1.py @@ -105,6 +105,51 @@ class Route(RouteBase): if self.minimap.position_diff(enemy3.position) > 25: self.clear_enemy(enemy3) + def Luofu_ArtisanshipCommission_F1_X481Y920(self): + """ + | Waypoint | Position | Direction | Rotation | + | ----------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((473.5, 920.9)), | 4.5 | 4 | + | enemy1left | Waypoint((475.0, 848.4)), | 4.4 | 4 | + | enemy2right | Waypoint((493.5, 807.4)), | 157.1 | 48 | + | enemy3 | Waypoint((528.9, 782.9)), | 198.5 | 91 | + | exit_ | Waypoint((528.9, 782.9)), | 198.5 | 91 | + | exit1 | Waypoint((537.0, 773.2)), | 99.0 | 89 | + | exit2 | Waypoint((537.5, 790.6)), | 101.1 | 91 | + """ + self.map_init(plane=Luofu_ArtisanshipCommission, floor="F1", position=(481.5, 920.9)) + self.register_domain_exit( + Waypoint((528.9, 782.9)), end_rotation=91, + left_door=Waypoint((537.0, 773.2)), right_door=Waypoint((537.5, 790.6))) + enemy1left = Waypoint((475.0, 848.4)) + enemy2right = Waypoint((493.5, 807.4)) + enemy3 = Waypoint((528.9, 782.9)) + # ===== End of generated waypoints ===== + + self.rotation_set(30) + self.clear_enemy( + enemy1left, + enemy2right, + ) + self.clear_enemy(enemy3) + if self.minimap.position_diff(enemy3.position) > 25: + self.clear_enemy(enemy3) + + """ + Notes + Luofu_ArtisanshipCommission_F1_X481Y920 is the same as Luofu_ArtisanshipCommission_F1_X473Y920 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Combat_Luofu_ArtisanshipCommission_F1_X473Y920', 0.168, (481.0, 920.9)), + # ('Combat_Luofu_ArtisanshipCommission_F1_X41Y640', 0.163, (40.8, 664.0)), + # ('Combat_Luofu_ArtisanshipCommission_F1_X667Y189', 0.14, (705.0, 193.3)) + # ] + # Best 3 nearby predictions: [ + # ('Combat_Herta_SupplyZone_F2_X45Y369', 0.128, (47.6, 369.4)), + # ('Combat_Herta_SupplyZone_F2Rogue_X219Y112', 0.093, (219.6, 108.7)) + # ] + def Luofu_ArtisanshipCommission_F1_X543Y269(self): """ | Waypoint | Position | Direction | Rotation | diff --git a/route/rogue/Combat/Luofu_Cloudford_F1.py b/route/rogue/Combat/Luofu_Cloudford_F1.py index a8c5fde6e..e320e3973 100644 --- a/route/rogue/Combat/Luofu_Cloudford_F1.py +++ b/route/rogue/Combat/Luofu_Cloudford_F1.py @@ -256,3 +256,58 @@ class Route(RouteBase): ) self.clear_item(item4) self.clear_enemy(enemy4) + + def Luofu_Cloudford_F1_X432Y685(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((435.4, 669.2)), | 6.7 | 4 | + | item1 | Waypoint((432.2, 628.3)), | 2.7 | 357 | + | enemy1 | Waypoint((428.6, 598.8)), | 8.0 | 177 | + | node2 | Waypoint((421.2, 590.8)), | 44.2 | 285 | + | node3 | Waypoint((366.6, 588.2)), | 274.2 | 274 | + | enemy3 | Waypoint((344.9, 590.4)), | 191.8 | 357 | + | item4 | Waypoint((309.6, 580.2)), | 290.1 | 281 | + | enemy4 | Waypoint((271.3, 585.5)), | 285.0 | 274 | + | exit_ | Waypoint((271.3, 585.5)), | 285.0 | 274 | + | exit1 | Waypoint((267.9, 592.3)), | 275.9 | 274 | + | exit2 | Waypoint((267.8, 580.0)), | 275.8 | 274 | + """ + self.map_init(plane=Luofu_Cloudford, floor="F1", position=(432.8, 685.1)) + self.register_domain_exit( + Waypoint((271.3, 585.5)), end_rotation=274, + left_door=Waypoint((267.9, 592.3)), right_door=Waypoint((267.8, 580.0))) + item1 = Waypoint((432.2, 628.3)) + enemy1 = Waypoint((428.6, 598.8)) + node2 = Waypoint((421.2, 590.8)) + node3 = Waypoint((366.6, 588.2)) + enemy3 = Waypoint((344.9, 590.4)) + item4 = Waypoint((309.6, 580.2)) + enemy4 = Waypoint((271.3, 585.5)) + # ===== End of generated waypoints ===== + + self.clear_item(item1) + self.clear_enemy(enemy1) + # Go through bridges + self.rotation_set(270) + self.minimap.lock_rotation(270) + self.clear_enemy( + node2.set_threshold(3), + node3.set_threshold(3), + enemy3, + ) + self.clear_item(item4) + self.clear_enemy(enemy4) + + """ + Notes + Luofu_Cloudford_F1_X435Y685 is the same as Luofu_Cloudford_F1_X435Y669 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Combat_Luofu_Cloudford_F1_X433Y617', 0.195, (432.8, 668.4)), + # ('Combat_Herta_SupplyZone_F2_X45Y369', 0.18, (24.2, 372.2)), + # ('Combat_Luofu_Cloudford_F1_X435Y669', 0.18, (432.8, 685.1)) + # ] + # (432.9, 684.9) + # ('Combat_Luofu_Cloudford_F1_X435Y669', 0.172, (432.8, 685.0)) diff --git a/route/rogue/Combat/Luofu_Cloudford_F1Rogue.py b/route/rogue/Combat/Luofu_Cloudford_F1Rogue.py index e737dab7c..9b0f5e6b0 100644 --- a/route/rogue/Combat/Luofu_Cloudford_F1Rogue.py +++ b/route/rogue/Combat/Luofu_Cloudford_F1Rogue.py @@ -105,3 +105,62 @@ class Route(RouteBase): Luofu_Cloudford_F1Rogue_X49Y405 is the same as Luofu_Cloudford_F1Rogue_X59Y405 but for wrong spawn point detected """ + + def Luofu_Cloudford_F1Rogue_X44Y405(self): + """ + | Waypoint | Position | Direction | Rotation | + | ------------ | ------------------------- | --------- | -------- | + | spawn | Waypoint((59.3, 405.6)), | 96.7 | 91 | + | item1 | Waypoint((96.9, 393.0)), | 87.7 | 84 | + | enemy1 | Waypoint((126.2, 402.5)), | 96.8 | 101 | + | node2 | Waypoint((142.9, 413.0)), | 96.8 | 101 | + | enemy2top | Waypoint((214.6, 432.8)), | 94.1 | 87 | + | enemy2bottom | Waypoint((211.4, 483.3)), | 191.8 | 174 | + | enemy3 | Waypoint((288.0, 452.2)), | 87.7 | 260 | + | exit_ | Waypoint((291.8, 454.4)), | 5.7 | 91 | + | exit1 | Waypoint((295.0, 451.4)), | 96.7 | 89 | + | exit2 | Waypoint((296.0, 460.2)), | 96.9 | 89 | + """ + self.map_init(plane=Luofu_Cloudford, floor="F1Rogue", position=(43.8, 405.0)) + self.register_domain_exit( + Waypoint((291.8, 454.4)), end_rotation=91, + left_door=Waypoint((295.0, 451.4)), right_door=Waypoint((296.0, 460.2))) + item1 = Waypoint((96.9, 393.0)) + enemy1 = Waypoint((126.2, 402.5)) + node2 = Waypoint((142.9, 413.0)) + enemy2top = Waypoint((214.6, 432.8)) + enemy2bottom = Waypoint((211.4, 483.3)) + enemy3 = Waypoint((288.0, 452.2)) + # ===== End of generated waypoints ===== + + self.rotation_set(120) + self.minimap.lock_rotation(120) + # 1, ignore item1, which position may cause detection error + # self.clear_item(item1) + self.clear_enemy(enemy1) + # 2 moving enemy + # Ignore enemy2, it might be a pig, you can never catch it. + # self.clear_enemy( + # enemy2top, + # enemy2bottom.straight_run(), + # ) + # 3 + self.clear_enemy( + node2.set_threshold(3), + enemy3, + ) + if self.minimap.position_diff(enemy3.position) > 60: + logger.info('Cleared an enemy but have not reached enemy3') + self.clear_enemy(enemy3) + + """ + Notes + Luofu_Cloudford_F1Rogue_X44Y405 is the same as Luofu_Cloudford_F1Rogue_X59Y405 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Combat_Luofu_Cloudford_F1Rogue_X59Y405', 0.349, (43.8, 405.0)), + # ('Combat_Luofu_Cloudford_F1Rogue_X49Y405', 0.349, (43.8, 405.0)), + # ('Combat_Herta_SupplyZone_F2_X594Y247', 0.262, (641.3, 250.5)) + # ] + # Best 3 nearby predictions: [('Combat_Jarilo_RivetTown_F1_X181Y439', 0.109, (178.5, 435.4))] diff --git a/route/rogue/Combat/Luofu_StargazerNavalia_F2.py b/route/rogue/Combat/Luofu_StargazerNavalia_F2.py index 597304409..90af3c630 100644 --- a/route/rogue/Combat/Luofu_StargazerNavalia_F2.py +++ b/route/rogue/Combat/Luofu_StargazerNavalia_F2.py @@ -70,6 +70,7 @@ class Route(RouteBase): # 1, enemy first self.clear_enemy(enemy1) - self.clear_item(item1) + # item1 is cleared on the way to enemy2, or will get stuck at corner + # self.clear_item(item1) # 2, ignore item2, bad way self.clear_enemy(enemy2) diff --git a/route/rogue/Elite/Jarilo_CorridorofFadingEchoes_F1.py b/route/rogue/Elite/Jarilo_CorridorofFadingEchoes_F1.py index db8fb74cc..9438b33b1 100644 --- a/route/rogue/Elite/Jarilo_CorridorofFadingEchoes_F1.py +++ b/route/rogue/Elite/Jarilo_CorridorofFadingEchoes_F1.py @@ -30,6 +30,36 @@ class Route(RouteBase): but for wrong spawn point detected """ + def Jarilo_CorridorofFadingEchoes_F1_X415Y953(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((415.5, 947.9)), | 96.7 | 91 | + | enemy | Waypoint((464.0, 953.0)), | 96.8 | 94 | + | reward | Waypoint((472.7, 958.5)), | 214.6 | 114 | + | exit_ | Waypoint((480.0, 944.0)), | 92.7 | 84 | + """ + self.map_init(plane=Jarilo_CorridorofFadingEchoes, floor="F1", position=(415.4, 953.3)) + enemy = Waypoint((464.0, 953.0)) + reward = Waypoint((472.7, 958.5)) + exit_ = Waypoint((480.0, 944.0)) + + self.clear_elite(enemy) + self.domain_reward(reward) + self.domain_single_exit(exit_) + # ===== End of generated waypoints ===== + + """ + Notes + Jarilo_CorridorofFadingEchoes_F1_X415Y953 is the same as Jarilo_CorridorofFadingEchoes_F1_X415Y947 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y933', 0.169, (415.4, 953.3)), + # ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y947', 0.169, (415.4, 953.3)), + # ('Elite_Herta_SupplyZone_F2_X680Y247', 0.162, (738.4, 252.2)) + # ] + def Jarilo_CorridorofFadingEchoes_F1_X415Y947(self): """ | Waypoint | Position | Direction | Rotation | diff --git a/route/rogue/Elite/Luofu_ArtisanshipCommission_F1.py b/route/rogue/Elite/Luofu_ArtisanshipCommission_F1.py index c563530d0..d376ff325 100644 --- a/route/rogue/Elite/Luofu_ArtisanshipCommission_F1.py +++ b/route/rogue/Elite/Luofu_ArtisanshipCommission_F1.py @@ -24,6 +24,36 @@ class Route(RouteBase): self.domain_single_exit(exit_) # ===== End of generated waypoints ===== + def Luofu_ArtisanshipCommission_F1_X391Y493(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((385.2, 494.6)), | 94.2 | 91 | + | enemy | Waypoint((444.2, 490.5)), | 94.2 | 91 | + | reward | Waypoint((448.6, 497.2)), | 149.7 | 91 | + | exit_ | Waypoint((458.0, 483.7)), | 94.2 | 91 | + """ + self.map_init(plane=Luofu_ArtisanshipCommission, floor="F1", position=(391.1, 493.2)) + enemy = Waypoint((444.2, 490.5)) + reward = Waypoint((448.6, 497.2)) + exit_ = Waypoint((458.0, 483.7)) + + self.clear_elite(enemy) + self.domain_reward(reward) + self.domain_single_exit(exit_) + # ===== End of generated waypoints ===== + + """ + Notes + Luofu_ArtisanshipCommission_F1_X391Y493 is the same as Luofu_ArtisanshipCommission_F1_X385Y494 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Elite_Luofu_ArtisanshipCommission_F1_X385Y494', 0.182, (391.1, 493.2)), + # ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y933', 0.157, (364.0, 951.0)), + # ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y947', 0.157, (364.0, 951.0)) + # ] + def Luofu_ArtisanshipCommission_F1_X504Y493(self): """ | Waypoint | Position | Direction | Rotation | diff --git a/route/rogue/Elite/Luofu_Cloudford_F1.py b/route/rogue/Elite/Luofu_Cloudford_F1.py index 2ade4c4ab..6d9c7d26a 100644 --- a/route/rogue/Elite/Luofu_Cloudford_F1.py +++ b/route/rogue/Elite/Luofu_Cloudford_F1.py @@ -25,3 +25,34 @@ class Route(RouteBase): self.domain_reward(reward) self.domain_single_exit(exit_) # ===== End of generated waypoints ===== + + @locked_rotation(0) + def Luofu_Cloudford_F1_X342Y1003(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | -------------------------- | --------- | -------- | + | spawn | Waypoint((337.3, 1003.4)), | 6.7 | 4 | + | enemy | Waypoint((336.2, 962.2)), | 6.7 | 4 | + | reward | Waypoint((342.9, 950.8)), | 44.2 | 31 | + | exit_ | Waypoint((328.8, 942.8)), | 316.1 | 331 | + """ + self.map_init(plane=Luofu_Cloudford, floor="F1", position=(342.3, 1003.4)) + enemy = Waypoint((336.2, 962.2)) + reward = Waypoint((342.9, 950.8)) + exit_ = Waypoint((328.8, 942.8)) + + self.clear_elite(enemy) + self.domain_reward(reward) + self.domain_single_exit(exit_) + # ===== End of generated waypoints ===== + + """ + Notes + Luofu_Cloudford_F1_X342Y1003 is the same as Luofu_Cloudford_F1_X337Y1003 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Elite_Luofu_Cloudford_F1_X337Y1003', 0.169, (342.3, 1002.7)), + # ('Elite_Luofu_ArtisanshipCommission_F1_X504Y493', 0.106, (519.3, 452.7)), + # ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y933', 0.104, (433.8, 982.0)) + # ] diff --git a/route/rogue/Elite/Luofu_StargazerNavalia_F1.py b/route/rogue/Elite/Luofu_StargazerNavalia_F1.py index ca696d3aa..cc1d221f7 100644 --- a/route/rogue/Elite/Luofu_StargazerNavalia_F1.py +++ b/route/rogue/Elite/Luofu_StargazerNavalia_F1.py @@ -25,3 +25,34 @@ class Route(RouteBase): self.domain_reward(reward) self.domain_single_exit(exit_) # ===== End of generated waypoints ===== + + @locked_rotation(90) + def Luofu_StargazerNavalia_F1_X617Y511(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((617.5, 511.5)), | 96.7 | 91 | + | enemy | Waypoint((664.6, 512.6)), | 96.8 | 94 | + | reward | Waypoint((677.1, 521.2)), | 212.8 | 108 | + | exit_ | Waypoint((684.6, 505.0)), | 91.3 | 82 | + """ + self.map_init(plane=Luofu_StargazerNavalia, floor="F1", position=(617.5, 511.5)) + enemy = Waypoint((664.6, 512.6)) + reward = Waypoint((677.1, 521.2)) + exit_ = Waypoint((684.6, 505.0)) + + self.clear_elite(enemy) + self.domain_reward(reward) + self.domain_single_exit(exit_) + # ===== End of generated waypoints ===== + + """ + Notes + Herta_SupplyZone_F2_X397Y239 is the same as Herta_SupplyZone_F2_X397Y233 + but for wrong spawn point detected + """ + # Best 3 predictions: [ + # ('Elite_Luofu_StargazerNavalia_F1_X617Y511', 0.338, (621.0, 507.0)), + # ('Elite_Luofu_ArtisanshipCommission_F1_X385Y494', 0.203, (329.2, 492.8)), + # ('Elite_Jarilo_SilvermaneGuardRestrictedZone_F1_X225Y425', 0.181, (224.8, 423.2)) + # ] diff --git a/route/rogue/Occurrence/Jarilo_BackwaterPass_F1.py b/route/rogue/Occurrence/Jarilo_BackwaterPass_F1.py index 35d322a9b..75fd8c9d6 100644 --- a/route/rogue/Occurrence/Jarilo_BackwaterPass_F1.py +++ b/route/rogue/Occurrence/Jarilo_BackwaterPass_F1.py @@ -67,3 +67,35 @@ class Route(RouteBase): self.clear_item(item) self.clear_event(event) # ===== End of generated waypoints ===== + + def Jarilo_BackwaterPass_F1_X611Y761(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((613.3, 755.7)), | 319.8 | 318 | + | item | Waypoint((603.0, 734.6)), | 342.6 | 343 | + | event | Waypoint((586.8, 724.7)), | 318.0 | 315 | + | exit_ | Waypoint((576.9, 728.6)), | 126.2 | 304 | + | exit1 | Waypoint((567.0, 732.7)), | 311.8 | 306 | + | exit2 | Waypoint((576.2, 722.0)), | 308.1 | 306 | + """ + self.map_init(plane=Jarilo_BackwaterPass, floor="F1", position=(611.4, 761.2)) + self.register_domain_exit( + Waypoint((576.9, 728.6)), end_rotation=304, + left_door=Waypoint((567.0, 732.7)), right_door=Waypoint((576.2, 722.0))) + item = Waypoint((603.0, 734.6)) + event = Waypoint((586.8, 724.7)) + + self.clear_item(item) + self.clear_event(event) + # ===== End of generated waypoints ===== + # Best 3 predictions: [ + # ('Occurrence_Jarilo_BackwaterPass_F1_X613Y755', 0.203, (611.4, 761.2)), + # ('Occurrence_Herta_SupplyZone_F2Rogue_X397Y223', 0.148, (381.4, 207.5)), + # ('Occurrence_Herta_SupplyZone_F2Rogue_X397Y227', 0.148, (381.4, 207.5)) + # ] + """ + Notes + Jarilo_BackwaterPass_F1_X611Y761 is the same as Jarilo_BackwaterPass_F1_X613Y755 + but for wrong spawn point detected + """ diff --git a/route/rogue/Occurrence/Jarilo_SilvermaneGuardRestrictedZone_F1.py b/route/rogue/Occurrence/Jarilo_SilvermaneGuardRestrictedZone_F1.py index 53bd2c1ea..3e91c22c6 100644 --- a/route/rogue/Occurrence/Jarilo_SilvermaneGuardRestrictedZone_F1.py +++ b/route/rogue/Occurrence/Jarilo_SilvermaneGuardRestrictedZone_F1.py @@ -49,6 +49,38 @@ class Route(RouteBase): self.clear_event(event) # ===== End of generated waypoints ===== + def Jarilo_SilvermaneGuardRestrictedZone_F1_X435Y233(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((439.3, 237.1)), | 354.1 | 348 | + | item | Waypoint((440.8, 215.2)), | 15.6 | 11 | + | event | Waypoint((434.8, 192.4)), | 355.9 | 359 | + | exit_ | Waypoint((428.6, 190.4)), | 76.4 | 338 | + | exit1 | Waypoint((416.8, 184.4)), | 337.5 | 334 | + | exit2 | Waypoint((428.8, 180.4)), | 339.1 | 336 | + """ + self.map_init(plane=Jarilo_SilvermaneGuardRestrictedZone, floor="F1", position=(435.5, 233.5)) + self.register_domain_exit( + Waypoint((428.6, 190.4)), end_rotation=338, + left_door=Waypoint((416.8, 184.4)), right_door=Waypoint((428.8, 180.4))) + item = Waypoint((440.8, 215.2)) + event = Waypoint((434.8, 192.4)) + + self.clear_item(item) + self.clear_event(event) + # ===== End of generated waypoints ===== + # Best 3 predictions: [ + # ('Occurrence_Jarilo_SilvermaneGuardRestrictedZone_F1_X439Y237', 0.194, (435.5, 233.5)), + # ('Occurrence_Luofu_ScalegorgeWaterscape_F1_X619Y387', 0.118, (593.5, 412.0)), + # ('Occurrence_Luofu_Cloudford_F1_X244Y951', 0.098, (193.8, 931.7)) + # ] + """ + Notes + Jarilo_SilvermaneGuardRestrictedZone_F1_X435Y233 is the same as Jarilo_SilvermaneGuardRestrictedZone_F1_X439Y237 + but for wrong spawn point detected + """ + def Jarilo_SilvermaneGuardRestrictedZone_F1_X509Y541(self): """ | Waypoint | Position | Direction | Rotation | diff --git a/route/rogue/Occurrence/Luofu_Cloudford_F1.py b/route/rogue/Occurrence/Luofu_Cloudford_F1.py index c57a7ea90..5930d0de9 100644 --- a/route/rogue/Occurrence/Luofu_Cloudford_F1.py +++ b/route/rogue/Occurrence/Luofu_Cloudford_F1.py @@ -27,6 +27,32 @@ class Route(RouteBase): self.clear_event(event) # ===== End of generated waypoints ===== + @locked_rotation(270) + def Luofu_Cloudford_F1_X244Y951(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((241.4, 947.5)), | 274.2 | 274 | + | event | Waypoint((199.0, 940.8)), | 300.1 | 294 | + | exit_ | Waypoint((193.1, 947.2)), | 12.8 | 274 | + | exit1 | Waypoint((179.0, 956.4)), | 279.8 | 278 | + | exit2 | Waypoint((184.1, 940.2)), | 282.9 | 278 | + """ + self.map_init(plane=Luofu_Cloudford, floor="F1", position=(244, 951)) + self.register_domain_exit( + Waypoint((193.1, 947.2)), end_rotation=274, + left_door=Waypoint((179.0, 956.4)), right_door=Waypoint((184.1, 940.2))) + event = Waypoint((199.0, 940.8)) + + self.clear_event(event) + # ===== End of generated waypoints ===== + + """ + Notes + Luofu_Cloudford_F1_X244Y951 is the same as Luofu_Cloudford_F1_X241Y947 + but for wrong spawn point detected + """ + @locked_position @locked_rotation(0) def Luofu_Cloudford_F1_X281Y873(self): diff --git a/route/rogue/Occurrence/Luofu_StargazerNavalia_F1.py b/route/rogue/Occurrence/Luofu_StargazerNavalia_F1.py index 3e1af7a7a..fa8f6f8fd 100644 --- a/route/rogue/Occurrence/Luofu_StargazerNavalia_F1.py +++ b/route/rogue/Occurrence/Luofu_StargazerNavalia_F1.py @@ -28,3 +28,8 @@ class Route(RouteBase): self.clear_item(item_X504Y610) self.clear_event(event_X510Y626) # ===== End of generated waypoints ===== + + def clear_event(self, *waypoints): + # Too many clicks on A_BUTTON, so no items enroute in Luofu_StargazerNavalia_F1_X521Y595 + self.enroute_add_item = False + return super().clear_event(*waypoints) diff --git a/route/rogue/Respite/Herta_SupplyZone_F2.py b/route/rogue/Respite/Herta_SupplyZone_F2.py index 93668ee33..9fe8041f0 100644 --- a/route/rogue/Respite/Herta_SupplyZone_F2.py +++ b/route/rogue/Respite/Herta_SupplyZone_F2.py @@ -23,3 +23,29 @@ class Route(RouteBase): self.domain_herta(herta) self.domain_single_exit(exit_) # ===== End of generated waypoints ===== + + def Herta_SupplyZone_F2_X550Y248(self): + """ + | Waypoint | Position | Direction | Rotation | + | -------- | ------------------------- | --------- | -------- | + | spawn | Waypoint((554.6, 245.0)), | 274.2 | 274 | + | item | Waypoint((541.9, 238.4)), | 308.0 | 301 | + | herta | Waypoint((506.8, 238.4)), | 283.0 | 281 | + | exit_ | Waypoint((495.0, 244.8)), | 283.0 | 274 | + """ + self.map_init(plane=Herta_SupplyZone, floor="F2", position=(550.7, 248.7)) + item = Waypoint((541.9, 238.4)) + herta = Waypoint((506.8, 238.4)) + exit_ = Waypoint((495.0, 244.8)) + + self.clear_item(item) + self.domain_herta(herta) + self.domain_single_exit(exit_) + # ===== End of generated waypoints ===== + + # Best 3 predictions: [('Respite_Herta_SupplyZone_F2_X554Y245', 0.257, (550.7, 248.7)), + """ + Notes + Herta_SupplyZone_F2_X550Y248 is the same as Herta_SupplyZone_F2_X554Y245 + but for wrong spawn point detected + """ diff --git a/route/rogue/route.json b/route/rogue/route.json index 198c9be88..6f78834c5 100644 --- a/route/rogue/route.json +++ b/route/rogue/route.json @@ -241,6 +241,17 @@ ], "domain": "Combat" }, + { + "name": "Combat_Jarilo_BackwaterPass_F1_X503Y736", + "route": "route.rogue.Combat.Jarilo_BackwaterPass_F1:Jarilo_BackwaterPass_F1_X503Y736", + "plane": "Jarilo_BackwaterPass", + "floor": "F1", + "position": [ + 503.2, + 736.9 + ], + "domain": "Combat" + }, { "name": "Combat_Jarilo_BackwaterPass_F1_X555Y643", "route": "route.rogue.Combat.Jarilo_BackwaterPass_F1:Jarilo_BackwaterPass_F1_X555Y643", @@ -769,6 +780,17 @@ ], "domain": "Combat" }, + { + "name": "Combat_Luofu_ArtisanshipCommission_F1_X481Y920", + "route": "route.rogue.Combat.Luofu_ArtisanshipCommission_F1:Luofu_ArtisanshipCommission_F1_X481Y920", + "plane": "Luofu_ArtisanshipCommission", + "floor": "F1", + "position": [ + 481.5, + 920.9 + ], + "domain": "Combat" + }, { "name": "Combat_Luofu_ArtisanshipCommission_F1_X543Y269", "route": "route.rogue.Combat.Luofu_ArtisanshipCommission_F1:Luofu_ArtisanshipCommission_F1_X543Y269", @@ -890,6 +912,17 @@ ], "domain": "Combat" }, + { + "name": "Combat_Luofu_Cloudford_F1_X432Y685", + "route": "route.rogue.Combat.Luofu_Cloudford_F1:Luofu_Cloudford_F1_X432Y685", + "plane": "Luofu_Cloudford", + "floor": "F1", + "position": [ + 432.8, + 685.1 + ], + "domain": "Combat" + }, { "name": "Combat_Luofu_Cloudford_F1Rogue_X59Y405", "route": "route.rogue.Combat.Luofu_Cloudford_F1Rogue:Luofu_Cloudford_F1Rogue_X59Y405", @@ -912,6 +945,17 @@ ], "domain": "Combat" }, + { + "name": "Combat_Luofu_Cloudford_F1Rogue_X44Y405", + "route": "route.rogue.Combat.Luofu_Cloudford_F1Rogue:Luofu_Cloudford_F1Rogue_X44Y405", + "plane": "Luofu_Cloudford", + "floor": "F1Rogue", + "position": [ + 43.8, + 405.0 + ], + "domain": "Combat" + }, { "name": "Combat_Luofu_Cloudford_F2_X425Y111", "route": "route.rogue.Combat.Luofu_Cloudford_F2:Luofu_Cloudford_F2_X425Y111", @@ -1264,6 +1308,17 @@ ], "domain": "Elite" }, + { + "name": "Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y953", + "route": "route.rogue.Elite.Jarilo_CorridorofFadingEchoes_F1:Jarilo_CorridorofFadingEchoes_F1_X415Y953", + "plane": "Jarilo_CorridorofFadingEchoes", + "floor": "F1", + "position": [ + 415.4, + 953.3 + ], + "domain": "Elite" + }, { "name": "Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y947", "route": "route.rogue.Elite.Jarilo_CorridorofFadingEchoes_F1:Jarilo_CorridorofFadingEchoes_F1_X415Y947", @@ -1363,6 +1418,17 @@ ], "domain": "Elite" }, + { + "name": "Elite_Luofu_ArtisanshipCommission_F1_X391Y493", + "route": "route.rogue.Elite.Luofu_ArtisanshipCommission_F1:Luofu_ArtisanshipCommission_F1_X391Y493", + "plane": "Luofu_ArtisanshipCommission", + "floor": "F1", + "position": [ + 391.1, + 493.2 + ], + "domain": "Elite" + }, { "name": "Elite_Luofu_ArtisanshipCommission_F1_X504Y493", "route": "route.rogue.Elite.Luofu_ArtisanshipCommission_F1:Luofu_ArtisanshipCommission_F1_X504Y493", @@ -1385,6 +1451,17 @@ ], "domain": "Elite" }, + { + "name": "Elite_Luofu_Cloudford_F1_X342Y1003", + "route": "route.rogue.Elite.Luofu_Cloudford_F1:Luofu_Cloudford_F1_X342Y1003", + "plane": "Luofu_Cloudford", + "floor": "F1", + "position": [ + 342.3, + 1003.4 + ], + "domain": "Elite" + }, { "name": "Elite_Luofu_DivinationCommission_F2_X338Y345", "route": "route.rogue.Elite.Luofu_DivinationCommission_F2:Luofu_DivinationCommission_F2_X338Y345", @@ -1418,6 +1495,17 @@ ], "domain": "Elite" }, + { + "name": "Elite_Luofu_StargazerNavalia_F1_X617Y511", + "route": "route.rogue.Elite.Luofu_StargazerNavalia_F1:Luofu_StargazerNavalia_F1_X617Y511", + "plane": "Luofu_StargazerNavalia", + "floor": "F1", + "position": [ + 617.5, + 511.5 + ], + "domain": "Elite" + }, { "name": "Occurrence_Herta_StorageZone_F1_X273Y93", "route": "route.rogue.Occurrence.Herta_StorageZone_F1:Herta_StorageZone_F1_X273Y93", @@ -1572,6 +1660,17 @@ ], "domain": "Occurrence" }, + { + "name": "Occurrence_Jarilo_BackwaterPass_F1_X611Y761", + "route": "route.rogue.Occurrence.Jarilo_BackwaterPass_F1:Jarilo_BackwaterPass_F1_X611Y761", + "plane": "Jarilo_BackwaterPass", + "floor": "F1", + "position": [ + 611.4, + 761.2 + ], + "domain": "Occurrence" + }, { "name": "Occurrence_Jarilo_CorridorofFadingEchoes_F1_X236Y903", "route": "route.rogue.Occurrence.Jarilo_CorridorofFadingEchoes_F1:Jarilo_CorridorofFadingEchoes_F1_X236Y903", @@ -1693,6 +1792,17 @@ ], "domain": "Occurrence" }, + { + "name": "Occurrence_Jarilo_SilvermaneGuardRestrictedZone_F1_X435Y233", + "route": "route.rogue.Occurrence.Jarilo_SilvermaneGuardRestrictedZone_F1:Jarilo_SilvermaneGuardRestrictedZone_F1_X435Y233", + "plane": "Jarilo_SilvermaneGuardRestrictedZone", + "floor": "F1", + "position": [ + 435.5, + 233.5 + ], + "domain": "Occurrence" + }, { "name": "Occurrence_Jarilo_SilvermaneGuardRestrictedZone_F1_X509Y541", "route": "route.rogue.Occurrence.Jarilo_SilvermaneGuardRestrictedZone_F1:Jarilo_SilvermaneGuardRestrictedZone_F1_X509Y541", @@ -1781,6 +1891,17 @@ ], "domain": "Occurrence" }, + { + "name": "Occurrence_Luofu_Cloudford_F1_X244Y951", + "route": "route.rogue.Occurrence.Luofu_Cloudford_F1:Luofu_Cloudford_F1_X244Y951", + "plane": "Luofu_Cloudford", + "floor": "F1", + "position": [ + 244.0, + 951.0 + ], + "domain": "Occurrence" + }, { "name": "Occurrence_Luofu_Cloudford_F1_X281Y873", "route": "route.rogue.Occurrence.Luofu_Cloudford_F1:Luofu_Cloudford_F1_X281Y873", @@ -1935,6 +2056,17 @@ ], "domain": "Respite" }, + { + "name": "Respite_Herta_SupplyZone_F2_X550Y248", + "route": "route.rogue.Respite.Herta_SupplyZone_F2:Herta_SupplyZone_F2_X550Y248", + "plane": "Herta_SupplyZone", + "floor": "F2", + "position": [ + 550.7, + 248.7 + ], + "domain": "Respite" + }, { "name": "Respite_Jarilo_BackwaterPass_F1_X581Y403", "route": "route.rogue.Respite.Jarilo_BackwaterPass_F1:Jarilo_BackwaterPass_F1_X581Y403", diff --git a/tasks/assignment/assets/assets_assignment_dispatch.py b/tasks/assignment/assets/assets_assignment_dispatch.py index 25758c969..c1965a3f3 100644 --- a/tasks/assignment/assets/assets_assignment_dispatch.py +++ b/tasks/assignment/assets/assets_assignment_dispatch.py @@ -34,40 +34,40 @@ CHARACTER_1 = ButtonWrapper( name='CHARACTER_1', share=Button( file='./assets/share/assignment/dispatch/CHARACTER_1.png', - area=(110, 202, 202, 309), - search=(90, 182, 222, 329), + area=(96, 200, 188, 307), + search=(76, 180, 208, 327), color=(153, 141, 159), - button=(110, 202, 202, 309), + button=(96, 200, 188, 307), ), ) CHARACTER_1_SELECTED = ButtonWrapper( name='CHARACTER_1_SELECTED', share=Button( file='./assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png', - area=(107, 199, 126, 217), - search=(87, 179, 146, 237), + area=(93, 197, 112, 215), + search=(73, 177, 132, 235), color=(217, 218, 216), - button=(107, 199, 126, 217), + button=(93, 197, 112, 215), ), ) CHARACTER_2 = ButtonWrapper( name='CHARACTER_2', share=Button( file='./assets/share/assignment/dispatch/CHARACTER_2.png', - area=(222, 202, 314, 309), - search=(202, 182, 334, 329), + area=(208, 200, 300, 307), + search=(188, 180, 320, 327), color=(120, 120, 138), - button=(222, 202, 314, 309), + button=(208, 200, 300, 307), ), ) CHARACTER_2_SELECTED = ButtonWrapper( name='CHARACTER_2_SELECTED', share=Button( file='./assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png', - area=(219, 199, 238, 217), - search=(199, 179, 258, 237), + area=(205, 197, 224, 215), + search=(185, 177, 244, 235), color=(206, 207, 204), - button=(219, 199, 238, 217), + button=(205, 197, 224, 215), ), ) CHARACTER_LIST = ButtonWrapper( diff --git a/tasks/assignment/assets/assets_assignment_ui.py b/tasks/assignment/assets/assets_assignment_ui.py index 572411124..da093ac84 100644 --- a/tasks/assignment/assets/assets_assignment_ui.py +++ b/tasks/assignment/assets/assets_assignment_ui.py @@ -7,10 +7,10 @@ CHARACTER_MATERIALS_CHECK = ButtonWrapper( name='CHARACTER_MATERIALS_CHECK', cn=Button( file='./assets/cn/assignment/ui/CHARACTER_MATERIALS_CHECK.png', - area=(346, 97, 421, 117), - search=(326, 77, 441, 137), - color=(177, 176, 173), - button=(346, 97, 421, 117), + area=(190, 98, 265, 120), + search=(170, 78, 285, 140), + color=(195, 194, 191), + button=(190, 98, 265, 120), ), en=[ Button( @@ -33,10 +33,10 @@ CHARACTER_MATERIALS_CLICK = ButtonWrapper( name='CHARACTER_MATERIALS_CLICK', cn=Button( file='./assets/cn/assignment/ui/CHARACTER_MATERIALS_CLICK.png', - area=(347, 97, 421, 117), - search=(327, 77, 441, 137), - color=(60, 60, 60), - button=(347, 97, 421, 117), + area=(190, 98, 265, 119), + search=(170, 78, 285, 139), + color=(64, 63, 61), + button=(190, 98, 265, 119), ), en=[ Button( @@ -86,10 +86,10 @@ EXP_MATERIALS_CREDITS_CHECK = ButtonWrapper( name='EXP_MATERIALS_CREDITS_CHECK', cn=Button( file='./assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CHECK.png', - area=(514, 97, 614, 117), - search=(494, 77, 634, 137), - color=(178, 177, 174), - button=(514, 97, 614, 117), + area=(420, 100, 518, 118), + search=(400, 80, 538, 138), + color=(170, 169, 166), + button=(420, 100, 518, 118), ), en=[ Button( @@ -112,10 +112,10 @@ EXP_MATERIALS_CREDITS_CLICK = ButtonWrapper( name='EXP_MATERIALS_CREDITS_CLICK', cn=Button( file='./assets/cn/assignment/ui/EXP_MATERIALS_CREDITS_CLICK.png', - area=(514, 97, 614, 117), - search=(494, 77, 634, 137), - color=(61, 60, 60), - button=(514, 97, 614, 117), + area=(420, 100, 518, 118), + search=(400, 80, 538, 138), + color=(64, 65, 64), + button=(420, 100, 518, 118), ), en=[ Button( @@ -229,10 +229,10 @@ SYNTHESIS_MATERIALS_CHECK = ButtonWrapper( name='SYNTHESIS_MATERIALS_CHECK', cn=Button( file='./assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CHECK.png', - area=(708, 97, 783, 117), - search=(688, 77, 803, 137), - color=(180, 179, 176), - button=(708, 97, 783, 117), + area=(676, 98, 749, 119), + search=(656, 78, 769, 139), + color=(182, 181, 178), + button=(676, 98, 749, 119), ), en=[ Button( @@ -255,10 +255,10 @@ SYNTHESIS_MATERIALS_CLICK = ButtonWrapper( name='SYNTHESIS_MATERIALS_CLICK', cn=Button( file='./assets/cn/assignment/ui/SYNTHESIS_MATERIALS_CLICK.png', - area=(709, 97, 783, 117), - search=(689, 77, 803, 137), - color=(68, 66, 65), - button=(709, 97, 783, 117), + area=(676, 99, 749, 119), + search=(656, 79, 769, 139), + color=(57, 58, 57), + button=(676, 99, 749, 119), ), en=[ Button( diff --git a/tasks/assignment/keywords/__init__.py b/tasks/assignment/keywords/__init__.py index ab95755a7..eec743922 100644 --- a/tasks/assignment/keywords/__init__.py +++ b/tasks/assignment/keywords/__init__.py @@ -31,30 +31,30 @@ KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = ( KEYWORDS_ASSIGNMENT_ENTRY.Scalpel_and_Screwdriver, ) KEYWORDS_ASSIGNMENT_EVENT_GROUP.Space_Station_Task_Force.entries = ( - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Repulsion_Bridge_Errors, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Meal_Delivery_Robot_Check_Up, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Noise_Complaint, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Interior_Temperature_Modulator, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Researcher_Health_Reports, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Confidential_Investigation, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Borrowed_Equipment, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Booking_System, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Non_Digital_Documents, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Drip_Feed_Errors, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Pet_Movement_Route_Planning, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Curio_Distribution, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Super_Urgent_Waiting_Online, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Ventilation_Problem, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Unstable_Connection, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Chronology_Checks, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Supply_Chain_Management, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Malicious_Occupation_of_Public_Space, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Uniform_Material, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Virus_Re_creation_Report, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Abnormal_Signal, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Flexible_Working_Approval, - KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Lighting_Issue, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Activate_Genetic_Samples, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Reproduce_Experimental_Data, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Burned_Warehouse, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Car_Thief, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Synesthesia_Beacon_Function_Iteration, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Hunters_Wanted_No_Newbies_Please, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Peaceful_Life_for_Good_People, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Closed_Beta_Test_Recruitment, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Protect_Digital_Exhibits, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Final_Survivor, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Water_Pollution_Control, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Hook_Line_and_Sinker, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Investigation_Order_Boothill, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Volunteers_Wanted, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Burn_Treatment, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.I_Want_to_Speak_to_Your_Manager, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Licensed_Product_Damage_Assessment, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Annoying_Flies, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Urgent_Protection_Services, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.A_Dream_Is_Born, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Wanted_Boothill, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Casual_Cowboy, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Dangerous_Journey, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Crossing_the_Fire_Line, ) for group in ( KEYWORDS_ASSIGNMENT_GROUP.Character_Materials, diff --git a/tasks/assignment/keywords/classes.py b/tasks/assignment/keywords/classes.py index cbb2cd008..566a5f29f 100644 --- a/tasks/assignment/keywords/classes.py +++ b/tasks/assignment/keywords/classes.py @@ -19,6 +19,16 @@ class AssignmentEntry(Keyword): def __hash__(self) -> int: return super().__hash__() + @classmethod + def _compare(cls, name, keyword): + # 2024.05.08 Assignment names are omitted in EN + if name == keyword: + return True + # namelesslandnameless.. Nameless Land, Nameless People + if name[:17] == keyword[:17]: + return True + return False + @dataclass(repr=False) class AssignmentEntryDetailed(Keyword): diff --git a/tasks/assignment/keywords/entry.py b/tasks/assignment/keywords/entry.py index cab71c000..49fc7f34b 100644 --- a/tasks/assignment/keywords/entry.py +++ b/tasks/assignment/keywords/entry.py @@ -3,71 +3,8 @@ from .classes import AssignmentEntry # This file was auto-generated, do not modify it manually. To generate: # ``` python -m dev_tools.keyword_extract ``` -Nine_Billion_Names = AssignmentEntry( - id=1, - name='Nine_Billion_Names', - cn='九十亿个名字', - cht='九十億個名字', - en='Nine Billion Names', - jp='九十億の御名', - es='Nueve mil millones de nombres', -) -Destruction_of_the_Destroyer = AssignmentEntry( - id=2, - name='Destruction_of_the_Destroyer', - cn='毁灭者的覆灭', - cht='毀滅者的覆滅', - en='Destruction of the Destroyer', - jp='壊滅者の覆没', - es='La destrucción del destructor', -) -Winter_Soldiers = AssignmentEntry( - id=3, - name='Winter_Soldiers', - cn='寒冬的战士们', - cht='寒冬的戰士們', - en='Winter Soldiers', - jp='寒冬の戦士たち', - es='Los guerreros del invierno', -) -Born_to_Obey = AssignmentEntry( - id=4, - name='Born_to_Obey', - cn='生而服从', - cht='生而服從', - en='Born to Obey', - jp='生まれながらに服従する', - es='Creados para obedecer', -) -Root_Out_the_Turpitude = AssignmentEntry( - id=5, - name='Root_Out_the_Turpitude', - cn='根除恶孽', - cht='根除惡孽', - en='Root Out the Turpitude', - jp='悪孽を根絶やしに', - es='La raíz del mal', -) -Fire_Lord_Inflames_Blades_of_War = AssignmentEntry( - id=6, - name='Fire_Lord_Inflames_Blades_of_War', - cn='火帝动炉销剑戟', - cht='火帝動爐銷劍戟', - en='Fire Lord Inflames Blades of War', - jp='剣戟を焼却する火帝炉', - es='Prendan los fuelles, fundan las armas', -) -A_Startling_Night_Terror = AssignmentEntry( - id=7, - name='A_Startling_Night_Terror', - cn='劫梦惊魂', - cht='劫夢驚魂', - en='A Startling Night Terror', - jp='魂震える悪夢', - es='Pesadilla aterradora', -) Tranquility_of_Vimala_bhumi = AssignmentEntry( - id=8, + id=1, name='Tranquility_of_Vimala_bhumi', cn='离垢清净', cht='離垢清淨', @@ -75,17 +12,71 @@ Tranquility_of_Vimala_bhumi = AssignmentEntry( jp='離垢清浄', es='Limpieza y purificación', ) -Nameless_Land_Nameless_People = AssignmentEntry( - id=9, - name='Nameless_Land_Nameless_People', - cn='无名之地,无名之人', - cht='無名之地,無名之人', - en='Nameless Land, Nameless People', - jp='無名の地、無名の人', - es='Lugar anónimo, personas anónimas', +A_Startling_Night_Terror = AssignmentEntry( + id=2, + name='A_Startling_Night_Terror', + cn='劫梦惊魂', + cht='劫夢驚魂', + en='A Startling Night Terror', + jp='魂震える悪夢', + es='Pesadilla aterradora', +) +Fire_Lord_Inflames_Blades_of_War = AssignmentEntry( + id=3, + name='Fire_Lord_Inflames_Blades_of_War', + cn='火帝动炉销剑戟', + cht='火帝動爐銷劍戟', + en='Fire Lord Inflames Blades of War', + jp='剣戟を焼却する火帝炉', + es='Prendan los fuelles, fundan las armas', +) +Root_Out_the_Turpitude = AssignmentEntry( + id=4, + name='Root_Out_the_Turpitude', + cn='根除恶孽', + cht='根除惡孽', + en='Root Out the Turpitude', + jp='悪孽を根絶やしに', + es='La raíz del mal', +) +Born_to_Obey = AssignmentEntry( + id=5, + name='Born_to_Obey', + cn='生而服从', + cht='生而服從', + en='Born to Obey', + jp='生まれながらに服従する', + es='Creados para obedecer', +) +Winter_Soldiers = AssignmentEntry( + id=6, + name='Winter_Soldiers', + cn='寒冬的战士们', + cht='寒冬的戰士們', + en='Winter Soldiers', + jp='寒冬の戦士たち', + es='Los guerreros del invierno', +) +Destruction_of_the_Destroyer = AssignmentEntry( + id=7, + name='Destruction_of_the_Destroyer', + cn='毁灭者的覆灭', + cht='毀滅者的覆滅', + en='Destruction of the Destroyer', + jp='壊滅者の覆没', + es='La destrucción del destructor', +) +Nine_Billion_Names = AssignmentEntry( + id=8, + name='Nine_Billion_Names', + cn='九十亿个名字', + cht='九十億個名字', + en='Nine Billion Names', + jp='九十億の御名', + es='Nueve mil millones de nombres', ) Akashic_Records = AssignmentEntry( - id=10, + id=9, name='Akashic_Records', cn='阿卡夏记录', cht='阿卡夏紀錄', @@ -93,6 +84,15 @@ Akashic_Records = AssignmentEntry( jp='アーカーシャの記録', es='Los Registros de Akasha', ) +Nameless_Land_Nameless_People = AssignmentEntry( + id=10, + name='Nameless_Land_Nameless_People', + cn='无名之地,无名之人', + cht='無名之地,無名之人', + en='Nameless Land, Nameless People', + jp='無名の地、無名の人', + es='Lugar anónimo, personas anónimas', +) The_Invisible_Hand = AssignmentEntry( id=11, name='The_Invisible_Hand', @@ -102,71 +102,8 @@ The_Invisible_Hand = AssignmentEntry( jp='見えざる手', es='La mano invisible', ) -Abandoned_and_Insulted = AssignmentEntry( - id=12, - name='Abandoned_and_Insulted', - cn='被废弃与损害的', - cht='被廢棄與損害的', - en='Abandoned and Insulted', - jp='捨てられしものと傷つけられしもの', - es='Abandonado e insultado', -) -Spring_of_Life = AssignmentEntry( - id=13, - name='Spring_of_Life', - cn='生命之泉', - cht='生命之泉', - en='Spring of Life', - jp='生命の泉', - es='La fuente de la vida', -) -The_Land_of_Gold = AssignmentEntry( - id=14, - name='The_Land_of_Gold', - cn='黄金大地', - cht='黃金大地', - en='The Land of Gold', - jp='黄金の大地', - es='Tierra de oportunidades', -) -The_Blossom_in_the_Storm = AssignmentEntry( - id=15, - name='The_Blossom_in_the_Storm', - cn='风暴中怒放的花', - cht='風暴中怒放的花', - en='The Blossom in the Storm', - jp='嵐の中で咲き誇る花', - es='Flores en la tormenta', -) -Legend_of_the_Puppet_Master = AssignmentEntry( - id=16, - name='Legend_of_the_Puppet_Master', - cn='偃师传说', - cht='偃師傳說', - en='Legend of the Puppet Master', - jp='傀儡師伝説', - es='La leyenda del titiritero', -) -The_Wages_of_Humanity = AssignmentEntry( - id=17, - name='The_Wages_of_Humanity', - cn='赡养人类', - cht='贍養人類', - en='The Wages of Humanity', - jp='人類扶養', - es='La paga de la humanidad', -) -Fragments_of_Illusory_Dreams = AssignmentEntry( - id=18, - name='Fragments_of_Illusory_Dreams', - cn='幻梦的残片', - cht='幻夢的殘片', - en='Fragments of Illusory Dreams', - jp='幻夢の残片', - es='Fragmentos de sueños ilusorios', -) Scalpel_and_Screwdriver = AssignmentEntry( - id=19, + id=12, name='Scalpel_and_Screwdriver', cn='手术刀与螺丝刀', cht='手術刀與螺絲起子', @@ -174,3 +111,66 @@ Scalpel_and_Screwdriver = AssignmentEntry( jp='メスとスクリュードライバー', es='Bisturí y destornillador', ) +The_Wages_of_Humanity = AssignmentEntry( + id=13, + name='The_Wages_of_Humanity', + cn='赡养人类', + cht='贍養人類', + en='The Wages of Humanity', + jp='人類扶養', + es='La paga de la humanidad', +) +Legend_of_the_Puppet_Master = AssignmentEntry( + id=14, + name='Legend_of_the_Puppet_Master', + cn='偃师传说', + cht='偃師傳說', + en='Legend of the Puppet Master', + jp='傀儡師伝説', + es='La leyenda del titiritero', +) +The_Land_of_Gold = AssignmentEntry( + id=15, + name='The_Land_of_Gold', + cn='黄金大地', + cht='黃金大地', + en='The Land of Gold', + jp='黄金の大地', + es='Tierra de oportunidades', +) +Spring_of_Life = AssignmentEntry( + id=16, + name='Spring_of_Life', + cn='生命之泉', + cht='生命之泉', + en='Spring of Life', + jp='生命の泉', + es='La fuente de la vida', +) +Fragments_of_Illusory_Dreams = AssignmentEntry( + id=17, + name='Fragments_of_Illusory_Dreams', + cn='幻梦的残片', + cht='幻夢的殘片', + en='Fragments of Illusory Dreams', + jp='幻夢の残片', + es='Fragmentos de sueños ilusorios', +) +The_Blossom_in_the_Storm = AssignmentEntry( + id=18, + name='The_Blossom_in_the_Storm', + cn='风暴中怒放的花', + cht='風暴中怒放的花', + en='The Blossom in the Storm', + jp='嵐の中で咲き誇る花', + es='Flores en la tormenta', +) +Abandoned_and_Insulted = AssignmentEntry( + id=19, + name='Abandoned_and_Insulted', + cn='被废弃与损害的', + cht='被廢棄與損害的', + en='Abandoned and Insulted', + jp='捨てられしものと傷つけられしもの', + es='Abandonado e insultado', +) diff --git a/tasks/assignment/keywords/entry_detailed.py b/tasks/assignment/keywords/entry_detailed.py index 418e8f578..f295f67e7 100644 --- a/tasks/assignment/keywords/entry_detailed.py +++ b/tasks/assignment/keywords/entry_detailed.py @@ -3,71 +3,8 @@ from .classes import AssignmentEntryDetailed # This file was auto-generated, do not modify it manually. To generate: # ``` python -m dev_tools.keyword_extract ``` -Nine_Billion_Names = AssignmentEntryDetailed( - id=1, - name='Nine_Billion_Names', - cn='熄灭原核(九十亿个名字)', - cht='熄滅原核(九十億個名字)', - en='Extinguished Core (Nine Billion Names)', - jp='消滅した原核(九十億の御名)', - es='Núcleo apagado (Nueve mil millones de nombres)', -) -Destruction_of_the_Destroyer = AssignmentEntryDetailed( - id=2, - name='Destruction_of_the_Destroyer', - cn='掠夺的本能(毁灭者的覆灭)', - cht='掠奪的本能(毀滅者的覆滅)', - en="Thief's Instinct (Destruction of the Destroyer)", - jp='略奪の本能(壊滅者の覆没)', - es='Instinto del ladrón (La destrucción del destructor)', -) -Winter_Soldiers = AssignmentEntryDetailed( - id=3, - name='Winter_Soldiers', - cn='铁卫扣饰(寒冬的战士们)', - cht='鐵衛扣飾(寒冬的戰士們)', - en='Silvermane Badge (Winter Soldiers)', - jp='シルバーメインの釦(寒冬の戦士たち)', - es='Pin del guardia (Los guerreros del invierno)', -) -Born_to_Obey = AssignmentEntryDetailed( - id=4, - name='Born_to_Obey', - cn='古代零件(生而服从)', - cht='古代零件(生而服從)', - en='Ancient Part (Born to Obey)', - jp='古代パーツ(生まれながらに服従する)', - es='Componente antiguo (Creados para obedecer)', -) -Root_Out_the_Turpitude = AssignmentEntryDetailed( - id=5, - name='Root_Out_the_Turpitude', - cn='永寿幼芽(根除恶孽)', - cht='永壽幼芽(根除惡孽)', - en='Immortal Scionette (Root Out the Turpitude)', - jp='永寿の萌芽(悪孽を根絶やしに)', - es='Brote verde inmortal (La raíz del mal)', -) -Fire_Lord_Inflames_Blades_of_War = AssignmentEntryDetailed( - id=6, - name='Fire_Lord_Inflames_Blades_of_War', - cn='工造机杼(火帝动炉销剑戟)', - cht='工造機杼(火帝動爐銷劍戟)', - en="Artifex's Module (Fire Lord Inflames Blades of War)", - jp='工造機関(剣戟を焼却する火帝炉)', - es='Componente artificial mecánico (Prendan los fuelles, fundan las armas)', -) -A_Startling_Night_Terror = AssignmentEntryDetailed( - id=7, - name='A_Startling_Night_Terror', - cn='蓄梦元件(劫梦惊魂)', - cht='蓄夢元件(劫夢驚魂)', - en='Dream Collection Component (A Startling Night Terror)', - jp='ドリームコレクションパーツ(魂震える悪夢)', - es='Componente del acumulador de sueños (Pesadilla aterradora)', -) Tranquility_of_Vimala_bhumi = AssignmentEntryDetailed( - id=8, + id=1, name='Tranquility_of_Vimala_bhumi', cn='思绪末屑(离垢清净)', cht='思緒末屑(離垢清淨)', @@ -75,17 +12,71 @@ Tranquility_of_Vimala_bhumi = AssignmentEntryDetailed( jp='思考の粉末(離垢清浄)', es='Jirones de pensamientos (Limpieza y purificación)', ) -Nameless_Land_Nameless_People = AssignmentEntryDetailed( - id=9, - name='Nameless_Land_Nameless_People', - cn='角色经验材料(无名之地,无名之人)', - cht='角色經驗素材(無名之地,無名之人)', - en='Character EXP Material (Nameless Land, Nameless People)', - jp='キャラクター経験値素材(無名の地、無名の人)', - es='Material de EXP de personaje (Lugar anónimo, personas anónimas)', +A_Startling_Night_Terror = AssignmentEntryDetailed( + id=2, + name='A_Startling_Night_Terror', + cn='蓄梦元件(劫梦惊魂)', + cht='蓄夢元件(劫夢驚魂)', + en='Dream Collection Component (A Startling Night Terror)', + jp='ドリームコレクションパーツ(魂震える悪夢)', + es='Componente del acumulador de sueños (Pesadilla aterradora)', +) +Fire_Lord_Inflames_Blades_of_War = AssignmentEntryDetailed( + id=3, + name='Fire_Lord_Inflames_Blades_of_War', + cn='工造机杼(火帝动炉销剑戟)', + cht='工造機杼(火帝動爐銷劍戟)', + en="Artifex's Module (Fire Lord Inflames Blades of War)", + jp='工造機関(剣戟を焼却する火帝炉)', + es='Componente artificial mecánico (Prendan los fuelles, fundan las armas)', +) +Root_Out_the_Turpitude = AssignmentEntryDetailed( + id=4, + name='Root_Out_the_Turpitude', + cn='永寿幼芽(根除恶孽)', + cht='永壽幼芽(根除惡孽)', + en='Immortal Scionette (Root Out the Turpitude)', + jp='永寿の萌芽(悪孽を根絶やしに)', + es='Brote verde inmortal (La raíz del mal)', +) +Born_to_Obey = AssignmentEntryDetailed( + id=5, + name='Born_to_Obey', + cn='古代零件(生而服从)', + cht='古代零件(生而服從)', + en='Ancient Part (Born to Obey)', + jp='古代パーツ(生まれながらに服従する)', + es='Componente antiguo (Creados para obedecer)', +) +Winter_Soldiers = AssignmentEntryDetailed( + id=6, + name='Winter_Soldiers', + cn='铁卫扣饰(寒冬的战士们)', + cht='鐵衛扣飾(寒冬的戰士們)', + en='Silvermane Badge (Winter Soldiers)', + jp='シルバーメインの釦(寒冬の戦士たち)', + es='Pin del guardia (Los guerreros del invierno)', +) +Destruction_of_the_Destroyer = AssignmentEntryDetailed( + id=7, + name='Destruction_of_the_Destroyer', + cn='掠夺的本能(毁灭者的覆灭)', + cht='掠奪的本能(毀滅者的覆滅)', + en="Thief's Instinct (Destruction of the Destroyer)", + jp='略奪の本能(壊滅者の覆没)', + es='Instinto del ladrón (La destrucción del destructor)', +) +Nine_Billion_Names = AssignmentEntryDetailed( + id=8, + name='Nine_Billion_Names', + cn='熄灭原核(九十亿个名字)', + cht='熄滅原核(九十億個名字)', + en='Extinguished Core (Nine Billion Names)', + jp='消滅した原核(九十億の御名)', + es='Núcleo apagado (Nueve mil millones de nombres)', ) Akashic_Records = AssignmentEntryDetailed( - id=10, + id=9, name='Akashic_Records', cn='光锥经验材料(阿卡夏记录)', cht='光錐經驗素材(阿卡夏紀錄)', @@ -93,6 +84,15 @@ Akashic_Records = AssignmentEntryDetailed( jp='光円錐経験値素材(アーカーシャの記録)', es='Material de EXP de conos de luz (Los Registros de Akasha)', ) +Nameless_Land_Nameless_People = AssignmentEntryDetailed( + id=10, + name='Nameless_Land_Nameless_People', + cn='角色经验材料(无名之地,无名之人)', + cht='角色經驗素材(無名之地,無名之人)', + en='Character EXP Material (Nameless Land, Nameless People)', + jp='キャラクター経験値素材(無名の地、無名の人)', + es='Material de EXP de personaje (Lugar anónimo, personas anónimas)', +) The_Invisible_Hand = AssignmentEntryDetailed( id=11, name='The_Invisible_Hand', @@ -102,71 +102,8 @@ The_Invisible_Hand = AssignmentEntryDetailed( jp='信用ポイント(見えざる手)', es='Crédito (La mano invisible)', ) -Abandoned_and_Insulted = AssignmentEntryDetailed( - id=12, - name='Abandoned_and_Insulted', - cn='燃素 & 金属(被废弃与损害的)', - cht='燃素 & 金屬(被廢棄與損害的)', - en='Phlogiston & Metal (Abandoned and Insulted)', - jp='燃素 & 金属(捨てられしものと傷つけられしもの)', - es='Flogisto & Metal (Abandonado e insultado)', -) -Spring_of_Life = AssignmentEntryDetailed( - id=13, - name='Spring_of_Life', - cn='固态净水 & 虚粒子(生命之泉)', - cht='固態淨水 & 虛粒子(生命之泉)', - en='Solid Water & Virtual Particle (Spring of Life)', - jp='固形純水 & 仮想粒子(生命の泉)', - es='Agua sólida & Partícula virtual (La fuente de la vida)', -) -The_Land_of_Gold = AssignmentEntryDetailed( - id=14, - name='The_Land_of_Gold', - cn='基本食材 & 蛋白米(黄金大地)', - cht='基本食材 & 蛋白米(黃金大地)', - en='Basic Ingredients & Protein Rice (The Land of Gold)', - jp='基本食材 & タンパク米(黄金の大地)', - es='Ingredientes básicos & Arroz proteico (Tierra de oportunidades)', -) -The_Blossom_in_the_Storm = AssignmentEntryDetailed( - id=15, - name='The_Blossom_in_the_Storm', - cn='气态流体 & 种子(风暴中怒放的花)', - cht='氣態流體 & 種子(風暴中怒放的花)', - en='Gaseous Liquid & Seed (The Blossom in the Storm)', - jp='気態流体 & 種子(嵐の中で咲き誇る花)', - es='Líquido gaseoso & Semilla (Flores en la tormenta)', -) -Legend_of_the_Puppet_Master = AssignmentEntryDetailed( - id=16, - name='Legend_of_the_Puppet_Master', - cn='废弃机巧零件 & 玉兆单元(偃师传说)', - cht='廢棄機巧零件 & 玉兆單元(偃師傳說)', - en='Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)', - jp='廃棄された機巧部品 & 玉兆単元(傀儡師伝説)', - es='Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)', -) -The_Wages_of_Humanity = AssignmentEntryDetailed( - id=17, - name='The_Wages_of_Humanity', - cn='一人嘉禾 & 药草提取物(赡养人类)', - cht='一人嘉禾 & 藥草萃取物(贍養人類)', - en='Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)', - jp='一人稲 & 薬草抽出物(人類扶養)', - es='Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)', -) -Fragments_of_Illusory_Dreams = AssignmentEntryDetailed( - id=18, - name='Fragments_of_Illusory_Dreams', - cn='安逸 & 碎梦(幻梦的残片)', - cht='安逸 & 碎夢(幻夢的殘片)', - en='Tranquility & Broken Dreams (Fragments of Illusory Dreams)', - jp='安逸 & 砕けた夢(幻夢の残片)', - es='Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)', -) Scalpel_and_Screwdriver = AssignmentEntryDetailed( - id=19, + id=12, name='Scalpel_and_Screwdriver', cn='锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)', cht='鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)', @@ -174,3 +111,66 @@ Scalpel_and_Screwdriver = AssignmentEntryDetailed( jp='錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)', es='Engranaje oxidado & Muela vieja (Bisturí y destornillador)', ) +The_Wages_of_Humanity = AssignmentEntryDetailed( + id=13, + name='The_Wages_of_Humanity', + cn='一人嘉禾 & 药草提取物(赡养人类)', + cht='一人嘉禾 & 藥草萃取物(贍養人類)', + en='Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)', + jp='一人稲 & 薬草抽出物(人類扶養)', + es='Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)', +) +Legend_of_the_Puppet_Master = AssignmentEntryDetailed( + id=14, + name='Legend_of_the_Puppet_Master', + cn='废弃机巧零件 & 玉兆单元(偃师传说)', + cht='廢棄機巧零件 & 玉兆單元(偃師傳說)', + en='Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)', + jp='廃棄された機巧部品 & 玉兆単元(傀儡師伝説)', + es='Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)', +) +The_Land_of_Gold = AssignmentEntryDetailed( + id=15, + name='The_Land_of_Gold', + cn='基本食材 & 蛋白米(黄金大地)', + cht='基本食材 & 蛋白米(黃金大地)', + en='Basic Ingredients & Protein Rice (The Land of Gold)', + jp='基本食材 & タンパク米(黄金の大地)', + es='Ingredientes básicos & Arroz proteico (Tierra de oportunidades)', +) +Spring_of_Life = AssignmentEntryDetailed( + id=16, + name='Spring_of_Life', + cn='固态净水 & 虚粒子(生命之泉)', + cht='固態淨水 & 虛粒子(生命之泉)', + en='Solid Water & Virtual Particle (Spring of Life)', + jp='固形純水 & 仮想粒子(生命の泉)', + es='Agua sólida & Partícula virtual (La fuente de la vida)', +) +Fragments_of_Illusory_Dreams = AssignmentEntryDetailed( + id=17, + name='Fragments_of_Illusory_Dreams', + cn='安逸 & 碎梦(幻梦的残片)', + cht='安逸 & 碎夢(幻夢的殘片)', + en='Tranquility & Broken Dreams (Fragments of Illusory Dreams)', + jp='安逸 & 砕けた夢(幻夢の残片)', + es='Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)', +) +The_Blossom_in_the_Storm = AssignmentEntryDetailed( + id=18, + name='The_Blossom_in_the_Storm', + cn='气态流体 & 种子(风暴中怒放的花)', + cht='氣態流體 & 種子(風暴中怒放的花)', + en='Gaseous Liquid & Seed (The Blossom in the Storm)', + jp='気態流体 & 種子(嵐の中で咲き誇る花)', + es='Líquido gaseoso & Semilla (Flores en la tormenta)', +) +Abandoned_and_Insulted = AssignmentEntryDetailed( + id=19, + name='Abandoned_and_Insulted', + cn='燃素 & 金属(被废弃与损害的)', + cht='燃素 & 金屬(被廢棄與損害的)', + en='Phlogiston & Metal (Abandoned and Insulted)', + jp='燃素 & 金属(捨てられしものと傷つけられしもの)', + es='Flogisto & Metal (Abandonado e insultado)', +) diff --git a/tasks/assignment/keywords/event_entry.py b/tasks/assignment/keywords/event_entry.py index d6dec29c9..9dd113804 100644 --- a/tasks/assignment/keywords/event_entry.py +++ b/tasks/assignment/keywords/event_entry.py @@ -3,219 +3,219 @@ from .classes import AssignmentEventEntry # This file was auto-generated, do not modify it manually. To generate: # ``` python -m dev_tools.keyword_extract ``` -Repulsion_Bridge_Errors = AssignmentEventEntry( +Activate_Genetic_Samples = AssignmentEventEntry( id=1, - name='Repulsion_Bridge_Errors', - cn='斥力桥报错', - cht='斥力橋錯誤', - en='Repulsion Bridge Errors', - jp='斥力ブリッジエラー', - es='Reporte de error del puente de rechazo', + name='Activate_Genetic_Samples', + cn='激活基因样本', + cht='激發基因樣本', + en='Activate Genetic Samples', + jp='遺伝子サンプルの活性化', + es='Activación de muestras genéticas', ) -Meal_Delivery_Robot_Check_Up = AssignmentEventEntry( +Reproduce_Experimental_Data = AssignmentEventEntry( id=2, - name='Meal_Delivery_Robot_Check_Up', - cn='送餐机器人检修', - cht='送餐機器人檢修', - en='Meal-Delivery Robot Check-Up', - jp='配膳ロボット点検修理', - es='Mantenimiento de los robots de reparto de comida', + name='Reproduce_Experimental_Data', + cn='复现实验数据', + cht='重現實驗數據', + en='Reproduce Experimental Data', + jp='実験データの復元', + es='Recreación de datos experimentales', ) -Noise_Complaint = AssignmentEventEntry( +Burned_Warehouse = AssignmentEventEntry( id=3, - name='Noise_Complaint', - cn='噪音投诉问题', - cht='噪音投訴問題', - en='Noise Complaint', - jp='騒音苦情問題', - es='Quejas por ruidos', + name='Burned_Warehouse', + cn='仓库被烧', + cht='倉庫被燒', + en='Burned Warehouse', + jp='燃えた倉庫', + es='Almacén incinerado', ) -Interior_Temperature_Modulator = AssignmentEventEntry( +Car_Thief = AssignmentEventEntry( id=4, - name='Interior_Temperature_Modulator', - cn='室内温度调节器', - cht='室內溫度調節器', - en='Interior Temperature Modulator', - jp='室内温度調節器', - es='Regulador de temperatura ambiental', + name='Car_Thief', + cn='███的偷车贼!', + cht='███的偷車賊!', + en='███████ Car Thief!', + jp='███の車泥棒!', + es='¡████ ladrón de autos!', ) -Researcher_Health_Reports = AssignmentEventEntry( +Synesthesia_Beacon_Function_Iteration = AssignmentEventEntry( id=5, - name='Researcher_Health_Reports', - cn='科员的体检报告', - cht='組員的體檢報告', - en="Researchers' Health Reports", - jp='スタッフの健康診断報告', - es='Informe médico de los investigadores', + name='Synesthesia_Beacon_Function_Iteration', + cn='联觉信标功能迭代', + cht='聯覺信標功能更新', + en='Synesthesia Beacon Function Iteration', + jp='共感覚ビーコン機能アップデート', + es='Iteración de funciones de la baliza sinestésica', ) -Confidential_Investigation = AssignmentEventEntry( +Hunters_Wanted_No_Newbies_Please = AssignmentEventEntry( id=6, - name='Confidential_Investigation', - cn='秘密调查行动', - cht='秘密調查行動', - en='Confidential Investigation', - jp='秘密裏の調査', - es='Investigación encubierta', + name='Hunters_Wanted_No_Newbies_Please', + cn='来猎人,菜鸟勿扰', + cht='來獵人,菜鳥勿擾', + en='Hunters Wanted, No Newbies Please', + jp='プロハンター歓迎、初心者お断り', + es='Se necesitan cazadores. Novatos abstenerse', ) -Borrowed_Equipment = AssignmentEventEntry( +Peaceful_Life_for_Good_People = AssignmentEventEntry( id=7, - name='Borrowed_Equipment', - cn='实验器械借用', - cht='實驗器械借用', - en='Borrowed Equipment', - jp='実験機器借用', - es='Préstamo de instrumentos de laboratorio', + name='Peaceful_Life_for_Good_People', + cn='好人一生平安', + cht='好人一生平安', + en='Peaceful Life for Good People', + jp='善人は一生安泰', + es='Vida pacífica para la gente de bien', ) -Booking_System = AssignmentEventEntry( +Closed_Beta_Test_Recruitment = AssignmentEventEntry( id=8, - name='Booking_System', - cn='会议室预约系统', - cht='會議室預約系統', - en='Booking System', - jp='会議室予約システム', - es='Sistema de reserva de las salas de reuniones', + name='Closed_Beta_Test_Recruitment', + cn='征集内测玩家', + cht='徵求封測玩家', + en='Closed Beta Test Recruitment', + jp='テスター募集', + es='Reclutamiento para beta cerrada', ) -Non_Digital_Documents = AssignmentEventEntry( +Protect_Digital_Exhibits = AssignmentEventEntry( id=9, - name='Non_Digital_Documents', - cn='非电子版文件', - cht='非電子版文件', - en='Non-Digital Documents', - jp='非デジタル版ファイル', - es='Documentos no electrónicos', + name='Protect_Digital_Exhibits', + cn='保护数字展品', + cht='保護數位展品', + en='Protect Digital Exhibits', + jp='デジタル展示品を守れ', + es='Protección de objetos de exposición digitales', ) -Drip_Feed_Errors = AssignmentEventEntry( +Final_Survivor = AssignmentEventEntry( id=10, - name='Drip_Feed_Errors', - cn='液滴系统报错', - cht='液滴系統錯誤', - en='Drip-Feed Errors', - jp='水やりシステムエラー', - es='Reporte de error del sistema de goteo', + name='Final_Survivor', + cn='最后的生还者', + cht='最後的生還者', + en='Final Survivor', + jp='最後の生還者', + es='Última superviviente', ) -Pet_Movement_Route_Planning = AssignmentEventEntry( +Water_Pollution_Control = AssignmentEventEntry( id=11, - name='Pet_Movement_Route_Planning', - cn='宠物行动路线规划', - cht='寵物行動路線規劃', - en='Pet Movement Route Planning', - jp='ペットの行動ルート規制', - es='Planificación de las rutas de paseo de mascotas', + name='Water_Pollution_Control', + cn='水域排污', + cht='水域排汙', + en='Water Pollution Control', + jp='水質汚染', + es='Control de contaminación acuática', ) -Food_Improvement_Plan = AssignmentEventEntry( +Hook_Line_and_Sinker = AssignmentEventEntry( id=12, - name='Food_Improvement_Plan', - cn='餐饮优化方案', - cht='餐飲改良方案', - en='Food Improvement Plan', - jp='飲食優良化法案', - es='Programa de mejora de comida', + name='Hook_Line_and_Sinker', + cn='愿者上钩', + cht='願者上鉤', + en='Hook, Line, and Sinker', + jp='いずれ捕まる', + es='Quien muerde el anzuelo es porque quiere', ) -Curio_Distribution = AssignmentEventEntry( +Investigation_Order_Boothill = AssignmentEventEntry( id=13, - name='Curio_Distribution', - cn='奇物借用问题', - cht='奇物借用問題', - en='Curio Distribution', - jp='奇物借用問題', - es='Problemas con el préstamo de objetos raros', + name='Investigation_Order_Boothill', + cn='追查令:波提欧', + cht='追查令:波提歐', + en='Investigation Order: Boothill', + jp='調査命令:ブートヒル', + es='Orden de investigación: Boothill', ) -Super_Urgent_Waiting_Online = AssignmentEventEntry( +Volunteers_Wanted = AssignmentEventEntry( id=14, - name='Super_Urgent_Waiting_Online', - cn='来活人很急在线等', - cht='急,線上等', - en='Super Urgent, Waiting Online', - jp='緊急助っ人求むオンラインにて待つ', - es='Muy urgente, esperando en línea', + name='Volunteers_Wanted', + cn='寻找志愿者', + cht='尋找志向者', + en='Volunteers Wanted', + jp='ボランティアを探せ', + es='Se necesitan voluntarios', ) -Ventilation_Problem = AssignmentEventEntry( +Burn_Treatment = AssignmentEventEntry( id=15, - name='Ventilation_Problem', - cn='空气流通问题', - cht='空氣流通問題', - en='Ventilation Problem', - jp='換気問題', - es='Problemas con la circulación del aire', + name='Burn_Treatment', + cn='烧伤治疗', + cht='燒傷治療', + en='Burn Treatment', + jp='火傷の治療', + es='Tratamiento cauterizador', ) -Unstable_Connection = AssignmentEventEntry( +I_Want_to_Speak_to_Your_Manager = AssignmentEventEntry( id=16, - name='Unstable_Connection', - cn='连接不稳定问题', - cht='連線不穩定問題', - en='Unstable Connection', - jp='接続不安定問題', - es='Conexión inestable', + name='I_Want_to_Speak_to_Your_Manager', + cn='去把你们经理喊过来', + cht='叫你們經理出來', + en='I Want to Speak to Your Manager', + jp='責任者を呼べ', + es='Quiero hablar con su gerente', ) -Chronology_Checks = AssignmentEventEntry( +Licensed_Product_Damage_Assessment = AssignmentEventEntry( id=17, - name='Chronology_Checks', - cn='编年史校对', - cht='編年史校對', - en='Chronology Checks', - jp='編年史校正', - es='Corrección de registros', + name='Licensed_Product_Damage_Assessment', + cn='授权产品定损', + cht='授權產品損失鑑定', + en='Licensed Product Damage Assessment', + jp='ライセンス製品の損失額評価', + es='Evaluación de pérdidas de productos autorizados', ) -Supply_Chain_Management = AssignmentEventEntry( +Annoying_Flies = AssignmentEventEntry( id=18, - name='Supply_Chain_Management', - cn='物流供应链管理', - cht='物流供應鏈管理', - en='Supply Chain Management', - jp='物流供給路線管理', - es='Gestión de pedidos', + name='Annoying_Flies', + cn='烦人的苍蝇', + cht='煩人的蒼蠅', + en='Annoying Flies', + jp='鬱陶しいハエ', + es='Moscas molestas', ) -Malicious_Occupation_of_Public_Space = AssignmentEventEntry( +Urgent_Protection_Services = AssignmentEventEntry( id=19, - name='Malicious_Occupation_of_Public_Space', - cn='公共区域被恶意侵占', - cht='公共區域被惡意侵佔', - en='Malicious Occupation of Public Space', - jp='公共区域の悪意による独占', - es='Invasión de espacios públicos', + name='Urgent_Protection_Services', + cn='紧急护卫', + cht='緊急護衛', + en='Urgent Protection Services', + jp='緊急護衛', + es='Se necesita protección urgente', ) -Uniform_Material = AssignmentEventEntry( +A_Dream_Is_Born = AssignmentEventEntry( id=20, - name='Uniform_Material', - cn='科室服装面料', - cht='科室服裝材質', - en='Uniform Material', - jp='スタッフ制服の素材', - es='Material del uniforme', + name='A_Dream_Is_Born', + cn='星梦起航', + cht='星夢啟航', + en='A Dream Is Born', + jp='星の夢の門出', + es='Ha nacido una estrella', ) -Virus_Re_creation_Report = AssignmentEventEntry( +Wanted_Boothill = AssignmentEventEntry( id=21, - name='Virus_Re_creation_Report', - cn='病毒溯源报告', - cht='病毒溯源報告', - en='Virus Re-creation Report', - jp='ウイルス根源報告', - es='Informe de rastreo de virus', + name='Wanted_Boothill', + cn='通缉令:波提欧', + cht='通緝令:波提歐', + en='Wanted: Boothill', + jp='指名手配:ブートヒル', + es='Se busca: Boothill', ) -Abnormal_Signal = AssignmentEventEntry( +Casual_Cowboy = AssignmentEventEntry( id=22, - name='Abnormal_Signal', - cn='舱段信号异常', - cht='艙段訊號異常', - en='Abnormal Signal', - jp='部分の信号異常', - es='Mala señal en las cabinas', + name='Casual_Cowboy', + cn='牛仔不忙', + cht='牛仔不忙', + en='Casual Cowboy', + jp='暇を持て余したカウボーイ', + es='Vaquero ocioso', ) -Flexible_Working_Approval = AssignmentEventEntry( +Dangerous_Journey = AssignmentEventEntry( id=23, - name='Flexible_Working_Approval', - cn='轮休审批流程', - cht='輪休審批流程', - en='Flexible Working Approval', - jp='交代休み審査フロー', - es='Aprobación de días de descanso', + name='Dangerous_Journey', + cn='危险之旅', + cht='危險之旅', + en='Dangerous Journey', + jp='危険な旅', + es='Viaje peligroso', ) -Lighting_Issue = AssignmentEventEntry( +Crossing_the_Fire_Line = AssignmentEventEntry( id=24, - name='Lighting_Issue', - cn='灯光照明问题', - cht='燈光照明問題', - en='Lighting Issue', - jp='照明の色問題', - es='Problemas de iluminación', + name='Crossing_the_Fire_Line', + cn='穿越火线', + cht='穿越火線', + en='Crossing the Fire Line', + jp='クロスファイア', + es='Cruzando la línea de fuego', ) diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py index 895fe0c38..efbeb0f67 100644 --- a/tasks/assignment/ui.py +++ b/tasks/assignment/ui.py @@ -40,8 +40,8 @@ class AssignmentOcr(Ocr): (KEYWORDS_ASSIGNMENT_ENTRY.The_Wages_of_Humanity.name, '[赠]养人类'), ], 'en': [ - (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan.name, - 'Food\s*[I]{0}mprovement Plan'), + # (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan.name, + # 'Food\s*[I]{0}mprovement Plan'), ] } diff --git a/tasks/base/assets/assets_base_page.py b/tasks/base/assets/assets_base_page.py index 7480b5ddf..c43a575ae 100644 --- a/tasks/base/assets/assets_base_page.py +++ b/tasks/base/assets/assets_base_page.py @@ -5,12 +5,19 @@ from module.base.button import Button, ButtonWrapper ASSIGNMENT_CHECK = ButtonWrapper( name='ASSIGNMENT_CHECK', - share=Button( - file='./assets/share/base/page/ASSIGNMENT_CHECK.png', - area=(45, 21, 70, 53), - search=(25, 1, 90, 73), - color=(162, 145, 112), - button=(45, 21, 70, 53), + cn=Button( + file='./assets/cn/base/page/ASSIGNMENT_CHECK.png', + area=(535, 165, 587, 181), + search=(515, 145, 607, 201), + color=(207, 199, 181), + button=(535, 165, 587, 181), + ), + en=Button( + file='./assets/en/base/page/ASSIGNMENT_CHECK.png', + area=(535, 165, 581, 180), + search=(515, 145, 601, 200), + color=(204, 195, 176), + button=(535, 165, 581, 180), ), ) BACK = ButtonWrapper( @@ -273,13 +280,22 @@ MAP_CHECK = ButtonWrapper( ) MAP_EXIT = ButtonWrapper( name='MAP_EXIT', - share=Button( - file='./assets/share/base/page/MAP_EXIT.png', - area=(27, 46, 44, 74), - search=(7, 26, 64, 94), - color=(142, 144, 148), - button=(27, 46, 44, 74), - ), + share=[ + Button( + file='./assets/share/base/page/MAP_EXIT.png', + area=(27, 46, 44, 74), + search=(7, 26, 64, 94), + color=(142, 144, 148), + button=(27, 46, 44, 74), + ), + Button( + file='./assets/share/base/page/MAP_EXIT.2.png', + area=(27, 46, 44, 74), + search=(7, 26, 64, 94), + color=(160, 187, 206), + button=(27, 46, 44, 74), + ), + ], ) MAP_GOTO_WORLD = ButtonWrapper( name='MAP_GOTO_WORLD', diff --git a/tasks/base/main_page.py b/tasks/base/main_page.py index 4ea355919..1238ee9ed 100644 --- a/tasks/base/main_page.py +++ b/tasks/base/main_page.py @@ -5,11 +5,9 @@ from module.config.server import VALID_LANG from module.exception import RequestHumanTakeover, ScriptError from module.logger import logger from module.ocr.ocr import OcrWhiteLetterOnComplexBackground -from tasks.base.assets.assets_base_main_page import OCR_MAP_NAME, ROGUE_LEAVE_FOR_NOW -from tasks.base.assets.assets_base_page import CLOSE, MAP_EXIT -from tasks.base.page import Page, page_gacha, page_main +from tasks.base.assets.assets_base_main_page import OCR_MAP_NAME +from tasks.base.page import Page, page_main from tasks.base.popup import PopupHandler -from tasks.daily.assets.assets_daily_trial import START_TRIAL from tasks.map.keywords import KEYWORDS_MAP_PLANE, MapPlane @@ -39,9 +37,11 @@ class OcrPlaneName(OcrWhiteLetterOnComplexBackground): # 区域-战 result = re.sub(r'区域.*战$', '区域战斗', result) # 区域-事 - result = re.sub(r'区域.*事$', '区域事件', result) + result = re.sub(r'区域.*[事件]$', '区域事件', result) # 区域-战 result = re.sub(r'区域.*交$', '区域交易', result) + # 区域-战 + result = re.sub(r'区域.*[精英]$', '区域精英', result) # 区域-事伴, 区域-事祥 result = re.sub(r'事[伴祥]', '事件', result) # 医域-战斗 @@ -54,6 +54,7 @@ class OcrPlaneName(OcrWhiteLetterOnComplexBackground): result = result.replace('累塔', '黑塔') if '星港' in result: result = '迴星港' + result = result.replace('太司', '太卜司') result = result.replace(' ', '') @@ -157,49 +158,3 @@ class MainPage(PopupHandler): self.handle_lang_check(page=page_main) return True - - def ui_leave_special(self): - """ - Leave from: - - Rogue domains - - Character trials - - Returns: - bool: If left a special plane - - Pages: - in: Any - out: page_main - """ - if not self.appear(MAP_EXIT): - return False - - logger.info('UI leave special') - skip_first_screenshot = True - clicked = False - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - # End - if clicked: - if self.appear(page_main.check_button): - logger.info(f'Leave to {page_main}') - break - - if self.appear_then_click(MAP_EXIT, interval=2): - continue - if self.handle_popup_confirm(): - continue - if self.match_template_color(START_TRIAL, interval=2): - logger.info(f'{START_TRIAL} -> {CLOSE}') - self.device.click(CLOSE) - clicked = True - continue - if self.handle_ui_close(page_gacha.check_button, interval=2): - continue - if self.appear_then_click(ROGUE_LEAVE_FOR_NOW, interval=2): - clicked = True - continue diff --git a/tasks/base/ui.py b/tasks/base/ui.py index 09d18db42..0c159cce3 100644 --- a/tasks/base/ui.py +++ b/tasks/base/ui.py @@ -4,13 +4,14 @@ 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_page import MAP_EXIT +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.main_page import MainPage -from tasks.base.page import Page, page_main +from tasks.base.page import Page, page_gacha, page_main from tasks.combat.assets.assets_combat_finish import COMBAT_EXIT from tasks.combat.assets.assets_combat_interact import MAP_LOADING from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE -from tasks.daily.assets.assets_daily_trial import INFO_CLOSE +from tasks.daily.assets.assets_daily_trial import INFO_CLOSE, START_TRIAL from tasks.login.assets.assets_login import LOGIN_CONFIRM @@ -24,6 +25,8 @@ class UI(MainPage): page (Page): interval: """ + if page == page_main: + return self.is_in_main(interval=interval) return self.appear(page.check_button, interval=interval) def ui_get_current_page(self, skip_first_screenshot=True): @@ -139,7 +142,7 @@ class UI(MainPage): for page in Page.iter_pages(): if page.parent is None or page.check_button is None: continue - if self.appear(page.check_button, interval=5): + if self.ui_page_appear(page, interval=5): logger.info(f'Page switch: {page} -> {page.parent}') self.handle_lang_check(page) if self.ui_page_confirm(page): @@ -297,14 +300,41 @@ class UI(MainPage): if additional(): continue - def is_in_main(self): - if self.appear(page_main.check_button): - if self.image_color_count(page_main.check_button, color=(235, 235, 235), threshold=234, count=400): - return True - if self.appear(MAP_EXIT): + def is_in_main(self, interval=0): + self.device.stuck_record_add(MAIN_GOTO_CHARACTER) + + if interval and not self.interval_is_reached(MAIN_GOTO_CHARACTER, interval=interval): + return False + + appear = False + if MAIN_GOTO_CHARACTER.match_template_binary(self.device.image): + if self.image_color_count(MAIN_GOTO_CHARACTER, color=(235, 235, 235), threshold=234, count=400): + appear = True + if not appear: + if MAP_EXIT.match_template_binary(self.device.image): + if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): + appear = True + + if appear and interval: + self.interval_reset(MAIN_GOTO_CHARACTER, interval=interval) + + return appear + + def is_in_map_exit(self, interval=0): + self.device.stuck_record_add(MAP_EXIT) + + if interval and not self.interval_is_reached(MAP_EXIT, interval=interval): + return False + + appear = False + if MAP_EXIT.match_template_binary(self.device.image): if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): - return True - return False + appear = True + + if appear and interval: + self.interval_reset(MAP_EXIT, interval=interval) + + return appear def ui_goto_main(self): return self.ui_ensure(destination=page_main) @@ -380,3 +410,50 @@ class UI(MainPage): button (Button): """ pass + + def ui_leave_special(self): + """ + Leave from: + - Rogue domains + - Character trials + + Returns: + bool: If left a special plane + + Pages: + in: Any + out: page_main + """ + if not self.is_in_map_exit(): + return False + + logger.info('UI leave special') + skip_first_screenshot = True + clicked = False + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if clicked: + if self.is_in_main(): + logger.info(f'Leave to {page_main}') + break + + if self.is_in_map_exit(interval=2): + self.device.click(MAP_EXIT) + continue + if self.handle_popup_confirm(): + continue + if self.match_template_color(START_TRIAL, interval=2): + logger.info(f'{START_TRIAL} -> {CLOSE}') + self.device.click(CLOSE) + clicked = True + continue + if self.handle_ui_close(page_gacha.check_button, interval=2): + continue + if self.appear_then_click(ROGUE_LEAVE_FOR_NOW, interval=2): + clicked = True + continue diff --git a/tasks/battle_pass/battle_pass.py b/tasks/battle_pass/battle_pass.py index a7e172b41..4386dd136 100644 --- a/tasks/battle_pass/battle_pass.py +++ b/tasks/battle_pass/battle_pass.py @@ -96,6 +96,10 @@ class BattlePassUI(UI): MAX_LEVEL = 70 def _battle_pass_wait_rewards_loaded(self, skip_first_screenshot=True): + """ + Returns: + bool: If load success + """ timeout = Timer(2, count=4).start() while 1: if skip_first_screenshot: @@ -105,12 +109,16 @@ class BattlePassUI(UI): if timeout.reached(): logger.warning('Wait rewards tab loaded timeout') - break + return False if self.appear(REWARDS_LOADED): logger.info('Rewards tab loaded') - break + return True def _battle_pass_wait_missions_loaded(self, skip_first_screenshot=True): + """ + Returns: + bool: If load success + """ timeout = Timer(2, count=4).start() while 1: if skip_first_screenshot: @@ -120,14 +128,14 @@ class BattlePassUI(UI): if timeout.reached(): logger.warning('Wait missions tab loaded timeout') - break + return False # Has scroll and last mission loaded if self.appear(MISSION_PAGE_SCROLL): color = get_color(self.device.image, MISSIONS_LOADED.area) if np.mean(color) > 128: logger.info('Missions tab loaded') - break + return True def battle_pass_goto(self, state: KEYWORDS_BATTLE_PASS_TAB): """ diff --git a/tasks/character/keywords/__init__.py b/tasks/character/keywords/__init__.py index cde9badfe..346ed08ad 100644 --- a/tasks/character/keywords/__init__.py +++ b/tasks/character/keywords/__init__.py @@ -9,6 +9,7 @@ DICT_SORTED_RANGES = { RuanMei, DanHengImbibitorLunae, Welt, + Aventurine, FuXuan, # Longer precast BlackSwan, diff --git a/tasks/character/keywords/character_list.py b/tasks/character/keywords/character_list.py index fefdb2a7e..f65273e05 100644 --- a/tasks/character/keywords/character_list.py +++ b/tasks/character/keywords/character_list.py @@ -75,8 +75,17 @@ Blade = CharacterList( jp='刃', es='Blade', ) -Bronya = CharacterList( +Boothill = CharacterList( id=9, + name='Boothill', + cn='波提欧', + cht='波提歐', + en='Boothill', + jp='ブートヒル', + es='Boothill', +) +Bronya = CharacterList( + id=10, name='Bronya', cn='布洛妮娅', cht='布洛妮婭', @@ -85,7 +94,7 @@ Bronya = CharacterList( es='Bronya', ) Clara = CharacterList( - id=10, + id=11, name='Clara', cn='克拉拉', cht='克拉拉', @@ -94,7 +103,7 @@ Clara = CharacterList( es='Clara', ) DanHeng = CharacterList( - id=11, + id=12, name='DanHeng', cn='丹恒', cht='丹恆', @@ -103,7 +112,7 @@ DanHeng = CharacterList( es='Dan Heng', ) DanHengImbibitorLunae = CharacterList( - id=12, + id=13, name='DanHengImbibitorLunae', cn='丹恒•饮月', cht='丹恆•飲月', @@ -112,7 +121,7 @@ DanHengImbibitorLunae = CharacterList( es='Dan Heng - Imbibitor Lunae', ) DrRatio = CharacterList( - id=13, + id=14, name='DrRatio', cn='真理医生', cht='真理醫生', @@ -121,7 +130,7 @@ DrRatio = CharacterList( es='Dr. Ratio', ) FuXuan = CharacterList( - id=14, + id=15, name='FuXuan', cn='符玄', cht='符玄', @@ -130,7 +139,7 @@ FuXuan = CharacterList( es='Fu Xuan', ) Gallagher = CharacterList( - id=15, + id=16, name='Gallagher', cn='加拉赫', cht='加拉赫', @@ -139,7 +148,7 @@ Gallagher = CharacterList( es='Gallagher', ) Gepard = CharacterList( - id=16, + id=17, name='Gepard', cn='杰帕德', cht='傑帕德', @@ -148,7 +157,7 @@ Gepard = CharacterList( es='Gepard', ) Guinaifen = CharacterList( - id=17, + id=18, name='Guinaifen', cn='桂乃芬', cht='桂乃芬', @@ -157,7 +166,7 @@ Guinaifen = CharacterList( es='Guinaifen', ) Hanya = CharacterList( - id=18, + id=19, name='Hanya', cn='寒鸦', cht='寒鴉', @@ -166,7 +175,7 @@ Hanya = CharacterList( es='Hanya', ) Herta = CharacterList( - id=19, + id=20, name='Herta', cn='黑塔', cht='黑塔', @@ -175,7 +184,7 @@ Herta = CharacterList( es='Herta', ) Himeko = CharacterList( - id=20, + id=21, name='Himeko', cn='姬子', cht='姬子', @@ -184,7 +193,7 @@ Himeko = CharacterList( es='Himeko', ) Hook = CharacterList( - id=21, + id=22, name='Hook', cn='虎克', cht='虎克', @@ -193,7 +202,7 @@ Hook = CharacterList( es='Hook', ) Huohuo = CharacterList( - id=22, + id=23, name='Huohuo', cn='藿藿', cht='藿藿', @@ -202,7 +211,7 @@ Huohuo = CharacterList( es='Huohuo', ) JingYuan = CharacterList( - id=23, + id=24, name='JingYuan', cn='景元', cht='景元', @@ -211,7 +220,7 @@ JingYuan = CharacterList( es='Jing Yuan', ) Jingliu = CharacterList( - id=24, + id=25, name='Jingliu', cn='镜流', cht='鏡流', @@ -220,7 +229,7 @@ Jingliu = CharacterList( es='Jingliu', ) Kafka = CharacterList( - id=25, + id=26, name='Kafka', cn='卡芙卡', cht='卡芙卡', @@ -229,7 +238,7 @@ Kafka = CharacterList( es='Kafka', ) Luka = CharacterList( - id=26, + id=27, name='Luka', cn='卢卡', cht='盧卡', @@ -238,7 +247,7 @@ Luka = CharacterList( es='Luka', ) Luocha = CharacterList( - id=27, + id=28, name='Luocha', cn='罗刹', cht='羅剎', @@ -247,7 +256,7 @@ Luocha = CharacterList( es='Luocha', ) Lynx = CharacterList( - id=28, + id=29, name='Lynx', cn='玲可', cht='玲可', @@ -256,7 +265,7 @@ Lynx = CharacterList( es='Lynx', ) March7th = CharacterList( - id=29, + id=30, name='March7th', cn='三月七', cht='三月七', @@ -265,7 +274,7 @@ March7th = CharacterList( es='Siete de Marzo', ) Misha = CharacterList( - id=30, + id=31, name='Misha', cn='米沙', cht='米沙', @@ -274,7 +283,7 @@ Misha = CharacterList( es='Misha', ) Natasha = CharacterList( - id=31, + id=32, name='Natasha', cn='娜塔莎', cht='娜塔莎', @@ -283,7 +292,7 @@ Natasha = CharacterList( es='Natasha', ) Pela = CharacterList( - id=32, + id=33, name='Pela', cn='佩拉', cht='佩拉', @@ -292,7 +301,7 @@ Pela = CharacterList( es='Pela', ) Qingque = CharacterList( - id=33, + id=34, name='Qingque', cn='青雀', cht='青雀', @@ -300,8 +309,17 @@ Qingque = CharacterList( jp='青雀', es='Qingque', ) +Robin = CharacterList( + id=35, + name='Robin', + cn='知更鸟', + cht='知更鳥', + en='Robin', + jp='ロビン', + es='Robin', +) RuanMei = CharacterList( - id=34, + id=36, name='RuanMei', cn='阮•梅', cht='阮•梅', @@ -310,7 +328,7 @@ RuanMei = CharacterList( es='Ruan Mei', ) Sampo = CharacterList( - id=35, + id=37, name='Sampo', cn='桑博', cht='桑博', @@ -319,7 +337,7 @@ Sampo = CharacterList( es='Sampo', ) Seele = CharacterList( - id=36, + id=38, name='Seele', cn='希儿', cht='希兒', @@ -328,7 +346,7 @@ Seele = CharacterList( es='Seele', ) Serval = CharacterList( - id=37, + id=39, name='Serval', cn='希露瓦', cht='希露瓦', @@ -337,7 +355,7 @@ Serval = CharacterList( es='Serval', ) SilverWolf = CharacterList( - id=38, + id=40, name='SilverWolf', cn='银狼', cht='銀狼', @@ -346,7 +364,7 @@ SilverWolf = CharacterList( es='Silver Wolf', ) Sparkle = CharacterList( - id=39, + id=41, name='Sparkle', cn='花火', cht='花火', @@ -355,7 +373,7 @@ Sparkle = CharacterList( es='Sparkle', ) Sushang = CharacterList( - id=40, + id=42, name='Sushang', cn='素裳', cht='素裳', @@ -364,7 +382,7 @@ Sushang = CharacterList( es='Sushang', ) Tingyun = CharacterList( - id=41, + id=43, name='Tingyun', cn='停云', cht='停雲', @@ -373,7 +391,7 @@ Tingyun = CharacterList( es='Tingyun', ) TopazNumby = CharacterList( - id=42, + id=44, name='TopazNumby', cn='托帕&账账', cht='托帕&帳帳', @@ -382,7 +400,7 @@ TopazNumby = CharacterList( es='Topaz y Conti', ) TrailblazerDestruction = CharacterList( - id=43, + id=45, name='TrailblazerDestruction', cn='Trailblazer•毁灭', cht='Trailblazer•毀滅', @@ -390,8 +408,17 @@ TrailblazerDestruction = CharacterList( jp='Trailblazer・壊滅', es='Trailblazer: Destrucción', ) +TrailblazerHarmony = CharacterList( + id=46, + name='TrailblazerHarmony', + cn='Trailblazer•同谐', + cht='Trailblazer•同諧', + en='Trailblazer: Harmony', + jp='Trailblazer・調和', + es='Trailblazer: Armonía', +) TrailblazerPreservation = CharacterList( - id=44, + id=47, name='TrailblazerPreservation', cn='Trailblazer•存护', cht='Trailblazer•存護', @@ -400,7 +427,7 @@ TrailblazerPreservation = CharacterList( es='Trailblazer: Conservación', ) Welt = CharacterList( - id=45, + id=48, name='Welt', cn='瓦尔特', cht='瓦爾特', @@ -409,7 +436,7 @@ Welt = CharacterList( es='Welt', ) Xueyi = CharacterList( - id=46, + id=49, name='Xueyi', cn='雪衣', cht='雪衣', @@ -418,7 +445,7 @@ Xueyi = CharacterList( es='Xueyi', ) Yanqing = CharacterList( - id=47, + id=50, name='Yanqing', cn='彦卿', cht='彥卿', @@ -427,7 +454,7 @@ Yanqing = CharacterList( es='Yanqing', ) Yukong = CharacterList( - id=48, + id=51, name='Yukong', cn='驭空', cht='馭空', diff --git a/tasks/character/switch.py b/tasks/character/switch.py index 6c625d9e9..a30e25796 100644 --- a/tasks/character/switch.py +++ b/tasks/character/switch.py @@ -16,7 +16,7 @@ from tasks.character.keywords import CharacterList, DICT_SORTED_RANGES, KEYWORDS class OcrCharacterName(OcrWhiteLetterOnComplexBackground): merge_thres_x = 20 - merge_thres_y = 20 + merge_thres_y = 10 def after_process(self, result): result = result.replace('蛆', '妲') @@ -32,6 +32,7 @@ class CharacterSwitch(UI): characters: list[CharacterList] = [] character_current: CharacterList | None = None character_buttons: list[OcrResultButton] = [] + character_is_ranged: t.Optional[bool] = None def character_update(self, skip_first_screenshot=True) -> list[CharacterList]: """ @@ -187,7 +188,7 @@ class CharacterSwitch(UI): skip_first_screenshot: Returns: - bool: If chose + bool: If chose success """ logger.info(f'Character choose: {character}') if isinstance(character, int): @@ -220,8 +221,12 @@ class CharacterSwitch(UI): # End selected = self._update_current_character() if index in selected: - logger.info('Character chose') - return True + if len(selected) > 1: + logger.warning('Multiple selected characters found, cannot guarantee character selected') + return False + else: + logger.info('Character chose') + return True if count > 3: logger.warning('Failed to choose character, assume chose') return False @@ -255,22 +260,35 @@ class CharacterSwitch(UI): logger.info('No ranged characters in team') return False - def character_switch_to_ranged(self, update=True) -> bool: + def character_switch_to_ranged(self, update=True) -> bool | None: """ Args: update: If update characters before switching Returns: bool: If using a ranged character now + or None if failed to switch """ + if self.character_is_ranged is not None: + return self.character_is_ranged + logger.hr('Character switch to ranged') if update: self.character_update() character = self._get_ranged_character() if character is True: + self.character_is_ranged = True return True elif character is False: + self.character_is_ranged = False return False + + success = self.character_switch(character) + if success: + self.character_is_ranged = True + return True else: - return self.character_switch(character) + # Cannot switch, keep None to retry at next call + self.character_is_ranged = None + return None diff --git a/tasks/combat/assets/assets_combat_support.py b/tasks/combat/assets/assets_combat_support.py index 10dc25671..982f5eaeb 100644 --- a/tasks/combat/assets/assets_combat_support.py +++ b/tasks/combat/assets/assets_combat_support.py @@ -17,40 +17,30 @@ COMBAT_SUPPORT_LIST = ButtonWrapper( name='COMBAT_SUPPORT_LIST', share=Button( file='./assets/share/combat/support/COMBAT_SUPPORT_LIST.png', - area=(67, 645, 93, 669), - search=(47, 625, 113, 689), - color=(204, 205, 205), - button=(67, 645, 93, 669), + area=(39, 645, 65, 669), + search=(19, 625, 85, 689), + color=(204, 206, 206), + button=(39, 645, 65, 669), ), ) COMBAT_SUPPORT_LIST_GRID = ButtonWrapper( name='COMBAT_SUPPORT_LIST_GRID', share=Button( file='./assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png', - area=(64, 158, 155, 627), - search=(44, 138, 175, 647), - color=(117, 107, 124), - button=(64, 158, 155, 627), + area=(36, 148, 128, 629), + search=(16, 128, 148, 649), + color=(135, 124, 133), + button=(36, 148, 128, 629), ), ) COMBAT_SUPPORT_LIST_SCROLL = ButtonWrapper( name='COMBAT_SUPPORT_LIST_SCROLL', share=Button( file='./assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png', - area=(448, 156, 452, 592), - search=(428, 136, 472, 612), - color=(130, 133, 153), - button=(448, 156, 452, 592), - ), -) -COMBAT_SUPPORT_SELECTED = ButtonWrapper( - name='COMBAT_SUPPORT_SELECTED', - share=Button( - file='./assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png', - area=(69, 114, 91, 116), - search=(49, 94, 111, 136), - color=(254, 254, 254), - button=(69, 114, 91, 116), + area=(472, 162, 476, 598), + search=(452, 142, 496, 618), + color=(150, 151, 166), + button=(472, 162, 476, 598), ), ) SUPPORT_SELECTED = ButtonWrapper( @@ -59,28 +49,28 @@ SUPPORT_SELECTED = ButtonWrapper( Button( file='./assets/share/combat/support/SUPPORT_SELECTED.png', area=(54, 202, 60, 220), - search=(40, 146, 68, 628), + search=(15, 148, 45, 629), color=(125, 126, 131), button=(54, 202, 60, 220), ), Button( file='./assets/share/combat/support/SUPPORT_SELECTED.2.png', area=(53, 547, 59, 565), - search=(40, 146, 68, 628), + search=(15, 148, 45, 629), color=(144, 146, 147), button=(53, 547, 59, 565), ), Button( file='./assets/share/combat/support/SUPPORT_SELECTED.3.png', area=(54, 432, 61, 450), - search=(40, 146, 68, 628), + search=(15, 148, 45, 629), color=(139, 139, 141), button=(54, 432, 61, 450), ), Button( file='./assets/share/combat/support/SUPPORT_SELECTED.4.png', area=(52, 417, 59, 435), - search=(40, 146, 68, 628), + search=(15, 148, 45, 629), color=(140, 138, 145), button=(52, 417, 59, 435), ), diff --git a/tasks/combat/assets/assets_combat_team.py b/tasks/combat/assets/assets_combat_team.py index 757daa624..01fd9adf8 100644 --- a/tasks/combat/assets/assets_combat_team.py +++ b/tasks/combat/assets/assets_combat_team.py @@ -50,16 +50,6 @@ TEAM_1_CHECK = ButtonWrapper( button=(391, 29, 424, 56), ), ) -TEAM_1_CLICK = ButtonWrapper( - name='TEAM_1_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_1_CLICK.png', - area=(395, 32, 421, 54), - search=(375, 12, 441, 74), - color=(80, 79, 96), - button=(395, 32, 421, 54), - ), -) TEAM_2_CHECK = ButtonWrapper( name='TEAM_2_CHECK', share=Button( @@ -70,16 +60,6 @@ TEAM_2_CHECK = ButtonWrapper( button=(491, 29, 528, 56), ), ) -TEAM_2_CLICK = ButtonWrapper( - name='TEAM_2_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_2_CLICK.png', - area=(512, 32, 525, 54), - search=(492, 12, 545, 74), - color=(86, 89, 108), - button=(512, 32, 525, 54), - ), -) TEAM_3_CHECK = ButtonWrapper( name='TEAM_3_CHECK', share=Button( @@ -90,16 +70,6 @@ TEAM_3_CHECK = ButtonWrapper( button=(591, 29, 629, 56), ), ) -TEAM_3_CLICK = ButtonWrapper( - name='TEAM_3_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_3_CLICK.png', - area=(612, 32, 625, 54), - search=(592, 12, 645, 74), - color=(78, 82, 95), - button=(612, 32, 625, 54), - ), -) TEAM_4_CHECK = ButtonWrapper( name='TEAM_4_CHECK', share=Button( @@ -110,16 +80,6 @@ TEAM_4_CHECK = ButtonWrapper( button=(691, 29, 730, 56), ), ) -TEAM_4_CLICK = ButtonWrapper( - name='TEAM_4_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_4_CLICK.png', - area=(712, 32, 726, 54), - search=(692, 12, 746, 74), - color=(67, 69, 82), - button=(712, 32, 726, 54), - ), -) TEAM_5_CHECK = ButtonWrapper( name='TEAM_5_CHECK', share=Button( @@ -130,16 +90,6 @@ TEAM_5_CHECK = ButtonWrapper( button=(791, 29, 829, 56), ), ) -TEAM_5_CLICK = ButtonWrapper( - name='TEAM_5_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_5_CLICK.png', - area=(813, 32, 825, 54), - search=(793, 12, 845, 74), - color=(83, 83, 93), - button=(813, 32, 825, 54), - ), -) TEAM_6_CHECK = ButtonWrapper( name='TEAM_6_CHECK', share=Button( @@ -150,16 +100,6 @@ TEAM_6_CHECK = ButtonWrapper( button=(551, 29, 589, 56), ), ) -TEAM_6_CLICK = ButtonWrapper( - name='TEAM_6_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_6_CLICK.png', - area=(572, 32, 586, 54), - search=(552, 12, 606, 74), - color=(86, 91, 109), - button=(572, 32, 586, 54), - ), -) TEAM_7_CHECK = ButtonWrapper( name='TEAM_7_CHECK', share=Button( @@ -170,16 +110,6 @@ TEAM_7_CHECK = ButtonWrapper( button=(651, 29, 689, 56), ), ) -TEAM_7_CLICK = ButtonWrapper( - name='TEAM_7_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_7_CLICK.png', - area=(673, 32, 685, 54), - search=(653, 12, 705, 74), - color=(63, 66, 76), - button=(673, 32, 685, 54), - ), -) TEAM_8_CHECK = ButtonWrapper( name='TEAM_8_CHECK', share=Button( @@ -190,16 +120,6 @@ TEAM_8_CHECK = ButtonWrapper( button=(751, 29, 789, 56), ), ) -TEAM_8_CLICK = ButtonWrapper( - name='TEAM_8_CLICK', - share=Button( - file='./assets/share/combat/team/TEAM_8_CLICK.png', - area=(772, 32, 786, 54), - search=(752, 12, 806, 74), - color=(92, 92, 100), - button=(772, 32, 786, 54), - ), -) TEAM_9_CHECK = ButtonWrapper( name='TEAM_9_CHECK', share=Button( @@ -210,14 +130,24 @@ TEAM_9_CHECK = ButtonWrapper( button=(851, 29, 889, 56), ), ) -TEAM_9_CLICK = ButtonWrapper( - name='TEAM_9_CLICK', +TEAM_NEXT = ButtonWrapper( + name='TEAM_NEXT', share=Button( - file='./assets/share/combat/team/TEAM_9_CLICK.png', - area=(872, 32, 886, 54), - search=(852, 12, 906, 74), - color=(73, 73, 83), - button=(872, 32, 886, 54), + file='./assets/share/combat/team/TEAM_NEXT.png', + area=(1202, 313, 1234, 375), + search=(1182, 293, 1254, 395), + color=(52, 56, 77), + button=(1202, 313, 1234, 375), + ), +) +TEAM_PREV = ButtonWrapper( + name='TEAM_PREV', + share=Button( + file='./assets/share/combat/team/TEAM_PREV.png', + area=(46, 314, 78, 375), + search=(26, 294, 98, 395), + color=(108, 113, 136), + button=(46, 314, 78, 375), ), ) TEAM_SEARCH = ButtonWrapper( diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index 9767615cd..c460b1f77 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -82,14 +82,11 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo logger.hr('Combat prepare') skip_first_screenshot = True if support_character: - # To set team before support set - pre_set_team = True # Block COMBAT_TEAM_PREPARE before support set support_set = False else: - pre_set_team = False support_set = True - logger.info([support_character, pre_set_team, support_set]) + logger.info([support_character, support_set]) trial = 0 while 1: if skip_first_screenshot: @@ -107,18 +104,17 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo raise RequestHumanTakeover # Click - if support_character and self.appear(COMBAT_TEAM_SUPPORT): - if pre_set_team: - self.team_set(team) - pre_set_team = False - continue + if support_character and self.appear(COMBAT_TEAM_SUPPORT, interval=2): + self.team_set(team) self.support_set(support_character) + self.interval_reset(COMBAT_TEAM_SUPPORT) support_set = True continue if support_set and self.appear(COMBAT_TEAM_PREPARE, interval=2): self.team_set(team) self.device.click(COMBAT_TEAM_PREPARE) self.interval_reset(COMBAT_TEAM_PREPARE) + self.interval_reset(COMBAT_TEAM_SUPPORT) continue if self.appear(COMBAT_TEAM_PREPARE): self.interval_reset(COMBAT_PREPARE) @@ -131,6 +127,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo trial += 1 continue if self.handle_combat_interact(): + self.map_A_timer.reset() continue if self.handle_ascension_dungeon_prepare(): continue @@ -160,7 +157,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo if callable(expected_end) and expected_end(): logger.info(f'Combat execute ended at {expected_end.__name__}') break - if self.appear(COMBAT_AGAIN): + if (self.appear(COMBAT_AGAIN) and + self.image_color_count(COMBAT_AGAIN, color=(227, 227, 228), threshold=221, count=50)): logger.info(f'Combat execute ended at {COMBAT_AGAIN}') break if self.is_in_main(): @@ -177,6 +175,9 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo is_executing = False if self.handle_combat_state(): continue + # Battle pass popup appears just after combat finished and before blessings + if self.handle_battle_pass_notification(): + continue def _combat_can_again(self) -> bool: """ @@ -215,6 +216,9 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo Returns: bool: True to re-enter combat and run with another wave settings """ + if self.config.stored.TrailblazePower.value < self.combat_wave_cost: + logger.info('Current trailblaze power is not enough for next run') + return False # Wave limit if self.combat_wave_limit: if self.combat_wave_done < self.combat_wave_limit: @@ -227,6 +231,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo if self.config.stored.TrailblazePower.value >= self.combat_wave_cost: logger.info('Still having some trailblaze power run with less waves to empty it') return True + return False def combat_finish(self) -> bool: """ diff --git a/tasks/combat/skill.py b/tasks/combat/skill.py index 35e8c05fa..081c9b44d 100644 --- a/tasks/combat/skill.py +++ b/tasks/combat/skill.py @@ -13,7 +13,7 @@ class CombatSkill(UI): if not self.appear(IN_SKILL): return False - if not self.image_color_count(IN_SKILL, color=(255, 255, 255), threshold=221, count=50): + if not self.image_color_count(IN_SKILL, color=(255, 255, 255), threshold=180, count=50): return False return True @@ -51,6 +51,10 @@ class CombatSkill(UI): logger.info(f'Skill used: {button} (icon changed)') break + if self.is_in_main(): + logger.warning('_skill_click ended at is_in_main') + break + def _is_skill_active(self, button): flag = self.image_color_count(button, color=(220, 196, 145), threshold=221, count=50) return flag diff --git a/tasks/combat/support.py b/tasks/combat/support.py index 568b74b68..d53548248 100644 --- a/tasks/combat/support.py +++ b/tasks/combat/support.py @@ -9,7 +9,7 @@ from module.ui.scroll import AdaptiveScroll from tasks.base.assets.assets_base_popup import POPUP_CANCEL from tasks.base.ui import UI from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST, \ - COMBAT_SUPPORT_LIST_GRID, COMBAT_SUPPORT_LIST_SCROLL, COMBAT_SUPPORT_SELECTED, SUPPORT_SELECTED + COMBAT_SUPPORT_LIST_GRID, COMBAT_SUPPORT_LIST_SCROLL, SUPPORT_SELECTED from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_DISMISSSUPPORT, COMBAT_TEAM_SUPPORT @@ -79,8 +79,8 @@ class SupportCharacter: Returns: tuple: (x1, y1, x2, y2) of selected icon search area """ - return ( - self.button[0], self.button[1] - 5, self.button[0] + 30, self.button[1]) if self.button else None + # Check the left of character avatar + return 0, self.button[1], self.button[0], self.button[3] class NextSupportCharacter: @@ -130,6 +130,7 @@ class CombatSupport(UI): out: COMBAT_PREPARE """ logger.hr("Combat support") + self.interval_clear(COMBAT_TEAM_SUPPORT) skip_first_screenshot = True selected_support = False while 1: @@ -143,7 +144,7 @@ class CombatSupport(UI): return True # Click - if self.appear(COMBAT_TEAM_SUPPORT, interval=1): + if self.appear(COMBAT_TEAM_SUPPORT, interval=2): self.device.click(COMBAT_TEAM_SUPPORT) self.interval_reset(COMBAT_TEAM_SUPPORT) continue @@ -154,15 +155,30 @@ class CombatSupport(UI): self._select_next_support() self.interval_reset(POPUP_CANCEL) continue - if self.appear(COMBAT_SUPPORT_LIST, interval=1): + if self.appear(COMBAT_SUPPORT_LIST, interval=2): + scroll = AdaptiveScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, + name=COMBAT_SUPPORT_LIST_SCROLL.name) + if not scroll.appear(main=self): + self.interval_clear(COMBAT_SUPPORT_LIST) + continue if not selected_support and support_character_name != "FirstCharacter": - self._search_support( - support_character_name) # Search support + self._search_support(support_character_name) # Search support selected_support = True self.device.click(COMBAT_SUPPORT_ADD) self.interval_reset(COMBAT_SUPPORT_LIST) continue + def _get_character(self, support_character_name: str) -> SupportCharacter: + if support_character_name.startswith("Trailblazer"): + character = SupportCharacter(f"Stelle{support_character_name[11:]}", self.device.image) + if character: + return character + character = SupportCharacter(f"Caelum{support_character_name[11:]}", self.device.image) + # Should return something + return character + else: + return SupportCharacter(support_character_name, self.device.image) + def _search_support(self, support_character_name: str = "JingYuan"): """ Args: @@ -176,6 +192,14 @@ class CombatSupport(UI): out: COMBAT_SUPPORT_LIST """ logger.hr("Combat support search") + # Search prioritize characters + character = self._get_character(support_character_name) + if character: + logger.info("Support found in first page") + if self._select_support(character): + return True + + # Search in the following pages scroll = AdaptiveScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, name=COMBAT_SUPPORT_LIST_SCROLL.name) if scroll.appear(main=self): @@ -186,37 +210,30 @@ class CombatSupport(UI): scroll.drag_threshold = backup scroll.set_top(main=self) - logger.info("Searching support") - skip_first_screenshot = False - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() + logger.info("Searching support") + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() - if not support_character_name.startswith("Trailblazer"): - character = SupportCharacter( - support_character_name, self.device.image) + character = self._get_character(support_character_name) + if character: + logger.info("Support found") + if self._select_support(character): + return True else: - character = SupportCharacter(f"Stelle{support_character_name[11:]}", - self.device.image) or SupportCharacter( - f"Caelum{support_character_name[11:]}", self.device.image) - - if character: - logger.info("Support found") - if self._select_support(character): - return True - else: - logger.warning("Support not selected") - return False - - if not scroll.at_bottom(main=self): - scroll.next_page(main=self) - continue - else: - logger.info("Support not found") + logger.warning("Support not selected") return False + if not scroll.at_bottom(main=self): + scroll.next_page(main=self) + continue + else: + logger.info("Support not found") + return False + def _select_support(self, character: SupportCharacter): """ Args: @@ -227,7 +244,7 @@ class CombatSupport(UI): out: COMBAT_SUPPORT_LIST """ logger.hr("Combat support select") - COMBAT_SUPPORT_SELECTED.matched_button.search = character.selected_icon_search() + logger.info(f'Select: {character}') skip_first_screenshot = False interval = Timer(2) while 1: @@ -237,7 +254,10 @@ class CombatSupport(UI): self.device.screenshot() # End - if self.appear(COMBAT_SUPPORT_SELECTED, similarity=0.75): + area = character.selected_icon_search() + image = self.image_crop(area, copy=False) + if SUPPORT_SELECTED.match_template(image, similarity=0.75, direct_match=True): + logger.info('Character support selected') return True if interval.reached(): @@ -274,6 +294,7 @@ class CombatSupport(UI): in: COMBAT_SUPPORT_LIST out: COMBAT_SUPPORT_LIST """ + logger.hr("Next support select") skip_first_screenshot = True scroll = AdaptiveScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, name=COMBAT_SUPPORT_LIST_SCROLL.name) @@ -288,6 +309,7 @@ class CombatSupport(UI): # End if next_support is not None and next_support.is_next_support_character_selected(self.device.image): + logger.info('Next support selected') return if interval.reached(): diff --git a/tasks/combat/team.py b/tasks/combat/team.py index babd4d29e..e2535b3ab 100644 --- a/tasks/combat/team.py +++ b/tasks/combat/team.py @@ -1,7 +1,6 @@ import re from module.base.timer import Timer -from module.base.utils import random_rectangle_vector_opted from module.logger import logger from tasks.base.ui import UI from tasks.combat.assets.assets_combat_team import * @@ -13,74 +12,32 @@ def button_to_index(button: ButtonWrapper) -> int: return int(res.group(1)) else: logger.warning(f'Cannot convert team button to index: {button}') - return 1 - - -def index_to_button(index: int) -> ButtonWrapper: - match index: - case 1: - return TEAM_1_CLICK - case 2: - return TEAM_2_CLICK - case 3: - return TEAM_3_CLICK - case 4: - return TEAM_4_CLICK - case 5: - return TEAM_5_CLICK - case 6: - return TEAM_6_CLICK - case 7: - return TEAM_7_CLICK - case 8: - return TEAM_8_CLICK - case 9: - return TEAM_9_CLICK - case _: - logger.warning(f'Invalid team index: {index}') - return TEAM_1_CLICK + return 0 class CombatTeam(UI): - def _get_team(self) -> tuple[list[int], int]: + def _get_team(self) -> int: """ Returns: - list[str]: List of displayed team index. - int: Current team index, or None if current team is not insight + int: Current team index, or 0 if current team is not insight """ - list_team = [] - for button in [ - TEAM_1_CLICK, TEAM_2_CLICK, TEAM_3_CLICK, TEAM_4_CLICK, TEAM_5_CLICK, - TEAM_6_CLICK, TEAM_7_CLICK, TEAM_8_CLICK, TEAM_9_CLICK - ]: - button.load_search(TEAM_SEARCH.area) - if self.appear(button): - list_team.append(button_to_index(button)) - current_team = None + team = 0 for button in [ TEAM_1_CHECK, TEAM_2_CHECK, TEAM_3_CHECK, TEAM_4_CHECK, TEAM_5_CHECK, TEAM_6_CHECK, TEAM_7_CHECK, TEAM_8_CHECK, TEAM_9_CHECK ]: button.load_search(TEAM_SEARCH.area) - if self.appear(button): - current_team = button_to_index(button) - list_team.append(button_to_index(button)) - list_team = list(sorted(list_team)) + if self.appear(button, similarity=0.92): + if self.image_color_count(button.button, color=(255, 234, 191), threshold=180, count=50): + team = button_to_index(button) + break - def show(index): - if index == current_team: - return f'*0{index}*' - else: - return f'0{index}' + return team - # [Team] 01 02 *03* 04 05 06 - logger.attr('Team', ' '.join([show(i) for i in list_team])) - return list_team, current_team - - def team_set(self, team: int = 1, skip_first_screenshot=True) -> bool: + def team_set(self, index: int = 1, skip_first_screenshot=True) -> bool: """ Args: - team: Team index, 1 to 9. + index: Team index, 1 to 9. skip_first_screenshot: Returns: @@ -89,7 +46,7 @@ class CombatTeam(UI): Pages: in: page_team """ - logger.info(f'Team set: {team}') + logger.info(f'Team set: {index}') # Wait teams show up timeout = Timer(1, count=5).start() while 1: @@ -102,18 +59,19 @@ class CombatTeam(UI): if timeout.reached(): logger.warning('Wait current team timeout') break - _, current = self._get_team() + current = self._get_team() if current: - if current == team: - logger.info(f'Selected to the correct team') + if current == index: + logger.attr('Team', current) + logger.info(f'Already selected to the correct team') return False else: break # Set team - click_interval = Timer(2) - swipe_interval = Timer(2) + retry = Timer(2, count=10) skip_first_screenshot = True + clicked = False while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -121,31 +79,26 @@ class CombatTeam(UI): self.device.screenshot() # End - list_team, current = self._get_team() - if current and current == team: + current = self._get_team() + logger.attr('Team', current) + if current == index: logger.info(f'Selected to the correct team') - return True - + return clicked # Click - if team in list_team: - if click_interval.reached(): - self.device.click(index_to_button(team)) - click_interval.reset() - continue - # At left - elif team < min(list_team): - if swipe_interval.reached(): - p1, p2 = random_rectangle_vector_opted( - (350, 0), box=TEAM_SEARCH.area, random_range=(-20, -10, 20, 10)) - self.device.drag(p1, p2, name=f'TEAM_DRAG') - swipe_interval.reset() - # At right - elif team > max(list_team): - if swipe_interval.reached(): - p1, p2 = random_rectangle_vector_opted( - (-350, 0), box=TEAM_SEARCH.area, random_range=(-20, -10, 20, 10)) - self.device.drag(p1, p2, name=f'TEAM_DRAG') - swipe_interval.reset() + if retry.reached(): + diff = index - current + right = diff % 9 + left = -diff % 9 + if right <= left: + self.device.multi_click(TEAM_NEXT, right) + clicked = True + else: + self.device.multi_click(TEAM_PREV, left) + clicked = True + retry.reset() + continue + + return clicked def handle_combat_team_prepare(self, team: int = 1) -> bool: """ diff --git a/tasks/daily/trail.py b/tasks/daily/trail.py index ed16487a0..1b2d8d611 100644 --- a/tasks/daily/trail.py +++ b/tasks/daily/trail.py @@ -99,7 +99,8 @@ class CharacterTrial(UI): if self.match_template_color(START_TRIAL): break - if self.appear_then_click(MAP_EXIT): + if self.is_in_map_exit(interval=2): + self.device.click(MAP_EXIT) continue if self.handle_popup_confirm(): continue diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index 94fb93bef..a2affe666 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -215,7 +215,7 @@ class Dungeon(DungeonStamina, DungeonEvent, Combat): calyx = self.get_double_event_remain() if self.has_double_relic_event(): self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Cavern_of_Corrosion) - relic = self.get_double_event_remain() + relic = self.get_double_rogue_remain() with self.config.multi_set(): self.config.stored.DungeonDouble.calyx = calyx self.config.stored.DungeonDouble.relic = relic diff --git a/tasks/dungeon/event.py b/tasks/dungeon/event.py index cde2c8951..291baec5e 100644 --- a/tasks/dungeon/event.py +++ b/tasks/dungeon/event.py @@ -32,6 +32,8 @@ class DungeonEvent(UI): """ has = self.image_color_count(DOUBLE_CALYX_EVENT_TAG, color=(252, 209, 123), threshold=221, count=50) has |= self.image_color_count(DOUBLE_CALYX_EVENT_TAG, color=(252, 251, 140), threshold=221, count=50) + # Anniversary 3x rogue event + has |= self.image_color_count(DOUBLE_CALYX_EVENT_TAG, color=(229, 62, 44), threshold=221, count=50) logger.attr('Double calyx', has) return has @@ -42,6 +44,8 @@ class DungeonEvent(UI): """ has = self.image_color_count(DOUBLE_RELIC_EVENT_TAG, color=(252, 209, 123), threshold=221, count=50) has |= self.image_color_count(DOUBLE_RELIC_EVENT_TAG, color=(252, 251, 140), threshold=221, count=50) + # Anniversary 3x rogue event + has |= self.image_color_count(DOUBLE_RELIC_EVENT_TAG, color=(229, 62, 44), threshold=221, count=50) logger.attr('Double relic', has) return has @@ -67,6 +71,12 @@ class DungeonEvent(UI): color=(231, 188, 103), threshold=240, count=1000 ) + # Anniversary 3x event + has |= self.image_color_count( + OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT, + color=(229, 62, 44), + threshold=221, count=50 + ) logger.attr('Double event at combat', has) return has diff --git a/tasks/dungeon/keywords/classes.py b/tasks/dungeon/keywords/classes.py index 88bccc895..5b9da5a36 100644 --- a/tasks/dungeon/keywords/classes.py +++ b/tasks/dungeon/keywords/classes.py @@ -3,7 +3,7 @@ from functools import cached_property from typing import ClassVar from module.exception import ScriptError -from module.ocr.keyword import Keyword +from module.ocr.keyword import Keyword, parse_name @dataclass(repr=False) @@ -183,6 +183,48 @@ class DungeonList(Keyword): else: return '' + @classmethod + def find_dungeon_by_string(cls, cn='', en='', **kwargs): + """ + Args: + cn: Any substring in dungeon name + en: + **kwargs: Filter properties, e.g. is_Echo_of_War=True + + Returns: + DungeonList: or None + """ + if cn: + string = parse_name(cn) + lang = 'cn' + elif en: + string = parse_name(en) + lang = 'en' + else: + return None + + def find(obj: "DungeonList"): + name = obj._keywords_to_find(lang=lang, ignore_punctuation=True)[0] + if string in name: + return True + return False + + # From SelectedGrids + def matched(obj): + flag = True + for k, v in kwargs.items(): + obj_v = obj.__getattribute__(k) + if type(obj_v) != type(v) or obj_v != v: + flag = False + return flag + + dungeons = [grid for grid in cls.instances.values() if find(grid) and matched(grid)] + if len(dungeons) == 1: + return dungeons[0] + else: + return None + + @dataclass(repr=False) class DungeonEntrance(Keyword): diff --git a/tasks/dungeon/keywords/dungeon.py b/tasks/dungeon/keywords/dungeon.py index 35ee6a29a..c4301cba0 100644 --- a/tasks/dungeon/keywords/dungeon.py +++ b/tasks/dungeon/keywords/dungeon.py @@ -98,7 +98,7 @@ Calyx_Crimson_Destruction_Herta_StorageZone = DungeonList( name='Calyx_Crimson_Destruction_Herta_StorageZone', cn='毁灭之蕾•拟造花萼(赤)', cht='毀滅之蕾•擬造花萼(赤)', - en='Bud of Destruction', + en='Calyx (Crimson): Bud of Destruction', jp='疑似花萼(赤)・壊滅の蕾', es='Flor de la Destrucción', plane_id=2000201, @@ -108,7 +108,7 @@ Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape = DungeonList( name='Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape', cn='毁灭之蕾•拟造花萼(赤)', cht='毀滅之蕾•擬造花萼(赤)', - en='Bud of Destruction', + en='Calyx (Crimson): Bud of Destruction', jp='疑似花萼(赤)・壊滅の蕾', es='Flor de la Destrucción', plane_id=2023201, @@ -118,7 +118,7 @@ Calyx_Crimson_Preservation_Herta_SupplyZone = DungeonList( name='Calyx_Crimson_Preservation_Herta_SupplyZone', cn='存护之蕾•拟造花萼(赤)', cht='存護之蕾•擬造花萼(赤)', - en='Bud of Preservation', + en='Calyx (Crimson): Bud of Preservation', jp='疑似花萼(赤)・存護の蕾', es='Flor de la Conservación', plane_id=2000301, @@ -128,7 +128,7 @@ Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark = DungeonList( name='Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark', cn='存护之蕾•拟造花萼(赤)', cht='存護之蕾•擬造花萼(赤)', - en='Bud of Preservation', + en='Calyx (Crimson): Bud of Preservation', jp='疑似花萼(赤)・存護の蕾', es='Flor de la Conservación', plane_id=2032101, @@ -138,383 +138,413 @@ Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains = DungeonList( name='Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains', cn='巡猎之蕾•拟造花萼(赤)', cht='巡獵之蕾•擬造花萼(赤)', - en='Bud of The Hunt', + en='Calyx (Crimson): Bud of The Hunt', jp='疑似花萼(赤)・巡狩の蕾', es='Flor de la Cacería', plane_id=2010101, ) -Calyx_Crimson_Abundance_Jarilo_BackwaterPass = DungeonList( +Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue = DungeonList( id=15, + name='Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue', + cn='巡猎之蕾•拟造花萼(赤)', + cht='巡獵之蕾•擬造花萼(赤)', + en='Calyx (Crimson): Bud of The Hunt', + jp='疑似花萼(赤)・巡狩の蕾', + es='Cáliz (carmesí): Flor de la Cacería', + plane_id=2033101, +) +Calyx_Crimson_Abundance_Jarilo_BackwaterPass = DungeonList( + id=16, name='Calyx_Crimson_Abundance_Jarilo_BackwaterPass', cn='丰饶之蕾•拟造花萼(赤)', cht='豐饒之蕾•擬造花萼(赤)', - en='Bud of Abundance', + en='Calyx (Crimson): Bud of Abundance', jp='疑似花萼(赤)・豊穣の蕾', es='Flor de la Abundancia', plane_id=2011101, ) Calyx_Crimson_Abundance_Luofu_FyxestrollGarden = DungeonList( - id=16, + id=17, name='Calyx_Crimson_Abundance_Luofu_FyxestrollGarden', cn='丰饶之蕾•拟造花萼(赤)', cht='豐饒之蕾•擬造花萼(赤)', - en='Bud of Abundance', + en='Calyx (Crimson): Bud of Abundance', jp='疑似花萼(赤)・豊穣の蕾', es='Flor de la Abundancia', plane_id=2022301, ) Calyx_Crimson_Erudition_Jarilo_RivetTown = DungeonList( - id=17, + id=18, name='Calyx_Crimson_Erudition_Jarilo_RivetTown', cn='智识之蕾•拟造花萼(赤)', cht='智識之蕾•擬造花萼(赤)', - en='Bud of Erudition', + en='Calyx (Crimson): Bud of Erudition', jp='疑似花萼(赤)・知恵の蕾', es='Flor de la Erudición', plane_id=2012201, ) Calyx_Crimson_Harmony_Jarilo_RobotSettlement = DungeonList( - id=18, + id=19, name='Calyx_Crimson_Harmony_Jarilo_RobotSettlement', cn='同谐之蕾•拟造花萼(赤)', cht='同諧之蕾•擬造花萼(赤)', - en='Bud of Harmony', + en='Calyx (Crimson): Bud of Harmony', jp='疑似花萼(赤)・調和の蕾', es='Flor de la Armonía', plane_id=2012301, ) Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape = DungeonList( - id=19, + id=20, name='Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape', cn='同谐之蕾•拟造花萼(赤)', cht='同諧之蕾•擬造花萼(赤)', - en='Bud of Harmony', + en='Calyx (Crimson): Bud of Harmony', jp='疑似花萼(赤)・調和の蕾', es='Flor de la Armonía', plane_id=2031101, ) Calyx_Crimson_Nihility_Jarilo_GreatMine = DungeonList( - id=20, + id=21, name='Calyx_Crimson_Nihility_Jarilo_GreatMine', cn='虚无之蕾•拟造花萼(赤)', cht='虛無之蕾•擬造花萼(赤)', - en='Bud of Nihility', + en='Calyx (Crimson): Bud of Nihility', jp='疑似花萼(赤)・虚無の蕾', es='Flor de la Nihilidad', plane_id=2012101, ) Calyx_Crimson_Nihility_Luofu_AlchemyCommission = DungeonList( - id=21, + id=22, name='Calyx_Crimson_Nihility_Luofu_AlchemyCommission', cn='虚无之蕾•拟造花萼(赤)', cht='虛無之蕾•擬造花萼(赤)', - en='Bud of Nihility', + en='Calyx (Crimson): Bud of Nihility', jp='疑似花萼(赤)・虚無の蕾', es='Flor de la Nihilidad', plane_id=2023101, ) Stagnant_Shadow_Quanta = DungeonList( - id=22, + id=23, name='Stagnant_Shadow_Quanta', cn='空海之形•凝滞虚影', cht='空海之形•凝滯虛影', - en='Shape of Quanta', + en='Stagnant Shadow: Shape of Quanta', jp='凝結虚影・虚海の形', es='Forma del cuanto', plane_id=2000101, ) Stagnant_Shadow_Gust = DungeonList( - id=23, + id=24, name='Stagnant_Shadow_Gust', cn='巽风之形•凝滞虚影', cht='巽風之形•凝滯虛影', - en='Shape of Gust', + en='Stagnant Shadow: Shape of Gust', jp='凝結虚影・薫風の形', es='Forma del aire', plane_id=2012201, ) Stagnant_Shadow_Fulmination = DungeonList( - id=24, + id=25, name='Stagnant_Shadow_Fulmination', cn='鸣雷之形•凝滞虚影', cht='鳴雷之形•凝滯虛影', - en='Shape of Fulmination', + en='Stagnant Shadow: Shape of Fulmination', jp='凝結虚影・鳴雷の形', es='Forma del trueno', plane_id=2013201, ) Stagnant_Shadow_Blaze = DungeonList( - id=25, + id=26, name='Stagnant_Shadow_Blaze', cn='炎华之形•凝滞虚影', cht='炎華之形•凝滯虛影', - en='Shape of Blaze', + en='Stagnant Shadow: Shape of Blaze', jp='凝結虚影・炎華の形', es='Forma de las llamas', plane_id=2013101, ) Stagnant_Shadow_Spike = DungeonList( - id=26, + id=27, name='Stagnant_Shadow_Spike', cn='锋芒之形•凝滞虚影', cht='鋒芒之形•凝滯虛影', - en='Shape of Spike', + en='Stagnant Shadow: Shape of Spike', jp='凝結虚影・切先の形', es='Forma afilada', plane_id=2012101, ) Stagnant_Shadow_Rime = DungeonList( - id=27, + id=28, name='Stagnant_Shadow_Rime', cn='霜晶之形•凝滞虚影', cht='霜晶之形•凝滯虛影', - en='Shape of Rime', + en='Stagnant Shadow: Shape of Rime', jp='凝結虚影・霜晶の形', es='Forma de la escarcha', plane_id=2013201, ) Stagnant_Shadow_Mirage = DungeonList( - id=28, + id=29, name='Stagnant_Shadow_Mirage', cn='幻光之形•凝滞虚影', cht='幻光之形•凝滯虛影', - en='Shape of Mirage', + en='Stagnant Shadow: Shape of Mirage', jp='凝結虚影・幻光の形', es='Forma del espejismo', plane_id=2011101, ) Stagnant_Shadow_Icicle = DungeonList( - id=29, + id=30, name='Stagnant_Shadow_Icicle', cn='冰棱之形•凝滞虚影', cht='冰稜之形•凝滯虛影', - en='Shape of Icicle', + en='Stagnant Shadow: Shape of Icicle', jp='凝結虚影・氷柱の形', es='Forma del témpano', plane_id=2021101, ) Stagnant_Shadow_Doom = DungeonList( - id=30, + id=31, name='Stagnant_Shadow_Doom', cn='震厄之形•凝滞虚影', cht='震厄之形•凝滯虛影', - en='Shape of Doom', + en='Stagnant Shadow: Shape of Doom', jp='凝結虚影・震厄の形', es='Forma de la perdición', plane_id=2021201, ) Stagnant_Shadow_Puppetry = DungeonList( - id=31, + id=32, name='Stagnant_Shadow_Puppetry', cn='偃偶之形•凝滞虚影', cht='偃偶之形•凝滯虛影', - en='Shape of Puppetry', + en='Stagnant Shadow: Shape of Puppetry', jp='凝結虚影・傀儡の形', es='Forma de las marionetas', plane_id=2022201, ) Stagnant_Shadow_Abomination = DungeonList( - id=32, + id=33, name='Stagnant_Shadow_Abomination', cn='孽兽之形•凝滞虚影', cht='孽獸之形•凝滯虛影', - en='Shape of Abomination', + en='Stagnant Shadow: Shape of Abomination', jp='凝結虚影・厄獣の形', es='Forma de la abominación', plane_id=2023201, ) Stagnant_Shadow_Scorch = DungeonList( - id=33, + id=34, name='Stagnant_Shadow_Scorch', cn='燔灼之形•凝滞虚影', cht='燔灼之形•凝滯虛影', - en='Shape of Scorch', + en='Stagnant Shadow: Shape of Scorch', jp='凝結虚影・燔灼の形', es='Forma abrasada', plane_id=2012101, ) Stagnant_Shadow_Celestial = DungeonList( - id=34, + id=35, name='Stagnant_Shadow_Celestial', cn='天人之形•凝滞虚影', cht='天人之形•凝滯虛影', - en='Shape of Celestial', + en='Stagnant Shadow: Shape of Celestial', jp='凝結虚影・天人の形', es='Forma de lo celestial', plane_id=2023101, ) Stagnant_Shadow_Perdition = DungeonList( - id=35, + id=36, name='Stagnant_Shadow_Perdition', cn='幽府之形•凝滞虚影', cht='幽府之形•凝滯虛影', - en='Shape of Perdition', + en='Stagnant Shadow: Shape of Perdition', jp='凝結虚影・幽府の形', es='Forma del aislamiento', plane_id=2022301, ) Stagnant_Shadow_Nectar = DungeonList( - id=36, + id=37, name='Stagnant_Shadow_Nectar', cn='冰酿之形•凝滞虚影', cht='冰釀之形•凝滯虛影', - en='Shape of Nectar', + en='Stagnant Shadow: Shape of Nectar', jp='凝結虚影・氷醸の形', es='Forma del néctar', plane_id=2031101, ) Stagnant_Shadow_Roast = DungeonList( - id=37, + id=38, name='Stagnant_Shadow_Roast', cn='焦炙之形•凝滞虚影', cht='焦炙之形•凝滯虛影', - en='Shape of Roast', + en='Stagnant Shadow: Shape of Roast', jp='凝結虚影・焦灼の形', es='Forma del agostamiento', plane_id=2031301, ) Stagnant_Shadow_Ire = DungeonList( - id=38, + id=39, name='Stagnant_Shadow_Ire', cn='嗔怒之形•凝滞虚影', cht='嗔怒之形•凝滯虛影', - en='Shape of Ire', + en='Stagnant Shadow: Shape of Ire', jp='凝結虚影・憤怒の形', es='Forma de la ira', plane_id=2032201, ) +Stagnant_Shadow_Duty = DungeonList( + id=40, + name='Stagnant_Shadow_Duty', + cn='职司之形•凝滞虚影', + cht='職司之形•凝滯虛影', + en='Stagnant Shadow: Shape of Duty', + jp='凝結虚影・職掌の形', + es='Sombra paralizada: Forma del deber', + plane_id=2032101, +) Cavern_of_Corrosion_Path_of_Gelid_Wind = DungeonList( - id=39, + id=41, name='Cavern_of_Corrosion_Path_of_Gelid_Wind', cn='霜风之径•侵蚀隧洞', cht='霜風之徑•侵蝕隧洞', - en='Path of Gelid Wind', + en='Cavern of Corrosion: Path of Gelid Wind', jp='侵蝕トンネル・霜風の路', es='Senda del viento gélido', plane_id=2000201, ) Cavern_of_Corrosion_Path_of_Jabbing_Punch = DungeonList( - id=40, + id=42, name='Cavern_of_Corrosion_Path_of_Jabbing_Punch', cn='迅拳之径•侵蚀隧洞', cht='迅拳之徑•侵蝕隧洞', - en='Path of Jabbing Punch', + en='Cavern of Corrosion: Path of Jabbing Punch', jp='侵蝕トンネル・迅拳の路', es='Senda de los puños rápidos', plane_id=2013101, ) Cavern_of_Corrosion_Path_of_Drifting = DungeonList( - id=41, + id=43, name='Cavern_of_Corrosion_Path_of_Drifting', cn='漂泊之径•侵蚀隧洞', cht='漂泊之徑•侵蝕隧洞', - en='Path of Drifting', + en='Cavern of Corrosion: Path of Drifting', jp='侵蝕トンネル・漂泊の路', es='Senda de la deriva', plane_id=2013201, ) Cavern_of_Corrosion_Path_of_Providence = DungeonList( - id=42, + id=44, name='Cavern_of_Corrosion_Path_of_Providence', cn='睿治之径•侵蚀隧洞', cht='睿治之徑•侵蝕隧洞', - en='Path of Providence', + en='Cavern of Corrosion: Path of Providence', jp='侵蝕トンネル・睿治の路', es='Senda de la providencia', plane_id=2013401, ) Cavern_of_Corrosion_Path_of_Holy_Hymn = DungeonList( - id=43, + id=45, name='Cavern_of_Corrosion_Path_of_Holy_Hymn', cn='圣颂之径•侵蚀隧洞', cht='聖頌之徑•侵蝕隧洞', - en='Path of Holy Hymn', + en='Cavern of Corrosion: Path of Holy Hymn', jp='侵蝕トンネル・聖頌の路', es='Senda del himno sagrado', plane_id=2021101, ) Cavern_of_Corrosion_Path_of_Conflagration = DungeonList( - id=44, + id=46, name='Cavern_of_Corrosion_Path_of_Conflagration', cn='野焰之径•侵蚀隧洞', cht='野焰之徑•侵蝕隧洞', - en='Path of Conflagration', + en='Cavern of Corrosion: Path of Conflagration', jp='侵蝕トンネル・野焔の路', es='Senda de la conflagración', plane_id=2021201, ) Cavern_of_Corrosion_Path_of_Elixir_Seekers = DungeonList( - id=45, + id=47, name='Cavern_of_Corrosion_Path_of_Elixir_Seekers', cn='药使之径•侵蚀隧洞', cht='藥使之徑•侵蝕隧洞', - en='Path of Elixir Seekers', + en='Cavern of Corrosion: Path of Elixir Seekers', jp='侵蝕トンネル・薬使の路', es='Senda de los elixires', plane_id=2023101, ) Cavern_of_Corrosion_Path_of_Darkness = DungeonList( - id=46, + id=48, name='Cavern_of_Corrosion_Path_of_Darkness', cn='幽冥之径•侵蚀隧洞', cht='幽冥之徑•侵蝕隧洞', - en='Path of Darkness', + en='Cavern of Corrosion: Path of Darkness', jp='侵蝕トンネル・幽冥の路', es='Senda de la oscuridad', plane_id=2022301, ) Cavern_of_Corrosion_Path_of_Dreamdive = DungeonList( - id=47, + id=49, name='Cavern_of_Corrosion_Path_of_Dreamdive', cn='梦潜之径•侵蚀隧洞', cht='夢潛之徑•侵蝕隧洞', - en='Path of Dreamdive', + en='Cavern of Corrosion: Path of Dreamdive', jp='侵蝕トンネル・夢潜の路', es='Senda de los sueños', plane_id=2031101, ) Echo_of_War_Destruction_Beginning = DungeonList( - id=48, + id=50, name='Echo_of_War_Destruction_Beginning', cn='毁灭的开端•历战余响', cht='毀滅的開端•歷戰餘響', - en="Destruction's Beginning", + en="Echo of War: Destruction's Beginning", jp='歴戦余韻・壊滅の始まり', es='El principio de la Destrucción', plane_id=2000301, ) Echo_of_War_End_of_the_Eternal_Freeze = DungeonList( - id=49, + id=51, name='Echo_of_War_End_of_the_Eternal_Freeze', cn='寒潮的落幕•历战余响', cht='寒潮的落幕•歷戰餘響', - en='End of the Eternal Freeze', + en='Echo of War: End of the Eternal Freeze', jp='歴戦余韻・寒波の幕切れ', es='El fin del Hielo Eterno', plane_id=2013401, ) Echo_of_War_Divine_Seed = DungeonList( - id=50, + id=52, name='Echo_of_War_Divine_Seed', cn='不死的神实•历战余响', cht='不死的神實•歷戰餘響', - en='Divine Seed', + en='Echo of War: Divine Seed', jp='歴戦余韻・不死の神実', es='Semilla divina', plane_id=2023201, ) Echo_of_War_Borehole_Planet_Old_Crater = DungeonList( - id=51, + id=53, name='Echo_of_War_Borehole_Planet_Old_Crater', cn='蛀星的旧靥•历战余响', cht='蛀星的舊靨•歷戰餘響', - en="Borehole Planet's Old Crater", + en="Echo of War: Borehole Planet's Old Crater", jp='歴戦余韻・星を蝕む往日の面影', es='Cráter del planeta devorado', plane_id=2000401, ) +Echo_of_War_Salutations_of_Ashen_Dreams = DungeonList( + id=54, + name='Echo_of_War_Salutations_of_Ashen_Dreams', + cn='尘梦的赞礼•历战余响', + cht='塵夢的讚禮•歷戰餘響', + en='Echo of War: Salutations of Ashen Dreams', + jp='歴戦余韻・現世の夢の礼賛', + es='Ecos de la guerra: Tributo del sueño ceniciento', + plane_id=2033201, +) Simulated_Universe_World_1 = DungeonList( - id=52, + id=55, name='Simulated_Universe_World_1', cn='第一世界•模拟宇宙', cht='第一世界•模擬宇宙', @@ -524,7 +554,7 @@ Simulated_Universe_World_1 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_3 = DungeonList( - id=53, + id=56, name='Simulated_Universe_World_3', cn='第三世界•模拟宇宙', cht='第三世界•模擬宇宙', @@ -534,7 +564,7 @@ Simulated_Universe_World_3 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_4 = DungeonList( - id=54, + id=57, name='Simulated_Universe_World_4', cn='第四世界•模拟宇宙', cht='第四世界•模擬宇宙', @@ -544,7 +574,7 @@ Simulated_Universe_World_4 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_5 = DungeonList( - id=55, + id=58, name='Simulated_Universe_World_5', cn='第五世界•模拟宇宙', cht='第五世界•模擬宇宙', @@ -554,7 +584,7 @@ Simulated_Universe_World_5 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_6 = DungeonList( - id=56, + id=59, name='Simulated_Universe_World_6', cn='第六世界•模拟宇宙', cht='第六世界•模擬宇宙', @@ -564,7 +594,7 @@ Simulated_Universe_World_6 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_7 = DungeonList( - id=57, + id=60, name='Simulated_Universe_World_7', cn='第七世界•模拟宇宙', cht='第七世界•模擬宇宙', @@ -574,7 +604,7 @@ Simulated_Universe_World_7 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_8 = DungeonList( - id=58, + id=61, name='Simulated_Universe_World_8', cn='第八世界•模拟宇宙', cht='第八世界•模擬宇宙', @@ -584,7 +614,7 @@ Simulated_Universe_World_8 = DungeonList( plane_id=100000104, ) Simulated_Universe_World_9 = DungeonList( - id=59, + id=62, name='Simulated_Universe_World_9', cn='第九世界•模拟宇宙', cht='第九世界•模擬宇宙', @@ -594,7 +624,7 @@ Simulated_Universe_World_9 = DungeonList( plane_id=100000104, ) Simulated_Universe_The_Swarm_Disaster = DungeonList( - id=60, + id=63, name='Simulated_Universe_The_Swarm_Disaster', cn='寰宇蝗灾', cht='寰宇蝗災', @@ -604,7 +634,7 @@ Simulated_Universe_The_Swarm_Disaster = DungeonList( plane_id=-1, ) Simulated_Universe_Gold_and_Gears = DungeonList( - id=61, + id=64, name='Simulated_Universe_Gold_and_Gears', cn='黄金与机械', cht='黃金與機械', @@ -614,7 +644,7 @@ Simulated_Universe_Gold_and_Gears = DungeonList( plane_id=-1, ) Memory_of_Chaos = DungeonList( - id=62, + id=65, name='Memory_of_Chaos', cn='混沌回忆', cht='混沌回憶', @@ -624,7 +654,7 @@ Memory_of_Chaos = DungeonList( plane_id=-1, ) The_Voyage_of_Navis_Astriger = DungeonList( - id=63, + id=66, name='The_Voyage_of_Navis_Astriger', cn='天艟求仙迷航录', cht='天艟求仙迷航錄', @@ -634,7 +664,7 @@ The_Voyage_of_Navis_Astriger = DungeonList( plane_id=-1, ) The_Last_Vestiges_of_Towering_Citadel = DungeonList( - id=64, + id=67, name='The_Last_Vestiges_of_Towering_Citadel', cn='永屹之城遗秘', cht='永屹之城遺秘', diff --git a/tasks/dungeon/keywords/dungeon_detailed.py b/tasks/dungeon/keywords/dungeon_detailed.py index 4258f6e68..a81442fd5 100644 --- a/tasks/dungeon/keywords/dungeon_detailed.py +++ b/tasks/dungeon/keywords/dungeon_detailed.py @@ -156,3 +156,12 @@ Stagnant_Shadow_Ire = DungeonDetailed( jp='キャラクター昇格素材:炎(ギャラガー)', es='Ascension: Fuego (Gallagher)', ) +Stagnant_Shadow_Duty = DungeonDetailed( + id=18, + name='Stagnant_Shadow_Duty', + cn='角色晋阶材料:物理(波提欧 / 知更鸟)', + cht='角色晉階材料:物理(波提歐 / 知更鳥)', + en='Ascension: Physical (Boothill / Robin)', + jp='キャラクター昇格素材:物理(ブートヒル / ロビン)', + es='Ascension: Físico (Boothill / Robin)', +) diff --git a/tasks/dungeon/ui.py b/tasks/dungeon/ui.py index 8bf8bd27c..4d02ff30c 100644 --- a/tasks/dungeon/ui.py +++ b/tasks/dungeon/ui.py @@ -82,6 +82,9 @@ class OcrDungeonList(Ocr): # 乙太之蕾•雅利洛-Ⅵ result = re.sub(r'-[VⅤ][IⅠ]', '-Ⅵ', result) + # 苏乐达™热砂海选会场 + result = re.sub(r'(苏乐达|蘇樂達|SoulGlad|スラーダ|FelizAlma)[rtT]*M', r'\1', result) + result = super().after_process(result) if self.lang == 'cn': @@ -90,6 +93,10 @@ class OcrDungeonList(Ocr): # 燔灼之形•凝滞虚影 result = result.replace('熠', '燔') result = re.sub('^灼之形', '燔灼之形', result) + # 偃偶之形•凝滞虚影 + result = re.sub('^偶之形', '偃偶之形', result) + # 嗔怒之形•凝滞虚影 + result = re.sub('^怒之形', '嗔怒之形', result) # 蛀星的旧·历战余响 result = re.sub(r'蛀星的旧.*?历战', '蛀星的旧靥•历战', result) @@ -610,23 +617,46 @@ class DungeonUI(DungeonState): logger.info('No dungeon interact') return None + self.acquire_lang_checked() + ocr = OcrDungeonList(DUNGEON_COMBAT_INTERACT_TEXT) result = ocr.detect_and_ocr(self.device.image) + dungeon = None + # Special match names in English + # Second row must have at least 3 characters which is the shortest name "Ire" + # Stangnant Shadow: Shape of + # Quanta + if len(result) == 2 and len(result[1].ocr_text) >= 3: + first, second = result[0].ocr_text, result[1].ocr_text + if re.search(r'Stagnant\s*Shadow', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Stagnant_Shadow=True) + elif re.search(r'Cavern\s*of\s*Corrosion', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Cavern_of_Corrosion=True) + elif re.search(r'Echo\s*of\s*War', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Echo_of_War=True) + elif re.search(r'Calyx[\s(]+Golden', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Calyx_Golden=True, world=self.plane.world) + elif re.search(r'Calyx[\s(]+Crimson', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Calyx_Crimson=True, plane=self.plane) + if dungeon is not None: + logger.attr('DungeonInteract', dungeon) + return dungeon + + # Join result = ' '.join([row.ocr_text for row in result]) - # Calyx (Crimson): Bud of XXX -> Bud of XXX - result = re.sub(r'Calyx\s*\(.*?\):*', '', result) - # Stagnant Shadow: Shap XXX -> Shape of XXX - result = re.sub(r'Stagnant\s*Shadow[:\s]*\w*', 'Shape of', result) - # Cavern of Corrosion: Pa XXX -> Path of XXX - result = re.sub(r'Cavern\s*of\s*Corrosion[:\s]*\w*', 'Path of', result) - # Echo of War: XXX -> XXX - result = re.sub(r'Echo\s*of\s*War:*', '', result) - # Divine See -> Divine Seed - result = re.sub(r'Divine\s*\w*', 'Divine Seed', result) - # Destructio Beginning -> Destruction's Beginning - result = re.sub(r"Destruct[a-zA-Z0-9_']*", "Destruction's", result) + # Special match names in Chinese + # Only calyxes need spacial match + if res := re.search(r'(^.+之蕾)', result): + dungeon = DungeonList.find_dungeon_by_string(cn=res.group(1), is_Calyx_Crimson=True, plane=self.plane) + if dungeon is not None: + logger.attr('DungeonInteract', dungeon) + return dungeon + dungeon = DungeonList.find_dungeon_by_string(cn=res.group(1), is_Calyx_Golden=True, world=self.plane.world) + if dungeon is not None: + logger.attr('DungeonInteract', dungeon) + return dungeon # Dungeons try: diff --git a/tasks/dungeon/weekly.py b/tasks/dungeon/weekly.py index 846104f47..facf25d01 100644 --- a/tasks/dungeon/weekly.py +++ b/tasks/dungeon/weekly.py @@ -21,6 +21,10 @@ class WeeklyDungeon(Dungeon): dungeon=dungeon, team=team, wave_limit=wave_limit, support_character=support_character, skip_ui_switch=skip_ui_switch) + def handle_ascension_dungeon_prepare(self): + # combat_wave_cost==30 in weekly, but no handle_ascension_dungeon_prepare required + return False + def get_weekly_remain(self) -> int: """ Pages: diff --git a/tasks/forgotten_hall/ui.py b/tasks/forgotten_hall/ui.py index a07c08753..14e9c3d59 100644 --- a/tasks/forgotten_hall/ui.py +++ b/tasks/forgotten_hall/ui.py @@ -210,7 +210,8 @@ class ForgottenHallUI(DungeonUI, ForgottenHallTeam): logger.info("Forgotten hall dungeon exited") break - if self.appear_then_click(MAP_EXIT): + if self.is_in_map_exit(interval=2): + self.device.click(MAP_EXIT) continue if self.handle_popup_confirm(): continue diff --git a/tasks/freebies/assets/assets_freebies_support_reward.py b/tasks/freebies/assets/assets_freebies_support_reward.py index 56d950635..64acd0200 100644 --- a/tasks/freebies/assets/assets_freebies_support_reward.py +++ b/tasks/freebies/assets/assets_freebies_support_reward.py @@ -7,20 +7,20 @@ CAN_GET_REWARD = ButtonWrapper( name='CAN_GET_REWARD', share=Button( file='./assets/share/freebies/support_reward/CAN_GET_REWARD.png', - area=(1066, 121, 1097, 144), - search=(1046, 101, 1117, 164), - color=(245, 225, 170), - button=(1066, 121, 1097, 144), + area=(1092, 181, 1123, 204), + search=(1072, 161, 1143, 224), + color=(246, 227, 173), + button=(1092, 181, 1123, 204), ), ) IN_PROFILE = ButtonWrapper( name='IN_PROFILE', share=Button( file='./assets/share/freebies/support_reward/IN_PROFILE.png', - area=(647, 128, 673, 146), - search=(627, 108, 693, 166), + area=(640, 155, 666, 173), + search=(620, 135, 686, 193), color=(109, 97, 83), - button=(647, 128, 673, 146), + button=(640, 155, 666, 173), ), ) MENU_TO_PROFILE = ButtonWrapper( @@ -50,3 +50,13 @@ PROFILE = ButtonWrapper( button=(907, 102, 1092, 132), ), ) +REWARD_POPUP = ButtonWrapper( + name='REWARD_POPUP', + share=Button( + file='./assets/share/freebies/support_reward/REWARD_POPUP.png', + area=(883, 552, 914, 580), + search=(863, 532, 934, 600), + color=(88, 89, 114), + button=(883, 552, 914, 580), + ), +) diff --git a/tasks/freebies/support_reward.py b/tasks/freebies/support_reward.py index c3313b7e8..c6fe72148 100644 --- a/tasks/freebies/support_reward.py +++ b/tasks/freebies/support_reward.py @@ -1,13 +1,14 @@ from module.base.timer import Timer from module.logger import logger -from tasks.base.assets.assets_base_page import MENU_CHECK +from tasks.base.assets.assets_base_page import CLOSE, MENU_CHECK from tasks.base.page import page_menu from tasks.base.ui import UI from tasks.freebies.assets.assets_freebies_support_reward import ( CAN_GET_REWARD, IN_PROFILE, MENU_TO_PROFILE, - PROFILE + PROFILE, + REWARD_POPUP, ) @@ -70,6 +71,9 @@ class SupportReward(UI): if self.reward_appear(): logger.info('Got reward') break + if self.appear(REWARD_POPUP): + logger.info('Got reward popup') + break if timeout.reached(): logger.warning('Get support reward timeout') break @@ -96,10 +100,14 @@ class SupportReward(UI): if self.appear(MENU_CHECK): return True + if self.appear_then_click(REWARD_POPUP, interval=2): + logger.info(f'{REWARD_POPUP} - {CLOSE}') + self.device.click(CLOSE) + continue if self.handle_ui_close(IN_PROFILE, interval=2): continue if self.handle_reward(click_button=CAN_GET_REWARD): - # # Avoid clicking on some other buttons + # Avoid clicking on some other buttons continue diff --git a/tasks/item/data_update.py b/tasks/item/data_update.py index 40174d836..bd87efdb6 100644 --- a/tasks/item/data_update.py +++ b/tasks/item/data_update.py @@ -1,3 +1,5 @@ +import re + from module.base.timer import Timer from module.logger import logger from module.ocr.ocr import Digit @@ -19,7 +21,7 @@ class DataUpdate(UI): while 1: data = ocr.detect_and_ocr(self.device.image) if len(data) == 2: - credit, jade = [int(d.ocr_text) for d in data] + credit, jade = [int(re.sub(r'\s', '', d.ocr_text)) for d in data] if credit > 0 or jade > 0: break diff --git a/tasks/map/control/control.py b/tasks/map/control/control.py index e9da793a5..bf99c8302 100644 --- a/tasks/map/control/control.py +++ b/tasks/map/control/control.py @@ -59,8 +59,11 @@ class MapControl(Combat, AimDetectorMixin): skip_first_screenshot: Returns: - bool: If swiped rotation + list[str]: A list of walk result + Enemy may attack character during rotation_set, function returns walk result """ + logger.hr('Rotation set') + result = [] interval = Timer(1, count=2) while 1: if skip_first_screenshot: @@ -70,15 +73,27 @@ class MapControl(Combat, AimDetectorMixin): self.minimap.update_rotation(self.device.image) self.minimap.log_minimap() - # End - if self.minimap.is_rotation_near(target, threshold=threshold): - logger.info(f'Rotation is now at: {target}') - break + # Additional + if self.is_combat_executing(): + logger.info('Walk result add: enemy') + result.append('enemy') + logger.hr('Combat', level=2) + self.combat_execute() + if self.walk_additional(): + continue - if interval.reached(): - if self.handle_rotation_set(target, threshold=threshold): - interval.reset() - continue + if self.is_in_main(): + # End + if self.minimap.is_rotation_near(target, threshold=threshold): + logger.info(f'Rotation is now at: {target}') + break + # Swipe + if interval.reached(): + if self.handle_rotation_set(target, threshold=threshold): + interval.reset() + continue + + return result def walk_additional(self) -> bool: """ @@ -118,6 +133,7 @@ class MapControl(Combat, AimDetectorMixin): """ logger.hr('Goto', level=2) logger.info(f'Goto {waypoint}') + self.screenshot_tracking_add() self.waypoint = waypoint self.device.stuck_record_clear() self.device.click_record_clear() @@ -338,7 +354,6 @@ class MapControl(Combat, AimDetectorMixin): list[str]: A list of walk result """ logger.hr('Goto', level=1) - self.screenshot_tracking_add() self.map_A_timer.clear() self.map_E_timer.clear() self.map_run_2x_timer.clear() @@ -386,7 +401,7 @@ class MapControl(Combat, AimDetectorMixin): end_point = waypoints[-1] end_point.expected_end.append('item') - self.goto(*waypoints) + return self.goto(*waypoints) def clear_enemy(self, *waypoints): """ @@ -403,7 +418,7 @@ class MapControl(Combat, AimDetectorMixin): end_point = waypoints[-1] end_point.expected_end.append('enemy') - self.goto(*waypoints) + return self.goto(*waypoints) if __name__ == '__main__': diff --git a/tasks/map/control/waypoint.py b/tasks/map/control/waypoint.py index 829a4c05f..0a058a463 100644 --- a/tasks/map/control/waypoint.py +++ b/tasks/map/control/waypoint.py @@ -116,6 +116,10 @@ class Waypoint: return list(same) + def enroute_add_item(self): + if 'item' not in self.expected_enroute: + self.expected_enroute.append('item') + def ensure_waypoint(point) -> Waypoint: """ diff --git a/tasks/map/keywords/plane.py b/tasks/map/keywords/plane.py index b77c5adc6..075ad9d77 100644 --- a/tasks/map/keywords/plane.py +++ b/tasks/map/keywords/plane.py @@ -487,3 +487,36 @@ Penacony_ClockStudiosThemePark = MapPlane( world_id=3, plane_id=2032101, ) +Penacony_DreamfluxReef = MapPlane( + id=45, + name='Penacony_DreamfluxReef', + cn='流梦礁', + cht='流夢礁', + en='Dreamflux Reef', + jp='ドリームリーフ', + es='Arrecife Flujosueño', + world_id=3, + plane_id=1030401, +) +Penacony_SoulGladScorchsandAuditionVenue = MapPlane( + id=46, + name='Penacony_SoulGladScorchsandAuditionVenue', + cn='苏乐达热砂海选会场', + cht='蘇樂達熱砂海選會場', + en='SoulGlad Scorchsand Audition Venue', + jp='スラーダ熱砂オーディション会場', + es='Recinto de las Audiciones FelizAlma en la Arena Ardiente', + world_id=3, + plane_id=2033101, +) +Penacony_PenaconyGrandTheater = MapPlane( + id=47, + name='Penacony_PenaconyGrandTheater', + cn='匹诺康尼大剧院', + cht='匹諾康尼大劇院', + en='Penacony Grand Theater', + jp='ピノコニー大劇場', + es='Gran Teatro de Colonipenal', + world_id=3, + plane_id=2033201, +) diff --git a/tasks/rogue/assets/assets_rogue_path.py b/tasks/rogue/assets/assets_rogue_path.py index 5dcf2d590..013112c5f 100644 --- a/tasks/rogue/assets/assets_rogue_path.py +++ b/tasks/rogue/assets/assets_rogue_path.py @@ -238,3 +238,13 @@ PAGE_ROGUE_PATH = ButtonWrapper( button=(606, 89, 656, 108), ), ) +PATH_LOADED_CHECK = ButtonWrapper( + name='PATH_LOADED_CHECK', + share=Button( + file='./assets/share/rogue/path/PATH_LOADED_CHECK.png', + area=(989, 18, 1175, 59), + search=(969, 0, 1195, 79), + color=(45, 45, 47), + button=(989, 18, 1175, 59), + ), +) diff --git a/tasks/rogue/assets/assets_rogue_ui.py b/tasks/rogue/assets/assets_rogue_ui.py index 0b7972b28..da1687c7b 100644 --- a/tasks/rogue/assets/assets_rogue_ui.py +++ b/tasks/rogue/assets/assets_rogue_ui.py @@ -77,6 +77,17 @@ COSMIC_FRAGMENT = ButtonWrapper( button=(1146, 19, 1181, 55), ), ) +CURIO_FIXED = ButtonWrapper( + name='CURIO_FIXED', + cn=Button( + file='./assets/cn/rogue/ui/CURIO_FIXED.png', + area=(558, 54, 626, 75), + search=(538, 34, 646, 95), + color=(126, 123, 122), + button=(558, 54, 626, 75), + ), + en=None, +) CURIO_OBTAINED = ButtonWrapper( name='CURIO_OBTAINED', cn=[ diff --git a/tasks/rogue/blessing/blessing.py b/tasks/rogue/blessing/blessing.py index a83db202f..416723e31 100644 --- a/tasks/rogue/blessing/blessing.py +++ b/tasks/rogue/blessing/blessing.py @@ -187,6 +187,9 @@ class RogueBlessingSelector(RogueSelector): else: self.main.device.screenshot() + if self.main.handle_blessing_popup(): + logger.warning('Mistakenly recognized current page as blessing choosing page, quit') + return if is_card_selected(self.main, target, confirm_button=BLESSING_CONFIRM): if enforce: logger.info("Buff selected (enforce)") @@ -306,7 +309,7 @@ class RogueBlessingSelector(RogueSelector): "strategy_config": self.main.config.RogueBlessing_SelectionStrategy, "preset_values": { 'preset': RESONANCE_PRESET[self.main.config.RogueWorld_Path], - 'custom': self.main.config.RogueBlessing_PresetResonanceFilter, + 'custom': self.main.config.RogueBlessing_CustomResonanceFilter, }, } } diff --git a/tasks/rogue/blessing/ui.py b/tasks/rogue/blessing/ui.py index 1cab486a4..ad267fe04 100644 --- a/tasks/rogue/blessing/ui.py +++ b/tasks/rogue/blessing/ui.py @@ -4,8 +4,8 @@ from module.base.utils import area_offset from module.logger import logger from module.ocr.ocr import Digit, Ocr, OcrResultButton from tasks.base.ui import UI -from tasks.rogue.assets.assets_rogue_weekly import REWARD_ENTER from tasks.rogue.assets.assets_rogue_ui import * +from tasks.rogue.assets.assets_rogue_weekly import REWARD_ENTER from tasks.rogue.keywords import RoguePath @@ -80,4 +80,9 @@ class RogueUI(UI): logger.info(f'{CURIO_OBTAINED} -> {BLESSING_CONFIRM}') self.device.click(BLESSING_CONFIRM) return True + # Fixed a curio from occurrence + if self.appear(CURIO_FIXED, interval=2): + logger.info(f'{CURIO_FIXED} -> {BLESSING_CONFIRM}') + self.device.click(BLESSING_CONFIRM) + return True return False diff --git a/tasks/rogue/entry/entry.py b/tasks/rogue/entry/entry.py index 4a7bd2410..448e33888 100644 --- a/tasks/rogue/entry/entry.py +++ b/tasks/rogue/entry/entry.py @@ -249,7 +249,10 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonUI): self.device.click(WORLD_ENTER) self.interval_reset(REWARD_ENTER, interval=2) continue - if self.appear(LEVEL_CONFIRM, interval=2): + if self.match_template_color(LEVEL_CONFIRM, interval=2): + 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.check_stop_condition() self.device.click(LEVEL_CONFIRM) @@ -289,7 +292,8 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonUI): if self.handle_ui_back(self._is_page_rogue_path): continue # From ui_leave_special() - if self.appear_then_click(MAP_EXIT, interval=2): + if self.is_in_map_exit(interval=2): + self.device.click(MAP_EXIT) continue if self.handle_popup_confirm(): continue @@ -332,7 +336,8 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonUI): """ logger.info(f'RogueWorld_UseImmersifier={self.config.RogueWorld_UseImmersifier}, ' f'RogueWorld_UseStamina={self.config.RogueWorld_UseStamina}, ' - f'RogueWorld_DoubleEvent={self.config.RogueWorld_DoubleEvent}' + f'RogueWorld_DoubleEvent={self.config.RogueWorld_DoubleEvent}, ' + f'RogueWorld_WeeklyFarming={self.config.RogueWorld_WeeklyFarming}, ' f'RogueDebug_DebugMode={self.config.RogueDebug_DebugMode}') # This shouldn't happen if self.config.RogueWorld_UseStamina and not self.config.RogueWorld_UseImmersifier: @@ -345,13 +350,23 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonUI): if self.config.RogueDebug_DebugMode: # Always run return - + + if self.config.stored.SimulatedUniverseFarm.is_expired(): + # Expired, reset farming counter + self.config.stored.SimulatedUniverseFarm.set(0) + if self.config.stored.SimulatedUniverse.is_expired(): # Expired, do rogue pass elif self.config.stored.SimulatedUniverse.is_full(): if self.config.RogueWorld_UseImmersifier and self.config.stored.Immersifier.value > 0: - logger.info('Reached weekly point limit but still have immersifiers left, continue to use them') + logger.info( + 'Reached weekly point limit but still have immersifiers left, continue to use them') + elif self.config.RogueWorld_WeeklyFarming and not self.config.stored.SimulatedUniverseFarm.is_full(): + logger.info( + 'Reached weekly point limit but still continue to farm materials') + logger.attr( + "Farming Counter", self.config.stored.SimulatedUniverseFarm.to_counter()) else: raise RogueReachedWeeklyPointLimit else: diff --git a/tasks/rogue/entry/path.py b/tasks/rogue/entry/path.py index 8ab17b50b..ae1016357 100644 --- a/tasks/rogue/entry/path.py +++ b/tasks/rogue/entry/path.py @@ -205,7 +205,10 @@ class RoguePathHandler(RogueUI): logger.info('rogue_path_select ended at page_main') break - if self.appear(ROGUE_LAUNCH, interval=2): + if self.match_template_color(ROGUE_LAUNCH, interval=2): + if not self.image_color_count(ROGUE_LAUNCH, color=(223, 223, 225), threshold=240, count=50): + self.interval_clear(ROGUE_LAUNCH) + continue if not self._is_team_prepared(): raise RogueTeamNotPrepared self.device.click(ROGUE_LAUNCH) @@ -216,11 +219,19 @@ class RoguePathHandler(RogueUI): continue # Select path if self.interval_is_reached(entry, interval=2) and self._is_page_rogue_path(): - if self.appear_then_click(entry, interval=2): + if not self.image_color_count(PATH_LOADED_CHECK, color=(246, 246, 246), threshold=240, count=50): + self.interval_clear(entry) + continue + if self.appear(entry): + self.device.click(entry) self.interval_reset(entry, interval=2) continue # Confirm path if self.appear(CONFIRM_PATH, interval=2): + if not self.image_color_count(CONFIRM_PATH, color=(223, 223, 225), threshold=240, count=50): + self.interval_clear(CONFIRM_PATH) + continue if self._change_confirm_path(path): self.device.click(CONFIRM_PATH) + self.interval_reset(CONFIRM_PATH) continue diff --git a/tasks/rogue/event/event.py b/tasks/rogue/event/event.py index 97daf9ccc..d6aad912a 100644 --- a/tasks/rogue/event/event.py +++ b/tasks/rogue/event/event.py @@ -188,7 +188,11 @@ class RogueEvent(RogueUI): # Only one option, click directly if count == 1: if self.interval_is_reached(CHOOSE_OPTION, interval=2): - self.device.click(self.valid_options[0].prefix_icon) + button = self.valid_options[0].prefix_icon + # Option at bottom + if button.area[1] > 500 and SCROLL_OPTION.appear(main=self): + SCROLL_OPTION.set_bottom(main=self) + self.device.click(button) self.interval_reset(CHOOSE_OPTION, interval=2) return True diff --git a/tasks/rogue/keywords/event_option.py b/tasks/rogue/keywords/event_option.py index f391cec2c..4400d553d 100644 --- a/tasks/rogue/keywords/event_option.py +++ b/tasks/rogue/keywords/event_option.py @@ -162,7 +162,7 @@ Jim_Hulk_collection = RogueEventOption( cn='杰姆·哈克的藏品。', cht='傑姆•哈克的收藏。', en="Jim Hulk's collection.", - jp='ジェム・ハックの所蔵品', + jp='ジャック・ハックの所蔵品', es='Colección de Jim Hulk.', ) Walk_away = RogueEventOption( @@ -1097,7 +1097,7 @@ Hurry_and_delete_the_Cyclic_Demon_Lord_life_algorithm = RogueEventOption( name='Hurry_and_delete_the_Cyclic_Demon_Lord_life_algorithm', cn='抓紧时间,删除周期性魔王的生命方程。', cht='把握時間,刪除週期性魔王的生命方程式。', - en="Hurry and delete the Cyclic Demon Lord's life algorithm", + en="Hurry and delete the Cyclic Demon Lord's life algorithm.", jp='事態は一刻を争う、周期性魔王の生命方程式を削除する', es='Date prisa y borra el algoritmo vital del Rey Demonio Cíclico.', ) @@ -1187,7 +1187,7 @@ Purchase_a_1_2_star_Blessing = RogueEventOption( name='Purchase_a_1_2_star_Blessing', cn='购买1个1-2星祝福', cht='購買1個一至二星祝福', - en='Purchase a 1-2 star Blessing', + en='Purchase a 1–2 star Blessing', jp='★1~2の祝福を1個購入する', es='Compra 1 bendición de 1-2 estrellas.', ) @@ -1196,7 +1196,7 @@ Purchase_a_1_3_star_Blessing = RogueEventOption( name='Purchase_a_1_3_star_Blessing', cn='购买1个1-3星祝福', cht='購買1個一至三星祝福', - en='Purchase a 1-3 star Blessing', + en='Purchase a 1–3 star Blessing', jp='★1~3の祝福を1個購入する', es='Compra 1 bendición de 1-3 estrellas.', ) diff --git a/tasks/rogue/rogue.py b/tasks/rogue/rogue.py index 7635b25b8..5e2f56750 100644 --- a/tasks/rogue/rogue.py +++ b/tasks/rogue/rogue.py @@ -37,31 +37,35 @@ class Rogue(RouteLoader, RogueEntry): self.config.task_call('Dungeon') self.config.task_stop() - while 1: - # Run - success = self.rogue_once() + # Run + success = self.rogue_once() - # Scheduler - with self.config.multi_set(): - # Task switched - if self.config.task_switched(): + # Scheduler + with self.config.multi_set(): + # Task switched + if self.config.task_switched(): + self.config.task_stop() + # Archived daily quest + if success: + quests = self.config.stored.DailyQuest.load_quests() + if KEYWORDS_DAILY_QUEST.Complete_Simulated_Universe_1_times in quests: + logger.info('Achieve daily quest Complete_Simulated_Universe_1_times') + self.config.task_call('DailyQuest') self.config.task_stop() - # Archived daily quest - if success: - quests = self.config.stored.DailyQuest.load_quests() - if KEYWORDS_DAILY_QUEST.Complete_Simulated_Universe_1_times in quests: - logger.info('Achieve daily quest Complete_Simulated_Universe_1_times') - self.config.task_call('DailyQuest') - self.config.task_stop() - quests = self.config.stored.BattlePassWeeklyQuest.load_quests() - if KEYWORDS_BATTLE_PASS_QUEST.Complete_Simulated_Universe_1_times in quests: - logger.info('Achieve battle pass quest Complete_Simulated_Universe_1_times') - self.config.task_call('BattlePass') - self.config.task_stop() - # End - if not success: - self.config.task_delay(server_update=True) - break + quests = self.config.stored.BattlePassWeeklyQuest.load_quests() + if KEYWORDS_BATTLE_PASS_QUEST.Complete_Simulated_Universe_1_times in quests: + logger.info('Achieve battle pass quest Complete_Simulated_Universe_1_times') + self.config.task_call('BattlePass') + self.config.task_stop() + # End + if success: + logger.info('Rogue run success') + # Call rogue itself, so multiple rogue runs are considered as separated tasks + # which won't trigger failure count >= 3 when clearing 100 elites + self.config.task_call('Rogue') + else: + logger.info('Rogue run failed') + self.config.task_delay(server_update=True) if __name__ == '__main__': diff --git a/tasks/rogue/route/base.py b/tasks/rogue/route/base.py index b7017991e..859c6e7d8 100644 --- a/tasks/rogue/route/base.py +++ b/tasks/rogue/route/base.py @@ -17,6 +17,7 @@ from tasks.rogue.route.exit import RogueExit class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): registered_domain_exit = None + enroute_add_item = True def combat_expected_end(self): if self.is_page_choose_blessing(): @@ -32,7 +33,8 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): return False def combat_execute(self, expected_end=None): - return super().combat_execute(expected_end=self.combat_expected_end) + super().combat_execute(expected_end=self.combat_expected_end) + self.clear_blessing() def walk_additional(self) -> bool: if self.handle_blessing_popup(): @@ -125,12 +127,6 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): if self.handle_event_option(): continue - def _goto(self, *args, **kwargs): - result = super()._goto(*args, **kwargs) - if 'enemy' in result: - self.clear_blessing() - return result - def wait_until_minimap_stabled(self): logger.info('Wait until minimap stabled') radius = self.minimap.MINIMAP_RADIUS @@ -140,10 +136,9 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): def clear_enemy(self, *waypoints): waypoints = ensure_waypoints(waypoints) - if self.plane.is_rogue_combat: + if self.enroute_add_item and self.plane.is_rogue_combat: for point in waypoints: - if 'item' not in point.expected_enroute: - point.expected_enroute.append('item') + point.enroute_add_item() return super().clear_enemy(*waypoints) def clear_item(self, *waypoints): @@ -170,6 +165,14 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): pass result = super().clear_enemy(*waypoints) + # logger.attr("result",result) + + if 'enemy' in result: + # runs when one elite battle finishes, and increases rogue farming count by 1 + if not self.config.stored.SimulatedUniverseFarm.is_full(): + self.config.stored.SimulatedUniverseFarm.add() + logger.info( + f"Cleared elite boss, increasing farming count by 1, now " + self.config.stored.SimulatedUniverseFarm.to_counter()) return result def _domain_event_expected_end(self): @@ -191,10 +194,9 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): end_point.endpoint_threshold = 1.5 end_point.interact_radius = 7 end_point.expected_end.append(self._domain_event_expected_end) - if self.plane.is_rogue_occurrence: + if self.enroute_add_item and self.plane.is_rogue_occurrence: for point in waypoints: - if 'item' not in point.expected_enroute: - point.expected_enroute.append('item') + point.enroute_add_item() result = self.goto(*waypoints) self.clear_occurrence() @@ -294,9 +296,9 @@ 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') + if self.enroute_add_item: + for point in waypoints: + point.enroute_add_item() end_point = waypoints[-1] end_point.min_speed = 'run' diff --git a/tasks/rogue/route/exit.py b/tasks/rogue/route/exit.py index 7f385dedd..cf7c21bc8 100644 --- a/tasks/rogue/route/exit.py +++ b/tasks/rogue/route/exit.py @@ -112,6 +112,9 @@ class RogueExit(CombatInteract): distant_point = np.array((1509.46, 247.34)) name_y = 77.60 foot_y = 621.82 + if point[1] < 80: + logger.warning(f'screen2direction: Point {point} to high') + point = (point[0], 80) door_projection_bottom = ( Points([point]).link(vanish_point).get_x(name_y)[0], @@ -129,6 +132,8 @@ class RogueExit(CombatInteract): door_projection_bottom[0] - screen_middle[0], door_projection_bottom[0] - door_distant[0], ) + if planar_door[1] < 0: + logger.warning('screen2direction: planer_door at back') if abs(planar_door[0]) < 5: direction = 0 else: @@ -230,6 +235,10 @@ class RogueExit(CombatInteract): ocr = OcrDomainExit(OCR_DOMAIN_EXIT) results = ocr.matched_ocr(image, keyword_classes=MapPlane) + # Try without preprocess + if not len(results): + ocr.white_preprocess = False + results = ocr.matched_ocr(image, keyword_classes=MapPlane) centers = [area_center(result.area) for result in results] logger.info(f'DomainDoor: {centers}') directions = [self.screen2direction(center) for center in centers] @@ -244,8 +253,17 @@ class RogueExit(CombatInteract): else: return None, results[0].matched_keyword else: - results = [r for d, r in sorted(zip(directions, results))] - return results[0].matched_keyword, results[-1].matched_keyword + left = [r for d, r in sorted(zip(directions, results)) if d < 0] + right = [r for d, r in sorted(zip(directions, results)) if d >= 0] + if len(left): + left = left[0].matched_keyword + else: + left = None + if len(right): + right = right[-1].matched_keyword + else: + right = None + return left, right def choose_door(self, left_door: MapPlane | None, right_door: MapPlane | None) -> str | None: """ diff --git a/tasks/rogue/route/loader.py b/tasks/rogue/route/loader.py index b60be3d64..ecf78455c 100644 --- a/tasks/rogue/route/loader.py +++ b/tasks/rogue/route/loader.py @@ -164,13 +164,45 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): # if route.name == 'Occurrence_Herta_StorageZone_F2_X363Y166' and similarity > 0.05: # return True + # Before Combat_Herta_SupplyZone_F2_X45Y369 + if route.name in [ + 'Combat_Herta_SupplyZone_F2_X543Y255', # 0.462, (543.3, 255.4) + 'Combat_Luofu_DivinationCommission_F1_X737Y237', + # ('Occurrence_Luofu_Cloudford_F1_X241Y947', 0.307, (236.5, 949.6)), + # ('Occurrence_Luofu_Cloudford_F1_X244Y951', 0.307, (236.5, 949.6)), + # ('Occurrence_Jarilo_SilvermaneGuardRestrictedZone_F1_X509Y541', 0.154, (507.8, 515.2)) + 'Occurrence_Luofu_Cloudford_F1_X241Y947', + 'Occurrence_Luofu_Cloudford_F1_X244Y951', + ] and similarity > 0.25: + return True # Before Combat_Luofu_Cloudford_F1_X281Y873 if route.name in [ + # ('Combat_Jarilo_BackwaterPass_F1_X507Y733', 0.26, (503.2, 736.9)), + # ('Combat_Herta_SupplyZone_F2_X45Y369', 0.168, (46.5, 370.0)) + 'Jarilo_BackwaterPass_F1_X507Y733', + 'Jarilo_BackwaterPass_F1_X555Y643', 'Occurrence_Jarilo_BackwaterPass_F1_X553Y643', 'Combat_Jarilo_GreatMine_F1_X545Y513', 'Combat_Herta_SupplyZone_F2_X45Y369', ] and similarity > 0.20: return True + # Before Occurrence_Luofu_DivinationCommission_F2_X425Y791 + if route.name in [ + 'Occurrence_Jarilo_RivetTown_F1_X157Y435', + # ('Occurrence_Luofu_DivinationCommission_F2_X149Y659', 0.237, (148.9, 658.8)), + # ('Occurrence_Luofu_DivinationCommission_F2_X425Y791', 0.11, (425.2, 793.8)) + 'Occurrence_Luofu_DivinationCommission_F2_X149Y659', + # ('Combat_Luofu_DivinationCommission_F1_X97Y457', 0.222, (97.8, 456.9)), + # ('Combat_Luofu_ScalegorgeWaterscape_F1_X415Y261', 0.112, (371.8, 289.4)), + # ('Combat_Herta_SupplyZone_F2_X45Y369', 0.104, (11.7, 367.6)) + 'Combat_Luofu_DivinationCommission_F1_X97Y457', + # ('Occurrence_Jarilo_BackwaterPass_F1_X613Y755', 0.206, (611.3, 759.0)), + # ('Occurrence_Jarilo_BackwaterPass_F1_X611Y761', 0.206, (611.3, 759.0)), + # ('Occurrence_Luofu_DivinationCommission_F2_X425Y791', 0.105, (429.7, 791.6)) + 'Occurrence_Jarilo_BackwaterPass_F1_X613Y755', + 'Occurrence_Jarilo_BackwaterPass_F1_X611Y761', + ] and similarity > 0.15: + return True if route.name in [ 'Combat_Herta_StorageZone_F1_X273Y92', 'Occurrence_Herta_StorageZone_F1_X273Y93', @@ -266,6 +298,7 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): """ base = RouteBase(config=self.config, device=self.device, task=self.config.task.command) count = 1 + self.character_is_ranged = None while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -274,8 +307,7 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): logger.hr(f'Route run: {count}', level=1) base.clear_blessing() - if count == 1: - self.character_switch_to_ranged(update=True) + self.character_switch_to_ranged(update=True) self.route_run() # if not success: