diff --git a/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.BUTTON.png b/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.BUTTON.png index 3b9a21662..94e3bb82a 100644 Binary files a/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.BUTTON.png and b/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.BUTTON.png differ diff --git a/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.png b/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.png index d113173a9..52bc584f0 100644 Binary files a/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.png and b/assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.png differ diff --git a/assets/en/handler/AUTO_SEARCH_MENU_EXIT.BUTTON.png b/assets/en/handler/AUTO_SEARCH_MENU_EXIT.BUTTON.png index 76f8edf5a..ba14506fb 100644 Binary files a/assets/en/handler/AUTO_SEARCH_MENU_EXIT.BUTTON.png and b/assets/en/handler/AUTO_SEARCH_MENU_EXIT.BUTTON.png differ diff --git a/assets/en/handler/AUTO_SEARCH_MENU_EXIT.png b/assets/en/handler/AUTO_SEARCH_MENU_EXIT.png index 7430fe348..005d890c0 100644 Binary files a/assets/en/handler/AUTO_SEARCH_MENU_EXIT.png and b/assets/en/handler/AUTO_SEARCH_MENU_EXIT.png differ diff --git a/campaign/event_20230914_cn/sp.py b/campaign/event_20230914_cn/sp.py index 8e8eff5a3..827eca901 100644 --- a/campaign/event_20230914_cn/sp.py +++ b/campaign/event_20230914_cn/sp.py @@ -6,7 +6,7 @@ from module.logger import logger MAP = CampaignMap('SP') MAP.shape = 'H5' MAP.camera_data = ['E3'] -MAP.camera_data_spawn_point = ['D3'] +MAP.camera_data_spawn_point = ['C3'] MAP.map_data = """ -- ++ ++ ++ ++ -- ME -- SP -- -- -- ++ ME -- ME @@ -62,9 +62,10 @@ class Config: 'distance': 50, 'wlen': 1000 } + HOMO_STORAGE = ((9, 7), [(158.102, 59.806), (1142.124, 59.806), (-34.052, 695.951), (1324.267, 695.951)]) HOMO_EDGE_COLOR_RANGE = (0, 17) HOMO_EDGE_HOUGHLINES_THRESHOLD = 210 - MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom' + MAP_ENSURE_EDGE_INSIGHT_CORNER = 'bottom-right' MAP_IS_ONE_TIME_STAGE = True MAP_SWIPE_MULTIPLY = (1.229, 1.253) diff --git a/deploy/config.py b/deploy/config.py index 22cd410b4..b2885f358 100644 --- a/deploy/config.py +++ b/deploy/config.py @@ -61,6 +61,9 @@ class ConfigModel: CDN: Union[str, bool] = False Run: Optional[str] = None + # Dynamic + GitOverCdn: bool = False + class DeployConfig(ConfigModel): def __init__(self, file=DEPLOY_CONFIG): @@ -79,6 +82,18 @@ class DeployConfig(ConfigModel): 'https://git.saarcenter.com/LmeSzinc/AzurLaneAutoScript.git', ]: self.Repository = 'git://git.lyoko.io/AzurLaneAutoScript' + + # Bypass webui.config.DeployConfig.__setattr__() + # Don't write these into deploy.yaml + super().__setattr__( + 'GitOverCdn', + self.Repository == 'git://git.lyoko.io/AzurLaneAutoScript' and self.Branch == 'master' + ) + if self.Repository in ['global']: + super().__setattr__('Repository', 'https://github.com/LmeSzinc/AzurLaneAutoScript') + if self.Repository in ['cn']: + super().__setattr__('Repository', 'git://git.lyoko.io/AzurLaneAutoScript') + self.write() self.show_config() @@ -87,7 +102,7 @@ class DeployConfig(ConfigModel): for k, v in self.config.items(): if k in ("Password", "SSHUser"): continue - if self.config_template[k] == v: + if self.config_template.get(k) == v: continue logger.info(f"{k}: {v}") diff --git a/deploy/git.py b/deploy/git.py index 75defd392..aa4da24f2 100644 --- a/deploy/git.py +++ b/deploy/git.py @@ -1,6 +1,5 @@ -import os - from deploy.config import DeployConfig +from deploy.git_over_cdn.client import GitOverCdnClient from deploy.logger import logger from deploy.utils import * @@ -78,6 +77,18 @@ class GitManager(DeployConfig): logger.hr('Show Version', 1) self.execute(f'"{self.git}" --no-pager log --no-merges -1') + @property + def goc_client(self): + client = GitOverCdnClient( + url='https://vip.123pan.cn/1818706573/pack/LmeSzinc_AzurLaneAutoScript_master', + folder=self.root_filepath, + source='origin', + branch='master', + git=self.git, + ) + client.logger = logger + return client + def git_install(self): logger.hr('Update Alas', 0) @@ -85,6 +96,10 @@ class GitManager(DeployConfig): logger.info('AutoUpdate is disabled, skip') return + if self.GitOverCdn: + if self.goc_client.update(keep_changes=self.KeepLocalChanges): + return + self.git_repository_init( repo=self.Repository, source='origin', diff --git a/deploy/git_over_cdn/client.py b/deploy/git_over_cdn/client.py new file mode 100644 index 000000000..7a3d8cca6 --- /dev/null +++ b/deploy/git_over_cdn/client.py @@ -0,0 +1,263 @@ +import io +import json +import os +import re +import shutil +import subprocess +import zipfile +from typing import Callable, Generic, TypeVar + +import requests +from requests.adapters import HTTPAdapter + +T = TypeVar("T") + +TEMPLATE_FILE = './config/template.yaml' + + +class cached_property(Generic[T]): + """ + cached-property from https://github.com/pydanny/cached-property + Add typing support + + A property that is only computed once per instance and then replaces itself + with an ordinary attribute. Deleting the attribute resets the property. + Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 + """ + + def __init__(self, func: Callable[..., T]): + self.func = func + + def __get__(self, obj, cls) -> T: + if obj is None: + return self + + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class PrintLogger: + info = print + warning = print + error = print + + @staticmethod + def attr(name, text): + print(f'[{name}] {text}') + + +class GitOverCdnClient: + logger = PrintLogger() + + def __init__(self, url, folder, source='origin', branch='master', git='git'): + """ + Args: + url: http://127.0.0.1:22251/pack/LmeSzinc_AzurLaneAutoScript_master/ + folder: D:/AzurLaneAutoScript + """ + self.url = url.strip('/') + self.folder = folder.replace('\\', '/') + self.source = source + self.branch = branch + self.git = git + + def filepath(self, path): + path = os.path.join(self.folder, '.git', path) + return os.path.abspath(path).replace('\\', '/') + + def urlpath(self, path): + return f'{self.url}{path}' + + @cached_property + def current_commit(self) -> str: + for file in [ + f'./refs/remotes/{self.source}/{self.branch}', + f'./refs/heads/{self.branch}', + 'ORIG_HEAD', + ]: + file = self.filepath(file) + try: + with open(file, 'r', encoding='utf-8') as f: + commit = f.read() + res = re.search(r'([0-9a-f]{40})', commit) + if res: + commit = res.group(1) + self.logger.attr('CurrentCommit', commit) + return commit + except FileNotFoundError as e: + self.logger.error(f'Failed to get local commit: {e}') + except Exception as e: + self.logger.error(f'Failed to get local commit: {e}') + return '' + + @property + def session(self): + session = requests.Session() + session.trust_env = False + session.mount('http://', HTTPAdapter(max_retries=3)) + session.mount('https://', HTTPAdapter(max_retries=3)) + return session + + @cached_property + def latest_commit(self) -> str: + try: + url = self.urlpath('/latest.json') + self.logger.info(f'Fetch url: {url}') + resp = self.session.get(url, timeout=3) + except Exception as e: + self.logger.error(f'Failed to get remote commit: {e}') + return '' + + if resp.status_code == 200: + try: + info = json.loads(resp.text) + commit = info['commit'] + self.logger.attr('LatestCommit', commit) + return commit + except json.JSONDecodeError: + self.logger.error(f'Failed to get remote commit, response is not a json: {resp.text}') + return '' + except KeyError: + self.logger.error(f'Failed to get remote commit, key "commit" is not found: {resp.text}') + return '' + else: + self.logger.error(f'Failed to get remote commit, status={resp.status_code}, text={resp.text}') + return '' + + def download_pack(self): + try: + url = self.urlpath(f'/{self.latest_commit}/{self.current_commit}.zip') + self.logger.info(f'Fetch url: {url}') + resp = self.session.get(url, timeout=20) + except Exception as e: + self.logger.error(f'Failed to download pack: {e}') + return False + + if resp.status_code == 200: + try: + zipped = zipfile.ZipFile(io.BytesIO(resp.content)) + for file in [f'pack-{self.latest_commit}.pack', f'pack-{self.latest_commit}.idx']: + self.logger.info(f'Unzip {file}') + member = zipped.getinfo(file) + tmp = self.filepath(f'./objects/pack/{file}.tmp') + out = self.filepath(f'./objects/pack/{file}') + with zipped.open(member) as source, open(tmp, "wb") as target: + shutil.copyfileobj(source, target) + os.replace(tmp, out) + return True + except zipfile.BadZipFile as e: + # File is not a zip file + self.logger.error(e) + return False + except KeyError as e: + # There is no item named 'xxx.idx' in the archive + self.logger.error(e) + return False + except Exception as e: + self.logger.error(e) + return False + elif resp.status_code == 404: + self.logger.error(f'Failed to download pack, status={resp.status_code}, no such pack files provided') + return False + else: + self.logger.error(f'Failed to download pack, status={resp.status_code}, text={resp.text}') + return False + + def update_refs(self): + file = self.filepath(f'./refs/remotes/{self.source}/{self.branch}') + text = f'{self.latest_commit}\n' + self.logger.info(f'Update refs: {file}') + os.makedirs(os.path.dirname(file), exist_ok=True) + try: + with open(file, 'w', encoding='utf-8', newline='') as f: + f.write(text) + return True + except FileNotFoundError as e: + self.logger.error(f'Failed to get local commit: {e}') + except Exception as e: + self.logger.error(f'Failed to get local commit: {e}') + + return False + + def git_command(self, *args, timeout=300): + """ + Execute ADB commands in a subprocess, + usually to be used when pulling or pushing large files. + + Args: + timeout (int): + + Returns: + str: + """ + os.chdir(self.folder) + cmd = list(map(str, args)) + cmd = [self.git] + cmd + self.logger.info(f'Execute: {cmd}') + + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) + try: + stdout, stderr = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + stdout, stderr = process.communicate() + self.logger.warning(f'TimeoutExpired when calling {cmd}, stdout={stdout}, stderr={stderr}') + return stdout.decode() + + def git_reset(self, keep_changes=False): + """ + git reset --hard + """ + if keep_changes: + self.git_command('stash') + self.git_command('reset', '--hard', f'{self.source}/{self.branch}') + self.git_command('stash', 'pop') + else: + self.git_command('reset', '--hard', f'{self.source}/{self.branch}') + + def is_uptodate(self): + """ + Returns: + bool: If repo is up-to-date + """ + _ = self.current_commit + _ = self.latest_commit + if not self.current_commit: + self.logger.error('Failed to get current commit') + return False + if not self.latest_commit: + self.logger.error('Failed to get latest commit') + return False + if self.current_commit == self.latest_commit: + self.logger.info('Already up to date') + return True + return False + + def update(self, keep_changes=False): + """ + Args: + keep_changes: + + Returns: + bool: If repo is up-to-date + """ + _ = self.current_commit + _ = self.latest_commit + if not self.current_commit: + self.logger.error('Failed to get current commit') + return False + if not self.latest_commit: + self.logger.error('Failed to get latest commit') + return False + if self.current_commit == self.latest_commit: + self.logger.info('Already up to date') + self.git_reset(keep_changes=keep_changes) + return True + + if not self.download_pack(): + return False + if not self.update_refs(): + return False + self.git_reset(keep_changes=keep_changes) + self.logger.info('Update success') + return True diff --git a/deploy/logger.py b/deploy/logger.py index 5d487d938..f5d764cdc 100644 --- a/deploy/logger.py +++ b/deploy/logger.py @@ -33,4 +33,9 @@ def hr(title, level=3): logger.info(f"<<< {title} >>>") +def attr(name, text): + print(f'[{name}] {text}') + + logger.hr = hr +logger.attr = attr diff --git a/module/base/base.py b/module/base/base.py index 2823fc1a5..fc24743c7 100644 --- a/module/base/base.py +++ b/module/base/base.py @@ -172,16 +172,19 @@ class ModuleBase: logger.warning(f'wait_until_stable({button}) timeout') break - def image_crop(self, button): + def image_crop(self, button, copy=True): """Extract the area from image. Args: button(Button, tuple): Button instance or area tuple. + copy: """ if isinstance(button, Button): - return crop(self.device.image, button.area) + return crop(self.device.image, button.area, copy=copy) + elif hasattr(button, 'area'): + return crop(self.device.image, button.area, copy=copy) else: - return crop(self.device.image, button) + return crop(self.device.image, button, copy=copy) def image_color_count(self, button, color, threshold=221, count=50): """ @@ -194,9 +197,14 @@ class ModuleBase: Returns: bool: """ - image = self.image_crop(button) - mask = color_similarity_2d(image, color=color) > threshold - return np.sum(mask) > count + if isinstance(button, np.ndarray): + image = button + else: + image = self.image_crop(button, copy=False) + mask = color_similarity_2d(image, color=color) + cv2.inRange(mask, threshold, 255, dst=mask) + sum_ = cv2.countNonZero(mask) + return sum_ > count def image_color_button(self, area, color, color_threshold=250, encourage=5, name='COLOR_BUTTON'): """ diff --git a/module/handler/assets.py b/module/handler/assets.py index 985e1758a..9a63dc6cc 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -6,8 +6,8 @@ from module.base.template import Template AUTO_SEARCH_MAP_OPTION_OFF = Button(area={'cn': (1205, 549, 1275, 566), 'en': (1203, 552, 1277, 564), 'jp': (1204, 547, 1276, 568), 'tw': (1205, 546, 1275, 567)}, color={'cn': (196, 169, 169), 'en': (151, 132, 138), 'jp': (179, 153, 156), 'tw': (153, 132, 137)}, button={'cn': (1205, 549, 1275, 566), 'en': (1203, 552, 1277, 564), 'jp': (1204, 547, 1276, 568), 'tw': (1205, 546, 1275, 567)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MAP_OPTION_OFF.png', 'en': './assets/en/handler/AUTO_SEARCH_MAP_OPTION_OFF.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MAP_OPTION_OFF.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MAP_OPTION_OFF.png'}) AUTO_SEARCH_MAP_OPTION_ON = Button(area={'cn': (1205, 549, 1275, 566), 'en': (1203, 552, 1277, 564), 'jp': (1203, 547, 1276, 568), 'tw': (1204, 546, 1276, 567)}, color={'cn': (149, 176, 193), 'en': (113, 135, 157), 'jp': (132, 158, 177), 'tw': (110, 133, 156)}, button={'cn': (1205, 549, 1275, 566), 'en': (1203, 552, 1277, 564), 'jp': (1203, 547, 1276, 568), 'tw': (1204, 546, 1276, 567)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MAP_OPTION_ON.png', 'en': './assets/en/handler/AUTO_SEARCH_MAP_OPTION_ON.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MAP_OPTION_ON.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MAP_OPTION_ON.png'}) -AUTO_SEARCH_MENU_CONTINUE = Button(area={'cn': (789, 610, 903, 638), 'en': (851, 612, 981, 634), 'jp': (859, 610, 973, 638), 'tw': (790, 610, 903, 638)}, color={'cn': (147, 182, 224), 'en': (161, 188, 225), 'jp': (139, 173, 218), 'tw': (148, 181, 222)}, button={'cn': (773, 598, 919, 646), 'en': (772, 597, 920, 648), 'jp': (845, 597, 990, 646), 'tw': (776, 601, 919, 645)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MENU_CONTINUE.png', 'en': './assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MENU_CONTINUE.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MENU_CONTINUE.png'}) -AUTO_SEARCH_MENU_EXIT = Button(area={'cn': (419, 609, 475, 637), 'en': (352, 612, 402, 633), 'jp': (348, 609, 401, 636), 'tw': (414, 609, 477, 637)}, color={'cn': (198, 199, 201), 'en': (212, 212, 213), 'jp': (184, 184, 187), 'tw': (204, 204, 206)}, button={'cn': (373, 598, 520, 647), 'en': (372, 597, 521, 648), 'jp': (305, 597, 451, 645), 'tw': (393, 604, 498, 644)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MENU_EXIT.png', 'en': './assets/en/handler/AUTO_SEARCH_MENU_EXIT.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MENU_EXIT.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MENU_EXIT.png'}) +AUTO_SEARCH_MENU_CONTINUE = Button(area={'cn': (789, 610, 903, 638), 'en': (781, 612, 910, 634), 'jp': (859, 610, 973, 638), 'tw': (790, 610, 903, 638)}, color={'cn': (147, 182, 224), 'en': (159, 187, 224), 'jp': (139, 173, 218), 'tw': (148, 181, 222)}, button={'cn': (773, 598, 919, 646), 'en': (773, 598, 919, 647), 'jp': (845, 597, 990, 646), 'tw': (776, 601, 919, 645)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MENU_CONTINUE.png', 'en': './assets/en/handler/AUTO_SEARCH_MENU_CONTINUE.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MENU_CONTINUE.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MENU_CONTINUE.png'}) +AUTO_SEARCH_MENU_EXIT = Button(area={'cn': (419, 609, 475, 637), 'en': (421, 611, 472, 633), 'jp': (348, 609, 401, 636), 'tw': (414, 609, 477, 637)}, color={'cn': (198, 199, 201), 'en': (210, 210, 212), 'jp': (184, 184, 187), 'tw': (204, 204, 206)}, button={'cn': (373, 598, 520, 647), 'en': (373, 598, 520, 647), 'jp': (305, 597, 451, 645), 'tw': (393, 604, 498, 644)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MENU_EXIT.png', 'en': './assets/en/handler/AUTO_SEARCH_MENU_EXIT.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MENU_EXIT.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MENU_EXIT.png'}) AUTO_SEARCH_OFF = Button(area={'cn': (867, 588, 883, 604), 'en': (830, 588, 846, 604), 'jp': (849, 588, 865, 604), 'tw': (867, 588, 883, 604)}, color={'cn': (94, 92, 94), 'en': (90, 89, 92), 'jp': (99, 99, 109), 'tw': (94, 92, 94)}, button={'cn': (867, 588, 883, 604), 'en': (830, 588, 846, 604), 'jp': (849, 588, 865, 604), 'tw': (867, 588, 883, 604)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_OFF.png', 'en': './assets/en/handler/AUTO_SEARCH_OFF.png', 'jp': './assets/jp/handler/AUTO_SEARCH_OFF.png', 'tw': './assets/tw/handler/AUTO_SEARCH_OFF.png'}) AUTO_SEARCH_ON = Button(area={'cn': (867, 588, 883, 604), 'en': (830, 588, 846, 604), 'jp': (849, 588, 865, 604), 'tw': (867, 588, 883, 604)}, color={'cn': (140, 167, 120), 'en': (139, 168, 112), 'jp': (140, 167, 122), 'tw': (140, 167, 120)}, button={'cn': (867, 588, 883, 604), 'en': (830, 588, 846, 604), 'jp': (849, 588, 865, 604), 'tw': (867, 588, 883, 604)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_ON.png', 'en': './assets/en/handler/AUTO_SEARCH_ON.png', 'jp': './assets/jp/handler/AUTO_SEARCH_ON.png', 'tw': './assets/tw/handler/AUTO_SEARCH_ON.png'}) AUTO_SEARCH_SET_ALL = Button(area={'cn': (830, 189, 849, 207), 'en': (814, 192, 832, 209), 'jp': (830, 189, 849, 207), 'tw': (830, 189, 849, 207)}, color={'cn': (38, 39, 40), 'en': (33, 33, 35), 'jp': (43, 41, 42), 'tw': (42, 41, 43)}, button={'cn': (830, 189, 849, 207), 'en': (814, 192, 832, 209), 'jp': (830, 189, 849, 207), 'tw': (830, 189, 849, 207)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_SET_ALL.png', 'en': './assets/en/handler/AUTO_SEARCH_SET_ALL.png', 'jp': './assets/jp/handler/AUTO_SEARCH_SET_ALL.png', 'tw': './assets/tw/handler/AUTO_SEARCH_SET_ALL.png'}) diff --git a/module/handler/info_handler.py b/module/handler/info_handler.py index 17bbfbd0d..8488a5315 100644 --- a/module/handler/info_handler.py +++ b/module/handler/info_handler.py @@ -316,7 +316,8 @@ class InfoHandler(ModuleBase): image = color_similarity_2d(self.image_crop(story_detect_area), color=story_option_color) line = cv2.reduce(image, 1, cv2.REDUCE_AVG).flatten() - line[line < 128] = 0 + line[line < 200] = 0 + line[line >= 200] = 255 parameters = { # Option is 300`320px x 50~52px. diff --git a/module/os_handler/shop.py b/module/os_handler/shop.py index b9f07a1c8..fda7d56ee 100644 --- a/module/os_handler/shop.py +++ b/module/os_handler/shop.py @@ -40,7 +40,7 @@ class OSShopHandler(OSStatus, MapEventHandler): ItemGrid: """ shop_grid = ButtonGrid( - origin=(238, 220), delta=(188, 225), button_shape=(98, 98), grid_shape=(4, 2), name='SHOP_GRID') + origin=(233, 224), delta=(193, 228), button_shape=(98, 98), grid_shape=(4, 2), name='SHOP_GRID') shop_items = ItemGrid( shop_grid, templates={}, amount_area=(60, 74, 96, 95), price_area=(52, 132, 132, 165)) shop_items.price_ocr = OSShopPrice([], letter=(255, 223, 57), threshold=32, name='Price_ocr') diff --git a/module/webui/updater.py b/module/webui/updater.py index af39ad932..915c55816 100644 --- a/module/webui/updater.py +++ b/module/webui/updater.py @@ -68,6 +68,15 @@ class Updater(DeployConfig, GitManager, PipManager): def _check_update(self) -> bool: self.state = "checking" + + if State.deploy_config.GitOverCdn: + if self.goc_client.is_uptodate(): + logger.info(f"No update") + return False + else: + logger.info(f"New update available") + return True + source = "origin" for _ in range(3): if self.execute(