├── .gitignore ├── LICENSE ├── README.md ├── _version.py ├── logger.py ├── main.py ├── metadata.json ├── requirements.txt ├── runner ├── __init__.py ├── base.py ├── connector │ ├── __init__.py │ ├── adb_connector.py │ ├── base.py │ └── mumu_connector.py ├── gui │ └── __init__.py ├── player │ ├── __init__.py │ ├── base.py │ └── cv_player.py └── simple.py ├── schedule.py ├── util.py └── wanted ├── active_begin.jpg ├── back.jpg ├── box.jpg ├── confirm.jpg ├── hunwang_jixu.jpg ├── jixu2.jpg ├── k28.jpg ├── ok.jpg ├── refuse.jpg ├── tansuo_begin.jpg ├── tansuo_boss.jpg ├── tansuo_confirm.jpg ├── tansuo_exit.jpg ├── tansuo_fight.jpg ├── yj_exp_begin.jpg ├── yuling_begin.jpg ├── yyh_begin.jpg ├── yys_begin.jpg ├── yys_jieshu.jpg ├── yys_jixu.jpg └── yys_jixu2.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | test.* 163 | /test/ 164 | /cache/ 165 | 166 | main.exe 167 | *.zip 168 | 169 | release/ 170 | 171 | tools/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 tbjuechen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 阴阳师脚本 2 | 3 | 使用[adb-shell](https://adb-shell.readthedocs.io/en/stable/)连接模拟器/手机,通过adb标准规范与安卓设备通信,实现截图、虚拟点击等核心功能。 4 | 5 | 使用[opencv](https://github.com/opencv/opencv-python)的模板匹配模块检测截图中的目标图像位置,在找到目标后进行更多操作。 6 | 7 | 感谢@[anywhere2go](https://github.com/anywhere2go)的[工作](https://github.com/anywhere2go/auto_player),本项目基于这个思路开发。 8 | 9 | QQ交流群:157307963 10 | 11 | ### 写在前面 12 | 13 | 阴阳师已经来到第八个年头,进入了游戏的生命周期末期,大部分玩家可能已经失去了当初的激情,懒得再每天挖土、爬塔,这也是这个项目诞生的原因。使用本脚本可以避免绝大部分游戏中的重复劳动,解放你的业余时间。但是,作为脚本用户,请不要因此跳脸普通玩家,也不要在墙内公共社交平台大肆宣扬这个项目,更不要用这个脚本去做代肝。 14 | 15 | 此外,由于精力有限,这个项目并不会很快的发展到一个完全完善、稳定的版本,但是在退坑前会一直保持维护,只要你还能看到句话,欢迎[issue](https://github.com/tbjuechen/script/issues)或[pr](https://github.com/tbjuechen/script/pulls),不过请保护好自己的游戏id。 16 | 17 | 最后,一句老生常谈的话送给大家——**用别怕,怕别用**。 18 | 19 | ### 使用方法 20 | 21 | 1. 从[release](https://github.com/tbjuechen/script/releases)下载最新的版本。 22 | 2. 运行程序,根据流程选择需要执行的脚本并连接到您的设备,然后,do anything you want~ 23 | 24 | ### 注意事项 25 | 26 | 1. 因为使用纯视觉方案,脚本所需资源需要动态下载,请确保和`raw.githubusercontent.com`的连接。在当前版本(v1.2.1)中,在版本检查过程中出错后会自动进入离线模式。**离线模式不会更新最新的资源**,在某些情况下可能会导致脚本失效,例如每个月的伴生爬塔活动。 27 | 2. 关于连接到设备,本脚本使用`abc-shell`库提供的tcp连接方法连接到您的设备,所以设备的`ip`和`端口`是必须的。如果你不理解这是什么,那么推荐你使用mumu模拟器。在成功运行模拟器后,在mumu多开器(**注意不是模拟器**)右上角点击带有`adb`字样的图标即可获得正在运行的模拟器的`端口号`,而在本机运行的模拟器`ip`均为**127.0.0.1**。 28 | 3. 目前只提供了mumu模拟器选项,但是在当前版本并未区分不同模拟器的特性(~~其实是我懒~~),固对于不同模拟器甚至是手机,都是可以通过mumu选项进行正常连接的。 29 | 4. 因为种种原因,目前仅支持**1920*1080分辨率**的设备,请将你的设备调整到这个分辨率,不然将会导致脚本无法使用。 30 | 5. 由于精力有限,无法每次面面俱到地测试程序,所以在遇到问题后请尝试更新到最新版本、使用mumu模拟器(因为开发时用它测试的)、使用默认的游戏设置等。如果问题还不能解决,欢迎大家将发现的问题提到[issue](https://github.com/tbjuechen/script/issues)中,我会尽量帮助解决,但是请不要做不会思考的伸手党。 31 | 32 | ### 目前实现: 33 | 34 | * [X] 基本流程框架 35 | * [X] 业原火 36 | * [X] 御魂组队副本 37 | * [X] 活动爬塔 38 | * [X] 御灵 39 | * [X] 源赖光经验副本 40 | 41 | 如果你有更多的需求或者idea,也欢迎提到issue中。 42 | 43 | 如果你想加入开发,请继续看下面的部分 44 | 45 | ### 写给开发者 46 | 47 | 无论你是大佬还是萌新,甚至是0基础想学习代码,这里都欢迎你。如果你有这个意向,请通过QQ或者GitHub联系我。 48 | 49 | ### 未来规划 50 | 51 | 1. 更易用的前端界面:目前使用Node.js风格的inquirer命令行,对于当前已经够用,但是对于复杂功能的开发就显得捉襟见肘了,所以希望擅长pyqt的大佬可以协助重构这个前端。 52 | 2. 更多的识别方式:目前的视觉方案中主要使用了opencv中`matchTemplate`这个api,在低性能的机器上存在低效的问题,所以希望在未来能够探索更轻量的模板匹配模型或是OCR模型,例如[paddleOCR](https://github.com/PaddlePaddle/PaddleOCR)。 53 | 3. 更优秀的脚本流程:目前的流程使用简单粗暴的循环,可以稳定运行,但是效率显然不是最佳的,希望可以通过优化脚本流程、增加状态控制等方法加速脚本执行的速度。 54 | 4. 更合理的架构:本项目时从过去我一直使用的脚本重构而来,架构均为一拍脑袋想的,在未来开发中可能发现越来越多的不足,所以在开发过程中会不断优化架构。 55 | -------------------------------------------------------------------------------- /_version.py: -------------------------------------------------------------------------------- 1 | __version__ :str = "v1.2.1" 2 | __author__:str = "tbjuechen <1324374092@qq.com>" 3 | __license__:str = "MIT" 4 | __date__:str = "2024-09-11" 5 | __description__:list[str] = [f'阴阳师自动化脚本工具 {__version__}', 6 | '游戏版本:20240911-周年庆'] 7 | __online__:bool = True 8 | 9 | from util import format_output_info 10 | 11 | __description__ = format_output_info(__description__, 50) 12 | 13 | from performer import loader 14 | from logger import logger 15 | 16 | try: 17 | remote_version = loader.check_remote_version() 18 | if remote_version != __version__: 19 | if __version__[:4] == remote_version[:4]: 20 | logger.warning(f'检测到新版本: {remote_version},请前往Github下载') 21 | else: 22 | logger.error(f'检测到新版本: {remote_version},当前版本: {__version__}已过期') 23 | while True:... 24 | else: 25 | logger.info('当前版本为最新版本') 26 | except Exception as e: 27 | logger.debug(f'检查版本失败: {type(e).__name__} - {str(e)}') 28 | if type(e).__name__ == 'ReadTimeout': 29 | logger.error('请检查网络连接') 30 | logger.info('进入离线模式') 31 | __online__ = False 32 | else: 33 | while True:... -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: colored logger 6 | License: MIT 7 | ''' 8 | 9 | import logging 10 | 11 | class ColoredFormatter(logging.Formatter): 12 | # define color for different log levels 13 | COLORS = { 14 | 'DEBUG': '\033[90m', # Grey 15 | 'INFO': '\033[94m', # Blue 16 | 'WARNING': '\033[93m', # Yellow 17 | 'ERROR': '\033[91m', # Red 18 | 'CRITICAL': '\033[95m' # Magenta 19 | } 20 | RESET = '\033[0m' 21 | 22 | MSG_FORMAT = lambda t:f'%(asctime)s {t} %(module)s - %(message)s' 23 | 24 | # make sure the log level is in the right color and width 25 | FORMATS = { 26 | logging.DEBUG: MSG_FORMAT(COLORS['DEBUG'] + ' [DEBUG] ' + RESET), 27 | logging.INFO: MSG_FORMAT(COLORS['INFO'] + ' [INFO] ' + RESET), 28 | logging.WARNING: MSG_FORMAT(COLORS['WARNING'] + ' [WARNING] ' + RESET), 29 | logging.ERROR: MSG_FORMAT(COLORS['ERROR'] + ' [ERROR] ' + RESET), 30 | logging.CRITICAL: MSG_FORMAT(COLORS['CRITICAL'] + '[CRITICAL] ' + RESET) 31 | } 32 | 33 | def format(self, record): 34 | log_fmt = self.FORMATS.get(record.levelno) 35 | formatter = logging.Formatter(log_fmt, datefmt=self.datefmt) 36 | return formatter.format(record) 37 | 38 | # create logger with 'logger' 39 | logger = logging.getLogger('logger') 40 | logger.setLevel(logging.INFO) 41 | 42 | # create console handler and set level to debug 43 | console_handler = logging.StreamHandler() 44 | console_handler.setLevel(logging.DEBUG) 45 | 46 | # create formatter with datefmt and add it to the handler 47 | formatter = ColoredFormatter(datefmt='%H:%M:%S') 48 | console_handler.setFormatter(formatter) 49 | 50 | # add the handler to the logger 51 | logger.addHandler(console_handler) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: main file 6 | License: MIT 7 | ''' 8 | 9 | from logger import logger 10 | import os 11 | 12 | from performer import scripts 13 | from performer.performer import Performer 14 | from _version import __description__, __online__ 15 | 16 | from InquirerPy import inquirer 17 | from InquirerPy.base.control import Choice 18 | 19 | 20 | print(__description__) 21 | logger.warning('默认使用1920*1080分辨率,如需更改请修改wanted文件夹下的图片') 22 | performer:Performer = inquirer.select( 23 | message="Select a script", 24 | choices=[Choice(value=script,name=script.description) for script in scripts] 25 | ).execute()(logger=logger, online=__online__) 26 | 27 | performer.run() 28 | 29 | os.system('pause') -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1.2.1" 3 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | adb-shell==0.4.4 2 | altgraph==0.17.4 3 | cffi==1.17.0 4 | cryptography==43.0.0 5 | importlib_metadata==8.2.0 6 | inquirerpy==0.3.4 7 | numpy==2.0.1 8 | opencv-python==4.10.0.84 9 | packaging==24.1 10 | pefile==2023.2.7 11 | pfzy==0.3.4 12 | prompt_toolkit==3.0.47 13 | pyasn1==0.6.0 14 | pycparser==2.22 15 | pyinstaller==6.10.0 16 | pyinstaller-hooks-contrib==2024.8 17 | pywin32-ctypes==0.2.2 18 | rsa==4.9 19 | wcwidth==0.2.13 20 | zipp==3.20.0 21 | -------------------------------------------------------------------------------- /runner/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Runner 2 | 3 | -------------------------------------------------------------------------------- /runner/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from multiprocessing import Process, Event, Manager 3 | import os 4 | import time 5 | 6 | 7 | from loguru import logger 8 | import numpy as np 9 | 10 | from .connector import Connector 11 | from .player import Player, CVPlayer 12 | from .gui import GUI 13 | 14 | class Runner(ABC, Process): 15 | name:str = 'Base' 16 | '''Base class for the runner 17 | ''' 18 | def __init__(self, connection_class:type, connector_args:dict={}, player:Player=CVPlayer(), time_interval:int=1, **kwargs): 19 | super().__init__(**kwargs) 20 | self.status = Manager().Value('s', 'idle') 21 | self.time_interval = time_interval 22 | self.connection:Connector = None 23 | self.player = player 24 | self.logger = None 25 | 26 | self.pause_event = Event() 27 | self.stop_event = Event() 28 | self.pause_event.set() 29 | 30 | self.connection_class = connection_class 31 | self.connector_args = connector_args 32 | 33 | def path_init(self): 34 | '''initialize the path 35 | ''' 36 | local_abs_path = os.getcwd() 37 | self.cache_path = os.path.join(local_abs_path, 'cache', f'{self.connection.name}-cache') 38 | os.makedirs(self.cache_path, exist_ok=True) 39 | self.wanted_path = os.path.join(local_abs_path, 'wanted') 40 | self.connection.config.SCREEN_SHOT_PATH_LOCAL = self.cache_path 41 | 42 | def _init(self): 43 | '''Initialize the runner 44 | ''' 45 | self.logger = logger 46 | 47 | self.connection:Connector = self.connection_class(**self.connector_args) 48 | self.status.value = 'idle' 49 | 50 | self.path_init() 51 | self.logger.debug(f'Runner {self.name} initialized') 52 | 53 | self.gui = GUI() 54 | 55 | @abstractmethod 56 | def work(self): 57 | '''The main sequence of the script 58 | ''' 59 | pass 60 | 61 | def run(self): 62 | '''The main loop 63 | ''' 64 | self._init() 65 | self.gui.start() 66 | 67 | self.status.value = 'running' 68 | self.logger.info(f'{self.name} started') 69 | self.connection.connect() 70 | while not self.stop_event.is_set(): 71 | self.pause_event.wait() # wait for resume 72 | self.work() 73 | time.sleep(self.time_interval) 74 | 75 | self.connection.disconnect() 76 | self.logger.info(f'{self.name} stopped') 77 | self.gui.stop() 78 | 79 | def pause(self): 80 | '''Pause the runner 81 | ''' 82 | if self.status.value == 'running': 83 | self.status.value = 'paused' 84 | self.pause_event.clear() 85 | else: 86 | raise ValueError(f'Runner {self.name} is not running') 87 | 88 | def resume(self): 89 | '''Resume the runner 90 | ''' 91 | if self.status.value == 'paused': 92 | self.status.value = 'running' 93 | self.pause_event.set() 94 | else: 95 | raise ValueError(f'Runner {self.name} is not paused') 96 | 97 | def stop(self): 98 | '''Stop the runner 99 | ''' 100 | self.status.value = 'stopped' 101 | self.stop_event.set() 102 | 103 | def _find(self, target:str): 104 | '''Find the target in the screenshot 105 | 106 | Parameters 107 | ---------- 108 | target : str 109 | The target image file name 110 | ''' 111 | screenshot:str = self.connection.screen_shot() 112 | location = self.player.locate(os.path.join(self.wanted_path, target), screenshot) 113 | if location: 114 | self.logger.info(f'Found {target} at {location}') 115 | return location 116 | else: 117 | self.logger.debug(f'{target} not found') 118 | return None 119 | 120 | def _touch(self, x:int, y:int): 121 | '''Touch the screen at the location 122 | 123 | Parameters 124 | ---------- 125 | x : int 126 | The x coordinate 127 | y : int 128 | The y coordinate 129 | ''' 130 | self.connection.touch(x, y) 131 | self.logger.debug(f'Touched at {x}, {y}') 132 | 133 | def generate_position_by_normal(self, x:int, y:int, offset:int=10)->tuple: 134 | '''Generate the position by normal distribution 135 | 136 | Parameters 137 | ---------- 138 | x : int 139 | The x coordinate 140 | y : int 141 | The y coordinate 142 | offset : int, optional 143 | The offset, by default 10 144 | ''' 145 | return int(x + offset * np.random.normal()), int(y + offset * np.random.normal()) 146 | 147 | def find_and_touch(self, target:str): 148 | '''Find and touch the target 149 | 150 | Parameters 151 | ---------- 152 | target : str 153 | The target image file name 154 | ''' 155 | location = self._find(target) 156 | if location: 157 | location = self.generate_position_by_normal(*location) 158 | self._touch(*location) 159 | time.sleep(0.05) 160 | return True 161 | else: 162 | return False 163 | 164 | class FindListRunner(Runner): 165 | '''virtual class for finding a list of targets script 166 | ''' 167 | name:str = 'FindListBase' 168 | targets:list = [] 169 | discription:str = 'FindListBase' 170 | def __init__(self, **kwargs): 171 | super().__init__(**kwargs) 172 | 173 | def work(self): 174 | for target in self.targets: 175 | self.find_and_touch(target) -------------------------------------------------------------------------------- /runner/connector/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: module declaration 6 | License: MIT 7 | ''' 8 | 9 | from .mumu_connector import MumuConnector 10 | from .adb_connector import AdbConnector 11 | from .base import Connector -------------------------------------------------------------------------------- /runner/connector/adb_connector.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: This is a connector with adb-shell, which is used to connect to the android device. 6 | License: MIT 7 | ''' 8 | 9 | from logging import Logger 10 | from typing import Union 11 | import os 12 | 13 | from loguru import logger 14 | 15 | from .base import Connector, BaseConnectorConfig, DefaultConnectorConfig 16 | 17 | from adb_shell.adb_device import AdbDeviceTcp 18 | 19 | class AdbConnector(AdbDeviceTcp, Connector): 20 | ''' 21 | A class with methods for connecting to a device via TCP and executing ADB commands base on AdbDeviceTcp. 22 | 23 | Parameters 24 | ---------- 25 | host : str 26 | The address of the device; may be an IP address or a host name 27 | port : int 28 | The device port to which we are connecting (default is 5555) 29 | logger : Logger 30 | The logger object for logging messages 31 | config : BaseConnector 32 | The configuration of the connector 33 | default_transport_timeout_s : float, None 34 | Default timeout in seconds for TCP packets, or ``None`` 35 | banner : str, bytes, None 36 | The hostname of the machine where the Python interpreter is currently running; if 37 | it is not provided, it will be determined via ``socket.gethostname()`` 38 | ''' 39 | def __init__(self, 40 | host:str, 41 | port:int, 42 | config:BaseConnectorConfig=DefaultConnectorConfig(), 43 | **kwargs): 44 | default_transport_timeout_s = kwargs.get('default_transport_timeout_s', None) 45 | banner = kwargs.get('banner', None) 46 | self.host = host 47 | self.port = port 48 | super().__init__(host, port, default_transport_timeout_s, banner) 49 | Connector.__init__(self) 50 | 51 | self.config:BaseConnectorConfig = config 52 | logger.debug(f'build adb connector with host: {host}, port: {port}') 53 | 54 | def connect(self)->bool: 55 | '''Connect to the device. 56 | 57 | Returns 58 | ------- 59 | bool 60 | True if the connection was successful, False otherwise 61 | ''' 62 | logger.debug(f'try to connect to {self.host}:{self.port}') 63 | try: 64 | super().connect() 65 | logger.info(f'connected to {self.host}:{self.port}') 66 | except Exception as e: 67 | logger.error(type(e).__name__ + ': ' + str(e)) 68 | finally: 69 | return self._available 70 | 71 | def disconnect(self) -> bool: 72 | '''Disconnect from the device. 73 | 74 | Returns 75 | ------- 76 | bool 77 | True if the disconnection was successful, False otherwise 78 | ''' 79 | logger.debug(f'try to disconnect from {self.host}:{self.port}') 80 | try: 81 | if not self._available: 82 | return True 83 | super().close() 84 | logger.info(f'disconnected from {self.host}:{self.port}') 85 | except Exception as e: 86 | logger.error(type(e).__name__ + ': ' + str(e)) 87 | finally: 88 | return not self._available 89 | 90 | def shell(self, cmd:str)->Union[str, None]: 91 | '''execute a shell command on the device. 92 | 93 | Parameters 94 | ---------- 95 | cmd : str 96 | The command to be executed 97 | 98 | Returns 99 | ------- 100 | Union[str, None] 101 | The output of the command if the command was successful, None otherwise 102 | ''' 103 | logger.debug(f'try to execute shell command: {cmd}') 104 | try: 105 | result = super().shell(cmd) 106 | logger.debug(f'executed shell command: {cmd}') 107 | return result 108 | except Exception as e: 109 | logger.error(type(e).__name__ + ': ' + str(e)) 110 | 111 | def screen_shot(self)->Union[str, None]: 112 | '''Take a screenshot of the device. 113 | 114 | Returns 115 | ------- 116 | str 117 | Path to the screenshot file if the screenshot was successful, None otherwise 118 | ''' 119 | logger.debug(f'try to take a screenshot of the device') 120 | target_path = self.config.SCREEN_SHOT_PATH_LOCAL 121 | screenshot_name = self.config.SCREEN_SHOT_NAME 122 | 123 | # check if the target path exists, if not create it 124 | if not os.path.exists(target_path): 125 | os.makedirs(target_path) 126 | 127 | try: 128 | cmd:str = f'screencap -p sdcard/Pictures/{screenshot_name}.jpg' 129 | response = self.shell(cmd) 130 | if not response: 131 | logger.debug('Screenshot success') 132 | else: 133 | raise RuntimeError('Screenshot failed with response: ' + response) 134 | 135 | # pull the screenshot from the device 136 | screenshot_path:str = os.path.join(target_path, screenshot_name + '.jpg') 137 | self.pull(f'sdcard/Pictures/{screenshot_name}.jpg', screenshot_path) 138 | logger.info(f'screenshot saved to {screenshot_path}') 139 | return screenshot_path 140 | except Exception as e: 141 | logger.error(type(e).__name__ + ': ' + str(e)) 142 | 143 | def touch(self, x:int, y:int)->bool: 144 | '''Touch the screen at the specified position. 145 | 146 | Parameters 147 | ---------- 148 | x : int 149 | The x coordinate 150 | y : int 151 | The y coordinate 152 | 153 | Returns 154 | ------- 155 | bool 156 | True if the touch was successful, False otherwise 157 | ''' 158 | logger.debug(f'try to touch the screen at position ({x}, {y})') 159 | try: 160 | cmd:str = f'input tap {x} {y}' 161 | response:str = self.shell(cmd) 162 | if not response: 163 | logger.info(f'touched the screen at position ({x}, {y})') 164 | return True 165 | else: 166 | raise RuntimeError('Touch failed with response: ' + response) 167 | except Exception as e: 168 | logger.error(type(e).__name__ + ': ' + str(e)) 169 | return False 170 | 171 | def drag(self, x1:int, y1:int, x2:int, y2:int, duration:float)->bool: 172 | '''Drag the screen from the start position to the end position. 173 | 174 | Parameters 175 | ---------- 176 | x1 : int 177 | The start x coordinate 178 | y1 : int 179 | The start y coordinate 180 | x2 : int 181 | The end x coordinate 182 | y2 : int 183 | The end y coordinate 184 | duration : float 185 | The duration of the drag 186 | 187 | Returns 188 | ------- 189 | bool 190 | True if the drag was successful, False otherwise 191 | ''' 192 | logger.debug(f'try to drag the screen from ({x1}, {y1}) to ({x2}, {y2})') 193 | try: 194 | cmd:str = f'input swipe {x1} {y1} {x2} {y2} {int(duration * 1000)}' 195 | response:str = self.shell(cmd) 196 | if not response: 197 | logger.info(f'dragged the screen from ({x1}, {y1}) to ({x2}, {y2})') 198 | return True 199 | else: 200 | raise RuntimeError('Drag failed with response: ' + response) 201 | except Exception as e: 202 | logger.error(type(e).__name__ + ': ' + str(e)) 203 | return False -------------------------------------------------------------------------------- /runner/connector/base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: Abstract base class for the connector. 6 | License: MIT 7 | ''' 8 | 9 | from abc import ABC, abstractmethod 10 | import os 11 | 12 | cnt:int = 1 13 | 14 | class Connector(ABC): 15 | def __init__(self): 16 | global cnt 17 | self.name:str = f'{self.__class__.__name__}-{cnt}' 18 | cnt += 1 19 | 20 | @abstractmethod 21 | def connect(self)->bool:... 22 | 23 | @abstractmethod 24 | def disconnect(self)->bool:... 25 | 26 | @abstractmethod 27 | def screen_shot(self, target_path:str)->str:... 28 | 29 | @abstractmethod 30 | def touch(self, x:int, y:int):... 31 | 32 | @abstractmethod 33 | def drag(self, x1:int, y1:int, x2:int, y2:int, duration:float):... 34 | 35 | class BaseConnectorConfig: 36 | '''Base class for the configuration of the connector. 37 | 38 | Attributes 39 | ---------- 40 | SCREEN_SHOT_PATH_LOCAL : str 41 | The local path where the screenshot should be stored 42 | 43 | SCREEN_SHOT_NAME : str 44 | The name of the screenshot file 45 | 46 | OFFSET : tuple 47 | The offset for the touch and drag functions 48 | ''' 49 | def __init__(self, SCREEN_SHOT_PATH_LOCAL:str, SCREEN_SHOT_NAME:str, OFFSET:tuple): 50 | self.SCREEN_SHOT_PATH_LOCAL:str = SCREEN_SHOT_PATH_LOCAL 51 | self.SCREEN_SHOT_NAME:str = SCREEN_SHOT_NAME 52 | self.OFFSET:tuple = OFFSET 53 | 54 | 55 | class DefaultConnectorConfig(BaseConnectorConfig): 56 | '''Default configuration of the connector. 57 | 58 | Attributes 59 | ---------- 60 | SCREEN_SHOT_PATH_LOCAL : str 61 | cache folder in the current working directory 62 | 63 | SCREEN_SHOT_NAME : str 64 | screenshot 65 | 66 | OFFSET : tuple 67 | (10, 10) 68 | ''' 69 | def __init__(self): 70 | super().__init__(SCREEN_SHOT_PATH_LOCAL = os.path.join(os.getcwd(), 'cache'), 71 | SCREEN_SHOT_NAME = 'screenshot', 72 | OFFSET = (10, 10)) -------------------------------------------------------------------------------- /runner/connector/mumu_connector.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: An ADB connector for the Mumu emulator. 6 | License: MIT 7 | ''' 8 | 9 | from .adb_connector import AdbConnector 10 | 11 | class MumuConnector(AdbConnector): 12 | name:str = 'Mumu' 13 | '''Connector for the Mumu emulator. 14 | 15 | Attributes 16 | ---------- 17 | host : str 18 | The host of the emulator default:`127.0.0.1` 19 | 20 | port : int 21 | The port of the emulator default:`16384` 22 | ''' 23 | def __init__(self, host:str='127.0.0.1', port:int=16384, **kwargs): 24 | super().__init__(host, port,**kwargs) -------------------------------------------------------------------------------- /runner/gui/__init__.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | import time 4 | from typing import Callable 5 | 6 | import tkinter as tk 7 | from tkinter import font 8 | from multiprocessing import current_process 9 | 10 | from loguru import logger 11 | 12 | class GUI(Thread): 13 | ''' 14 | GUI for display the log of the runner in another thread 15 | ''' 16 | def __init__(self, **kwargs): 17 | super().__init__(**kwargs) 18 | self.logger = logger 19 | 20 | def init_gui(self): 21 | '''Initialize the GUI 22 | ''' 23 | self.root = tk.Tk() 24 | self.root.title(f'Runner {current_process().name} Log') 25 | self.root.geometry('800x600') 26 | self.text = tk.Text(self.root, font=font.Font(family='Consolas', size=10)) 27 | self.text.pack(expand=True, fill='both') 28 | 29 | class MultiColorTextHandler: 30 | """ 31 | 自定义日志处理类,为 time、level 和 message 设置不同颜色。 32 | """ 33 | def __init__(self, text_widget:tk.Text): 34 | self.text_widget:tk.Text = text_widget 35 | self.text_widget.config(state=tk.DISABLED) 36 | 37 | # 配置颜色 38 | self.colors:dict = { 39 | "time": "blue", 40 | "level": { 41 | "DEBUG": "gray", 42 | "INFO": "green", 43 | "WARNING": "orange", 44 | "ERROR": "red", 45 | "CRITICAL": "purple", 46 | }, 47 | "message": "black", 48 | } 49 | 50 | # 定义标签样式 51 | self.text_widget.tag_configure("time", foreground=self.colors["time"]) 52 | for level, color in self.colors["level"].items(): 53 | self.text_widget.tag_configure(f"level_{level}", foreground=color) 54 | self.text_widget.tag_configure("message", foreground=self.colors["message"]) 55 | 56 | def write(self, record): 57 | """ 58 | 根据日志记录动态应用不同颜色到 time、level 和 message。 59 | """ 60 | time = record["time"].strftime("%Y-%m-%d %H:%M:%S") 61 | level = record["level"].name.ljust(8) 62 | message = record["message"] 63 | 64 | self.text_widget.config(state=tk.NORMAL) 65 | 66 | # 插入 time 67 | self.text_widget.insert(tk.END, f"{time} ", "time") 68 | 69 | self.text_widget.insert(tk.END, " | ") 70 | 71 | # 插入 level 72 | self.text_widget.insert(tk.END, f"{level} ", f"level_{level}") 73 | 74 | self.text_widget.insert(tk.END, " | ") 75 | 76 | # 插入 message 77 | self.text_widget.insert(tk.END, f"{message}\n", "message") 78 | 79 | self.text_widget.yview(tk.END) 80 | self.text_widget.config(state=tk.DISABLED) 81 | 82 | def flush(self): 83 | """ 84 | 保留兼容性,但不执行具体操作。 85 | """ 86 | pass 87 | 88 | self.text_handler = MultiColorTextHandler(self.text) 89 | 90 | def log_message_sink(message): 91 | record = message.record # 获取日志记录 92 | self.text_handler.write(record) # 传递记录给自定义处理器 93 | 94 | self.logger.remove() 95 | self.logger.add(log_message_sink, level='DEBUG') 96 | 97 | self.root.mainloop() 98 | 99 | def run(self): 100 | '''Run the GUI 101 | ''' 102 | self.init_gui() 103 | 104 | def stop(self): 105 | '''Stop the GUI 106 | ''' 107 | self.root.quit() -------------------------------------------------------------------------------- /runner/player/__init__.py: -------------------------------------------------------------------------------- 1 | from .cv_player import CVPlayer 2 | from .base import Player -------------------------------------------------------------------------------- /runner/player/base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: base class for all players 6 | License: MIT 7 | ''' 8 | 9 | from abc import ABC, abstractmethod 10 | 11 | class Player(ABC): 12 | '''Base class for all players 13 | 14 | Attributes 15 | ---------- 16 | acc : float 17 | The accuracy of the player model 18 | ''' 19 | def __init__(self, acc:float=0.8, **kwargs): 20 | self.acc = acc 21 | 22 | @abstractmethod 23 | def locate(self, target:str, screenshot:str, debug:bool=False)->tuple:... 24 | 25 | @abstractmethod 26 | def load(self, path:str)->bool:... -------------------------------------------------------------------------------- /runner/player/cv_player.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: tbjuechen 3 | Date: 2024-08-12 4 | Version: 1.0 5 | Description: player with opencv 6 | License: MIT 7 | ''' 8 | 9 | from loguru import logger 10 | import time 11 | 12 | from .base import Player 13 | 14 | import cv2 15 | import numpy as np 16 | 17 | class CVPlayer(Player): 18 | '''Player with opencv 19 | 20 | Attributes 21 | ---------- 22 | acc : float 23 | The accuracy of the player model (default 0.8) 24 | ''' 25 | def __init__(self, acc:float=0.8, **kwargs): 26 | super().__init__(acc, **kwargs) 27 | 28 | def load(self, path:str)->cv2.typing.MatLike: 29 | '''Load an image from a file 30 | 31 | Parameters 32 | ---------- 33 | path : str 34 | The path of the image file 35 | 36 | Returns 37 | ------- 38 | cv2.typing.MatLike 39 | The image 40 | ''' 41 | logger.debug(f'load image from {path}') 42 | try: 43 | return cv2.imread(path) 44 | except Exception as e: 45 | logger.error(f'Error loading image from {path}: {e}') 46 | 47 | def locate(self, target:str, screenshot:str, debug:bool=False)->tuple: 48 | '''Locate the target in the screenshot 49 | 50 | Parameters 51 | ---------- 52 | target : str 53 | The path of the target image file 54 | screenshot : str 55 | The path of the screenshot image file 56 | debug : bool 57 | Whether to show the debug information (default False) 58 | ''' 59 | logger.debug(f'locate {target} in {screenshot}') 60 | 61 | target_img:cv2.typing.MatLike = self.load(target) 62 | screenshot_img:cv2.typing.MatLike = self.load(screenshot) 63 | 64 | marks = [] # debug only 65 | positon:list = [] # result 66 | 67 | target_h, target_w = target_img.shape[:2] # shape: (height, width, channels) 68 | result = cv2.matchTemplate(screenshot_img, target_img, cv2.TM_CCOEFF_NORMED) 69 | location = np.where(result >= self.acc) 70 | 71 | dis = lambda a, b: ((a[0]-b[0])**2 + (a[1]-b[1])**2) **0.5 # distance between two points 72 | 73 | for y, x in zip(*location): 74 | center: tuple[int,int] = x + int(target_w/2), y + int(target_h/2) 75 | if positon and dis(positon[-1], center) < 20: # ignore nearby points 76 | continue 77 | else: 78 | positon.append(center) 79 | p2: tuple[int,int] = x + target_w, y + target_h 80 | marks.append(((x, y), p2)) 81 | 82 | logger.info(f'Found {len(positon)} target(s) of {target}') 83 | 84 | if debug: 85 | logger.debug(f'Found {len(positon)} target(s) ' + ' '.join([f'{i}: {mark}' for i, mark in enumerate(positon)])) 86 | for i, mark in enumerate(marks): 87 | screenshot_img = self.mark(screenshot_img, mark[0], mark[1]) 88 | cv2.imshow(f'result for {target}:', screenshot_img) 89 | cv2.waitKey(0) 90 | cv2.destroyAllWindows() 91 | 92 | return positon[0] if positon else None 93 | 94 | def mark(self, img:cv2.typing.MatLike, p1:tuple[int,int], p2:tuple[int,int])->None: 95 | '''Mark a rectangle on the image 96 | 97 | Parameters 98 | ---------- 99 | img : cv2.typing.MatLike 100 | The image 101 | p1 : tuple 102 | The top-left point of the rectangle 103 | p2 : tuple 104 | The bottom-right point of the rectangle 105 | ''' 106 | cv2.rectangle(img, p1, p2, (0, 0, 255), 2) 107 | return img 108 | 109 | def _wait(self, ms:int)->None: 110 | '''Wait for a while 111 | 112 | Parameters 113 | ---------- 114 | ms : int 115 | The time to wait in milliseconds 116 | ''' 117 | logger.debug(f'Waiting for {ms} seconds') 118 | time.sleep(ms / 1000) -------------------------------------------------------------------------------- /runner/simple.py: -------------------------------------------------------------------------------- 1 | # simple task runner 2 | 3 | from .base import FindListRunner 4 | 5 | class Active(FindListRunner): 6 | name = 'active' 7 | description = '月伴生活动' 8 | targets = ['refuse.jpg','active_begin.jpg', 'yys_jixu.jpg', 'yys_jieshu.jpg'] 9 | def __init__(self, **kwargs): 10 | super().__init__(**kwargs) 11 | 12 | class Mitama(FindListRunner): 13 | name = 'Mitama' 14 | description = '多人御魂副本' 15 | targets = ['refuse.jpg','yys_begin.jpg', 'yys_jieshu.jpg', 'yys_jixu.jpg'] 16 | def __init__(self, **kwargs): 17 | super().__init__(**kwargs) 18 | 19 | class HreoExp(FindListRunner): 20 | name = 'HeroExp' 21 | description = '英杰经验副本' 22 | targets = ['refuse.jpg','yj_exp_begin.jpg', 'yys_jieshu.jpg', 'yys_jixu.jpg'] 23 | def __init__(self, **kwargs): 24 | super().__init__(**kwargs) 25 | 26 | class Spirit(FindListRunner): 27 | name = 'Spirit' 28 | description = '御灵副本' 29 | targets = ['refuse.jpg','confirm.jpg','yuling_begin.jpg', 'yys_jieshu.jpg', 'yys_jixu.jpg'] 30 | def __init__(self, **kwargs): 31 | super().__init__(**kwargs) 32 | 33 | class Fire(FindListRunner): 34 | name = 'Fire' 35 | description = '业原火' 36 | targets = ['refuse.jpg','yyh_begin.jpg', 'yys_jieshu.jpg', 'yys_jixu.jpg'] 37 | def __init__(self, **kwargs): 38 | super().__init__(**kwargs) -------------------------------------------------------------------------------- /schedule.py: -------------------------------------------------------------------------------- 1 | from runner import Runner 2 | from runner.connector import Connector, AdbConnector 3 | from runner.player import Player, CVPlayer 4 | 5 | from functools import singledispatch 6 | from loguru import logger 7 | 8 | class Schedule: 9 | def __init__(self, config:dict=None): 10 | self.logger = logger 11 | self.connectors:list[Connector] = [] 12 | self.runners:list[Runner] = [] 13 | 14 | def create_connector(self, host:str, port:int, conn=AdbConnector)->Connector: 15 | '''Create a connector. 16 | 17 | Parameters 18 | ---------- 19 | host : str 20 | The host of the connector 21 | port : int 22 | The port of the connector 23 | conn : Connector, optional 24 | The connector class, by default AdbConnector 25 | 26 | Returns 27 | ------- 28 | Connector 29 | The connector 30 | ''' 31 | connector = conn(host, port) 32 | self.connectors.append(connector) 33 | return connector 34 | 35 | @singledispatch 36 | def remove_connector(self, param): 37 | raise NotImplementedError(f'Unsupported type {type(param)}') 38 | 39 | @remove_connector.register(Connector) 40 | def _(self, connector:Connector): 41 | '''Remove a connector. 42 | 43 | Parameters 44 | ---------- 45 | connector : Connector 46 | The connector to be removed 47 | ''' 48 | self.connectors.remove(connector) 49 | 50 | @remove_connector.register(int) 51 | def _(self, index:int): 52 | '''Remove a connector by index. 53 | 54 | Parameters 55 | ---------- 56 | index : int 57 | The index of the connector to be removed 58 | ''' 59 | self.connectors.pop(index) 60 | 61 | def list_connectors(self): 62 | '''List all connectors. 63 | ''' 64 | return self.connectors 65 | 66 | def remove_all_connectors(self): 67 | '''Remove all connectors. 68 | ''' 69 | for connector in self.connectors: 70 | connector.disconnect() 71 | self.remove_connector(connector) 72 | 73 | def get_connector(self, index:int)->Connector: 74 | '''Get a connector by index. 75 | 76 | Parameters 77 | ---------- 78 | index : int 79 | The index of the connector 80 | 81 | Returns 82 | ------- 83 | Connector 84 | The connector 85 | ''' 86 | return self.connectors[index] 87 | 88 | def create_runner(self, connector:Connector, script:object, player:Player = None)->Runner: 89 | '''Create a runner. 90 | 91 | Parameters 92 | ---------- 93 | connector : Connector 94 | The connector to use 95 | player : Player 96 | The player to use 97 | 98 | Returns 99 | ------- 100 | Runner 101 | The runner 102 | ''' 103 | if player is None: 104 | player = CVPlayer() 105 | 106 | runner:Runner = script(connector, player) 107 | self.runners.append(runner) 108 | 109 | def list_runners(self): 110 | '''List all runners. 111 | ''' 112 | return self.runners 113 | 114 | @singledispatch 115 | def remove_runner(self, param): 116 | raise NotImplementedError(f'Unsupported type {type(param)}') 117 | 118 | @remove_runner.register(Runner) 119 | def _(self, runner:Runner): 120 | '''Remove a runner. 121 | 122 | Parameters 123 | ---------- 124 | runner : Runner 125 | The runner to be removed 126 | ''' 127 | self.runners.remove(runner) 128 | 129 | @remove_runner.register(int) 130 | def _(self, index:int): 131 | '''Remove a runner by index. 132 | 133 | Parameters 134 | ---------- 135 | index : int 136 | The index of the runner to be removed 137 | ''' 138 | self.runners.pop(index) 139 | 140 | def get_runner(self, index:int)->Runner: 141 | '''Get a runner by index. 142 | 143 | Parameters 144 | ---------- 145 | index : int 146 | The index of the runner 147 | 148 | Returns 149 | ------- 150 | Runner 151 | The runner 152 | ''' 153 | return self.runners[index] 154 | 155 | def begin_runner(self, index:int): 156 | '''Begin a runner by index. 157 | 158 | Parameters 159 | ---------- 160 | index : int 161 | The index of the runner 162 | ''' 163 | self.runners[index].start() 164 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | from wcwidth import wcswidth 4 | 5 | def format_output_info(output:List[str], width:int)->str: 6 | '''format the output info like 7 | +---------------------------------+ 8 | |              | 9 | |   info          | 10 | |              | 11 | +---------------------------------+ 12 | 13 | Parameters 14 | ---------- 15 | output : List[str] 16 | The output info 17 | width : int 18 | The width of the output info 19 | 20 | Returns 21 | ------- 22 | str 23 | The formatted output info 24 | ''' 25 | terminal_width = os.get_terminal_size().columns 26 | max_width = wcswidth(max(*output, key=wcswidth)) 27 | width = width if width > max_width else max_width 28 | line_spcace = int((width - max_width)/2) 29 | format_space = int((terminal_width - width - 10)/2) 30 | ans = f'{" "*format_space}+{"-"*width}+\n' 31 | for item in output: 32 | ans += f'{" "*format_space}|{" "*(line_spcace) + item + " "*(width-line_spcace-wcswidth(item))}|\n' 33 | ans += f'{" "*format_space}+{"-"*width}+\n' 34 | return ans 35 | -------------------------------------------------------------------------------- /wanted/active_begin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/active_begin.jpg -------------------------------------------------------------------------------- /wanted/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/back.jpg -------------------------------------------------------------------------------- /wanted/box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/box.jpg -------------------------------------------------------------------------------- /wanted/confirm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/confirm.jpg -------------------------------------------------------------------------------- /wanted/hunwang_jixu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/hunwang_jixu.jpg -------------------------------------------------------------------------------- /wanted/jixu2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/jixu2.jpg -------------------------------------------------------------------------------- /wanted/k28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/k28.jpg -------------------------------------------------------------------------------- /wanted/ok.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/ok.jpg -------------------------------------------------------------------------------- /wanted/refuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/refuse.jpg -------------------------------------------------------------------------------- /wanted/tansuo_begin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/tansuo_begin.jpg -------------------------------------------------------------------------------- /wanted/tansuo_boss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/tansuo_boss.jpg -------------------------------------------------------------------------------- /wanted/tansuo_confirm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/tansuo_confirm.jpg -------------------------------------------------------------------------------- /wanted/tansuo_exit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/tansuo_exit.jpg -------------------------------------------------------------------------------- /wanted/tansuo_fight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/tansuo_fight.jpg -------------------------------------------------------------------------------- /wanted/yj_exp_begin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yj_exp_begin.jpg -------------------------------------------------------------------------------- /wanted/yuling_begin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yuling_begin.jpg -------------------------------------------------------------------------------- /wanted/yyh_begin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yyh_begin.jpg -------------------------------------------------------------------------------- /wanted/yys_begin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yys_begin.jpg -------------------------------------------------------------------------------- /wanted/yys_jieshu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yys_jieshu.jpg -------------------------------------------------------------------------------- /wanted/yys_jixu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yys_jixu.jpg -------------------------------------------------------------------------------- /wanted/yys_jixu2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbjuechen/script/43fe97a34fbb9b4f02f65af52753110594a8d71b/wanted/yys_jixu2.jpg --------------------------------------------------------------------------------