From d4fe1959a80b0e0aa2e6e0b1830de37e78f94c05 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 15 Apr 2024 02:38:04 +0800 Subject: [PATCH 01/12] Fix: No items enroute in Luofu_StargazerNavalia_F1_X521Y595 --- .../Occurrence/Luofu_StargazerNavalia_F1.py | 5 +++++ tasks/map/control/waypoint.py | 4 ++++ tasks/rogue/route/base.py | 17 ++++++++--------- 3 files changed, 17 insertions(+), 9 deletions(-) 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 @@ def Luofu_StargazerNavalia_F1_X521Y595(self): 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/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 @@ def match_results(self, results) -> list[str]: 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/rogue/route/base.py b/tasks/rogue/route/base.py index 2126852a3..d41522075 100644 --- a/tasks/rogue/route/base.py +++ b/tasks/rogue/route/base.py @@ -17,6 +17,7 @@ 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(): @@ -140,10 +141,9 @@ def wait_until_minimap_stabled(self): 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): @@ -199,10 +199,9 @@ def clear_event(self, *waypoints): 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() @@ -302,9 +301,9 @@ def domain_single_exit(self, *waypoints): 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' From a0fd30193cde9c11371055dbd3d1006e565b9522 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 15 Apr 2024 03:45:50 +0800 Subject: [PATCH 02/12] Fix: Special match Combat_Luofu_DivinationCommission_F1_X737Y237 --- tasks/rogue/route/loader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tasks/rogue/route/loader.py b/tasks/rogue/route/loader.py index b60be3d64..6c6eff19a 100644 --- a/tasks/rogue/route/loader.py +++ b/tasks/rogue/route/loader.py @@ -164,6 +164,11 @@ def _position_match_special( # 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_Luofu_DivinationCommission_F1_X737Y237', + ] and similarity > 0.25: + return True # Before Combat_Luofu_Cloudford_F1_X281Y873 if route.name in [ 'Occurrence_Jarilo_BackwaterPass_F1_X553Y643', From 39af23657e2140452d0fff50ab2b124b1f17642e Mon Sep 17 00:00:00 2001 From: Matrix_Cain Date: Mon, 15 Apr 2024 19:07:45 +0800 Subject: [PATCH 03/12] Feature: Migrate notify.py from AzurLaneAutoScript and upgrade dep onepush ver. to 1.3.0 (#418) Co-authored-by: LmeSzinc --- module/notify.py | 75 +++++++++++++++++++++++++++++++++++++++++++-- requirements-in.txt | 1 + requirements.txt | 4 ++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/module/notify.py b/module/notify.py index 4f8b470ac..9186e5c4a 100644 --- a/module/notify.py +++ b/module/notify.py @@ -1,4 +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 -def handle_notify(*args, **kwargs): - logger.error('Error notify is not supported yet') +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/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 From 6ec8df807668bdf74fe470d46833b5ec630671d0 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:27:03 +0800 Subject: [PATCH 04/12] Opt: Early init minitouch and MaaTouch for faster startup --- module/config/config.py | 4 +++ module/device/device.py | 7 +++++ module/device/method/maatouch.py | 43 +++++++++++++++++++++++++------ module/device/method/minitouch.py | 40 +++++++++++++++++++++++----- 4 files changed, 79 insertions(+), 15 deletions(-) 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 @@ def close_game(self): 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/device/device.py b/module/device/device.py index 948fefe03..9e882194f 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -88,6 +88,13 @@ def __init__(self, *args, **kwargs): 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() + # SRC only, use nemu_ipc if available available = self.nemu_ipc_available() logger.attr('nemu_ipc_available', available) diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index 8122505bc..300e8ff9c 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_wrapper(self, *args, **kwargs): 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 init(): 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) @@ -103,12 +104,38 @@ class MaaTouch(Connection): max_y: int _maatouch_stream = socket.socket _maatouch_stream_storage = None + _maatouch_init_thread = None @cached_property - def maatouch_builder(self): + 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 maatouch_init(self): logger.hr('MaaTouch init') max_x, max_y = 1280, 720 diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index 405bc0299..18a09009f 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 @@ 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 @@ -328,7 +328,7 @@ def init(): 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,7 +337,7 @@ def init(): 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): @@ -349,7 +349,7 @@ def init(): 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) @@ -370,12 +370,38 @@ class Minitouch(Connection): _minitouch_ws: websockets.WebSocketClientProtocol max_x: int max_y: int + _minitouch_init_thread = None @cached_property - def minitouch_builder(self): + 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') From 9449bf693b182803155c15dd74ae1a9a3d00f5f7 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:04:52 +0800 Subject: [PATCH 05/12] Opt: Patch pkg_resources for faster startup --- module/device/device.py | 6 ++ module/device/pkg_resources/__init__.py | 82 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 module/device/pkg_resources/__init__.py diff --git a/module/device/device.py b/module/device/device.py index 9e882194f..d9cdbf1f8 100644 --- a/module/device/device.py +++ b/module/device/device.py @@ -1,6 +1,12 @@ 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 diff --git a/module/device/pkg_resources/__init__.py b/module/device/pkg_resources/__init__.py new file mode 100644 index 000000000..61014ef79 --- /dev/null +++ b/module/device/pkg_resources/__init__.py @@ -0,0 +1,82 @@ +import os +import re +import sys + +from module.base.decorator import cached_property + +""" +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 +sys.modules['pkg_resources'] = sys.modules['module.device.pkg_resources'] + + +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 + res = re.match(r'^(.+)-(.+)\.dist-info$', file) + if res: + obj = FakeDistributionObject( + dist=res.group(1), + version=res.group(2), + ) + 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', '0.11.0') + if dist == 'uiautomator2': + return PACKAGE_CACHE.dict_installed_packages.get('uiautomator2', '2.16.17') + + +class DistributionNotFound(Exception): + pass From 47b8f4a37d04996cdf2788d0d219d02533a0a904 Mon Sep 17 00:00:00 2001 From: Zero <98764734+X-Zero-L@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:32:58 +0800 Subject: [PATCH 06/12] Upd: Character (#422) Co-authored-by: LmeSzinc --- assets/character/Aventurine.png | Bin 0 -> 17297 bytes module/config/argument/args.json | 2 ++ module/config/config_generated.py | 2 +- module/config/config_updater.py | 2 +- module/config/i18n/en-US.json | 1 + module/config/i18n/es-ES.json | 1 + module/config/i18n/ja-JP.json | 1 + module/config/i18n/zh-CN.json | 1 + module/config/i18n/zh-TW.json | 1 + 9 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 assets/character/Aventurine.png diff --git a/assets/character/Aventurine.png b/assets/character/Aventurine.png new file mode 100644 index 0000000000000000000000000000000000000000..2e1c1de4585027942473dc0884c8d79e1424c91f GIT binary patch literal 17297 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 literal 0 HcmV?d00001 diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 61de75e65..7e5fc8b74 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -427,6 +427,7 @@ "Argenti", "Arlan", "Asta", + "Aventurine", "Bailu", "BlackSwan", "Blade", @@ -1341,6 +1342,7 @@ "Argenti", "Arlan", "Asta", + "Aventurine", "Bailu", "BlackSwan", "Blade", diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 31363dddb..54781a597 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -59,7 +59,7 @@ class GeneratedConfig: # 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, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong # Group `DungeonStorage` DungeonStorage_TrailblazePower = {} diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 3b83ed950..b5e9c8e13 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -100,7 +100,7 @@ def option_add(keys, options): 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 = [] characters = [character.name for character in CharacterList.instances.values() if character.name not in unsupported_characters] option_add(keys='DungeonSupport.Character.option', options=characters) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index a062ac5ff..4639d1ff6 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -441,6 +441,7 @@ "Argenti": "Argenti", "Arlan": "Arlan", "Asta": "Asta", + "Aventurine": "Aventurine", "Bailu": "Bailu", "BlackSwan": "Black Swan", "Blade": "Blade", diff --git a/module/config/i18n/es-ES.json b/module/config/i18n/es-ES.json index 6130a4cce..d3c378f0c 100644 --- a/module/config/i18n/es-ES.json +++ b/module/config/i18n/es-ES.json @@ -441,6 +441,7 @@ "Argenti": "Argenti", "Arlan": "Arlan", "Asta": "Asta", + "Aventurine": "Aventurino", "Bailu": "Bailu", "BlackSwan": "Cisne Negro", "Blade": "Blade", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index b2ff361a2..00da6dd94 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -441,6 +441,7 @@ "Argenti": "アルジェンティ", "Arlan": "アーラン", "Asta": "アスター", + "Aventurine": "アベンチュリン", "Bailu": "白露", "BlackSwan": "ブラックスワン", "Blade": "刃", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index cf3a86232..e93f56d34 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -441,6 +441,7 @@ "Argenti": "银枝", "Arlan": "阿兰", "Asta": "艾丝妲", + "Aventurine": "砂金", "Bailu": "白露", "BlackSwan": "黑天鹅", "Blade": "刃", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index a9afefdfb..75d160610 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -441,6 +441,7 @@ "Argenti": "銀枝", "Arlan": "阿蘭", "Asta": "艾絲妲", + "Aventurine": "砂金", "Bailu": "白露", "BlackSwan": "黑天鵝", "Blade": "刃", From 82f5e8b6ab929b645f1572d96ccef90184352e91 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Apr 2024 22:55:53 +0800 Subject: [PATCH 07/12] Fix: site-packages detection on manual package builds --- module/device/method/utils.py | 2 +- module/device/pkg_resources/__init__.py | 37 +++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/module/device/method/utils.py b/module/device/method/utils.py index 36d6bbd49..50b7ecc70 100644 --- a/module/device/method/utils.py +++ b/module/device/method/utils.py @@ -253,7 +253,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 index 61014ef79..b685a6d9a 100644 --- a/module/device/pkg_resources/__init__.py +++ b/module/device/pkg_resources/__init__.py @@ -3,6 +3,7 @@ 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. @@ -18,7 +19,24 @@ ``` """ # Inject sys.modules, pretend we have pkg_resources imported -sys.modules['pkg_resources'] = sys.modules['module.device.pkg_resources'] +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: @@ -50,11 +68,14 @@ def dict_installed_packages(self): dic = {} for file in os.listdir(self.site_packages): # mxnet_cu101-1.6.0.dist-info - res = re.match(r'^(.+)-(.+)\.dist-info$', file) + # 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=res.group(2), + version=version, ) dic[obj.dist] = obj @@ -73,9 +94,15 @@ def resource_filename(*args): 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', '0.11.0') + return PACKAGE_CACHE.dict_installed_packages.get( + 'adbutils', + FakeDistributionObject('adbutils', '0.11.0'), + ) if dist == 'uiautomator2': - return PACKAGE_CACHE.dict_installed_packages.get('uiautomator2', '2.16.17') + return PACKAGE_CACHE.dict_installed_packages.get( + 'uiautomator2', + FakeDistributionObject('uiautomator2', '2.16.17'), + ) class DistributionNotFound(Exception): From fa209e1cf517ecc08479bce4c7255a48de7b2748 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Apr 2024 23:00:49 +0800 Subject: [PATCH 08/12] Fix: Character trial ended at page_main --- route/daily/HimekoTrial.py | 8 +++++++- tasks/combat/skill.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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 @@ def handle_combat_state(self, auto=True, speed_2x=True): 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/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 @@ def is_in_skill(self) -> bool: 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 @@ def _skill_click(self, button, skip_first_screenshot=True): 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 From 13199daeb7c8c068317357f9ba4611f98a439096 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 18 Apr 2024 23:15:00 +0800 Subject: [PATCH 09/12] Fix: Special match Combat_Herta_SupplyZone_F2_X543Y255 --- tasks/rogue/route/loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks/rogue/route/loader.py b/tasks/rogue/route/loader.py index 6c6eff19a..f482633a3 100644 --- a/tasks/rogue/route/loader.py +++ b/tasks/rogue/route/loader.py @@ -166,6 +166,7 @@ def _position_match_special( # 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', ] and similarity > 0.25: return True From 88b199fd6a671749bcf0cb22fa7851f4d3c56642 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 19 Apr 2024 00:08:49 +0800 Subject: [PATCH 10/12] Fix: Add route copy Occurrence_Luofu_Cloudford_F1_X244Y951 --- route/rogue/Occurrence/Luofu_Cloudford_F1.py | 26 ++++++++++++++++++++ route/rogue/route.json | 11 +++++++++ 2 files changed, 37 insertions(+) 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 @@ def Luofu_Cloudford_F1_X241Y947(self): 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/route.json b/route/rogue/route.json index 198c9be88..8df482bc9 100644 --- a/route/rogue/route.json +++ b/route/rogue/route.json @@ -1781,6 +1781,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", From d4d1b46dfd2164a38737cd9a8eda2c0d9a0134b5 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 19 Apr 2024 00:34:05 +0800 Subject: [PATCH 11/12] Fix: screen2direction gets high DomainDoor when going downhills --- tasks/rogue/route/exit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tasks/rogue/route/exit.py b/tasks/rogue/route/exit.py index 7f385dedd..f8ee3c319 100644 --- a/tasks/rogue/route/exit.py +++ b/tasks/rogue/route/exit.py @@ -112,6 +112,9 @@ def screen2direction(point): distant_point = np.array((1509.46, 247.34)) name_y = 77.60 foot_y = 621.82 + if point < 80: + logger.warning(f'screen2direction: Point {point} to high') + point[1] = 80 door_projection_bottom = ( Points([point]).link(vanish_point).get_x(name_y)[0], @@ -129,6 +132,8 @@ def screen2direction(point): 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: From 9216d8bb1232b9777ba67a02e8d2f6a6759d3cb2 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Fri, 19 Apr 2024 00:41:38 +0800 Subject: [PATCH 12/12] Fix: Touch builders have no retries if called directly --- module/device/method/maatouch.py | 6 ++++++ module/device/method/minitouch.py | 1 + 2 files changed, 7 insertions(+) diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index 300e8ff9c..d1a97d04b 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -107,6 +107,7 @@ class MaaTouch(Connection): _maatouch_init_thread = None @cached_property + @retry def _maatouch_builder(self): self.maatouch_init() return MaatouchBuilder(self) @@ -272,3 +273,8 @@ def drag_maatouch(self, p1, p2, point_random=(-10, -10, 10, 10)): 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 18a09009f..339c17cc1 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -373,6 +373,7 @@ class Minitouch(Connection): _minitouch_init_thread = None @cached_property + @retry def _minitouch_builder(self): self.minitouch_init() return CommandBuilder(self)