diff --git a/README.md b/README.md index 5ad7958de..448c765cd 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ - 自动登录 - 账户密码需要手动输入 - 自动访友收取信用点 +- 自动前往信用商店,领取信用点并按指定优先级购买商品 - 自动确认任务完成 - 自动刷体力 - 默认进行上一次完成的关卡 @@ -39,8 +40,8 @@ - 自动收取邮件奖励 - 自动收取并安放线索 - 自动消耗无人机加速制造站或贸易站 +- 自动更换基建排班干员(建议搭配配置文件使用, 也可命令行直接输入) - 自动使用菲亚梅塔恢复指定房间心情最低干员的心情并重回岗位(工作位置不变以避免重新暖机) [[参考使用场景](https://www.bilibili.com/video/BV1mZ4y1z7wx)] -- 自动更换基建排班干员(需要搭配配置文件使用) - 支持游戏任意分辨率(低于 1080p 的分辨率可能会有一些问题) ## 安装 @@ -101,7 +102,7 @@ usage: arknights-mower command [command args] [--config filepath] [--debug] commands (prefix abbreviation accepted): base [plan] [-c] [-d[F][N]] [-f[F][N]] 自动处理基建的信赖/货物/订单/线索/无人机 - plan 表示选择的基建干员排班计划(需要搭配配置文件使用) + plan 自动更换基建排班干员(建议搭配配置文件使用,也可命令行直接输入) -c 是否自动收集并使用线索 -d 是否自动消耗无人机,F 表示第几层(1-3),N 表示从左往右第几个房间(1-3) -f 是否自动使用菲亚梅塔恢复指定房间心情最差干员的心情并恢复原位,F、N 含义同上 @@ -158,9 +159,11 @@ arknights-mower recruit 因陀罗 火神 arknights-mower shop 招聘许可 赤金 龙门币 # 在商场使用信用点消费,购买物品的优先级从高到低分别是招聘许可、赤金和龙门币,其余物品不购买 arknights-mower base -f12 plan_2 -# 自动使用菲亚梅塔恢复B102房间心情最差干员的心情,并保持原位;自动进行名为`plan_2`的的基建排班(排班功能须搭配配置文件使用) +# 自动使用菲亚梅塔恢复B102房间心情最差干员的心情,并保持原位;自动进行配置文件中名为`plan_2`的的基建排班 arknights-mower base -c -d33 # 自动收取基建中的信赖/货物/订单;自动放置线索;自动前往 B303 房间(地下 3 层从左往右数第 3 间)使用无人机加速生产或贸易订单; +arknights-mower base room_1_2 柏喙 巫恋 龙舌兰 contact 絮雨 dormitory_1 杜林 Free Free Free Free +# 自动更换基建B102房间干员为 柏喙 巫恋 龙舌兰, 更换办公室干员为 絮雨, 更换1号宿舍干员为 杜林和任意4个空闲干员(房间名请参考 base.json) ``` 命令可使用前缀或首字母缩写,如: @@ -197,7 +200,7 @@ plan = { # 阶段 1 'plan_1': { # 控制中枢 - 'contral': ['夕', '令', '凯尔希', '阿米娅', '玛恩纳'], + 'central': ['夕', '令', '凯尔希', '阿米娅', '玛恩纳'], # 办公室 'contact': ['艾雅法拉'], # 宿舍 diff --git a/arknights_mower/__main__.py b/arknights_mower/__main__.py index a43835188..7fc5d3b4c 100644 --- a/arknights_mower/__main__.py +++ b/arknights_mower/__main__.py @@ -1,72 +1,215 @@ -import sys -import traceback -from pathlib import Path +import time +from datetime import datetime -from . import __pyinstall__, __rootdir__ -from .command import * -from .utils import config -from .utils.device import Device -from .utils.log import logger, set_debug_mode +conf = {} +plan = {} -def main(module: bool = True) -> None: - args = sys.argv[1:] - if not args and __pyinstall__: - logger.info('参数为空,默认执行 schedule 模式,按下 Ctrl+C 以结束脚本运行') - args.append('schedule') - config_path = None - debug_mode = False - while True: - if len(args) > 1 and args[-2] == '--config': - config_path = Path(args[-1]) - args = args[:-2] - continue - if len(args) > 0 and args[-1] == '--debug': - debug_mode = True - args = args[:-1] - continue - break - - if config_path is None: - if __pyinstall__: - config_path = Path(sys.executable).parent.joinpath('config.yaml') - elif module: - config_path = Path.home().joinpath('.ark_mower.yaml') +# 执行自动排班 +def main(c, p, child_conn): + __init_params__() + from arknights_mower.utils.log import logger, init_fhlr + from arknights_mower.utils import config + global plan + global conf + conf = c + plan = p + config.LOGFILE_PATH = './log' + config.SCREENSHOT_PATH = './screenshot' + config.SCREENSHOT_MAXNUM = 1000 + config.ADB_DEVICE = [conf['adb']] + config.ADB_CONNECT = [conf['adb']] + config.ADB_CONNECT = [conf['adb']] + config.APPNAME = 'com.hypergryph.arknights' if conf[ + 'package_type'] == 1 else 'com.hypergryph.arknights.bilibili' # 服务器 + init_fhlr(child_conn) + if conf['ling_xi'] == 1: + agent_base_config['令']['UpperLimit'] = 11 + agent_base_config['夕']['UpperLimit'] = 11 + agent_base_config['夕']['LowerLimit'] = 13 + elif conf['ling_xi'] == 2: + agent_base_config['夕']['UpperLimit'] = 11 + agent_base_config['令']['UpperLimit'] = 11 + agent_base_config['令']['LowerLimit'] = 13 + for key in list(filter(None, conf['rest_in_full'].replace(',', ',').split(','))): + if key in agent_base_config.keys(): + agent_base_config[key]['RestInFull'] = True else: - config_path = __rootdir__.parent.joinpath('config.yaml') - if not config_path.exists(): - config.build_config(config_path, module) - else: - if not config_path.exists(): - logger.error(f'The configuration file does not exist: {config_path}') - return - try: - logger.info(f'Loading the configuration file: {config_path}') - config.load_config(config_path) - except Exception as e: - logger.error('An error occurred when loading the configuration file') - raise e + agent_base_config[key] = {'RestInFull': True} + logger.info('开始运行Mower') + simulate() - if debug_mode and not config.DEBUG_MODE: - config.DEBUG_MODE = True - set_debug_mode() +def inialize(tasks, scheduler=None): + from arknights_mower.utils.log import logger + from arknights_mower.solvers.base_schedule import BaseSchedulerSolver + from arknights_mower.strategy import Solver + from arknights_mower.utils.device import Device + from arknights_mower.utils import config device = Device() + cli = Solver(device) + if scheduler is None: + base_scheduler = BaseSchedulerSolver(cli.device, cli.recog) + base_scheduler.operators = {} + plan1 = {} + for key in plan: + plan1[key] = plan[key]['plans'] + base_scheduler.package_name = config.APPNAME # 服务器 + base_scheduler.global_plan = {'default': "plan_1", "plan_1": plan1} + base_scheduler.current_base = {} + base_scheduler.resting = [] + base_scheduler.max_resting_count = conf['max_resting_count'] + base_scheduler.drone_count_limit = conf['drone_count_limit'] + base_scheduler.tasks = tasks + # 读取心情开关,有菲亚梅塔或者希望全自动换班得设置为 true + base_scheduler.read_mood = conf['run_mode'] == 1 + # 干员宿舍回复阈值 + # 高效组心情低于 UpperLimit * 阈值 (向下取整)的时候才会会安排休息 - logger.debug(args) - if len(args) == 0: - help() + base_scheduler.scan_time = {} + base_scheduler.last_room = '' + base_scheduler.free_blacklist = list(filter(None, conf['free_blacklist'].replace(',', ',').split(','))) + logger.info('宿舍黑名单:' + str(base_scheduler.free_blacklist)) + base_scheduler.resting_treshhold = 0.5 + base_scheduler.MAA = None + base_scheduler.email_config = { + 'mail_enable': conf['mail_enable'], + 'subject': '[Mower通知]', + 'account': conf['account'], + 'pass_code': conf['pass_code'], + 'receipts': [conf['account']], + 'notify': False + } + maa_config['maa_path'] = conf['maa_path'] + maa_config['maa_adb_path'] = conf['maa_adb_path'] + maa_config['maa_adb'] = conf['adb'] + maa_config['weekly_plan'] = conf['maa_weekly_plan'] + base_scheduler.maa_config = maa_config + base_scheduler.ADB_CONNECT = config.ADB_CONNECT[0] + base_scheduler.error = False + base_scheduler.drone_room = None if conf['drone_room'] == '' else conf['drone_room'] + base_scheduler.drone_execution_gap = 4 + base_scheduler.run_order_delay = conf['run_order_delay'] + base_scheduler.agent_base_config = agent_base_config + return base_scheduler else: - target_cmd = match_cmd(args[0]) - if target_cmd is not None: - try: - target_cmd(args[1:], device) - except ParamError: - logger.debug(traceback.format_exc()) - help() - else: - help() + scheduler.device = cli.device + scheduler.recog = cli.recog + scheduler.handle_error(True) + return scheduler + + +def simulate(): + from arknights_mower.utils.log import logger + ''' + 具体调用方法可见各个函数的参数说明 + ''' + tasks = [] + reconnect_max_tries = 10 + reconnect_tries = 0 + base_scheduler = inialize(tasks) + while True: + try: + if len(base_scheduler.tasks) > 0: + (base_scheduler.tasks.sort(key=lambda x: x["time"], reverse=False)) + sleep_time = (base_scheduler.tasks[0]["time"] - datetime.now()).total_seconds() + logger.debug(base_scheduler.tasks) + remaining_time = (base_scheduler.tasks[0]["time"] - datetime.now()).total_seconds() + if sleep_time > 540 and conf['maa_enable'] == 1: + subject = f"下次任务在{base_scheduler.tasks[0]['time'].strftime('%H:%M:%S')}" + context = f"下一次任务:{base_scheduler.tasks[0]['plan']}" + logger.info(context) + logger.info(subject) + base_scheduler.send_email(context, subject) + base_scheduler.maa_plan_solver() + elif sleep_time > 0: + subject = f"开始休息 {'%.2f' % (remaining_time / 60)} 分钟,到{base_scheduler.tasks[0]['time'].strftime('%H:%M:%S')}" + context = f"下一次任务:{base_scheduler.tasks[0]['plan']}" + logger.info(context) + logger.info(subject) + base_scheduler.send_email(context, subject) + time.sleep(sleep_time) + base_scheduler.run() + reconnect_tries = 0 + except ConnectionError as e: + reconnect_tries += 1 + if reconnect_tries < reconnect_max_tries: + logger.warning(f'连接端口断开....正在重连....') + connected = False + while not connected: + try: + base_scheduler = inialize([], base_scheduler) + break + except Exception as ce: + logger.error(ce) + time.sleep(5) + continue + continue + else: + raise Exception(e) + except Exception as E: + logger.exception(f"程序出错--->{E}") + + +agent_base_config = {} +maa_config = {} -if __name__ == '__main__': - main(module=True) +def __init_params__(): + global agent_base_config + global maa_config + agent_base_config = { + "Default": {"UpperLimit": 24, "LowerLimit": 0, "ExhaustRequire": False, "ArrangeOrder": [2, "false"], + "RestInFull": False}, + "令": {"ArrangeOrder": [2, "true"]}, + "夕": {"ArrangeOrder": [2, "true"]}, + "稀音": {"ExhaustRequire": True, "ArrangeOrder": [2, "true"], "RestInFull": True}, + "巫恋": {"ArrangeOrder": [2, "true"]}, + "柏喙": {"ExhaustRequire": True, "ArrangeOrder": [2, "true"]}, + "龙舌兰": {"ArrangeOrder": [2, "true"]}, + "空弦": {"ArrangeOrder": [2, "true"], "RestingPriority": "low"}, + "伺夜": {"ArrangeOrder": [2, "true"]}, + "绮良": {"ArrangeOrder": [2, "true"]}, + "但书": {"ArrangeOrder": [2, "true"]}, + "泡泡": {"ArrangeOrder": [2, "true"]}, + "火神": {"ArrangeOrder": [2, "true"]}, + "黑键": {"ArrangeOrder": [2, "true"]}, + "波登可": {"ArrangeOrder": [2, "false"]}, + "夜莺": {"ArrangeOrder": [2, "false"]}, + "菲亚梅塔": {"ArrangeOrder": [2, "false"]}, + "流明": {"ArrangeOrder": [2, "false"]}, + "蜜莓": {"ArrangeOrder": [2, "false"]}, + "闪灵": {"ArrangeOrder": [2, "false"]}, + "杜林": {"ArrangeOrder": [2, "false"]}, + "褐果": {"ArrangeOrder": [2, "false"]}, + "车尔尼": {"ArrangeOrder": [2, "false"]}, + "安比尔": {"ArrangeOrder": [2, "false"]}, + "爱丽丝": {"ArrangeOrder": [2, "false"]}, + "桃金娘": {"ArrangeOrder": [2, "false"]}, + "帕拉斯": {"RestingPriority": "low"}, + "红云": {"RestingPriority": "low", "ArrangeOrder": [2, "true"]}, + "承曦格雷伊": {"ArrangeOrder": [2, "true"]}, + "乌有": {"ArrangeOrder": [2, "true"], "RestingPriority": "low"}, + "图耶": {"ArrangeOrder": [2, "true"]}, + "鸿雪": {"ArrangeOrder": [2, "true"]}, + "孑": {"ArrangeOrder": [2, "true"]}, + "清道夫": {"ArrangeOrder": [2, "true"]}, + "临光": {"ArrangeOrder": [2, "true"]}, + "杜宾": {"ArrangeOrder": [2, "true"]}, + "焰尾": {"RestInFull": True}, + "重岳": {"ArrangeOrder": [2, "true"]}, + "坚雷": {"ArrangeOrder": [2, "true"]}, + "年": {"RestingPriority": "low"} + } + maa_config = { + # maa 运行的时间间隔,以小时计 + "maa_execution_gap": 4, + # 以下配置,第一个设置为true的首先生效 + # 是否启动肉鸽 + "roguelike": False, + # 是否启动生息演算 + "reclamation_algorithm": False, + # 是否启动保全派驻 + "stationary_security_service": False, + "last_execution": None + } diff --git a/arknights_mower/command.py b/arknights_mower/command.py index ad128426e..6c6cf814f 100644 --- a/arknights_mower/command.py +++ b/arknights_mower/command.py @@ -20,15 +20,19 @@ def base(args: list[str] = [], device: Device = None): """ base [plan] [-c] [-d[F][N]] [-f[F][N]] 自动处理基建的信赖/货物/订单/线索/无人机 - plan 表示选择的基建干员排班计划(需要搭配配置文件使用) + plan 表示选择的基建干员排班计划(建议搭配配置文件使用, 也可命令行直接输入) -c 是否自动收集并使用线索 -d 是否自动消耗无人机,F 表示第几层(1-3),N 表示从左往右第几个房间(1-3) -f 是否使用菲亚梅塔恢复特定房间干员心情,恢复后恢复原位且工作位置不变,F、N 含义同上 """ + from .data import base_room_list, agent_list + arrange = None clue_collect = False drone_room = None fia_room = None + any_room = [] + agents = [] try: for p in args: @@ -45,8 +49,17 @@ def base(args: list[str] = [], device: Device = None): fia_room = f'room_{p[2]}_{p[3]}' elif arrange is None: arrange = config.BASE_CONSTRUCT_PLAN.get(p) + if arrange is None: + if p in base_room_list: + any_room.append(p) + agents.append([]) + elif p in agent_list or 'free' == p.lower(): + agents[-1].append(p) except Exception: raise ParamError + + if arrange is None and any_room is not None and len(agents) > 0: + arrange = dict(zip(any_room, agents)) BaseConstructSolver(device).run(arrange, clue_collect, drone_room, fia_room) diff --git a/arknights_mower/data/__init__.py b/arknights_mower/data/__init__.py index b6911a39f..c94c0ae7b 100644 --- a/arknights_mower/data/__init__.py +++ b/arknights_mower/data/__init__.py @@ -7,6 +7,9 @@ agent_list = json.loads( Path(f'{__rootdir__}/data/agent.json').read_text('utf-8')) +# # agents base skills +# agent_base_config = json.loads( +# Path(f'{__rootdir__}/data/agent-base.json').read_text('utf-8')) # name of each room in the basement base_room_list = json.loads( diff --git a/arknights_mower/data/agent.json b/arknights_mower/data/agent.json index 82a46dd2e..d322f2e6e 100644 --- a/arknights_mower/data/agent.json +++ b/arknights_mower/data/agent.json @@ -87,6 +87,7 @@ "芙兰卡", "炎客", "因陀罗", + "石英", "燧石", "拉普兰德", "断崖", @@ -247,5 +248,31 @@ "杰克", "夜魔", "格拉尼", - "斯卡蒂" -] \ No newline at end of file + "斯卡蒂", + "罗小黑", + "海沫", + "铅踝", + "达格达", + "明椒", + "白铁", + "雪绒", + "子月", + "伺夜", + "斥罪", + "缄默德克萨斯", + "焰影苇草", + "和弦", + "谜图", + "重岳", + "林", + "火哨", + "截云", + "麒麟X夜刀", + "火龙S黑角", + "泰拉大陆调查团", + "伊内丝", + "洋灰", + "休谟斯", + "摩根", + "U-Official" +] diff --git a/arknights_mower/data/ocr.json b/arknights_mower/data/ocr.json index df08edb30..7d68cb6ca 100644 --- a/arknights_mower/data/ocr.json +++ b/arknights_mower/data/ocr.json @@ -27,5 +27,6 @@ "CastIe3": "Castle-3", "Castle3": "Castle-3", "Lancet2": "Lancet-2", - "THRMEX": "THRM-EX" + "THRMEX": "THRM-EX", + "U-Officia": "U-Official" } \ No newline at end of file diff --git a/arknights_mower/data/scene.json b/arknights_mower/data/scene.json index 29e85bf33..9dd47a732 100644 --- a/arknights_mower/data/scene.json +++ b/arknights_mower/data/scene.json @@ -91,6 +91,10 @@ "label": "LOGIN_CADPA_DETAIL", "comment": "游戏适龄提示" }, + "112": { + "label": "CLOSE_MINE", + "comment": "产业合作洽谈会" + }, "201": { "label": "INFRA_MAIN", "comment": "基建全局视角" diff --git a/arknights_mower/resources/agent_in_dormitory.png b/arknights_mower/resources/agent_in_dormitory.png new file mode 100644 index 000000000..1ef999dad Binary files /dev/null and b/arknights_mower/resources/agent_in_dormitory.png differ diff --git a/arknights_mower/resources/close_mine.png b/arknights_mower/resources/close_mine.png new file mode 100644 index 000000000..685e3d3db Binary files /dev/null and b/arknights_mower/resources/close_mine.png differ diff --git a/arknights_mower/resources/hypergryph.png b/arknights_mower/resources/hypergryph.png index 82263eede..3638d0948 100644 Binary files a/arknights_mower/resources/hypergryph.png and b/arknights_mower/resources/hypergryph.png differ diff --git a/arknights_mower/resources/room_detail.png b/arknights_mower/resources/room_detail.png new file mode 100644 index 000000000..7eb50cb74 Binary files /dev/null and b/arknights_mower/resources/room_detail.png differ diff --git a/arknights_mower/solvers/base_construct.py b/arknights_mower/solvers/base_construct.py index a76ac1b81..fbf27cda0 100644 --- a/arknights_mower/solvers/base_construct.py +++ b/arknights_mower/solvers/base_construct.py @@ -685,22 +685,28 @@ def choose_agent_in_order(self, agent: list[str], exclude: list[str] = None, exc found = 0 while found == 0: ret = character_recognize.agent(self.recog.img) - ret = np.array(ret, dtype=object).reshape(-1, 2, 2).reshape(-1, 2) # 'Free'代表占位符,选择空闲干员 if agent[idx] == 'Free': for x in ret: - x[1][0, 1] -= 155 - x[1][2, 1] -= 155 - # 不选择已进驻的干员,如果非宿舍则进一步不选择精神涣散的干员 - if not (self.find('agent_on_shift', scope=(x[1][0], x[1][2])) - or self.find('agent_resting', scope=(x[1][0], x[1][2])) - or (not dormitory and self.find('distracted', scope=(x[1][0], x[1][2])))): - if x[0] not in agent and x[0] not in exclude: - self.tap(x[1], x_rate=0.5, y_rate=0.5, interval=0) - agent[idx] = x[0] - _free = x[0] - found = 1 - break + status_coord = x[1].copy() + status_coord[0, 1] -= 0.147*self.recog.h + status_coord[2, 1] -= 0.135*self.recog.h + + room_coord = x[1].copy() + room_coord[0, 1] -= 0.340*self.recog.h + room_coord[2, 1] -= 0.340*self.recog.h + + if x[0] not in agent and x[0] not in exclude: + # 不选择已进驻的干员,如果非宿舍则进一步不选择精神涣散的干员 + if not (self.find('agent_on_shift', scope=(status_coord[0], status_coord[2])) + or self.find('agent_resting', scope=(status_coord[0], status_coord[2])) + or self.find('agent_in_dormitory', scope=(room_coord[0], room_coord[2])) + or (not dormitory and self.find('agent_distracted', scope=(status_coord[0], status_coord[2])))): + self.tap(x[1], x_rate=0.5, y_rate=0.5, interval=0) + agent[idx] = x[0] + _free = x[0] + found = 1 + break elif agent[idx] != 'Free': for x in ret: @@ -795,13 +801,12 @@ def fia(self, room: str): self.tap((self.recog.w*BY_STATUS[0], self.recog.h*BY_STATUS[1]), interval=0.1) # 记录房间中的干员及其工位顺序 ret = character_recognize.agent(self.recog.img) - ret = np.array(ret, dtype=object).reshape(-1, 2, 2).reshape(-1, 2) on_shift_agents = [] for x in ret: - x[1][0, 1] -= 155 - x[1][2, 1] -= 155 + x[1][0, 1] -= 0.147*self.recog.h + x[1][2, 1] -= 0.135*self.recog.h if self.find('agent_on_shift', scope=(x[1][0], x[1][2])) \ - or self.find('distracted', scope=(x[1][0], x[1][2])): + or self.find('agent_distracted', scope=(x[1][0], x[1][2])): self.tap(x[1], x_rate=0.5, y_rate=0.5, interval=0) on_shift_agents.append(x[0]) if len(on_shift_agents) == 0: @@ -817,7 +822,6 @@ def fia(self, room: str): _temp_on_shift_agents = on_shift_agents.copy() while 'Free' not in _temp_on_shift_agents: ret = character_recognize.agent(self.recog.img) - ret = np.array(ret, dtype=object).reshape(-1, 2, 2).reshape(-1, 2) for x in ret: if x[0] in _temp_on_shift_agents: # 用占位符替代on_shift_agents中这个agent @@ -840,10 +844,6 @@ def fia(self, room: str): if not self.find('arrange_check_in_on'): self.tap_element('arrange_check_in', interval=2, rebuild=False) self.tap((self.recog.w*0.82, self.recog.h*0.25), interval=2) - # 确保按心情升序排列 - self.tap((self.recog.w*BY_TRUST[0], self.recog.h*BY_TRUST[1]), interval=0) - self.tap((self.recog.w*BY_EMO[0], self.recog.h*BY_EMO[1]), interval=0) - self.tap((self.recog.w*BY_EMO[0], self.recog.h*BY_EMO[1]), interval=0.1) # 选择待恢复干员和菲亚梅塔 rest_agents = [_recover, '菲亚梅塔'] self.choose_agent_in_order(rest_agents, exclude_checked_in=False) diff --git a/arknights_mower/solvers/base_schedule.py b/arknights_mower/solvers/base_schedule.py new file mode 100644 index 000000000..31621d01f --- /dev/null +++ b/arknights_mower/solvers/base_schedule.py @@ -0,0 +1,1698 @@ +from __future__ import annotations +import copy +import time +from enum import Enum +from datetime import datetime, timedelta +import numpy as np +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +from ..data import agent_list +from ..utils import character_recognize, detector, segment +from ..utils.operators import Operators, Operator, Dormitory +from ..utils import typealias as tp +from ..utils.device import Device +from ..utils.log import logger +from ..utils.recognize import RecognizeError, Recognizer, Scene +from ..utils.solver import BaseSolver +from ..utils.datetime import get_server_weekday +from paddleocr import PaddleOCR +import cv2 + +## Maa +from arknights_mower.utils.asst import Asst, Message +import json + +ocr = None + + +class ArrangeOrder(Enum): + STATUS = 1 + SKILL = 2 + FEELING = 3 + TRUST = 4 + + +arrange_order_res = { + ArrangeOrder.STATUS: (1560 / 2496, 96 / 1404), + ArrangeOrder.SKILL: (1720 / 2496, 96 / 1404), + ArrangeOrder.FEELING: (1880 / 2496, 96 / 1404), + ArrangeOrder.TRUST: (2050 / 2496, 96 / 1404), +} + + +class BaseSchedulerSolver(BaseSolver): + """ + 收集基建的产物:物资、赤金、信赖 + """ + package_name = '' + + def __init__(self, device: Device = None, recog: Recognizer = None) -> None: + super().__init__(device, recog) + self.op_data = None + self.max_resting_count = 4 + self.party_time = None + self.drone_time = None + self.run_order_delay=10 + + def run(self) -> None: + """ + :param clue_collect: bool, 是否收取线索 + """ + self.currentPlan = self.global_plan[self.global_plan["default"]] + self.error = False + self.handle_error(True) + if len(self.tasks) > 0: + # 找到时间最近的一次单个任务 + self.task = self.tasks[0] + else: + self.task = None + if self.party_time is not None and self.party_time None: + self.recog.update() + if self.scene() == Scene.INDEX: + self.tap_element('index_infrastructure') + elif self.scene() == Scene.INFRA_MAIN: + return self.infra_main() + elif self.scene() == Scene.INFRA_TODOLIST: + return self.todo_list() + elif self.scene() == Scene.INFRA_DETAILS: + self.back() + elif self.scene() == Scene.LOADING: + self.sleep(3) + elif self.scene() == Scene.CONNECTING: + self.sleep(3) + elif self.get_navigation(): + self.tap_element('nav_infrastructure') + elif self.scene() == Scene.INFRA_ARRANGE_ORDER: + self.tap_element('arrange_blue_yes') + elif self.scene() != Scene.UNKNOWN: + self.back_to_index() + self.last_room = '' + logger.info("重设上次房间为空") + else: + raise RecognizeError('Unknown scene') + + def overtake_room(self): + candidates = self.task['type'].split(',') + # 在candidate 中,计算出需要的high free 和 Low free 数量 + _high_free = 0 + _low_free = 0 + for x in candidates: + if self.op_data.operators[x].resting_priority == 'high': + _high_free += 1 + else: + _low_free += 1 + self.agent_get_mood(force=True) + # 剩余高效组位置 + current_high = self.op_data.available_free(count=self.max_resting_count) + # 剩余低效位置 + current_low = self.op_data.available_free('low', count=self.max_resting_count) + if current_high >= _high_free and current_low >= _low_free: + # 检查是否目前宿舍满足 low free 和high free 的数量需求,如果满足,则直接安排 + _plan = {} + _replacement = [] + _replacement, _plan, current_high, current_low = self.get_resting_plan(candidates, + _replacement, + _plan, + current_high, + current_low) + if len(_plan.items()) > 0: + self.tasks.append({'time': datetime.now(), 'plan': _plan}) + else: + # 如果不满足,则找到并且执行最近一个type 包含 超过数量的high free 和low free 的 任务并且 干员没有 exaust_require 词条 + task_index = -1 + current_high, current_low = 0, 0 + for idx, task in enumerate(self.tasks): + if "type" in task.keys() and 'dorm' in task['type']: + # 检查数量 + ids = [int(w[4:]) for w in task['type'].split(',')] + is_exhaust_require = False + for _id in ids: + if not is_exhaust_require: + if self.op_data.dorm[_id].name in self.op_data.exhaust_agent: + is_exhaust_require = True + + if _id > self.max_resting_count - 1: + current_low += 1 + else: + current_high += 1 + # 休息满需要则跳过 + if current_high >= _high_free and current_low >= _low_free and not is_exhaust_require: + task_index = idx + else: + current_low, current_high = 0, 0 + if task_index > -1: + # 修改执行时间 + self.tasks[task_index]['time'] = datetime.now() + # 执行完提前换班任务再次执行本任务 + self.tasks.append({'time': datetime.now(), 'plan': self.task['plan']}) + else: + # 任务全清 + rooms = [] + remove_idx = [] + for idx, task in enumerate(self.tasks): + if "type" in task.keys() and 'dorm' in task['type']: + # 检查数量 + ids = [int(w[4:]) for w in task['type'].split(',')] + is_exhaust_require = False + _rooms = [] + for _id in ids: + if not is_exhaust_require: + if self.op_data.dorm[_id].name in self.op_data.exhaust_agent: + is_exhaust_require = True + __room = self.op_data.operators[self.op_data.dorm[_id].name].room + if __room not in _rooms: + _rooms.append(__room) + # 跳过需要休息满 + if not is_exhaust_require: + rooms.extend(__room) + remove_idx.append(idx) + for idx in remove_idx: + del self.tasks[idx] + plan = {} + for room in rooms: + if room not in plan.keys(): + plan[room] = [data["agent"] for data in self.currentPlan[room]] + if len(plan.keys()) > 0: + self.tasks.append({'time': datetime.now(), 'plan': plan}) + # 执行完提前换班任务再次执行本任务 + self.tasks.append({'time': datetime.now(), 'plan': self.task['plan']}) + self.skip() + return + + def handle_error(self, force=False): + # 如果有任何报错,则生成一个空 + if self.scene() == Scene.UNKNOWN: + self.device.exit(self.package_name) + if self.error or force: + # 如果没有任何时间小于当前时间的任务才生成空任务 + if (next((e for e in self.tasks if e['time'] < datetime.now()), None)) is None: + room = next(iter(self.currentPlan.keys())) + logger.debug("由于出现错误情况,生成一次空任务来执行纠错") + self.tasks.append({'time': datetime.now(), 'plan': {}}) + # 如果没有任何时间小于当前时间的任务-10分钟 则清空任务 + if (next((e for e in self.tasks if e['time'] < datetime.now() - timedelta(seconds=600)), None)) is not None: + logger.info("检测到执行超过10分钟的任务,清空全部任务") + self.tasks = [] + self.op_data = None + return True + + def plan_metadata(self): + planned_index = [] + for t in self.tasks: + if 'type' in t.keys() and 'dorm' in t['type']: + planned_index.extend([int(w[4:]) for w in t['type'].split(',')]) + _time = datetime.max + _plan = {} + _type = [] + # 第一个心情低的且小于3 则只休息半小时 + short_rest = False + self.total_agent = list( + v for k, v in self.op_data.operators.items() if v.is_high() and not v.room.startswith('dorm') and not v.current_room.startswith('dorm')) + self.total_agent.sort(key=lambda x: x.mood - x.lower_limit, reverse=False) + if next((a for a in self.total_agent if (a.name not in self.op_data.exhaust_agent) and a.mood<=3),None) is not None: + short_rest= True + low_priority = [] + for idx, dorm in enumerate(self.op_data.dorm): + # Filter out resting priority low + if idx >= self.max_resting_count: + break + # 如果已经plan了,则跳过 + if idx in planned_index or idx in low_priority: + continue + _name = dorm.name + if _name == '': + continue + # 如果是rest in full,则新增单独任务.. + if _name in self.op_data.operators.keys() and self.op_data.operators[_name].rest_in_full: + __plan = {} + __rest_agent = [] + __type = [] + if self.op_data.operators[dorm.name].group == "": + __rest_agent.append(dorm.name) + else: + __rest_agent.extend(self.op_data.groups[self.op_data.operators[dorm.name].group]) + if dorm.time is not None: + __time = dorm.time + else: + __time = datetime.max + for x in __rest_agent: + # 如果同小组也是rest_in_full则取最大休息时间 否则忽略 + _idx, __dorm = self.op_data.get_dorm_by_name(x) + if x in self.op_data.operators.keys() and self.op_data.operators[x].rest_in_full: + if __dorm is not None and __dorm.time is not None: + if __dorm.time > _time and self.op_data.operators[x].resting_priority == 'high': + _time = __dorm.time + if _idx is not None: + __type.append('dorm' + str(_idx)) + planned_index.append(_idx) + __room = self.op_data.operators[x].room + if __room not in __plan.keys(): + __plan[__room] = ['Current'] * len(self.currentPlan[__room]) + __plan[__room][self.op_data.operators[x].index] = x + if __time < datetime.now(): __time = datetime.now() + self.tasks.append({"type": ','.join(__type), 'plan': __plan, 'time': __time}) + # 如果非 rest in full, 则同组取时间最小值 + else: + if dorm.time is not None and dorm.time < _time: + _time = dorm.time + __room = self.op_data.operators[_name].room + __rest_agent = [] + if self.op_data.operators[_name].group == "": + __rest_agent.append(_name) + else: + __rest_agent.extend(self.op_data.groups[self.op_data.operators[_name].group]) + for x in __rest_agent: + if x in low_priority: + continue + __room = self.op_data.operators[x].room + if __room not in _plan.keys(): + _plan[__room] = ['Current'] * len(self.currentPlan[__room]) + _plan[__room][self.op_data.operators[x].index] = x + _dorm_idx, __dorm = self.op_data.get_dorm_by_name(x) + if __dorm is not None: + _type.append('dorm' + str(_dorm_idx)) + planned_index.append(_dorm_idx) + if __dorm.time is not None and __dorm.time < _time and self.op_data.operators[ + x].resting_priority == 'high': + _time = __dorm.time + if x not in low_priority: + low_priority.append(x) + # 生成单个任务 + if len(_plan.items()) > 0: + _time -= timedelta(minutes=8) + if _time < datetime.now(): _time = datetime.now() + self.tasks.append({"type": ','.join(_type), 'plan': _plan, + 'time': _time if not short_rest else (datetime.now() + timedelta(hours=0.5))}) + + + def infra_main(self): + """ 位于基建首页 """ + if self.find('control_central') is None: + self.back() + return + if self.task is not None: + try: + if len(self.task["plan"].keys()) > 0: + metadata = None + if 'metadata' in self.task.keys(): + metadata = self.task['metadata'] + self.agent_arrange(self.task["plan"], metadata) + if metadata is not None: + self.plan_metadata() + # 如果任务名称包含干员名,则为动态生成的 + elif 'type' in self.task.keys() and self.task['type'].split(',')[0] in agent_list: + self.overtake_room() + del self.tasks[0] + except Exception as e: + logger.exception(e) + self.skip() + self.error = True + self.task = None + elif not self.planned: + try: + # 如果有任何type 则会最后修正 + if self.read_mood: + mood_result = self.agent_get_mood(True) + if mood_result is not None: + return True + self.plan_solver() + except Exception as e: + # 重新扫描 + self.error = True + logger.exception({e}) + self.planned = True + elif not self.todo_task: + notification = detector.infra_notification(self.recog.img) + if self.drone_room is not None: + if self.drone_time is None or self.drone_time < datetime.now()- timedelta(hours=self.drone_execution_gap): + self.drone(self.drone_room) + logger.info(f"记录本次无人机使用时间为:{datetime.now()}") + self.drone_time = datetime.now() + if self.party_time is None: + self.clue() + if notification is None: + self.sleep(1) + notification = detector.infra_notification(self.recog.img) + if notification is not None: + self.tap(notification) + else: + self.todo_task = True + else: + return self.handle_error() + + def agent_get_mood(self, skip_dorm=False, force=False): + # 如果5分钟之内有任务则跳过心情读取 + if not force and next((k for k in self.tasks if k['time'] < datetime.now() + timedelta(seconds=300)), + None) is not None: + logger.info('有未完成的任务,跳过纠错') + self.skip() + return + logger.info('基建:记录心情') + need_read = set(v.room for k, v in self.op_data.operators.items() if v.need_to_refresh()) + for room in need_read: + error_count = 0 + while True: + try: + self.enter_room(room) + self.current_base[room] = self.get_agent_from_room(room) + logger.info(f'房间 {room} 心情为:{self.current_base[room]}') + break + except Exception as e: + if error_count > 3: raise e + logger.error(e) + error_count += 1 + self.back() + continue + self.back() + logger.debug(self.op_data.print()) + for room in self.currentPlan.keys(): + for idx, item in enumerate(self.currentPlan[room]): + _name = next((k for k, v in self.op_data.operators.items() if + v.current_room == room and v.current_index == idx), + None) + if room not in self.current_base.keys(): + self.current_base[room] = [''] * len(self.currentPlan[room]) + if _name is None or _name == '': + self.current_base[room][idx] = {"agent": "", "mood": -1} + else: + self.current_base[room][idx] = {"mood": self.op_data.operators[_name].mood, "agent": _name} + current_base = copy.deepcopy(self.current_base) + plan = self.currentPlan + fix_plan = {} + for key in current_base: + if key == 'train': continue + need_fix = False + for idx, operator in enumerate(current_base[key]): + data = current_base[key][idx] + # 如果是空房间 + if data["agent"] == '': + if not need_fix: + fix_plan[key] = ['Current'] * len(plan[key]) + need_fix = True + fix_plan[key][idx] = plan[key][idx]["agent"] + continue + # 随意人员则跳过 + _name = data['agent'] + if plan[key][idx]["agent"] == 'Free': + continue + if not (_name == plan[key][idx]['agent'] or ( + (_name in plan[key][idx]["replacement"]) and len(plan[key][idx]["replacement"]) > 0) or not + self.op_data.operators[_name].need_to_refresh()): + if not need_fix: + fix_plan[key] = ['Current'] * len(plan[key]) + need_fix = True + fix_plan[key][idx] = plan[key][idx]["agent"] + # 最后如果有任何高效组心情没有记录 或者高效组在宿舍 + miss_list = {k: v for (k, v) in self.op_data.operators.items() if v.not_valid()} + if len(miss_list.keys()) > 0: + # 替换到他应该的位置 + logger.debug(f"高效组心情没有记录{str(miss_list)}") + for key in miss_list: + _agent = miss_list[key] + if _agent.group != '' and _agent.current_room.startswith("dorm"): + # 如果还有其他小组成员在休息且没满心情则忽略 + if next((k for k, v in self.op_data.operators.items() if + v.group == _agent.group and not v.not_valid() and v.current_room.startswith( + "dorm")), None) is not None: + continue + elif _agent.group != '': + # 把所有小组成员都移到工作站 + agents = self.op_data.groups[_agent.group] + for a in agents: + __agent = self.op_data.operators[a] + if __agent.room not in fix_plan.keys(): + fix_plan[__agent.room] = ['Current'] * len(self.currentPlan[__agent.room]) + fix_plan[__agent.room][__agent.index] = a + if _agent.room not in fix_plan.keys(): + fix_plan[_agent.room] = ['Current'] * len(self.currentPlan[_agent.room]) + fix_plan[_agent.room][_agent.index] = key + # 如果是错位: + if (_agent.current_index != -1 and _agent.current_index != _agent.index) or (_agent.current_room !=""and _agent.room != _agent.current_room): + moved_room = _agent.current_room + moved_index = _agent.current_index + if moved_room not in fix_plan.keys(): + fix_plan[moved_room] = ['Current'] * len(self.currentPlan[moved_room]) + fix_plan[moved_room][moved_index] = self.currentPlan[moved_room][moved_index]["agent"] + if len(fix_plan.keys()) > 0: + # 不能在房间里安排同一个人 如果有重复则换成Free + # 还要修复确保同一组在同时上班 + fix_agents = [] + remove_keys = [] + logger.debug(f"Fix_plan {str(fix_plan)}") + for key in fix_plan: + if 'dormitory' in key: + # 如果宿舍差Free干员 则跳过 + if next((e for e in fix_plan[key] if e not in ['Free', 'Current']), + None) is None and skip_dorm: + remove_keys.append(key) + continue + if len(remove_keys) > 0: + for item in remove_keys: + del fix_plan[item] + if len(fix_plan.keys()) > 0: + self.tasks.append({"plan": fix_plan, "time": datetime.now()}) + logger.info(f'纠错任务为-->{fix_plan}') + return "self_correction" + + def plan_solver(self): + plan = self.currentPlan + # 如果下个 普通任务 <10 分钟则跳过 plan + if ( + next((e for e in self.tasks if e['time'] < datetime.now() + timedelta(seconds=600)), + None)) is not None: + return + if len(self.check_in_and_out()) > 0: + # 处理龙舌兰和但书的插拔 + for room in self.check_in_and_out(): + if any(room in obj["plan"].keys() and 'type' not in obj.keys() for obj in self.tasks): continue; + in_out_plan = {} + in_out_plan[room] = ['Current'] * len(plan[room]) + for idx, x in enumerate(plan[room]): + if '但书' in x['replacement'] or '龙舌兰' in x['replacement']: + in_out_plan[room][idx] = x['replacement'][0] + self.tasks.append({"time": self.get_run_roder_time(room), "plan": in_out_plan}) + # 准备数据 + logger.debug(self.op_data.print()) + if self.read_mood: + # 根据剩余心情排序 + self.total_agent = list( + v for k, v in self.op_data.operators.items() if v.is_high() and not v.room.startswith('dorm')) + self.total_agent.sort(key=lambda x: x.mood - x.lower_limit, reverse=False) + # 目前有换班的计划后面改 + logger.debug(f'当前基地数据--> {self.total_agent}') + fia_plan, fia_room = self.check_fia() + if fia_room is not None and fia_plan is not None: + if not any(fia_room in obj["plan"].keys() and len(obj["plan"][fia_room]) == 2 for obj in self.tasks): + fia_idx = self.op_data.operators['菲亚梅塔'].current_index if self.op_data.operators[ + '菲亚梅塔'].current_index != -1 else \ + self.op_data.operators['菲亚梅塔'].index + result = [{}] * (fia_idx + 1) + result[fia_idx]['time'] = datetime.now() + if self.op_data.operators["菲亚梅塔"].mood != 24: + self.enter_room(fia_room) + result = self.get_agent_from_room(fia_room, [fia_idx]) + self.back() + logger.info('下一次进行菲亚梅塔充能:' + result[fia_idx]['time'].strftime("%H:%M:%S")) + self.tasks.append({"time": result[fia_idx]['time'], "plan": {fia_room: [ + next(obj for obj in self.total_agent if obj.name in fia_plan).name, + "菲亚梅塔"]}}) + try: + # 自动生成任务 + self.plan_metadata() + # 剩余高效组位置 + high_free = self.op_data.available_free(count=self.max_resting_count) + # 剩余低效位置 + low_free = self.op_data.available_free('low', count=self.max_resting_count) + _replacement = [] + _plan = {} + for op in self.total_agent: + # 忽略掉菲亚梅塔充能的干员 + if high_free == 0 or low_free == 0: + break + if fia_room is not None and op.name in self.op_data.operators['菲亚梅塔'].replacement: + continue + # 忽略掉正在休息的 + if op.current_room.startswith("dorm") or op.current_room in ['factory']: + continue + # 忽略掉心情值没低于上限的的 + if op.mood > int((op.upper_limit - op.lower_limit) * self.resting_treshhold + op.lower_limit): + continue + if op.name in self.op_data.exhaust_agent: + if op.mood <= 2: + if next((e for e in self.tasks if 'type' in e.keys() and op.name in e['type']), + None) is None: + self.enter_room(op.current_room) + result = self.get_agent_from_room(op.current_room, [op.current_index]) + _time = datetime.now() + if result[op.current_index]['time'] is not None: + _time = result[op.current_index]['time'] + self.back() + # plan 是空的是因为得动态生成 + exhaust_type = op.name + if op.group != '': + exhaust_type = ','.join(self.op_data.groups[op.group]) + if _time0: + continue + elif _time >= datetime.now() or (_time 0: + self.tasks.append({'plan': _plan, 'time': datetime.now(), + 'metadata': {}}) + except Exception as e: + logger.exception(e) + # 如果下个 普通任务 >5 分钟则补全宿舍 + logger.debug('tasks:'+str(self.tasks)) + if (next((e for e in self.tasks if e['time'] < datetime.now() + timedelta(seconds=300)), + None)) is None: + self.agent_get_mood() + + def get_resting_plan(self, agents, exist_replacement, plan, high_free, low_free): + _low, _high = 0, 0 + __replacement = [] + __plan = {} + for x in agents: + if self.op_data.operators[x].resting_priority == 'low': + _low += 1 + else: + _high += 1 + # 排序 + agents.sort(key=lambda x: self.op_data.operators[x].mood) + # 进行位置数量的初步判定 + # 对于252可能需要进行额外判定,由于 low_free 性质等同于 high_free + success = True + if high_free - _high >= 0 and low_free - _low >= 0: + for agent in agents: + if not success: + break + x = self.op_data.operators[agent] + _rep = next((obj for obj in x.replacement if (not ( + self.op_data.operators[obj].current_room != '' and not self.op_data.operators[ + obj].current_room.startswith('dormitory'))) and obj not in ['但书', + '龙舌兰'] and obj not in exist_replacement and obj not in __replacement), + None) + if _rep is not None: + __replacement.append(_rep) + if x.room not in __plan.keys(): + __plan[x.room] = ['Current'] * len(self.currentPlan[x.room]) + __plan[x.room][x.index] = _rep + else: + success = False + if success: + # 记录替换组 + exist_replacement.extend(__replacement) + for x in agents: + _dorm = self.op_data.assign_dorm(x) + if _dorm.position[0] not in plan.keys(): + plan[_dorm.position[0]] = ['Current'] * 5 + plan[_dorm.position[0]][_dorm.position[1]] = _dorm.name + for k, v in __plan.items(): + if k not in plan.keys(): + plan[k] = __plan[k] + for idx, name in enumerate(__plan[k]): + if plan[k][idx] == 'Current' and name != 'Current': + plan[k][idx] = name + else: + success = False + if not success: + _high, _low = 0, 0 + return exist_replacement, plan, high_free - _high, low_free - _low + + def initialize_operators(self): + plan = self.currentPlan + self.op_data = Operators(self.agent_base_config, self.max_resting_count) + for room in plan.keys(): + for idx, data in enumerate(plan[room]): + self.op_data.add(Operator(data["agent"], room, idx, data["group"], data["replacement"], 'high', + operator_type="high")) + # 菲亚梅塔替换组做特例判断 + if "replacement" in data.keys() and data["agent"] != '菲亚梅塔': + for _replacement in data["replacement"]: + self.op_data.add(Operator(_replacement, "")) + dorm_names = [k for k in plan.keys() if k.startswith("dorm")] + dorm_names.sort(key=lambda x: x, reverse=False) + added = [] + # 竖向遍历出效率高到低 + for dorm in dorm_names: + for _idx, _dorm in enumerate(plan[dorm]): + if _dorm['agent'] == 'Free' and (dorm + str(_idx)) not in added and len(added) < self.max_resting_count: + self.op_data.dorm.append(Dormitory((dorm, _idx))) + added.append(dorm + str(_idx)) + break + # VIP休息位用完后横向遍历 + for dorm in dorm_names: + for _idx, _dorm in enumerate(plan[dorm]): + if _dorm['agent'] == 'Free' and (dorm + str(_idx)) not in added: + self.op_data.dorm.append(Dormitory((dorm, _idx))) + added.append(dorm + str(_idx)) + # low_free 的排序 + self.op_data.dorm[self.max_resting_count:len(self.op_data.dorm)] = sorted( + self.op_data.dorm[self.max_resting_count:len(self.op_data.dorm)], + key=lambda k: (k.position[0], k.position[1]), reverse=True) + + def check_in_and_out(self): + res = {} + for x, y in self.currentPlan.items(): + if not x.startswith('room'): continue + if any(('但书' in obj['replacement'] or '龙舌兰' in obj['replacement']) for obj in y): + res[x] = y + return res + + def check_fia(self): + if '菲亚梅塔' in self.op_data.operators.keys() and self.op_data.operators['菲亚梅塔'].room.startswith('dormitory'): + return self.op_data.operators['菲亚梅塔'].replacement, self.op_data.operators['菲亚梅塔'].room + return None, None + + def get_run_roder_time(self, room): + logger.info('基建:读取插拔时间') + # 点击进入该房间 + self.enter_room(room) + # 进入房间详情 + error_count = 0 + while self.find('bill_accelerate') is None: + if error_count > 5: + raise Exception('未成功进入无人机界面') + self.tap((self.recog.w * 0.05, self.recog.h * 0.95), interval=1) + error_count += 1 + execute_time = self.double_read_time((int(self.recog.w * 650 / 2496), int(self.recog.h * 660 / 1404), + int(self.recog.w * 815 / 2496), int(self.recog.h * 710 / 1404))) + execute_time = execute_time - timedelta(seconds=(60*self.run_order_delay)) + logger.info('下一次进行插拔的时间为:' + execute_time.strftime("%H:%M:%S")) + logger.info('返回基建主界面') + self.back(interval=2, rebuild=False) + self.back(interval=2) + return execute_time + + def double_read_time(self, cord, upperLimit=None): + if upperLimit is not None and upperLimit < 36000: + upperLimit = 36000 + self.recog.update() + time_in_seconds = self.read_time(cord, upperLimit) + if time_in_seconds is None: + return datetime.now() + execute_time = datetime.now() + timedelta(seconds=(time_in_seconds)) + return execute_time + + def initialize_paddle(self): + global ocr + if ocr is None: + ocr = PaddleOCR(enable_mkldnn=True, use_angle_cls=False) + + def read_screen(self, img, type="mood", limit=24, cord=None, change_color=False): + if cord is not None: + img = img[cord[1]:cord[3], cord[0]:cord[2]] + if 'mood' in type or type == "time": + # 心情图片太小,复制8次提高准确率 + for x in range(0, 4): + img = cv2.vconcat([img, img]) + if change_color: img[img == 137] = 255 + try: + self.initialize_paddle() + rets = ocr.ocr(img, cls=True) + line_conf = [] + for idx in range(len(rets[0])): + res = rets[0][idx] + if 'mood' in type: + # filter 掉不符合规范的结果 + if ('/' + str(limit)) in res[1][0]: + line_conf.append(res[1]) + else: + line_conf.append(res[1]) + logger.debug(line_conf) + if len(line_conf) == 0 and 'mood' in type: return -1 + x = [i[0] for i in line_conf] + __str = max(set(x), key=x.count) + print(__str) + if "mood" in type: + if '.' in __str: + __str = __str.replace(".", "") + number = int(__str[0:__str.index('/')]) + return number + elif 'time' in type: + if '.' in __str: + __str = __str.replace(".", ":") + return __str + except Exception as e: + logger.exception(e) + return limit + + def read_time(self, cord, upperlimit, error_count=0): + # 刷新图片 + self.recog.update() + time_str = self.read_screen(self.recog.img, type='time', cord=cord) + try: + h, m, s = str(time_str).split(':') + if int(m) > 60 or int(s) > 60: + raise Exception(f"读取错误") + res = int(h) * 3600 + int(m) * 60 + int(s) + if upperlimit is not None and res > upperlimit: + raise Exception(f"超过读取上限") + else: + return res + except: + logger.error("读取失败") + if error_count > 3: + logger.exception(f"读取失败{error_count}次超过上限") + return None + else: + return self.read_time(cord, upperlimit, error_count + 1) + + def todo_list(self) -> None: + """ 处理基建 Todo 列表 """ + tapped = False + trust = self.find('infra_collect_trust') + if trust is not None: + logger.info('基建:干员信赖') + self.tap(trust) + tapped = True + bill = self.find('infra_collect_bill') + if bill is not None: + logger.info('基建:订单交付') + self.tap(bill) + tapped = True + factory = self.find('infra_collect_factory') + if factory is not None: + logger.info('基建:可收获') + self.tap(factory) + tapped = True + if not tapped: + self.tap((self.recog.w * 0.05, self.recog.h * 0.95)) + self.todo_task = True + + def clue(self) -> None: + # 一些识别时会用到的参数 + global x1, x2, x3, x4, y0, y1, y2 + x1, x2, x3, x4 = 0, 0, 0, 0 + y0, y1, y2 = 0, 0, 0 + + logger.info('基建:线索') + + # 进入会客室 + self.enter_room('meeting') + + # 点击线索详情 + self.tap((self.recog.w * 0.1, self.recog.h * 0.9), interval=3) + + # 如果是线索交流的报告则返回 + self.find('clue_summary') and self.back() + + # 识别右侧按钮 + (x0, y0), (x1, y1) = self.find('clue_func', strict=True) + + logger.info('接收线索') + self.tap(((x0 + x1) // 2, (y0 * 3 + y1) // 4), interval=3, rebuild=False) + self.tap((self.recog.w - 10, self.recog.h - 10), interval=3, rebuild=False) + self.tap((self.recog.w * 0.05, self.recog.h * 0.95), interval=3, rebuild=False) + + logger.info('领取会客室线索') + self.tap(((x0 + x1) // 2, (y0 * 5 - y1) // 4), interval=3) + obtain = self.find('clue_obtain') + if obtain is not None and self.get_color(self.get_pos(obtain, 0.25, 0.5))[0] < 20: + self.tap(obtain, interval=2) + if self.find('clue_full') is not None: + self.back() + else: + self.back() + + logger.info('放置线索') + clue_unlock = self.find('clue_unlock') + if clue_unlock is not None: + # 当前线索交流未开启 + self.tap_element('clue', interval=3) + + # 识别阵营切换栏 + self.recog_bar() + + # 点击总览 + self.tap(((x1 * 7 + x2) // 8, y0 // 2), rebuild=False) + + # 获得和线索视图相关的数据 + self.recog_view(only_y2=False) + + # 检测是否拥有全部线索 + get_all_clue = True + for i in range(1, 8): + # 切换阵营 + self.tap(self.switch_camp(i), rebuild=False) + + # 清空界面内被选中的线索 + self.clear_clue_mask() + + # 获得和线索视图有关的数据 + self.recog_view() + + # 检测该阵营线索数量为 0 + if len(self.ori_clue()) == 0: + logger.info(f'无线索 {i}') + get_all_clue = False + break + + # 检测是否拥有全部线索 + if get_all_clue: + for i in range(1, 8): + # 切换阵营 + self.tap(self.switch_camp(i), rebuild=False) + + # 获得和线索视图有关的数据 + self.recog_view() + + # 放置线索 + logger.info(f'放置线索 {i}') + self.tap(((x1 + x2) // 2, y1 + 3), rebuild=False) + + # 返回线索主界面 + self.tap((self.recog.w * 0.05, self.recog.h * 0.95), interval=3, rebuild=False) + + # 线索交流开启 + if clue_unlock is not None and get_all_clue: + self.tap(clue_unlock) + self.party_time = datetime.now() + timedelta(days=1) + logger.info("为期一天的趴体开始") + elif clue_unlock is None: + # 记录趴体时间 + self.back(interval=2) + self.party_time = self.double_read_time((1765, 422, 1920, 515)) + logger.info(f"趴体结束时间为: {self.party_time}") + else: + self.back(interval=2) + logger.info('返回基建主界面') + self.back(interval=2) + + def switch_camp(self, id: int) -> tuple[int, int]: + """ 切换阵营 """ + x = ((id + 0.5) * x2 + (8 - id - 0.5) * x1) // 8 + y = (y0 + y1) // 2 + return x, y + + def recog_bar(self) -> None: + """ 识别阵营选择栏 """ + global x1, x2, y0, y1 + + (x1, y0), (x2, y1) = self.find('clue_nav', strict=True) + while int(self.recog.img[y0, x1 - 1].max()) - int(self.recog.img[y0, x1].max()) <= 1: + x1 -= 1 + while int(self.recog.img[y0, x2].max()) - int(self.recog.img[y0, x2 - 1].max()) <= 1: + x2 += 1 + while abs(int(self.recog.img[y1 + 1, x1].max()) - int(self.recog.img[y1, x1].max())) <= 1: + y1 += 1 + y1 += 1 + + logger.debug(f'recog_bar: x1:{x1}, x2:{x2}, y0:{y0}, y1:{y1}') + + def recog_view(self, only_y2: bool = True) -> None: + """ 识别另外一些和线索视图有关的数据 """ + global x1, x2, x3, x4, y0, y1, y2 + + # y2: 线索底部 + y2 = self.recog.h + while self.recog.img[y2 - 1, x1:x2].ptp() <= 24: + y2 -= 1 + if only_y2: + logger.debug(f'recog_view: y2:{y2}') + return y2 + # x3: 右边黑色 mask 边缘 + x3 = self.recog_view_mask_right() + # x4: 用来区分单个线索 + x4 = (54 * x1 + 25 * x2) // 79 + + logger.debug(f'recog_view: y2:{y2}, x3:{x3}, x4:{x4}') + + def recog_view_mask_right(self) -> int: + """ 识别线索视图中右边黑色 mask 边缘的位置 """ + x3 = x2 + while True: + max_abs = 0 + for y in range(y1, y2): + max_abs = max(max_abs, + abs(int(self.recog.img[y, x3 - 1, 0]) - int(self.recog.img[y, x3 - 2, 0]))) + if max_abs <= 5: + x3 -= 1 + else: + break + flag = False + for y in range(y1, y2): + if int(self.recog.img[y, x3 - 1, 0]) - int(self.recog.img[y, x3 - 2, 0]) == max_abs: + flag = True + if not flag: + self.tap(((x1 + x2) // 2, y1 + 10), rebuild=False) + x3 = x2 + while True: + max_abs = 0 + for y in range(y1, y2): + max_abs = max(max_abs, + abs(int(self.recog.img[y, x3 - 1, 0]) - int(self.recog.img[y, x3 - 2, 0]))) + if max_abs <= 5: + x3 -= 1 + else: + break + flag = False + for y in range(y1, y2): + if int(self.recog.img[y, x3 - 1, 0]) - int(self.recog.img[y, x3 - 2, 0]) == max_abs: + flag = True + if not flag: + x3 = None + return x3 + + def get_clue_mask(self) -> None: + """ 界面内是否有被选中的线索 """ + try: + mask = [] + for y in range(y1, y2): + if int(self.recog.img[y, x3 - 1, 0]) - int(self.recog.img[y, x3 - 2, 0]) > 20 and np.ptp( + self.recog.img[y, x3 - 2]) == 0: + mask.append(y) + if len(mask) > 0: + logger.debug(np.average(mask)) + return np.average(mask) + else: + return None + except Exception as e: + raise RecognizeError(e) + + def clear_clue_mask(self) -> None: + """ 清空界面内被选中的线索 """ + try: + while True: + mask = False + for y in range(y1, y2): + if int(self.recog.img[y, x3 - 1, 0]) - int(self.recog.img[y, x3 - 2, 0]) > 20 and np.ptp( + self.recog.img[y, x3 - 2]) == 0: + self.tap((x3 - 2, y + 1), rebuild=True) + mask = True + break + if mask: + continue + break + except Exception as e: + raise RecognizeError(e) + + def ori_clue(self): + """ 获取界面内有多少线索 """ + clues = [] + y3 = y1 + status = -2 + for y in range(y1, y2): + if self.recog.img[y, x4 - 5:x4 + 5].max() < 192: + if status == -1: + status = 20 + if status > 0: + status -= 1 + if status == 0: + status = -2 + clues.append(segment.get_poly(x1, x2, y3, y - 20)) + y3 = y - 20 + 5 + else: + status = -1 + if status != -2: + clues.append(segment.get_poly(x1, x2, y3, y2)) + + # 忽视一些只有一半的线索 + clues = [x.tolist() for x in clues if x[1][1] - x[0][1] >= self.recog.h / 5] + logger.debug(clues) + return clues + + def enter_room(self, room: str) -> tp.Rectangle: + """ 获取房间的位置并进入 """ + + # 获取基建各个房间的位置 + base_room = segment.base(self.recog.img, self.find('control_central', strict=True)) + # 将画面外的部分删去 + _room = base_room[room] + + for i in range(4): + _room[i, 0] = max(_room[i, 0], 0) + _room[i, 0] = min(_room[i, 0], self.recog.w) + _room[i, 1] = max(_room[i, 1], 0) + _room[i, 1] = min(_room[i, 1], self.recog.h) + + # 点击进入 + self.tap(_room[0], interval=3) + while self.find('control_central') is not None: + self.tap(_room[0], interval=3) + + def drone(self, room: str, not_customize=False, not_return=False): + logger.info('基建:无人机加速') + all_in = 0 + if not not_customize: + all_in = len(self.check_in_and_out()) + # 点击进入该房间 + self.enter_room(room) + # 进入房间详情 + + self.tap((self.recog.w * 0.05, self.recog.h * 0.95), interval=3) + # 关闭掉房间总览 + error_count = 0 + while self.find('factory_accelerate') is None and self.find('bill_accelerate') is None: + if error_count > 5: + raise Exception('未成功进入无人机界面') + self.tap((self.recog.w * 0.05, self.recog.h * 0.95), interval=3) + error_count += 1 + + accelerate = self.find('factory_accelerate') + if accelerate: + drone_count = self.read_screen(self.recog.img, type='drone_mood', cord=( + int(self.recog.w * 1150 / 1920), int(self.recog.h * 35 / 1080), int(self.recog.w * 1295 / 1920), + int(self.recog.h * 72 / 1080)), limit=201) + logger.info(f'当前无人机数量为:{drone_count}') + if drone_count< self.drone_count_limit or drone_count == 201: + logger.info(f"无人机数量小于{self.drone_count_limit}->停止") + return + logger.info('制造站加速') + self.tap(accelerate) + # self.tap_element('all_in') + # 如果不是全部all in + if all_in > 0: + tap_times = drone_count - self.drone_count_limit # 修改为无人机阈值 + _count = 0 + while _count < tap_times: + self.tap((self.recog.w * 0.7, self.recog.h * 0.5), interval=0.1, rebuild=False) + _count += 1 + else: + self.tap_element('all_in') + self.tap(accelerate, y_rate=1) + else: + accelerate = self.find('bill_accelerate') + while accelerate: + logger.info('贸易站加速') + self.tap(accelerate) + self.tap_element('all_in') + self.tap((self.recog.w * 0.75, self.recog.h * 0.8)) + while self.get_infra_scene() == Scene.CONNECTING: + self.sleep(3) + if self.drone_room is not None: + break + if not_customize: + drone_count = self.read_screen(self.recog.img, type='drone_mood', cord=( + int(self.recog.w * 1150 / 1920), int(self.recog.h * 35 / 1080), int(self.recog.w * 1295 / 1920), + int(self.recog.h * 72 / 1080)), limit=201) + logger.info(f'当前无人机数量为:{drone_count}') + self.recog.update() + self.recog.save_screencap('run_order') + # 200 为识别错误 + if drone_count < self.drone_count_limit or drone_count == 201: + logger.info(f"无人机数量小于{self.drone_count_limit}->停止") + break + st = accelerate[1] # 起点 + ed = accelerate[0] # 终点 + # 0.95, 1.05 are offset compensations + self.swipe_noinertia(st, (ed[0] * 0.95 - st[0] * 1.05, 0), rebuild=True) + accelerate = self.find('bill_accelerate') + if not_return: return + logger.info('返回基建主界面') + self.back(interval=2, rebuild=False) + self.back(interval=2) + + def get_arrange_order(self) -> ArrangeOrder: + best_score, best_order = 0, None + for order in ArrangeOrder: + score = self.recog.score(arrange_order_res[order][0]) + if score is not None and score[0] > best_score: + best_score, best_order = score[0], order + logger.debug((best_score, best_order)) + return best_order + + def switch_arrange_order(self, index: int, asc="false") -> None: + self.tap((self.recog.w * arrange_order_res[ArrangeOrder(index)][0], + self.recog.h * arrange_order_res[ArrangeOrder(index)][1]), interval=0, rebuild=False) + # 点个不需要的 + if index < 4: + self.tap((self.recog.w * arrange_order_res[ArrangeOrder(index + 1)][0], + self.recog.h * arrange_order_res[ArrangeOrder(index)][1]), interval=0, rebuild=False) + else: + self.tap((self.recog.w * arrange_order_res[ArrangeOrder(index - 1)][0], + self.recog.h * arrange_order_res[ArrangeOrder(index)][1]), interval=0, rebuild=False) + # 切回来 + self.tap((self.recog.w * arrange_order_res[ArrangeOrder(index)][0], + self.recog.h * arrange_order_res[ArrangeOrder(index)][1]), interval=0.2, rebuild=True) + # 倒序 + if asc != "false": + self.tap((self.recog.w * arrange_order_res[ArrangeOrder(index)][0], + self.recog.h * arrange_order_res[ArrangeOrder(index)][1]), interval=0.2, rebuild=True) + + def scan_agant(self, agent: list[str], error_count=0, max_agent_count=-1): + try: + # 识别干员 + self.recog.update() + ret = character_recognize.agent(self.recog.img) # 返回的顺序是从左往右从上往下 + # 提取识别出来的干员的名字 + select_name = [] + for y in ret: + name = y[0] + if name in agent: + select_name.append(name) + # self.get_agent_detail((y[1][0])) + self.tap((y[1][0])) + agent.remove(name) + # 如果是按照个数选择 Free + if max_agent_count != -1: + if len(select_name) >= max_agent_count: + return select_name, ret + return select_name, ret + except Exception as e: + error_count += 1 + if error_count < 3: + logger.exception(e) + self.sleep(3) + return self.scan_agant(agent, error_count, max_agent_count) + else: + raise e + + def get_order(self, name): + if (name in self.agent_base_config.keys()): + if "ArrangeOrder" in self.agent_base_config[name].keys(): + return True, self.agent_base_config[name]["ArrangeOrder"] + else: + return False, self.agent_base_config["Default"]["ArrangeOrder"] + return False, self.agent_base_config["Default"]["ArrangeOrder"] + + def detail_filter(self, turn_on, type="not_in_dorm"): + logger.info(f'开始 {("打开" if turn_on else "关闭")} {type} 筛选') + self.tap((self.recog.w * 0.95, self.recog.h * 0.05), interval=1) + if type == "not_in_dorm": + not_in_dorm = self.find('arrange_non_check_in', score=0.85) + if turn_on ^ (not_in_dorm is None): + self.tap((self.recog.w * 0.3, self.recog.h * 0.5), interval=0.5) + # 确认 + self.tap((self.recog.w * 0.8, self.recog.h * 0.8), interval=0.5) + + def choose_agent(self, agents: list[str], room: str) -> None: + """ + :param order: ArrangeOrder, 选择干员时右上角的排序功能 + """ + first_name = '' + max_swipe = 50 + for idx, n in enumerate(agents): + if n == '': + agents[idx] = 'Free' + # 如果是宿舍且干员不为高效组,则改为Free 加速换班时间 + elif room.startswith('dorm'): + if n not in self.op_data.operators.keys(): + agents[idx] = 'Free' + elif not self.op_data.operators[n].is_high(): + agents[idx] = 'Free' + agent = copy.deepcopy(agents) + logger.info(f'安排干员 :{agent}') + # 若不是空房间,则清空工作中的干员 + is_dorm = room.startswith("dorm") + h, w = self.recog.h, self.recog.w + first_time = True + # 在 agent 中 'Free' 表示任意空闲干员 + free_num = agent.count('Free') + for i in range(agent.count("Free")): + agent.remove("Free") + index_change = False + pre_order = [2, False] + right_swipe = 0 + retry_count = 0 + # 如果重复进入宿舍则需要排序 + selected = [] + logger.info(f'上次进入房间为:{self.last_room},本次房间为:{room}') + if self.last_room.startswith('dorm') and is_dorm: + self.detail_filter(False) + while len(agent) > 0: + if retry_count > 3: raise Exception(f"到达最大尝试次数 3次") + if right_swipe > max_swipe: + # 到底了则返回再来一次 + for _ in range(right_swipe): + self.swipe_only((w // 2, h // 2), (w // 2, 0), interval=0.5) + right_swipe = 0 + max_swipe = 50 + retry_count += 1 + self.detail_filter(False) + if first_time: + # 清空 + if is_dorm: + self.switch_arrange_order(3, "true") + pre_order = [3, 'true'] + self.tap((self.recog.w * 0.38, self.recog.h * 0.95), interval=0.5) + changed, ret = self.scan_agant(agent) + if changed: + selected.extend(changed) + if len(agent) == 0: break + index_change = True + + # 如果选中了人,则可能需要重新排序 + if index_change or first_time: + # 第一次则调整 + is_custom, arrange_type = self.get_order(agent[0]) + if is_dorm and not ( + agent[0] in self.op_data.operators.keys() and self.op_data.operators[agent[0]].room.startswith( + 'dormitory')): + arrange_type = (3, 'true') + # 如果重新排序则滑到最左边 + if pre_order[0] != arrange_type[0] or pre_order[1] != arrange_type[1]: + self.switch_arrange_order(arrange_type[0], arrange_type[1]) + # 滑倒最左边 + self.sleep(interval=0.5, rebuild=True) + right_swipe = self.swipe_left(right_swipe, w, h) + pre_order = arrange_type + first_time = False + + changed, ret = self.scan_agant(agent) + if changed: + selected.extend(changed) + # 如果找到了 + index_change = True + else: + # 如果没找到 而且右移次数大于5 + if ret[0][0] == first_name and right_swipe > 5: + max_swipe = right_swipe + else: + first_name = ret[0][0] + index_change = False + st = ret[-2][1][2] # 起点 + ed = ret[0][1][1] # 终点 + self.swipe_noinertia(st, (ed[0] - st[0], 0)) + right_swipe += 1 + if len(agent) == 0: break; + + # 安排空闲干员 + if free_num: + if free_num == len(agents): + self.tap((self.recog.w * 0.38, self.recog.h * 0.95), interval=0.5) + if not first_time: + # 滑动到最左边 + self.sleep(interval=0.5, rebuild=False) + right_swipe = self.swipe_left(right_swipe, w, h) + self.detail_filter(True) + self.switch_arrange_order(3, "true") + # 只选择在列表里面的 + # 替换组小于20才休息,防止进入就满心情进行网络连接 + free_list = [v.name for k, v in self.op_data.operators.items() if + v.name not in agents and v.operator_type != 'high'] + free_list.extend([_name for _name in agent_list if _name not in self.op_data.operators.keys()]) + free_list = list(set(free_list) - set(self.free_blacklist)) + while free_num: + selected_name, ret = self.scan_agant(free_list, max_agent_count=free_num) + selected.extend(selected_name) + free_num -= len(selected_name) + while len(selected_name) > 0: + agents[agents.index('Free')] = selected_name[0] + selected_name.remove(selected_name[0]) + if free_num == 0: + break + else: + st = ret[-2][1][2] # 起点 + ed = ret[0][1][1] # 终点 + self.swipe_noinertia(st, (ed[0] - st[0], 0)) + right_swipe += 1 + # 排序 + if len(agents) != 1: + # 左移 + self.swipe_left(right_swipe, w, h) + self.tap((self.recog.w * arrange_order_res[ArrangeOrder.SKILL][0], + self.recog.h * arrange_order_res[ArrangeOrder.SKILL][1]), interval=0.5, rebuild=False) + position = [(0.35, 0.35), (0.35, 0.75), (0.45, 0.35), (0.45, 0.75), (0.55, 0.35)] + not_match = False + for idx, item in enumerate(agents): + if agents[idx] != selected[idx] or not_match: + not_match = True + p_idx = selected.index(agents[idx]) + self.tap((self.recog.w * position[p_idx][0], self.recog.h * position[p_idx][1]), interval=0, + rebuild=False) + self.tap((self.recog.w * position[p_idx][0], self.recog.h * position[p_idx][1]), interval=0, + rebuild=False) + self.last_room = room + logger.info(f"设置上次房间为{self.last_room}") + + def swipe_left(self, right_swipe, w, h): + for _ in range(right_swipe): + self.swipe_only((w // 2, h // 2), (w // 2, 0), interval=0.5) + return 0 + + def get_agent_from_room(self, room, read_time_index=None): + if read_time_index is None: + read_time_index = [] + error_count = 0 + if room == 'meeting': + time.sleep(3) + while self.find('room_detail') is None: + if error_count > 3: + raise Exception('未成功进入房间') + self.tap((self.recog.w * 0.05, self.recog.h * 0.4), interval=0.5) + error_count += 1 + length = len(self.currentPlan[room]) + if length > 3: self.swipe((self.recog.w * 0.8, self.recog.h * 0.8), (0, self.recog.h * 0.4), interval=1, + rebuild=True) + name_p = [((1460, 155), (1700, 210)), ((1460, 370), (1700, 420)), ((1460, 585), (1700, 630)), + ((1460, 560), (1700, 610)), ((1460, 775), (1700, 820))] + time_p = [((1650, 270, 1780, 305)), ((1650, 480, 1780, 515)), ((1650, 690, 1780, 725)), + ((1650, 665, 1780, 700)), ((1650, 875, 1780, 910))] + mood_p = [((1685, 213, 1780, 256)), ((1685, 422, 1780, 465)), ((1685, 632, 1780, 675)), + ((1685, 612, 1780, 655)), ((1685, 822, 1780, 865))] + result = [] + swiped = False + for i in range(0, length): + if i >= 3 and not swiped: + self.swipe((self.recog.w * 0.8, self.recog.h * 0.8), (0, -self.recog.h * 0.4), interval=1, rebuild=True) + swiped = True + data = {} + _name = character_recognize.agent_name( + self.recog.img[name_p[i][0][1]:name_p[i][1][1], name_p[i][0][0]:name_p[i][1][0]], self.recog.h * 1.1) + error_count = 0 + while i >= 3 and _name != '' and ( + next((e for e in result if e['agent'] == _name), None)) is not None: + logger.warning("检测到滑动可能失败") + self.swipe((self.recog.w * 0.8, self.recog.h * 0.8), (0, -self.recog.h * 0.4), interval=1, rebuild=True) + _name = character_recognize.agent_name( + self.recog.img[name_p[i][0][1]:name_p[i][1][1], name_p[i][0][0]:name_p[i][1][0]], + self.recog.h * 1.1) + error_count += 1 + if error_count > 4: + raise Exception("超过出错上限") + _mood = 24 + # 如果房间不为空 + if _name != '': + if _name not in self.op_data.operators.keys() and _name in agent_list: + self.op_data.add(Operator(_name, "")) + update_time=False + if self.op_data.operators[_name].need_to_refresh(): + _mood = self.read_screen(self.recog.img, cord=mood_p[i], change_color=True) + update_time = True + else: + _mood = self.op_data.operators[_name].mood + high_no_time = self.op_data.update_detail(_name, _mood, room, i,update_time) + if high_no_time is not None: + logger.debug(f"检测到高效组休息时间数据不存在:{room},{high_no_time}") + read_time_index.append(high_no_time) + else: + _mood = -1 + data['agent'] = _name + data['mood'] = _mood + if i in read_time_index: + if _mood in [24] or (_mood == 0 and not room.startswith('dorm')): + data['time'] = datetime.now() + else: + upperLimit = 21600 + if _name in ['菲亚梅塔'] or _name in self.op_data.exhaust_agent: + upperLimit = 43200 + logger.debug(f"开始记录时间:{room},{i}") + data['time'] = self.double_read_time(time_p[i], upperLimit=upperLimit) + self.op_data.refresh_dorm_time(room, i, data) + logger.debug(f"停止记录时间:{str(data)}") + result.append(data) + for _operator in self.op_data.operators.keys(): + if self.op_data.operators[_operator].current_room == room and _operator not in [res['agent'] for res in + result]: + self.op_data.operators[_operator].current_room = '' + self.op_data.operators[_operator].current_index = -1 + logger.info(f'重设 {_operator} 至空闲') + return result + + def agent_arrange(self, plan: tp.BasePlan, metadata=None): + logger.info('基建:排班') + in_and_out = [] + fia_data = None + rooms = list(plan.keys()) + # 优先替换工作站再替换宿舍 + rooms.sort(key=lambda x: x.startswith('dorm'), reverse=False) + for room in rooms: + finished = False + choose_error = 0 + while not finished: + try: + error_count = 0 + self.enter_room(room) + while self.find('room_detail') is None: + if error_count > 3: + raise Exception('未成功进入房间') + self.tap((self.recog.w * 0.05, self.recog.h * 0.4), interval=0.5) + error_count += 1 + error_count = 0 + update_base = False + if ('但书' in plan[room] or '龙舌兰' in plan[room]) and not \ + room.startswith('dormitory') and room not in in_and_out: + in_and_out.append(room) + update_base = True + if '菲亚梅塔' in plan[room] and len(plan[room]) == 2: + fia_data = (room, plan[room][0]) + update_base = True + if update_base or 'Current' in plan[room]: + self.current_base[room] = self.get_agent_from_room(room) + # 纠错 因为网络连接导致房间移位 + if 'Current' in plan[room]: + # replace current + for current_idx, _name in enumerate(plan[room]): + if _name == 'Current': + current_name = self.current_base[room][current_idx]["agent"] + if current_name in agent_list: + plan[room][current_idx] = current_name + else: + # 如果空房间或者名字错误,则使用default干员 + plan[room][current_idx] = \ + self.currentPlan[room][current_idx]["agent"] + while self.find('arrange_order_options') is None: + if error_count > 3: + raise Exception('未成功进入干员选择界面') + self.tap((self.recog.w * 0.82, self.recog.h * 0.2), interval=1) + error_count += 1 + self.choose_agent(plan[room], room) + self.recog.update() + self.tap_element('confirm_blue', detected=True, judge=False, interval=3) + if self.get_infra_scene() == Scene.INFRA_ARRANGE_CONFIRM: + x0 = self.recog.w // 3 * 2 # double confirm + y0 = self.recog.h - 10 + self.tap((x0, y0), rebuild=True) + read_time_index = [] + if metadata is not None: + read_time_index = self.op_data.get_refresh_index(room, plan[room]) + if fia_data is not None: + # 移除时间以刷新心情 + self.op_data.operators['菲亚梅塔'].time_stamp = None + self.op_data.operators[fia_data[1]].time_stamp = None + current = self.get_agent_from_room(room, read_time_index) + for idx, name in enumerate(plan[room]): + if current[idx]['agent'] != name: + logger.error(f'检测到的干员{current[idx]["agent"]},需要安排的干员{name}') + raise Exception('检测到安排干员未成功') + finished = True + # 如果完成则移除该任务 + del plan[room] + # back to 基地主界面 + while self.scene() == Scene.CONNECTING: + self.sleep(3) + except Exception as e: + logger.exception(e) + choose_error += 1 + self.recog.update() + back_count = 0 + while self.get_infra_scene() != Scene.INFRA_MAIN: + self.back() + self.recog.update() + back_count += 1 + if back_count > 3: + raise e + if choose_error > 3: + raise e + else: + continue + self.back(0.5) + if len(in_and_out) > 0: + replace_plan = {} + for room in in_and_out: + logger.info("开始插拔") + self.drone(room, True, True) + in_and_out_plan = [data["agent"] for data in self.current_base[room]] + # 防止由于意外导致的死循环 + if '但书' in in_and_out_plan or '龙舌兰' in in_and_out_plan: + in_and_out_plan = [data["agent"] for data in self.currentPlan[room]] + replace_plan[room] = in_and_out_plan + self.back(interval=0.5) + self.back(interval=0.5) + self.tasks.append({'time': self.tasks[0]['time'], 'plan': replace_plan}) + self.skip() + if fia_data is not None: + replace_agent = fia_data[1] + fia_change_room = self.op_data.operators[replace_agent].room + fia_room_plan = [data["agent"] for data in self.current_base[fia_data[0]]] + fia_change_room_plan = ['Current'] * len(self.currentPlan[fia_change_room]) + fia_change_room_plan[self.op_data.operators[replace_agent].index] = replace_agent + self.tasks.append( + {'time': self.tasks[0]['time'], + 'plan': {fia_data[0]: fia_room_plan, fia_change_room: fia_change_room_plan}}) + # 急速换班 + self.skip() + logger.info('返回基建主界面') + + def skip(self): + self.todo_task = True + self.planned = True + + @Asst.CallBackType + def log_maa(msg, details, arg): + m = Message(msg) + d = json.loads(details.decode('utf-8')) + logger.debug(d) + logger.debug(m) + logger.debug(arg) + + def inialize_maa(self): + # 若需要获取详细执行信息,请传入 callback 参数 + # 例如 asst = Asst(callback=my_callback) + Asst.load(path=self.maa_config['maa_path']) + self.MAA = Asst(callback=self.log_maa) + # self.MAA.set_instance_option(2, 'maatouch') + # 请自行配置 adb 环境变量,或修改为 adb 可执行程序的路径 + # logger.info(self.device.client.device_id) + if self.MAA.connect(self.maa_config['maa_adb_path'], self.device.client.device_id): + logger.info("MAA 连接成功") + else: + logger.info("MAA 连接失败") + raise Exception("MAA 连接失败") + + def maa_plan_solver(self): + + try: + if self.maa_config['last_execution'] is not None and datetime.now() - timedelta( + seconds=self.maa_config['maa_execution_gap'] * 3600) < self.maa_config['last_execution']: + logger.info("间隔未超过设定时间,不启动maa") + else: + self.send_email('启动MAA') + self.back_to_index() + # 任务及参数请参考 docs/集成文档.md + self.inialize_maa() + self.MAA.append_task('StartUp') + _plan = self.maa_config['weekly_plan'][get_server_weekday()] + logger.info(f"现在服务器是{_plan['weekday']}") + fights = [] + for stage in _plan["stage"]: + logger.info(f"添加关卡:{stage}") + self.MAA.append_task('Fight', { + # 空值表示上一次 + # 'stage': '', + 'stage': stage, + 'medicine': _plan["medicine"], + 'stone': 0, + 'times': 999, + 'report_to_penguin': True, + 'client_type': '', + 'penguin_id': '', + 'DrGrandet': False, + 'server': 'CN' + }) + fights.append(stage) + self.MAA.append_task('Recruit', { + 'select': [4], + 'confirm': [3, 4], + 'times': 4, + 'refresh': True, + "recruitment_time": { + "3": 460, + "4": 540 + } + }) + self.MAA.append_task('Visit') + self.MAA.append_task('Mall', { + 'shopping': True, + 'buy_first': ['龙门币', '赤金'], + 'blacklist': ['家具', '碳', '加急'], + 'credit_fight': fights[len(fights) - 1] != '' + }) + self.MAA.append_task('Award') + # asst.append_task('Copilot', { + # 'stage_name': '千层蛋糕', + # 'filename': './GA-EX8-raid.json', + # 'formation': False + + # }) + self.MAA.start() + logger.info(f"MAA 启动") + hard_stop = False + while self.MAA.running(): + # 5分钟之前就停止 + if (self.tasks[0]["time"] - datetime.now()).total_seconds() < 300: + self.MAA.stop() + hard_stop = True + else: + time.sleep(0) + self.send_email('MAA停止') + if hard_stop: + logger.info(f"由于maa任务并未完成,等待3分钟重启软件") + time.sleep(180) + self.device.exit(self.package_name) + else: + logger.info(f"记录MAA 本次执行时间") + self.maa_config['last_execution'] = datetime.now() + logger.info(self.maa_config['last_execution']) + if self.maa_config['roguelike'] or self.maa_config['reclamation_algorithm'] or self.maa_config[ + 'stationary_security_service']: + while (self.tasks[0]["time"] - datetime.now()).total_seconds() > 30: + self.MAA = None + self.inialize_maa() + if self.maa_config['roguelike']: + self.MAA.append_task('Roguelike', { + 'mode': 0, + 'starts_count': 9999999, + 'investment_enabled': True, + 'investments_count': 9999999, + 'stop_when_investment_full': False, + 'squad': '指挥分队', + 'roles': '取长补短', + 'theme': 'Mizuki', + 'core_char': '海沫' + }) + elif self.maa_config['reclamation_algorithm']: + self.back_to_maa_config['reclamation_algorithm']() + self.MAA.append_task('ReclamationAlgorithm') + # elif self.maa_config['stationary_security_service'] : + # self.MAA.append_task('SSSCopilot', { + # 'filename': "F:\\MAA-v4.10.5-win-x64\\resource\\copilot\\SSS_阿卡胡拉丛林.json", + # 'formation': False, + # 'loop_times':99 + # }) + self.MAA.start() + while self.MAA.running(): + if (self.tasks[0]["time"] - datetime.now()).total_seconds() < 30: + self.MAA.stop() + break + else: + time.sleep(0) + self.device.exit(self.package_name) + # 生息演算逻辑 结束 + remaining_time = (self.tasks[0]["time"] - datetime.now()).total_seconds() + subject = f"开始休息 {'%.2f' % (remaining_time / 60)} 分钟,到{self.tasks[0]['time'].strftime('%H:%M:%S')}" + context = f"下一次任务:{self.tasks[0]['plan']}" + logger.info(context) + logger.info(subject) + self.send_email(context, subject) + time.sleep(remaining_time) + self.MAA = None + except Exception as e: + logger.error(e) + self.MAA = None + remaining_time = (self.tasks[0]["time"] - datetime.now()).total_seconds() + if remaining_time > 0: + logger.info(f"开始休息 {'%.2f' % (remaining_time / 60)} 分钟,到{self.tasks[0]['time'].strftime('%H:%M:%S')}") + time.sleep(remaining_time) + self.device.exit(self.package_name) + + def send_email(self, context, subject=''): + if 'mail_enable' in self.email_config.keys() and self.email_config['mail_enable'] == 0: + logger.info('邮件功能未开启') + return + try: + msg = MIMEMultipart() + msg.attach(MIMEText(str(context), 'plain', 'utf-8')) + msg['Subject'] = self.email_config['subject'] + (str(subject) if subject else '') + msg['From'] = self.email_config['account'] + s = smtplib.SMTP_SSL("smtp.qq.com", 465, timeout=10.0) + # 登录邮箱 + s.login(self.email_config['account'], self.email_config['pass_code']) + # 开始发送 + s.sendmail(self.email_config['account'], self.email_config['receipts'], msg.as_string()) + logger.info("邮件发送成功") + except Exception as e: + logger.error("邮件发送失败") + logger.exception(e) diff --git a/arknights_mower/solvers/shop.py b/arknights_mower/solvers/shop.py index fdcdd95e0..e4eb65e49 100644 --- a/arknights_mower/solvers/shop.py +++ b/arknights_mower/solvers/shop.py @@ -23,7 +23,7 @@ def run(self, priority: list[str] = None) -> None: :param priority: list[str], 使用信用点购买东西的优先级, 若无指定则默认购买第一件可购买的物品 """ self.priority = priority - + self.buying = None logger.info('Start: 商店') logger.info('购买期望:%s' % priority if priority else '无,购买到信用点用完为止') super().run() @@ -42,6 +42,11 @@ def transition(self) -> bool: elif self.scene() == Scene.SHOP_CREDIT_CONFIRM: if self.find('shop_credit_not_enough') is None: self.tap_element('shop_cart') + elif len(self.priority) > 0: + # 移除优先级中买不起的物品 + self.priority.remove(self.buying) + logger.info('信用点不足,放弃购买%s,看看别的...' % self.buying) + self.back() else: return True elif self.scene() == Scene.SHOP_ASSIST: @@ -84,4 +89,5 @@ def shop_credit(self) -> bool: if valid[0][1] not in priority: return True logger.info(f'实际购买顺序:{[x[1] for x in valid]}') + self.buying = valid[0][1] self.tap(valid[0][0], interval=3) diff --git a/arknights_mower/strategy.py b/arknights_mower/strategy.py index 1a4ec58ae..c6c2194e2 100644 --- a/arknights_mower/strategy.py +++ b/arknights_mower/strategy.py @@ -1,12 +1,11 @@ from __future__ import annotations import functools -import signal from .solvers import * +from .solvers.base_schedule import BaseSchedulerSolver from .utils import typealias as tp from .utils.device import Device -from .utils.log import logger from .utils.recognize import Recognizer from .utils.solver import BaseSolver @@ -22,20 +21,11 @@ def __init__(self, device: Device = None, recog: Recognizer = None, timeout: int self.recog = recog if recog is not None else Recognizer(self.device) self.timeout = timeout - def timer(f): - @functools.wraps(f) - def inner(self: Solver, *args, **kwargs): - def handler(signum, frame): - logger.warning('Operation timed out') - raise KeyboardInterrupt - signal.signal(signal.SIGALRM, handler) - signal.alarm(self.timeout * 3600) - ret = f(self, *args, **kwargs) - signal.alarm(0) - return ret - return inner - - @timer + + def base_scheduler (self,tasks=[],plan={},current_base={},)-> None: + # 返还所有排班计划以及 当前基建干员位置 + return BaseSchedulerSolver(self.device, self.recog).run(tasks,plan,current_base) + def base(self, arrange: tp.BasePlan = None, clue_collect: bool = False, drone_room: str = None, fia_room: str = None) -> None: """ :param arrange: dict(room_name: [agent,...]), 基建干员安排 @@ -43,25 +33,21 @@ def base(self, arrange: tp.BasePlan = None, clue_collect: bool = False, drone_ro :param drone_room: str, 是否使用无人机加速 :param fia_room: str, 是否使用菲亚梅塔恢复心情 """ - BaseConstructSolver(self.device, self.recog).run( + BaseSolver(self.device, self.recog).run( arrange, clue_collect, drone_room, fia_room) - @timer def credit(self) -> None: CreditSolver(self.device, self.recog).run() - @timer def mission(self) -> None: MissionSolver(self.device, self.recog).run() - @timer def recruit(self, priority: list[str] = None) -> None: """ :param priority: list[str], 优先考虑的公招干员,默认为高稀有度优先 """ RecruitSolver(self.device, self.recog).run(priority) - @timer def ope(self, level: str = None, times: int = -1, potion: int = 0, originite: int = 0, eliminate: int = 0, plan: list[tp.OpePlan] = None) -> list[tp.OpePlan]: """ :param level: str, 指定关卡,默认为前往上一次关卡或当前界面关卡 @@ -75,17 +61,14 @@ def ope(self, level: str = None, times: int = -1, potion: int = 0, originite: in """ return OpeSolver(self.device, self.recog).run(level, times, potion, originite, eliminate, plan) - @timer def shop(self, priority: bool = None) -> None: """ :param priority: list[str], 使用信用点购买东西的优先级, 若无指定则默认购买第一件可购买的物品 """ ShopSolver(self.device, self.recog).run(priority) - @timer def mail(self) -> None: MailSolver(self.device, self.recog).run() - @timer def index(self) -> None: BaseSolver(self.device, self.recog).back_to_index() diff --git a/arknights_mower/templates/config.yaml b/arknights_mower/templates/config.yaml index a0f336579..3d6e15c99 100644 --- a/arknights_mower/templates/config.yaml +++ b/arknights_mower/templates/config.yaml @@ -135,7 +135,7 @@ priority: arrangement: plan_1: # 控制中枢 - contral: + central: - 夕 - 令 - 凯尔希 diff --git a/arknights_mower/utils/asst.py b/arknights_mower/utils/asst.py new file mode 100644 index 000000000..e12eb266d --- /dev/null +++ b/arknights_mower/utils/asst.py @@ -0,0 +1,265 @@ +import ctypes +import os +import pathlib +import platform +import json + +from typing import Union, Dict, List, Any, Type, Optional +from enum import Enum, IntEnum, unique, auto + +JSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]] + + +class InstanceOptionType(IntEnum): + touch_type = 2 + deployment_with_pause = 3 + + +class Asst: + CallBackType = ctypes.CFUNCTYPE( + None, ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p) + """ + 回调函数,使用实例可参照 my_callback + :params: + ``param1 message``: 消息类型 + ``param2 details``: json string + ``param3 arg``: 自定义参数 + """ + + @staticmethod + def load(path: Union[pathlib.Path, str], incremental_path: Optional[Union[pathlib.Path, str]] = None, user_dir: Optional[Union[pathlib.Path, str]] = None) -> bool: + """ + 加载 dll 及资源 + :params: + ``path``: DLL及资源所在文件夹路径 + ``incremental_path``: 增量资源所在文件夹路径 + ``user_dir``: 用户数据(日志、调试图片等)写入文件夹路径 + """ + + platform_values = { + 'windows': { + 'libpath': 'MaaCore.dll', + 'environ_var': 'PATH' + }, + 'darwin': { + 'libpath': 'libMaaCore.dylib', + 'environ_var': 'DYLD_LIBRARY_PATH' + }, + 'linux': { + 'libpath': 'libMaaCore.so', + 'environ_var': 'LD_LIBRARY_PATH' + } + } + lib_import_func = None + + platform_type = platform.system().lower() + if platform_type == 'windows': + lib_import_func = ctypes.WinDLL + else: + lib_import_func = ctypes.CDLL + + Asst.__libpath = pathlib.Path(path) / platform_values[platform_type]['libpath'] + try: + os.environ[platform_values[platform_type]['environ_var']] += os.pathsep + str(path) + except KeyError: + os.environ[platform_values[platform_type]['environ_var']] = os.pathsep + str(path) + Asst.__lib = lib_import_func(str(Asst.__libpath)) + Asst.__set_lib_properties() + + ret: bool = True + if user_dir: + ret &= Asst.__lib.AsstSetUserDir(str(user_dir).encode('utf-8')) + + ret &= Asst.__lib.AsstLoadResource(str(path).encode('utf-8')) + if incremental_path: + ret &= Asst.__lib.AsstLoadResource( + str(incremental_path).encode('utf-8')) + + return ret + + def __init__(self, callback: CallBackType = None, arg=None): + """ + :params: + ``callback``: 回调函数 + ``arg``: 自定义参数 + """ + + if callback: + self.__ptr = Asst.__lib.AsstCreateEx(callback, arg) + else: + self.__ptr = Asst.__lib.AsstCreate() + + def __del__(self): + Asst.__lib.AsstDestroy(self.__ptr) + self.__ptr = None + + def set_instance_option(self, option_type: InstanceOptionType, option_value: str): + """ + 设置额外配置 + 参见${MaaAssistantArknights}/src/MaaCore/Assistant.cpp#set_instance_option + :params: + ``externa_config``: 额外配置类型 + ``config_value``: 额外配置的值 + :return: 是否设置成功 + """ + return Asst.__lib.AsstSetInstanceOption(self.__ptr, + int(option_type), option_value.encode('utf-8')) + + + def connect(self, adb_path: str, address: str, config: str = 'General'): + """ + 连接设备 + :params: + ``adb_path``: adb 程序的路径 + ``address``: adb 地址+端口 + ``config``: adb 配置,可参考 resource/config.json + :return: 是否连接成功 + """ + return Asst.__lib.AsstConnect(self.__ptr, + adb_path.encode('utf-8'), address.encode('utf-8'), config.encode('utf-8')) + + TaskId = int + + def append_task(self, type_name: str, params: JSON = {}) -> TaskId: + """ + 添加任务 + :params: + ``type_name``: 任务类型,请参考 docs/集成文档.md + ``params``: 任务参数,请参考 docs/集成文档.md + :return: 任务 ID, 可用于 set_task_params 接口 + """ + return Asst.__lib.AsstAppendTask(self.__ptr, type_name.encode('utf-8'), json.dumps(params, ensure_ascii=False).encode('utf-8')) + + def set_task_params(self, task_id: TaskId, params: JSON) -> bool: + """ + 动态设置任务参数 + :params: + ``task_id``: 任务 ID, 使用 append_task 接口的返回值 + ``params``: 任务参数,同 append_task 接口,请参考 docs/集成文档.md + :return: 是否成功 + """ + return Asst.__lib.AsstSetTaskParams(self.__ptr, task_id, json.dumps(params, ensure_ascii=False).encode('utf-8')) + + def start(self) -> bool: + """ + 开始任务 + :return: 是否成功 + """ + return Asst.__lib.AsstStart(self.__ptr) + + def stop(self) -> bool: + """ + 停止并清空所有任务 + :return: 是否成功 + """ + return Asst.__lib.AsstStop(self.__ptr) + + def running(self) -> bool: + """ + 是否正在运行 + :return: 是否正在运行 + """ + return Asst.__lib.AsstRunning(self.__ptr) + + @staticmethod + def log(level: str, message: str) -> None: + ''' + 打印日志 + :params: + ``level``: 日志等级标签 + ``message``: 日志内容 + ''' + + Asst.__lib.AsstLog(level.encode('utf-8'), message.encode('utf-8')) + + def get_version(self) -> str: + """ + 获取DLL版本号 + : return: 版本号 + """ + return Asst.__lib.AsstGetVersion().decode('utf-8') + + @staticmethod + def __set_lib_properties(): + Asst.__lib.AsstSetUserDir.restype = ctypes.c_bool + Asst.__lib.AsstSetUserDir.argtypes = ( + ctypes.c_char_p,) + + Asst.__lib.AsstLoadResource.restype = ctypes.c_bool + Asst.__lib.AsstLoadResource.argtypes = ( + ctypes.c_char_p,) + + Asst.__lib.AsstCreate.restype = ctypes.c_void_p + Asst.__lib.AsstCreate.argtypes = () + + Asst.__lib.AsstCreateEx.restype = ctypes.c_void_p + Asst.__lib.AsstCreateEx.argtypes = ( + ctypes.c_void_p, ctypes.c_void_p,) + + Asst.__lib.AsstDestroy.argtypes = (ctypes.c_void_p,) + + Asst.__lib.AsstSetInstanceOption.restype = ctypes.c_bool + Asst.__lib.AsstSetInstanceOption.argtypes = ( + ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p,) + + Asst.__lib.AsstConnect.restype = ctypes.c_bool + Asst.__lib.AsstConnect.argtypes = ( + ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p,) + + Asst.__lib.AsstAppendTask.restype = ctypes.c_int + Asst.__lib.AsstAppendTask.argtypes = ( + ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p) + + Asst.__lib.AsstSetTaskParams.restype = ctypes.c_bool + Asst.__lib.AsstSetTaskParams.argtypes = ( + ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p) + + Asst.__lib.AsstStart.restype = ctypes.c_bool + Asst.__lib.AsstStart.argtypes = (ctypes.c_void_p,) + + Asst.__lib.AsstStop.restype = ctypes.c_bool + Asst.__lib.AsstStop.argtypes = (ctypes.c_void_p,) + + Asst.__lib.AsstRunning.restype = ctypes.c_bool + Asst.__lib.AsstRunning.argtypes = (ctypes.c_void_p,) + + Asst.__lib.AsstGetVersion.restype = ctypes.c_char_p + + Asst.__lib.AsstLog.restype = None + Asst.__lib.AsstLog.argtypes = ( + ctypes.c_char_p, ctypes.c_char_p) + + +@unique +class Message(Enum): + """ + 回调消息 + 请参考 docs/回调消息.md + """ + InternalError = 0 + + InitFailed = auto() + + ConnectionInfo = auto() + + AllTasksCompleted = auto() + + TaskChainError = 10000 + + TaskChainStart = auto() + + TaskChainCompleted = auto() + + TaskChainExtraInfo = auto() + + TaskChainStopped = auto() + + SubTaskError = 20000 + + SubTaskStart = auto() + + SubTaskCompleted = auto() + + SubTaskExtraInfo = auto() + + SubTaskStopped = auto() \ No newline at end of file diff --git a/arknights_mower/utils/character_recognize.py b/arknights_mower/utils/character_recognize.py index 592a9c3a2..dd2adab26 100644 --- a/arknights_mower/utils/character_recognize.py +++ b/arknights_mower/utils/character_recognize.py @@ -9,12 +9,12 @@ from PIL import Image, ImageDraw, ImageFont from .. import __rootdir__ -from ..data import agent_list -from ..ocr import ocrhandle +from ..data import agent_list,ocr_error from . import segment from .image import saveimg from .log import logger from .recognize import RecognizeError +from ..ocr import ocrhandle def poly_center(poly): @@ -63,14 +63,15 @@ def agent_sift_init(): origin_kp, origin_des = SIFT.detectAndCompute(origin, None) -def sift_recog(query, resolution, draw=False): +def sift_recog(query, resolution, draw=False,reverse = False): """ 使用 SIFT 提取特征点识别干员名称 """ agent_sift_init() - query = cv2.cvtColor(np.array(query), cv2.COLOR_RGB2GRAY) - + if reverse : + # 干员总览界面图像色度反转 + query = 255 -query # the height & width of query image height, width = query.shape @@ -140,11 +141,8 @@ def agent(img, draw=False): ): x0 += 1 - # ocr 初步识别干员名称 - ocr = ocrhandle.predict(img[:, x0:right]) - # 获取分割结果 - ret = segment.agent(img, draw) + ret, ocr = segment.agent(img, draw) # 确定位置后开始精确识别 ret_succ = [] @@ -157,37 +155,51 @@ def agent(img, draw=False): if in_poly(poly, (cx + x0, cy)) and cx > fx: fx = cx found_ocr = x - - if found_ocr is not None: - x = found_ocr - if x[1] in agent_list and x[1] not in ['砾', '陈']: # ocr 经常会把这两个搞错 - ret_agent.append(x[1]) - ret_succ.append(poly) - continue - __img = img[poly[0, 1] : poly[2, 1], poly[0, 0] : poly[2, 0]] - res = sift_recog(__img, resolution, draw) - if res is not None: - logger.debug(f'干员名称识别修正:{x[1]} -> {res}') - ret_agent.append(res) + __img = img[poly[0, 1]: poly[2, 1], poly[0, 0]: poly[2, 0]] + try: + if found_ocr is not None: + x = found_ocr + if x[1] in agent_list and x[1] not in ['砾', '陈']: # ocr 经常会把这两个搞错 + ret_agent.append(x[1]) + ret_succ.append(poly) + continue + res = sift_recog(__img, resolution, draw) + if (res is not None) and res in agent_list: + ret_agent.append(res) + ret_succ.append(poly) + continue + logger.warning( + f'干员名称识别异常:{x[1]} 为不存在的数据,请报告至 https://github.com/Konano/arknights-mower/issues' + ) + saveimg(__img, 'failure_agent') + raise Exception(x[1]) + else: + if 80 <= np.min(__img): + continue + res = sift_recog(__img, resolution, draw) + if res is not None: + ret_agent.append(res) + ret_succ.append(poly) + continue + logger.warning(f'干员名称识别异常:区域 {poly.tolist()}') + saveimg(__img, 'failure_agent') + raise Exception("启动 Plan B") + ret_fail.append(poly) + raise Exception("启动 Plan B") + except Exception as e: + # 大哥不行了,二哥上! + _msg = str(e) + ret_fail.append(poly) + if "Plan B" not in _msg: + if _msg in ocr_error.keys(): + name = ocr_error[_msg] + elif "Off" in _msg: + name = 'U-Official' + else: + continue + ret_agent.append(name) ret_succ.append(poly) continue - logger.warning( - f'干员名称识别异常:{x[1]} 为不存在的数据,请报告至 https://github.com/Konano/arknights-mower/issues' - ) - saveimg(__img, 'failure_agent') - else: - __img = img[poly[0, 1] : poly[2, 1], poly[0, 0] : poly[2, 0]] - if 80 <= np.min(__img): - continue - res = sift_recog(__img, resolution, draw) - if res is not None: - ret_agent.append(res) - ret_succ.append(poly) - continue - logger.warning(f'干员名称识别异常:区域 {poly.tolist()}') - saveimg(__img, 'failure_agent') - ret_fail.append(poly) - if len(ret_fail): saveimg(img, 'failure') if draw: @@ -202,4 +214,26 @@ def agent(img, draw=False): except Exception as e: logger.debug(traceback.format_exc()) + saveimg(img, 'failure_agent') raise RecognizeError(e) + +def agent_name(__img, height,reverse = False, draw: bool = False): + query = cv2.cvtColor(np.array(__img), cv2.COLOR_RGB2GRAY) + h, w= query.shape + dim = (w*4, h*4) + # resize image + resized = cv2.resize(__img, dim, interpolation=cv2.INTER_AREA) + ocr = ocrhandle.predict(resized) + name = '' + try: + if len(ocr) > 0 and ocr[0][1] in agent_list and ocr[0][1] not in ['砾', '陈']: + name = ocr[0][1] + else: + res = sift_recog(__img, height, draw, reverse) + if (res is not None) and res in agent_list: + name = res + else: + raise Exception("识别错误") + except Exception as e: + saveimg(__img, 'failure_agent') + return name diff --git a/arknights_mower/utils/config.py b/arknights_mower/utils/config.py index abcac5a94..057ebd6d0 100644 --- a/arknights_mower/utils/config.py +++ b/arknights_mower/utils/config.py @@ -2,7 +2,10 @@ import shutil import sys -from collections import Mapping +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping from pathlib import Path from typing import Any @@ -132,8 +135,10 @@ def init_config() -> None: PASSWORD = __get('account/password', None) global APPNAME - APPNAME = __get('app/package_name', 'com.hypergryph.arknights') + \ - '/' + __get('app/activity_name', 'com.u8.sdk.U8UnityContext') + APPNAME = __get('app/package_name', 'com.hypergryph.arknights') + + global APP_ACTIVITY_NAME + APP_ACTIVITY_NAME = __get('app/activity_name','com.u8.sdk.U8UnityContext') global DEBUG_MODE, LOGFILE_PATH, LOGFILE_AMOUNT, SCREENSHOT_PATH, SCREENSHOT_MAXNUM DEBUG_MODE = __get('debug/enabled', False) diff --git a/arknights_mower/utils/datetime.py b/arknights_mower/utils/datetime.py index d19001044..735844cc7 100644 --- a/arknights_mower/utils/datetime.py +++ b/arknights_mower/utils/datetime.py @@ -1,7 +1,14 @@ -import datetime +from datetime import datetime +import pytz - -def the_same_day(a: datetime.datetime = None, b: datetime.datetime = None) -> bool: +def the_same_day(a: datetime = None, b: datetime = None) -> bool: if a is None or b is None: return False return a.year == b.year and a.month == b.month and a.day == b.day + +def the_same_time(a: datetime = None, b: datetime = None) -> bool: + if a is None or b is None: + return False + return a.year == b.year and a.month == b.month and a.day == b.day and a.hour ==b.hour and a.minute==b.minute and a.second == b.second +def get_server_weekday(): + return datetime.now(pytz.timezone('Asia/Dubai')).weekday() \ No newline at end of file diff --git a/arknights_mower/utils/device/adb_client/core.py b/arknights_mower/utils/device/adb_client/core.py index 630fe1689..b41af2b22 100644 --- a/arknights_mower/utils/device/adb_client/core.py +++ b/arknights_mower/utils/device/adb_client/core.py @@ -41,15 +41,27 @@ def __init_adb(self) -> None: def __init_device(self) -> None: # wait for the newly started ADB server to probe emulators time.sleep(1) - if self.device_id is None: + if self.device_id is None or self.device_id not in config.ADB_DEVICE: self.device_id = self.__choose_devices() - if self.device_id is None: + if self.device_id is None : if self.connect is None: - for connect in config.ADB_CONNECT: - Session().connect(connect) + if config.ADB_DEVICE[0] != '': + for connect in config.ADB_CONNECT: + Session().connect(connect) else: Session().connect(self.connect) self.device_id = self.__choose_devices() + elif self.connect is None: + Session().connect(self.device_id) + + # if self.device_id is None or self.device_id not in config.ADB_DEVICE: + # if self.connect is None or self.device_id not in config.ADB_CONNECT: + # for connect in config.ADB_CONNECT: + # Session().connect(connect) + # else: + # Session().connect(self.connect) + # self.device_id = self.__choose_devices() + logger.info(self.__available_devices()) if self.device_id not in self.__available_devices(): logger.error('未检测到相应设备。请运行 `adb devices` 确认列表中列出了目标模拟器或设备。') raise RuntimeError('Device connection failure') @@ -60,9 +72,11 @@ def __choose_devices(self) -> Optional[str]: for device in config.ADB_DEVICE: if device in devices: return device - if len(devices) > 0: + if len(devices) > 0 and config.ADB_DEVICE[0] == '': + logger.debug(devices[0]) return devices[0] + def __available_devices(self) -> list[str]: """ return available devices """ return [x[0] for x in Session().devices_list() if x[1] != 'offline'] @@ -163,7 +177,7 @@ def push(self, target_path: str, target: bytes) -> None: def stream(self, cmd: str) -> Socket: """ run adb command, return socket """ return self.session().request(cmd, True).sock - + def stream_shell(self, cmd: str) -> Socket: """ run adb shell command, return socket """ return self.stream('shell:' + cmd) diff --git a/arknights_mower/utils/device/device.py b/arknights_mower/utils/device/device.py index df7a373ab..1299520d5 100644 --- a/arknights_mower/utils/device/device.py +++ b/arknights_mower/utils/device/device.py @@ -82,6 +82,10 @@ def launch(self, app: str) -> None: """ launch the application """ self.run(f'am start -n {app}') + def exit(self, app: str) -> None: + """ launch the application """ + self.run(f'am force-stop {app}') + def send_keyevent(self, keycode: int) -> None: """ send a key event """ logger.debug(f'keyevent: {keycode}') @@ -138,7 +142,7 @@ def swipe_ext(self, points: list[tuple[int, int]], durations: list[int], up_wait def check_current_focus(self): """ check if the application is in the foreground """ - if self.current_focus() != config.APPNAME: - self.launch(config.APPNAME) + if self.current_focus() != f"{config.APPNAME}/{config.APP_ACTIVITY_NAME}": + self.launch(f"{config.APPNAME}/{config.APP_ACTIVITY_NAME}") # wait for app to finish launching time.sleep(10) diff --git a/arknights_mower/utils/device/utils.py b/arknights_mower/utils/device/utils.py index b412d2d43..2a203b80e 100644 --- a/arknights_mower/utils/device/utils.py +++ b/arknights_mower/utils/device/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +import http import socket import tempfile @@ -12,13 +13,12 @@ def download_file(target_url: str) -> str: """ download file to temp path, and return its file path for further usage """ logger.debug(f'downloading: {target_url}') - resp = requests.get(target_url) + resp = requests.get(target_url, verify=False) with tempfile.NamedTemporaryFile('wb+', delete=False) as f: file_name = f.name f.write(resp.content) return file_name - # def is_port_using(host: str, port: int) -> bool: # """ if port is using by others, return True. else return False """ # s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/arknights_mower/utils/log.py b/arknights_mower/utils/log.py index bce9e1d17..20e627044 100644 --- a/arknights_mower/utils/log.py +++ b/arknights_mower/utils/log.py @@ -7,7 +7,6 @@ from pathlib import Path import colorlog - from . import config BASIC_FORMAT = '%(asctime)s - %(levelname)s - %(relativepath)s:%(lineno)d - %(funcName)s - %(message)s' @@ -40,6 +39,16 @@ def filter(self, record: logging.LogRecord) -> bool: return True +class Handler(logging.StreamHandler): + def __init__(self, pipe): + logging.StreamHandler.__init__(self) + self.pipe = pipe + + def emit(self, record): + record = f'{record.message}' + self.pipe.send(record) + + chlr = logging.StreamHandler(stream=sys.stdout) chlr.setFormatter(color_formatter) chlr.setLevel('INFO') @@ -57,7 +66,7 @@ def filter(self, record: logging.LogRecord) -> bool: logger.addHandler(ehlr) -def init_fhlr() -> None: +def init_fhlr(pipe=None) -> None: """ initialize log file """ if config.LOGFILE_PATH is None: return @@ -73,6 +82,10 @@ def init_fhlr() -> None: fhlr.setLevel('DEBUG') fhlr.addFilter(PackagePathFilter()) logger.addHandler(fhlr) + if pipe is not None: + wh = Handler(pipe) + wh.setLevel(logging.INFO) + logger.addHandler(wh) def set_debug_mode() -> None: @@ -113,3 +126,5 @@ def run(self) -> None: while True: line = self.pipe.readline().strip() logger.debug(f'{self.process}: {line}') + + diff --git a/arknights_mower/utils/matcher.py b/arknights_mower/utils/matcher.py index 0a5ffae5d..3f3f56c11 100644 --- a/arknights_mower/utils/matcher.py +++ b/arknights_mower/utils/matcher.py @@ -55,7 +55,7 @@ def init_sift(self) -> None: """ get SIFT feature points """ self.kp, self.des = SIFT.detectAndCompute(self.origin, None) - def match(self, query: tp.GrayImage, draw: bool = False, scope: tp.Scope = None, judge: bool = True) -> Optional(tp.Scope): + def match(self, query: tp.GrayImage, draw: bool = False, scope: tp.Scope = None, judge: bool = True,prescore = 0.0) -> Optional(tp.Scope): """ check if the image can be matched """ rect_score = self.score(query, draw, scope) # get matching score if rect_score is None: @@ -68,6 +68,9 @@ def match(self, query: tp.GrayImage, draw: bool = False, scope: tp.Scope = None, logger.debug(f'match fail: {score}') return None # failed in matching else: + if prescore>0 and score[3]= self.max_resting_count: + # break + if dorm.position[0] == room and dorm.position[1] == index: + # 如果人为高效组,则记录时间 + _name = agent['agent'] + if _name in self.operators.keys() and self.operators[_name].is_high(): + dorm.name = _name + dorm.time = agent['time'] + else: + dorm.name = '' + dorm.time = None + break + + def get_refresh_index(self,room,plan): + ret = [] + for idx, dorm in enumerate(self.dorm): + # Filter out resting priority low + if idx >= self.max_resting_count: + break + if dorm.position[0] == room: + for i,_name in enumerate(plan): + if _name in self.operators.keys() and self.operators[_name].is_high() and self.operators[_name].resting_priority=='high' and not self.operators[_name].room.startswith('dorm'): + ret.append(i) + break + return ret + + def get_dorm_by_name(self,name): + for idx, dorm in enumerate(self.dorm): + if dorm.name == name: + return idx, dorm + return None,None + + def add(self, operator): + if operator.name not in agent_list: + return + if operator.name in self.config.keys() and 'RestingPriority' in self.config[operator.name].keys(): + operator.resting_priority = self.config[operator.name]['RestingPriority'] + if operator.name in self.config.keys() and 'ExhaustRequire' in self.config[operator.name].keys(): + operator.exhaust_require = self.config[operator.name]['ExhaustRequire'] + if operator.name in self.config.keys() and 'RestInFull' in self.config[operator.name].keys(): + operator.rest_in_full = self.config[operator.name]['RestInFull'] + if operator.name in self.config.keys() and 'LowerLimit' in self.config[operator.name].keys(): + operator.lower_limit = self.config[operator.name]['LowerLimit'] + if operator.name in self.config.keys() and 'UpperLimit' in self.config[operator.name].keys(): + operator.upper_limit = self.config[operator.name]['UpperLimit'] + self.operators[operator.name] = operator + # 需要用尽心情干员逻辑 + if (operator.exhaust_require or operator.group in self.exhaust_group) \ + and operator.name not in self.exhaust_agent: + self.exhaust_agent.append(operator.name) + if operator.group != '': + self.exhaust_group.append(operator.group) + # 干员分组逻辑 + if operator.group != "": + if operator.group not in self.groups.keys(): + self.groups[operator.group] = [operator.name] + else: + self.groups[operator.group].append(operator.name) + + def available_free(self, free_type='high', count=4): + ret = 0 + if free_type == 'high': + idx = 0 + for dorm in self.dorm: + if dorm.name =='' or (dorm.name in self.operators.keys() and not self.operators[dorm.name].is_high()): + ret += 1 + if idx == count - 1: + break + else: + idx += 1 + else: + idx = -1 + while idx < 0: + dorm = self.dorm[idx] + if dorm.name =='' or (dorm.name in self.operators.keys() and not self.operators[dorm.name].is_high()): + ret += 1 + if idx == count - len(self.dorm): + break + else: + idx -= 1 + return ret + + def assign_dorm(self,name): + is_high = self.operators[name].resting_priority=='high' + if is_high: + _room = next(obj for obj in self.dorm if obj.name not in self.operators.keys() or not self.operators[obj.name].is_high()) + else: + _room = None + idx = -1 + while idx < 0: + if self.dorm[idx].name=='': + _room = self.dorm[idx] + break + else: + idx -= 1 + _room.name = name + return _room + + def print(self): + ret = "{" + op = [] + dorm = [] + for k, v in self.operators.items(): + op.append("'"+k+"': "+ str(vars(v))) + ret += "'operators': {" + ','.join(op)+"}," + for v in self.dorm: + dorm.append(str(vars(v))) + ret += "'dorms': [" + ','.join(dorm) + "]}" + return ret + + +class Dormitory(object): + + def __init__(self, position, name='', time=None): + self.position = position + self.name = name + self.time = time + + +class Operator(object): + time_stamp = None + + def __init__(self, name, room, index=-1, group='', replacement=[], resting_priority='low', current_room='', + exhaust_require=False, + mood=24, upper_limit=24, rest_in_full=False, current_index=-1, lower_limit=0, operator_type="low"): + self.name = name + self.room = room + self.operator_type = operator_type + # if room.startswith('dormitory'): + # self.operator_type = "low" + self.index = index + self.group = group + self.replacement = replacement + self.resting_priority = resting_priority + self.current_room = current_room + self.exhaust_require = exhaust_require + self.upper_limit = upper_limit + self.rest_in_full = rest_in_full + self.mood = mood + self.current_index = current_index + self.lower_limit = lower_limit + + def is_high(self): + return self.operator_type =='high' + + def need_to_refresh(self, h=2): + # 是否需要读取心情 + if self.operator_type == 'high': + if self.time_stamp is None or ( + self.time_stamp is not None and self.time_stamp + timedelta(hours=h) < datetime.now()): + return True + return False + + def not_valid(self): + if self.operator_type == 'high': + if not self.room.startswith("dorm") and self.current_room.startswith("dorm"): + if self.mood == -1 or self.mood == 24: + return True + else: + return False + return self.need_to_refresh() or self.current_room != self.room or self.index != self.current_index + return False diff --git a/arknights_mower/utils/recognize.py b/arknights_mower/utils/recognize.py index 6e275a077..a014769ff 100644 --- a/arknights_mower/utils/recognize.py +++ b/arknights_mower/utils/recognize.py @@ -69,6 +69,8 @@ def get_scene(self) -> int: self.scene = Scene.INDEX elif self.find('nav_index') is not None: self.scene = Scene.NAVIGATION_BAR + elif self.find('close_mine') is not None: + self.scene = Scene.CLOSE_MINE elif self.find('materiel_ico') is not None: self.scene = Scene.MATERIEL elif self.find('read_mail') is not None: @@ -215,6 +217,51 @@ def get_scene(self) -> int: logger.info(f'Scene: {self.scene}: {SceneComment[self.scene]}') return self.scene + def get_infra_scene(self)-> int: + if self.scene != Scene.UNDEFINED: + return self.scene + if self.find('connecting', scope=((self.w//2, self.h//10*8), (self.w//4*3, self.h))) is not None: + self.scene = Scene.CONNECTING + elif self.find('double_confirm') is not None: + if self.find('network_check') is not None: + self.scene = Scene.NETWORK_CHECK + else: + self.scene = Scene.DOUBLE_CONFIRM + elif self.find('infra_overview') is not None: + self.scene = Scene.INFRA_MAIN + elif self.find('infra_todo') is not None: + self.scene = Scene.INFRA_TODOLIST + elif self.find('clue') is not None: + self.scene = Scene.INFRA_CONFIDENTIAL + elif self.find('arrange_check_in') or self.find('arrange_check_in_on') is not None: + self.scene = Scene.INFRA_DETAILS + elif self.find('infra_overview_in') is not None: + self.scene = Scene.INFRA_ARRANGE + elif self.find('arrange_confirm') is not None: + self.scene = Scene.INFRA_ARRANGE_CONFIRM + elif self.find('arrange_order_options_scene') is not None: + self.scene = Scene.INFRA_ARRANGE_ORDER + elif self.find('loading') is not None: + self.scene = Scene.LOADING + elif self.find('loading2') is not None: + self.scene = Scene.LOADING + elif self.find('loading3') is not None: + self.scene = Scene.LOADING + elif self.find('loading4') is not None: + self.scene = Scene.LOADING + elif self.find('index_nav', thres=250, scope=((0, 0), (100+self.w//4, self.h//10))) is not None: + self.scene = Scene.INDEX + elif self.is_black(): + self.scene = Scene.LOADING + else: + self.scene = Scene.UNKNOWN + self.device.check_current_focus() + # save screencap to analyse + if config.SCREENSHOT_PATH is not None: + self.save_screencap(self.scene) + logger.info(f'Scene: {self.scene}: {SceneComment[self.scene]}') + return self.scene + def is_black(self) -> None: """ check if the current scene is all black """ return np.max(self.gray[:, 105:-105]) < 16 @@ -223,7 +270,7 @@ def nav_button(self): """ find navigation button """ return self.find('nav_button', thres=128, scope=((0, 0), (100+self.w//4, self.h//10))) - def find(self, res: str, draw: bool = False, scope: tp.Scope = None, thres: int = None, judge: bool = True, strict: bool = False) -> tp.Scope: + def find(self, res: str, draw: bool = False, scope: tp.Scope = None, thres: int = None, judge: bool = True, strict: bool = False,score = 0.0) -> tp.Scope: """ 查找元素是否出现在画面中 @@ -233,6 +280,7 @@ def find(self, res: str, draw: bool = False, scope: tp.Scope = None, thres: int :param thres: 是否在匹配前对图像进行二值化处理 :param judge: 是否加入更加精确的判断 :param strict: 是否启用严格模式,未找到时报错 + :param strict: 是否启用分数限制,有些图片精确识别需要提高分数阈值 :return ret: 若匹配成功,则返回元素在游戏界面中出现的位置,否则返回 None """ @@ -244,11 +292,11 @@ def find(self, res: str, draw: bool = False, scope: tp.Scope = None, thres: int res_img = thres2(loadimg(res, True), thres) gray_img = cropimg(self.gray, scope) matcher = Matcher(thres2(gray_img, thres)) - ret = matcher.match(res_img, draw=draw, judge=judge) + ret = matcher.match(res_img, draw=draw, judge=judge,prescore=score) else: res_img = loadimg(res, True) matcher = self.matcher - ret = matcher.match(res_img, draw=draw, scope=scope, judge=judge) + ret = matcher.match(res_img, draw=draw, scope=scope, judge=judge,prescore=score) if strict and ret is None: raise RecognizeError(f"Can't find '{res}'") return ret diff --git a/arknights_mower/utils/segment.py b/arknights_mower/utils/segment.py index c2bc0f3d9..7c522cbac 100644 --- a/arknights_mower/utils/segment.py +++ b/arknights_mower/utils/segment.py @@ -5,8 +5,6 @@ import cv2 import numpy as np from matplotlib import pyplot as plt - -from .. import __rootdir__ from ..data import agent_list from ..ocr import ocrhandle from . import detector @@ -22,10 +20,10 @@ class FloodCheckFailed(Exception): def get_poly(x1: int, x2: int, y1: int, y2: int) -> tp.Rectangle: x1, x2 = int(x1), int(x2) y1, y2 = int(y1), int(y2) - return np.array([[x1, y1], [x1, y2], [x2, y2], [x2, y1]]) + return np.array([ [ x1, y1 ], [ x1, y2 ], [ x2, y2 ], [ x2, y1 ] ]) -def credit(img: tp.Image, draw: bool = False) -> list[tp.Scope]: +def credit(img: tp.Image, draw: bool = False) -> list[ tp.Scope ]: """ 信用交易所特供的图像分割算法 """ @@ -33,25 +31,25 @@ def credit(img: tp.Image, draw: bool = False) -> list[tp.Scope]: height, width, _ = img.shape left, right = 0, width - while np.max(img[:, right-1]) < 100: + while np.max(img[ :, right - 1 ]) < 100: right -= 1 - while np.max(img[:, left]) < 100: + while np.max(img[ :, left ]) < 100: left += 1 def average(i: int) -> int: num, sum = 0, 0 for j in range(left, right): - if img[i, j, 0] == img[i, j, 1] and img[i, j, 1] == img[i, j, 2]: + if img[ i, j, 0 ] == img[ i, j, 1 ] and img[ i, j, 1 ] == img[ i, j, 2 ]: num += 1 - sum += img[i, j, 0] + sum += img[ i, j, 0 ] return sum // num def ptp(j: int) -> int: maxval = -999999 minval = 999999 for i in range(up_1, up_2): - minval = min(minval, img[i, j, 0]) - maxval = max(maxval, img[i, j, 0]) + minval = min(minval, img[ i, j, 0 ]) + maxval = max(maxval, img[ i, j, 0 ]) return maxval - minval up_1 = 0 @@ -78,18 +76,18 @@ def ptp(j: int) -> int: while ptp(left) < 50: left += 1 - split_x = [left + (right - left) // 5 * i for i in range(0, 6)] - split_y = [up_1, (up_1 + down) // 2, down] + split_x = [ left + (right - left) // 5 * i for i in range(0, 6) ] + split_y = [ up_1, (up_1 + down) // 2, down ] - ret = [] - for y1, y2 in zip(split_y[:-1], split_y[1:]): - for x1, x2 in zip(split_x[:-1], split_x[1:]): + ret = [ ] + for y1, y2 in zip(split_y[ :-1 ], split_y[ 1: ]): + for x1, x2 in zip(split_x[ :-1 ], split_x[ 1: ]): ret.append(((x1, y1), (x2, y2))) if draw: - for y1, y2 in zip(split_y[:-1], split_y[1:]): - for x1, x2 in zip(split_x[:-1], split_x[1:]): - cv2.polylines(img, [get_poly(x1, x2, y1, y2)], + for y1, y2 in zip(split_y[ :-1 ], split_y[ 1: ]): + for x1, x2 in zip(split_x[ :-1 ], split_x[ 1: ]): + cv2.polylines(img, [ get_poly(x1, x2, y1, y2) ], True, 0, 10, cv2.LINE_AA) plt.imshow(img) plt.show() @@ -102,13 +100,13 @@ def ptp(j: int) -> int: raise RecognizeError(e) -def recruit(img: tp.Image, draw: bool = False) -> list[tp.Scope]: +def recruit(img: tp.Image, draw: bool = False) -> list[ tp.Scope ]: """ 公招特供的图像分割算法 """ try: height, width, _ = img.shape - left, right = width//2-100, width//2-50 + left, right = width // 2 - 100, width // 2 - 50 def adj_x(i: int) -> int: if i == 0: @@ -116,8 +114,8 @@ def adj_x(i: int) -> int: sum = 0 for j in range(left, right): for k in range(3): - sum += abs(int(img[i, j, k]) - int(img[i-1, j, k])) - return sum // (right-left) + sum += abs(int(img[ i, j, k ]) - int(img[ i - 1, j, k ])) + return sum // (right - left) def adj_y(j: int) -> int: if j == 0: @@ -125,54 +123,54 @@ def adj_y(j: int) -> int: sum = 0 for i in range(up_2, down_2): for k in range(3): - sum += abs(int(img[i, j, k]) - int(img[i, j-1, k])) - return int(sum / (down_2-up_2)) + sum += abs(int(img[ i, j, k ]) - int(img[ i, j - 1, k ])) + return int(sum / (down_2 - up_2)) def average(i: int) -> int: sum = 0 for j in range(left, right): - sum += np.sum(img[i, j, :3]) - return sum // (right-left) // 3 + sum += np.sum(img[ i, j, :3 ]) + return sum // (right - left) // 3 def minus(i: int) -> int: s = 0 for j in range(left, right): - s += int(img[i, j, 2]) - int(img[i, j, 0]) - return s // (right-left) + s += int(img[ i, j, 2 ]) - int(img[ i, j, 0 ]) + return s // (right - left) up = 0 while minus(up) > -100: up += 1 while not (adj_x(up) > 80 and minus(up) > -10 and average(up) > 210): up += 1 - up_2, down_2 = up-90, up-40 + up_2, down_2 = up - 90, up - 40 left = 0 - while np.max(img[:, left]) < 100: + while np.max(img[ :, left ]) < 100: left += 1 left += 1 while adj_y(left) < 50: left += 1 right = width - 1 - while np.max(img[:, right]) < 100: + while np.max(img[ :, right ]) < 100: right -= 1 while adj_y(right) < 50: right -= 1 - split_x = [left, (left + right) // 2, right] + split_x = [ left, (left + right) // 2, right ] down = height - 1 - split_y = [up, (up + down) // 2, down] + split_y = [ up, (up + down) // 2, down ] - ret = [] - for y1, y2 in zip(split_y[:-1], split_y[1:]): - for x1, x2 in zip(split_x[:-1], split_x[1:]): + ret = [ ] + for y1, y2 in zip(split_y[ :-1 ], split_y[ 1: ]): + for x1, x2 in zip(split_x[ :-1 ], split_x[ 1: ]): ret.append(((x1, y1), (x2, y2))) if draw: - for y1, y2 in zip(split_y[:-1], split_y[1:]): - for x1, x2 in zip(split_x[:-1], split_x[1:]): - cv2.polylines(img, [get_poly(x1, x2, y1, y2)], + for y1, y2 in zip(split_y[ :-1 ], split_y[ 1: ]): + for x1, x2 in zip(split_x[ :-1 ], split_x[ 1: ]): + cv2.polylines(img, [ get_poly(x1, x2, y1, y2) ], True, 0, 10, cv2.LINE_AA) plt.imshow(img) plt.show() @@ -185,22 +183,22 @@ def minus(i: int) -> int: raise RecognizeError(e) -def base(img: tp.Image, central: tp.Scope, draw: bool = False) -> dict[str, tp.Rectangle]: +def base(img: tp.Image, central: tp.Scope, draw: bool = False) -> dict[ str, tp.Rectangle ]: """ 基建布局的图像分割算法 """ try: ret = {} - x1, y1 = central[0] - x2, y2 = central[1] + x1, y1 = central[ 0 ] + x2, y2 = central[ 1 ] alpha = (y2 - y1) / 160 x1 -= 170 * alpha x2 += 182 * alpha y1 -= 67 * alpha y2 += 67 * alpha central = get_poly(x1, x2, y1, y2) - ret['central'] = central + ret[ 'central' ] = central for i in range(1, 5): y1 = y2 + 25 * alpha @@ -209,51 +207,51 @@ def base(img: tp.Image, central: tp.Scope, draw: bool = False) -> dict[str, tp.R dormitory = get_poly(x1, x2 - 158 * alpha, y1, y2) else: dormitory = get_poly(x1 + 158 * alpha, x2, y1, y2) - ret[f'dormitory_{i}'] = dormitory + ret[ f'dormitory_{i}' ] = dormitory - x1, y1 = ret['dormitory_1'][0] - x2, y2 = ret['dormitory_1'][2] + x1, y1 = ret[ 'dormitory_1' ][ 0 ] + x2, y2 = ret[ 'dormitory_1' ][ 2 ] x1 = x2 + 419 * alpha x2 = x1 + 297 * alpha factory = get_poly(x1, x2, y1, y2) - ret[f'factory'] = factory + ret[ f'factory' ] = factory y2 = y1 - 25 * alpha y1 = y2 - 134 * alpha meeting = get_poly(x1 - 158 * alpha, x2, y1, y2) - ret[f'meeting'] = meeting + ret[ f'meeting' ] = meeting y1 = y2 + 25 * alpha y2 = y1 + 134 * alpha y1 = y2 + 25 * alpha y2 = y1 + 134 * alpha contact = get_poly(x1, x2, y1, y2) - ret[f'contact'] = contact + ret[ f'contact' ] = contact y1 = y2 + 25 * alpha y2 = y1 + 134 * alpha train = get_poly(x1, x2, y1, y2) - ret[f'train'] = train + ret[ f'train' ] = train for floor in range(1, 4): - x1, y1 = ret[f'dormitory_{floor}'][0] - x2, y2 = ret[f'dormitory_{floor}'][2] + x1, y1 = ret[ f'dormitory_{floor}' ][ 0 ] + x2, y2 = ret[ f'dormitory_{floor}' ][ 2 ] x2 = x1 - 102 * alpha x1 = x2 - 295 * alpha if floor & 1 == 0: x2 = x1 - 24 * alpha x1 = x2 - 295 * alpha room = get_poly(x1, x2, y1, y2) - ret[f'room_{floor}_3'] = room + ret[ f'room_{floor}_3' ] = room x2 = x1 - 24 * alpha x1 = x2 - 295 * alpha room = get_poly(x1, x2, y1, y2) - ret[f'room_{floor}_2'] = room + ret[ f'room_{floor}_2' ] = room x2 = x1 - 24 * alpha x1 = x2 - 295 * alpha room = get_poly(x1, x2, y1, y2) - ret[f'room_{floor}_1'] = room + ret[ f'room_{floor}_1' ] = room if draw: polys = list(ret.values()) @@ -268,8 +266,7 @@ def base(img: tp.Image, central: tp.Scope, draw: bool = False) -> dict[str, tp.R logger.debug(traceback.format_exc()) raise RecognizeError(e) - -def worker(img: tp.Image, draw: bool = False) -> tuple[list[tp.Rectangle], tp.Rectangle, bool]: +def worker(img: tp.Image, draw: bool = False) -> tuple[ list[ tp.Rectangle ], tp.Rectangle, bool ]: """ 进驻总览的图像分割算法 """ @@ -277,23 +274,23 @@ def worker(img: tp.Image, draw: bool = False) -> tuple[list[tp.Rectangle], tp.Re height, width, _ = img.shape left, right = 0, width - while np.max(img[:, right-1]) < 100: + while np.max(img[ :, right - 1 ]) < 100: right -= 1 - while np.max(img[:, left]) < 100: + while np.max(img[ :, left ]) < 100: left += 1 - x0 = right-1 - while np.average(img[:, x0, 1]) >= 100: + x0 = right - 1 + while np.average(img[ :, x0, 1 ]) >= 100: x0 -= 1 x0 -= 2 - seg = [] + seg = [ ] remove_mode = False - pre, st = int(img[0, x0, 1]), 0 + pre, st = int(img[ 0, x0, 1 ]), 0 for y in range(1, height): - remove_mode |= int(img[y, x0, 0]) - int(img[y, x0, 1]) > 40 - if np.ptp(img[y, x0]) <= 1 or int(img[y, x0, 0]) - int(img[y, x0, 1]) > 40: - now = int(img[y, x0, 1]) + remove_mode |= int(img[ y, x0, 0 ]) - int(img[ y, x0, 1 ]) > 40 + if np.ptp(img[ y, x0 ]) <= 1 or int(img[ y, x0, 0 ]) - int(img[ y, x0, 1 ]) > 40: + now = int(img[ y, x0, 1 ]) if abs(now - pre) > 20: if now < pre and st == 0: st = y @@ -308,27 +305,27 @@ def worker(img: tp.Image, draw: bool = False) -> tuple[list[tp.Rectangle], tp.Re # seg.append((st, height)) logger.debug(f'segment.worker: seg {seg}') - remove_button = get_poly(x0-10, x0, seg[0][0], seg[0][1]) - seg = seg[1:] + remove_button = get_poly(x0 - 10, x0, seg[ 0 ][ 0 ], seg[ 0 ][ 1 ]) + seg = seg[ 1: ] for i in range(1, len(seg)): - if seg[i][1] - seg[i][0] > 9: + if seg[ i ][ 1 ] - seg[ i ][ 0 ] > 9: x1 = x0 - while img[seg[i][1]-3, x1-1, 2] < 100: + while img[ seg[ i ][ 1 ] - 3, x1 - 1, 2 ] < 100: x1 -= 1 break - ret = [] + ret = [ ] for i in range(len(seg)): - if seg[i][1] - seg[i][0] > 9: - ret.append(get_poly(x1, x0, seg[i][0], seg[i][1])) + if seg[ i ][ 1 ] - seg[ i ][ 0 ] > 9: + ret.append(get_poly(x1, x0, seg[ i ][ 0 ], seg[ i ][ 1 ])) if draw: cv2.polylines(img, ret, True, (255, 0, 0), 10, cv2.LINE_AA) plt.imshow(img) plt.show() - logger.debug(f'segment.worker: {[x.tolist() for x in ret]}') + logger.debug(f'segment.worker: {[ x.tolist() for x in ret ]}') return ret, remove_button, remove_mode except Exception as e: @@ -346,35 +343,38 @@ def agent(img, draw=False): left, right = 0, width # 异形屏适配 - while np.max(img[:, right-1]) < 100: + while np.max(img[ :, right - 1 ]) < 100: right -= 1 - while np.max(img[:, left]) < 100: + while np.max(img[ :, left ]) < 100: left += 1 # 去除左侧干员详情 x0 = left + 1 - while not (img[height-1, x0-1, 0] > img[height-1, x0, 0] + 10 and abs(int(img[height-1, x0, 0]) - int(img[height-1, x0+1, 0])) < 5): + while not (img[ height - 1, x0 - 1, 0 ] > img[ height - 1, x0, 0 ] + 10 and abs( + int(img[ height - 1, x0, 0 ]) - int(img[ height - 1, x0 + 1, 0 ])) < 5): x0 += 1 # ocr 初步识别干员名称 - ocr = ocrhandle.predict(img[:, x0:right]) + ocr = ocrhandle.predict(img[ :, x0:right ]) # 收集成功识别出来的干员名称识别结果,提取 y 范围,并将重叠的范围进行合并 - segs = [(min(x[2][0][1], x[2][1][1]), max(x[2][2][1], x[2][3][1])) - for x in ocr if x[1] in agent_list] + segs = [ (min(x[ 2 ][ 0 ][ 1 ], x[ 2 ][ 1 ][ 1 ]), max(x[ 2 ][ 2 ][ 1 ], x[ 2 ][ 3 ][ 1 ])) + for x in ocr if x[ 1 ] in agent_list ] while True: _a, _b = None, None for i in range(len(segs)): for j in range(len(segs)): - if i != j and (segs[i][0] <= segs[j][0] <= segs[i][1] or segs[i][0] <= segs[j][1] <= segs[i][1]): - _a, _b = segs[i], segs[j] + if i != j and ( + segs[ i ][ 0 ] <= segs[ j ][ 0 ] <= segs[ i ][ 1 ] or segs[ i ][ 0 ] <= segs[ j ][ 1 ] <= + segs[ i ][ 1 ]): + _a, _b = segs[ i ], segs[ j ] break if _b is not None: break if _b is not None: segs.remove(_a) segs.remove(_b) - segs.append((min(_a[0], _b[0]), max(_a[1], _b[1]))) + segs.append((min(_a[ 0 ], _b[ 0 ]), max(_a[ 1 ], _b[ 1 ]))) else: break segs = sorted(segs) @@ -382,41 +382,41 @@ def agent(img, draw=False): # 计算纵向的四个高度,[y0, y1] 是第一行干员名称的纵向坐标范围,[y2, y3] 是第二行干员名称的纵向坐标范围 y0 = y1 = y2 = y3 = None for x in segs: - if x[1] < height // 2: # FIXME 是否需要改成 x[0] + if x[ 1 ] < height // 2: y0, y1 = x else: y2, y3 = x if y0 is None or y2 is None: raise RecognizeError hpx = y1 - y0 # 卡片上干员名称的高度 - logger.debug((segs, [y0, y1, y2, y3])) + logger.debug((segs, [ y0, y1, y2, y3 ])) # 预计算:横向坐标范围集合 x_set = set() for x in ocr: - if x[1] in agent_list and (y0 <= x[2][0][1] <= y1 or y2 <= x[2][0][1] <= y3): + if x[ 1 ] in agent_list and (y0 <= x[ 2 ][ 0 ][ 1 ] <= y1 or y2 <= x[ 2 ][ 0 ][ 1 ] <= y3): # 只考虑矩形右边端点 - x_set.add(x[2][1][0]) - x_set.add(x[2][2][0]) + x_set.add(x[ 2 ][ 1 ][ 0 ]) + x_set.add(x[ 2 ][ 2 ][ 0 ]) x_set = sorted(x_set) logger.debug(x_set) # 排除掉一些重叠的范围,获得最终的横向坐标范围 gap = 160 * (resolution / 1080) # 卡片宽度下限 - x_set = [x_set[0]] + \ - [y for x, y in zip(x_set[:-1], x_set[1:]) if y - x > gap] - gap = [y - x for x, y in zip(x_set[:-1], x_set[1:])] + x_set = [ x_set[ 0 ] ] + \ + [ y for x, y in zip(x_set[ :-1 ], x_set[ 1: ]) if y - x > gap ] + gap = [ y - x for x, y in zip(x_set[ :-1 ], x_set[ 1: ]) ] logger.debug(sorted(gap)) gap = int(np.median(gap)) # 干员卡片宽度 - for x, y in zip(x_set[:-1], x_set[1:]): + for x, y in zip(x_set[ :-1 ], x_set[ 1: ]): if y - x > gap: gap_num = round((y - x) / gap) for i in range(1, gap_num): x_set.append(int(x + (y - x) / gap_num * i)) x_set = sorted(x_set) - if x_set[-1] - x_set[-2] < gap: + if x_set[ -1 ] - x_set[ -2 ] < gap: # 如果最后一个间隔不足宽度则丢弃,避免出现「梅尔」只露出一半识别成「梅」算作成功识别的情况 - x_set = x_set[:-1] + x_set = x_set[ :-1 ] while np.min(x_set) > 0: x_set.append(np.min(x_set) - gap) while np.max(x_set) < right - x0: @@ -425,11 +425,11 @@ def agent(img, draw=False): logger.debug(x_set) # 获得所有的干员名称对应位置 - ret = [] - for x1, x2 in zip(x_set[:-1], x_set[1:]): - if 0 <= x1+hpx and x0+x2+5 <= right: - ret += [get_poly(x0+x1+hpx, x0+x2+5, y0, y1), - get_poly(x0+x1+hpx, x0+x2+5, y2, y3)] + ret = [ ] + for x1, x2 in zip(x_set[ :-1 ], x_set[ 1: ]): + if 0 <= x1 + hpx and x0 + x2 + 5 <= right: + ret += [ get_poly(x0 + x1 + hpx, x0 + x2 + 5, y0, y1), + get_poly(x0 + x1 + hpx, x0 + x2 + 5, y2, y3) ] # draw for debug if draw: @@ -438,8 +438,8 @@ def agent(img, draw=False): plt.imshow(__img) plt.show() - logger.debug(f'segment.agent: {[x.tolist() for x in ret]}') - return ret + logger.debug(f'segment.agent: {[ x.tolist() for x in ret ]}') + return ret, ocr except Exception as e: logger.debug(traceback.format_exc()) @@ -456,34 +456,35 @@ def free_agent(img, draw=False): left, right = 0, width # 异形屏适配 - while np.max(img[:, right-1]) < 100: + while np.max(img[ :, right - 1 ]) < 100: right -= 1 - while np.max(img[:, left]) < 100: + while np.max(img[ :, left ]) < 100: left += 1 # 去除左侧干员详情 x0 = left + 1 - while not (img[height-1, x0-1, 0] > img[height-1, x0, 0] + 10 and abs(int(img[height-1, x0, 0]) - int(img[height-1, x0+1, 0])) < 5): + while not (img[ height - 1, x0 - 1, 0 ] > img[ height - 1, x0, 0 ] + 10 and abs( + int(img[ height - 1, x0, 0 ]) - int(img[ height - 1, x0 + 1, 0 ])) < 5): x0 += 1 # 获取分割结果 ret = agent(img, draw) - st = ret[-2][2] # 起点 - ed = ret[0][1] # 终点 + st = ret[ -2 ][ 2 ] # 起点 + ed = ret[ 0 ][ 1 ] # 终点 # 收集 y 坐标并初步筛选 y_set = set() - __ret = [] + __ret = [ ] for poly in ret: - __img = img[poly[0, 1]:poly[2, 1], poly[0, 0]:poly[2, 0]] - y_set.add(poly[0, 1]) - y_set.add(poly[2, 1]) + __img = img[ poly[ 0, 1 ]:poly[ 2, 1 ], poly[ 0, 0 ]:poly[ 2, 0 ] ] + y_set.add(poly[ 0, 1 ]) + y_set.add(poly[ 2, 1 ]) # 去除空白的干员框 if 80 <= np.min(__img): logger.debug(f'drop(empty): {poly.tolist()}') continue # 去除被选中的蓝框 - elif np.count_nonzero(__img[:, :, 0] >= 224) == 0 or np.count_nonzero(__img[:, :, 0] == 0) > 0: + elif np.count_nonzero(__img[ :, :, 0 ] >= 224) == 0 or np.count_nonzero(__img[ :, :, 0 ] == 0) > 0: logger.debug(f'drop(selected): {poly.tolist()}') continue __ret.append(poly) @@ -493,11 +494,11 @@ def free_agent(img, draw=False): y0 = height - y5 y3 = y0 - y2 + y5 - ret_free = [] + ret_free = [ ] for poly in ret: - poly[:, 1][poly[:, 1] == y1] = y0 - poly[:, 1][poly[:, 1] == y4] = y3 - __img = img[poly[0, 1]:poly[2, 1], poly[0, 0]:poly[2, 0]] + poly[ :, 1 ][ poly[ :, 1 ] == y1 ] = y0 + poly[ :, 1 ][ poly[ :, 1 ] == y4 ] = y3 + __img = img[ poly[ 0, 1 ]:poly[ 2, 1 ], poly[ 0, 0 ]:poly[ 2, 0 ] ] if not detector.is_on_shift(__img): ret_free.append(poly) @@ -507,7 +508,7 @@ def free_agent(img, draw=False): plt.imshow(__img) plt.show() - logger.debug(f'segment.free_agent: {[x.tolist() for x in ret_free]}') + logger.debug(f'segment.free_agent: {[ x.tolist() for x in ret_free ]}') return ret_free, st, ed except Exception as e: diff --git a/arknights_mower/utils/solver.py b/arknights_mower/utils/solver.py index d3346ca60..2ad143306 100644 --- a/arknights_mower/utils/solver.py +++ b/arknights_mower/utils/solver.py @@ -27,12 +27,14 @@ def __init__(self, device: Device = None, recog: Recognizer = None) -> None: self.recog = recog if recog is not None else Recognizer(self.device) self.device.check_current_focus() - def run(self) -> None: + def run(self)-> None: retry_times = config.MAX_RETRYTIME + result =None while retry_times > 0: try: - if self.transition(): - break + result = self.transition() + if result: + return result except RecognizeError as e: logger.warning(f'识别出了点小差错 qwq: {e}') self.recog.save_screencap('failure') @@ -90,8 +92,8 @@ def input(self, referent: str, input_area: tp.Scope, text: str = None) -> None: self.device.send_text(str(text)) self.device.tap((0, 0)) - def find(self, res: str, draw: bool = False, scope: tp.Scope = None, thres: int = None, judge: bool = True, strict: bool = False) -> tp.Scope: - return self.recog.find(res, draw, scope, thres, judge, strict) + def find(self, res: str, draw: bool = False, scope: tp.Scope = None, thres: int = None, judge: bool = True, strict: bool = False, score = 0.0) -> tp.Scope: + return self.recog.find(res, draw, scope, thres, judge, strict,score) def tap(self, poly: tp.Location, x_rate: float = 0.5, y_rate: float = 0.5, interval: float = 1, rebuild: bool = True) -> None: """ tap """ @@ -167,6 +169,10 @@ def scene(self) -> int: """ get the current scene in the game """ return self.recog.get_scene() + def get_infra_scene(self) -> int: + """ get the current scene in the infra """ + return self.recog.get_infra_scene() + def is_login(self): """ check if you are logged in """ return not (self.scene() // 100 == 1 or self.scene() // 100 == 99 or self.scene() == -1) @@ -255,6 +261,8 @@ def back_to_index(self): try: if self.get_navigation(): self.tap_element('nav_index') + elif self.scene() == Scene.CLOSE_MINE: + self.tap_element('close_mine') elif self.scene() == Scene.ANNOUNCEMENT: self.tap(detector.announcement_close(self.recog.img)) elif self.scene() == Scene.MATERIEL: @@ -304,3 +312,14 @@ def back_to_index(self): if self.scene() != Scene.INDEX: raise StrategyError + + def back_to_reclamation_algorithm(self): + self.recog.update() + while self.find('index_terminal') is None: + if self.scene() == Scene.UNKNOWN: + self.device.exit('com.hypergryph.arknights') + self.back_to_index() + logger.info('导航至生息演算') + self.tap_element('index_terminal', 0.5) + self.tap((self.recog.w*0.2, self.recog.h*0.8),interval=0.5) + diff --git a/diy.py b/diy.py index 4cd978be7..82f958495 100644 --- a/diy.py +++ b/diy.py @@ -1,111 +1,206 @@ import time -import schedule +from datetime import datetime + +from arknights_mower.solvers.base_schedule import BaseSchedulerSolver from arknights_mower.strategy import Solver +from arknights_mower.utils.device import Device from arknights_mower.utils.log import logger, init_fhlr from arknights_mower.utils import config -# 指定无人机加速第三层第三个房间的制造或贸易订单 -drone_room = 'room_3_3' +email_config= { + # 发信账户 + 'account':"xxx@qq.com", + # 在QQ邮箱“帐户设置-账户-开启SMTP服务”中,按照指示开启服务获得授权码 + 'pass_code':'xxx', + # 收件人邮箱 + 'receipts':['任何邮箱'], + # 是否启用邮件提醒 + 'mail_enable':False, + # 邮件主题 + 'subject': '任务数据' +} +maa_config = { + # 请设置为存放 dll 文件及资源的路径 + "maa_path":'F:\\MAA-v4.10.5-win-x64', + # 请设置为存放 dll 文件及资源的路径 + "maa_adb_path":"D:\\Program Files\\Nox\\bin\\adb.exe", + # adb 地址 + "maa_adb":['127.0.0.1:62001'], + # maa 运行的时间间隔,以小时计 + "maa_execution_gap":4, + # 以下配置,第一个设置为true的首先生效 + # 是否启动肉鸽 + "roguelike":False, + # 是否启动生息演算 + "reclamation_algorithm":False, + # 是否启动保全派驻 + "stationary_security_service":False, + "last_execution": None, + "weekly_plan":[{"weekday":"周一","stage":['AP-5'],"medicine":0}, + {"weekday":"周二","stage":['CE-6'],"medicine":0}, + {"weekday":"周三","stage":['1-7'],"medicine":0}, + {"weekday":"周四","stage":['AP-5'],"medicine":0}, + {"weekday":"周五","stage":['1-7'],"medicine":0}, + {"weekday":"周六","stage":['AP-5'],"medicine":0}, + {"weekday":"周日","stage":['AP-5'],"medicine":0}] +} -# 指定使用菲亚梅塔恢复第一层第二个房间心情最差的干员的心情 -# 恢复后回到原工作岗位,工作顺序不变,以保证最大效率 -fia_room = 'room_1_2' +# Free (宿舍填充)干员安排黑名单 +free_blacklist= [] -# 指定关卡序列的作战计划 -ope_lists = [['AP-5', 1], ['1-7', -1]] +# 干员宿舍回复阈值 + # 高效组心情低于 UpperLimit * 阈值 (向下取整)的时候才会会安排休息 + # UpperLimit:默认24,特殊技能干员如夕,令可能会有所不同(设置在 agent-base.json 文件可以自行更改) +resting_treshhold = 0.5 -# 使用信用点购买东西的优先级(从高到低) -shop_priority = ['招聘许可', '赤金', '龙门币', '初级作战记录', '技巧概要·卷2', '基础作战记录', '技巧概要·卷1'] +# 跑单如果all in 贸易站则 不需要修改设置 +# 如果需要无人机加速其他房间则可以修改成房间名字如 'room_1_1' +drone_room = None +# 无人机执行间隔时间 (小时) +drone_execution_gap = 4 -# 公招选取标签时优先选择的干员的优先级(从高到低) -recruit_priority = ['因陀罗', '火神'] +# 全自动基建排班计划: +# 这里定义了一套全自动基建的排班计划 plan_1 +# agent 为常驻高效组的干员名 -# 自定义基建排班 -# 这里自定义了一套排班策略,实现的是两班倒,分为四个阶段 -# 阶段 1 和 2 为第一班,阶段 3 和 4 为第二班 -# 第一班的干员在阶段 3 和 4 分两批休息,第二班同理 -# 每个阶段耗时 6 小时 +# group 为干员编队,你希望任何编队的人一起上下班则给他们编一样的名字 + # 编队最大数不支持超过4个干员 否则可能会在计算自动排班的时候报错 +# replacement 为替换组干员备选 + # 暖机干员的自动换班 + # 目前只支持一个暖机干员休息 + # !! 会吧其他正在休息的暖机干员赶出宿舍 + # 请尽量安排多的替换干员,且尽量不同干员的替换人员不冲突 + # 龙舌兰和但书默认为插拔干员 必须放在 replacement的第一位 + # 请把你所安排的替换组 写入replacement 否则程序可能报错 + # 替换组会按照从左到右的优先级选择可以编排的干员 + # 宿舍常驻干员不会被替换所以不需要设置替换组 + # 宿舍空余位置请编写为Free,请至少安排一个群补和一个单补 以达到最大恢复效率 + # 宿管必须安排靠左,后面为填充干员 + # 宿舍恢复速率务必1-4从高到低排列 + # 如果有菲亚梅塔则需要安排replacement 建议干员至少为三 + # 菲亚梅塔会从replacment里找最低心情的进行充能 plan = { # 阶段 1 - 'plan_1': { - # 控制中枢 - 'contral': ['夕', '令', '凯尔希', '阿米娅', '玛恩纳'], - # 办公室 - 'contact': ['艾雅法拉'], + "default": "plan_1", + "plan_1": { + # 中枢 + 'central': [{'agent': '焰尾', 'group': '红松骑士', 'replacement': ["凯尔希","诗怀雅"]}, + {'agent': '琴柳', 'group': '', 'replacement': ["凯尔希","阿米娅"]}, + {'agent': '重岳', 'group': '夕', 'replacement': ["玛恩纳", "清道夫", "凯尔希", "阿米娅", '坚雷']}, + {'agent': '夕', 'group': '夕', 'replacement': ["玛恩纳", "清道夫", "凯尔希", "阿米娅", '坚雷']}, + {'agent': '令', 'group': '夕', 'replacement': ["玛恩纳", "清道夫", "凯尔希", "阿米娅", '坚雷']}, + ], + 'contact': [{'agent': '絮雨', 'group': '絮雨', 'replacement': []}], # 宿舍 - 'dormitory_1': ['杜林', '闪灵', '安比尔', '空弦', '缠丸'], - 'dormitory_2': ['推进之王', '琴柳', '赫默', '杰西卡', '调香师'], - 'dormitory_3': ['夜莺', '波登可', '夜刀', '古米', '空爆'], - 'dormitory_4': ['空', 'Lancet-2', '香草', '史都华德', '刻俄柏'], + 'dormitory_1': [{'agent': '流明', 'group': '', 'replacement': []}, + {'agent': '闪灵', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []} + ], + 'dormitory_2': [{'agent': '杜林', 'group': '', 'replacement': []}, + {'agent': '蜜莓', 'group': '', 'replacement': []}, + {'agent': '褐果', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []} + ], + 'dormitory_3': [{'agent': '车尔尼', 'group': '', 'replacement': []}, + {'agent': '斥罪', 'group': '', 'replacement': []}, + {'agent': '爱丽丝', 'group': '', 'replacement': []}, + {'agent': '桃金娘', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []} + ], + 'dormitory_4': [{'agent': '波登可', 'group': '', 'replacement': []}, + {'agent': '夜莺', 'group': '', 'replacement': []}, + {'agent': '菲亚梅塔', 'group': '', 'replacement': ['迷迭香', '黑键', '絮雨','至简']}, + {'agent': 'Free', 'group': '', 'replacement': []}, + {'agent': 'Free', 'group': '', 'replacement': []}], + 'factory':[{'agent': '年', 'replacement': ['九色鹿','芳汀'], 'group': '夕'}], # 会客室 - 'meeting': ['陈', '红'], - # 制造站 + 贸易站 + 发电站 - 'room_1_1': ['德克萨斯', '能天使', '拉普兰德'], - 'room_1_2': ['断罪者', '食铁兽', '槐琥'], - 'room_1_3': ['阿消'], - 'room_2_1': ['巫恋', '柏喙', '慕斯'], - 'room_2_2': ['红豆', '霜叶', '白雪'], - 'room_2_3': ['雷蛇'], - 'room_3_1': ['Castle-3', '梅尔', '白面鸮'], - 'room_3_2': ['格雷伊'], - 'room_3_3': ['砾', '夜烟', '斑点'] - }, - # 阶段 2 - 'plan_2': { - # 注释掉了部分和阶段 1 一样排班计划的房间,加快排班速度 - # 'contact': ['艾雅法拉'], - 'dormitory_1': ['杜林', '闪灵', '芬', '稀音', '克洛丝'], - 'dormitory_2': ['推进之王', '琴柳', '清流', '森蚺', '温蒂'], - 'dormitory_3': ['夜莺', '波登可', '伊芙利特', '深靛', '炎熔'], - 'dormitory_4': ['空', 'Lancet-2', '远山', '星极', '普罗旺斯'], - # 'meeting': ['陈', '红'], - # 'room_1_1': ['德克萨斯', '能天使', '拉普兰德'], - # 'room_1_2': ['断罪者', '食铁兽', '槐琥'], - # 'room_1_3': ['阿消'], - # 'room_2_1': ['巫恋', '柏喙', '慕斯'], - # 'room_2_2': ['红豆', '霜叶', '白雪'], - # 'room_2_3': ['雷蛇'], - # 'room_3_1': ['Castle-3', '梅尔', '白面鸮'], - # 'room_3_2': ['格雷伊'], - # 'room_3_3': ['砾', '夜烟', '斑点'] - }, - 'plan_3': { - 'contact': ['普罗旺斯'], - 'dormitory_1': ['杜林', '闪灵', '格雷伊', '雷蛇', '阿消'], - 'dormitory_2': ['推进之王', '琴柳', '德克萨斯', '能天使', '拉普兰德'], - 'dormitory_3': ['夜莺', '波登可', '巫恋', '柏喙', '慕斯'], - 'dormitory_4': ['空', 'Lancet-2', '艾雅法拉', '陈', '红'], - 'meeting': ['远山', '星极'], - 'room_1_1': ['安比尔', '空弦', '缠丸'], - 'room_1_2': ['赫默', '杰西卡', '调香师'], - 'room_1_3': ['伊芙利特'], - 'room_2_1': ['夜刀', '古米', '空爆'], - 'room_2_2': ['香草', '史都华德', '刻俄柏'], - 'room_2_3': ['深靛'], - 'room_3_1': ['芬', '稀音', '克洛丝'], - 'room_3_2': ['炎熔'], - 'room_3_3': ['清流', '森蚺', '温蒂'] - }, - 'plan_4': { - # 'contact': ['普罗旺斯'], - 'dormitory_1': ['杜林', '闪灵', '断罪者', '食铁兽', '槐琥'], - 'dormitory_2': ['推进之王', '琴柳', '红豆', '霜叶', '白雪'], - 'dormitory_3': ['夜莺', '波登可', 'Castle-3', '梅尔', '白面鸮'], - 'dormitory_4': ['空', 'Lancet-2', '砾', '夜烟', '斑点'], - # 'meeting': ['远山', '星极'], - # 'room_1_1': ['安比尔', '空弦', '缠丸'], - # 'room_1_2': ['赫默', '杰西卡', '调香师'], - # 'room_1_3': ['伊芙利特'], - # 'room_2_1': ['夜刀', '古米', '空爆'], - # 'room_2_2': ['香草', '史都华德', '刻俄柏'], - # 'room_2_3': ['深靛'], - # 'room_3_1': ['芬', '稀音', '克洛丝'], - # 'room_3_2': ['炎熔'], - # 'room_3_3': ['清流', '森蚺', '温蒂'] + 'meeting': [{'agent': '陈', 'replacement': ['星极','远山'], 'group': ''}, + {'agent': '红', 'replacement': ['远山','星极'], 'group': ''} ], + 'room_1_1': [{'agent': '黑键', 'group': '', 'replacement': []}, + {'agent': '乌有', 'group': '夕', 'replacement': ['但书','图耶']}, + {'agent': '空弦', 'group': '夕', 'replacement': ['龙舌兰', '鸿雪']} + # {'agent': '伺夜', 'group': '图耶', 'replacement': ['但书','能天使']}, + # {'agent': '空弦', 'group': '图耶', 'replacement': ['龙舌兰', '雪雉']} + ], + 'room_1_2': [{'agent': '迷迭香', 'group': '', 'replacement': []}, + {'agent': '砾', 'group': '', 'Type': '', 'replacement': ['斑点','夜烟']}, + {'agent': '至简', 'group': '', 'replacement': []}], + 'room_1_3': [{'agent': '承曦格雷伊', 'group': '异客', 'replacement': ['炎狱炎熔','格雷伊']}], + 'room_2_2': [{'agent': '温蒂', 'group': '异客', 'replacement': ['火神']}, + # {'agent': '异客', 'group': '异客', 'Type': '', 'replacement': ['贝娜']}, + {'agent': '异客', 'group': '异客', 'Type': '', 'replacement': ['贝娜']}, + {'agent': '森蚺', 'group': '异客', 'replacement': ['泡泡']}], + 'room_3_1': [{'agent': '稀音', 'group': '稀音', 'replacement': ['贝娜']}, + {'agent': '帕拉斯', 'group': '稀音', 'Type': '', 'replacement': ['泡泡']}, + {'agent': '红云', 'group': '稀音', 'replacement': ['火神']}], + 'room_2_3': [{'agent': '澄闪', 'group': '', 'replacement': ['炎狱炎熔', '格雷伊']}], + 'room_2_1': [{'agent': '食铁兽', 'group': '食铁兽', 'replacement': ['泡泡']}, + {'agent': '断罪者', 'group': '食铁兽', 'replacement': ['火神']}, + {'agent': '槐琥', 'group': '食铁兽', 'replacement': ['贝娜']}], + 'room_3_2': [{'agent': '灰毫', 'group': '红松骑士', 'replacement': ['贝娜']}, + {'agent': '远牙', 'group': '红松骑士', 'Type': '', 'replacement': ['泡泡']}, + {'agent': '野鬃', 'group': '红松骑士', 'replacement': ['火神']}], + 'room_3_3': [{'agent': '雷蛇', 'group': '', 'replacement': ['炎狱炎熔','格雷伊']}] } } +# UpperLimit、LowerLimit:心情上下限 +# ExhaustRequire:是否强制工作到红脸再休息 +# ArrangeOrder:指定在宿舍外寻找干员的方式 +# RestInFull:是否强制休息到24心情再工作,与ExhaustRequire一起帮助暖机类技能工作更长时间 +# RestingPriority:休息优先级,低优先级不会使用单回技能。 + +agent_base_config = { + "Default":{"UpperLimit": 24,"LowerLimit": 0,"ExhaustRequire": False,"ArrangeOrder":[2,"false"],"RestInFull": False}, + # 卡贸易站 + "令":{"UpperLimit": 11,"LowerLimit": 13,"ArrangeOrder":[2,"true"]}, + "夕": {"UpperLimit": 11,"ArrangeOrder":[2,"true"]}, + # 卡制造站 + #"令": {"UpperLimit": 11, "LowerLimit": 13, "ArrangeOrder": [2, "true"]}, + #"夕": {"UpperLimit": 11, "ArrangeOrder": [2, "true"]}, + "稀音":{"ExhaustRequire": True,"ArrangeOrder":[2,"true"],"RestInFull": True}, + "巫恋":{"ArrangeOrder":[2,"true"]}, + "柏喙":{"ExhaustRequire": True,"ArrangeOrder":[2,"true"]}, + "龙舌兰":{"ArrangeOrder":[2,"true"]}, + "空弦":{"ArrangeOrder":[2,"true"],"RestingPriority": "low"}, + "伺夜":{"ArrangeOrder":[2,"true"]}, + "绮良":{"ArrangeOrder":[2,"true"]}, + "但书":{"ArrangeOrder":[2,"true"]}, + "泡泡":{"ArrangeOrder":[2,"true"]}, + "火神":{"ArrangeOrder":[2,"true"]}, + "黑键":{"ArrangeOrder":[2,"true"]}, + "波登可":{"ArrangeOrder":[ 2, "false" ]}, + "夜莺":{"ArrangeOrder":[ 2, "false" ]}, + "菲亚梅塔":{"ArrangeOrder":[ 2, "false" ]}, + "流明":{"ArrangeOrder":[ 2, "false" ]}, + "蜜莓":{"ArrangeOrder":[ 2, "false" ]}, + "闪灵":{"ArrangeOrder":[ 2, "false" ]}, + "杜林":{"ArrangeOrder":[ 2, "false" ]}, + "褐果":{"ArrangeOrder":[ 2, "false" ]}, + "车尔尼":{"ArrangeOrder":[ 2, "false" ]}, + "安比尔":{"ArrangeOrder":[ 2, "false" ]}, + "爱丽丝":{"ArrangeOrder":[ 2, "false" ]}, + "桃金娘":{"ArrangeOrder":[ 2, "false" ]}, + "帕拉斯": {"RestingPriority": "low"}, + "红云": {"RestingPriority": "low","ArrangeOrder":[2,"true"]}, + "承曦格雷伊": {"ArrangeOrder":[2,"true"]}, + "乌有":{"ArrangeOrder":[2,"true"],"RestingPriority": "low"}, + "图耶":{"ArrangeOrder":[2,"true"]}, + "鸿雪": {"ArrangeOrder":[2,"true"]}, + "孑":{"ArrangeOrder":[2,"true"]}, + "清道夫":{"ArrangeOrder":[2,"true"]}, + "临光":{"ArrangeOrder":[2,"true"]}, + "杜宾":{"ArrangeOrder":[2,"true"]}, + "焰尾":{"RestInFull": True}, + "重岳":{"ArrangeOrder":[2,"true"]}, + "坚雷":{"ArrangeOrder":[2,"true"]}, + "年":{"RestingPriority": "low"} +} + def debuglog(): ''' @@ -121,37 +216,102 @@ def savelog(): ''' config.LOGFILE_PATH = './log' config.SCREENSHOT_PATH = './screenshot' - config.SCREENSHOT_MAXNUM = 100 + config.SCREENSHOT_MAXNUM = 1000 + config.ADB_DEVICE = maa_config['maa_adb'] + config.ADB_CONNECT = maa_config['maa_adb'] + config.MAX_RETRYTIME = 10 + config.PASSWORD = '你的密码' + config.APPNAME = 'com.hypergryph.arknights' # 官服 + # com.hypergryph.arknights.bilibili # Bilibili 服 init_fhlr() +def inialize(tasks, scheduler=None): + device = Device() + cli = Solver(device) + if scheduler is None: + base_scheduler = BaseSchedulerSolver(cli.device, cli.recog) + base_scheduler.package_name = config.APPNAME + base_scheduler.operators = {} + base_scheduler.global_plan = plan + base_scheduler.current_base = {} + base_scheduler.resting = [] + # 同时休息最大人数 + base_scheduler.max_resting_count = 4 + base_scheduler.tasks = tasks + # 读取心情开关,有菲亚梅塔或者希望全自动换班得设置为 true + base_scheduler.read_mood = True + base_scheduler.scan_time = {} + base_scheduler.last_room = '' + base_scheduler.free_blacklist = free_blacklist + base_scheduler.resting_treshhold = resting_treshhold + base_scheduler.MAA = None + base_scheduler.email_config = email_config + base_scheduler.ADB_CONNECT = config.ADB_CONNECT[0] + base_scheduler.maa_config = maa_config + base_scheduler.error = False + base_scheduler.drone_count_limit = 92 # 无人机高于于该值时才使用 + base_scheduler.drone_room = drone_room + base_scheduler.drone_execution_gap = drone_execution_gap + base_scheduler.agent_base_config = agent_base_config + base_scheduler.run_order_delay = 10 # 跑单提前10分钟运行 + return base_scheduler + else: + scheduler.device = cli.device + scheduler.recog = cli.recog + scheduler.handle_error(True) + return scheduler + def simulate(): ''' 具体调用方法可见各个函数的参数说明 ''' - global ope_lists - cli = Solver() - cli.mail() # 邮件 - cli.base(clue_collect=True, drone_room=drone_room, fia_room=fia_room, arrange=plan) # 基建 - cli.credit() # 信用 - ope_lists = cli.ope(eliminate=True, plan=ope_lists) # 行动,返回未完成的作战计划 - cli.shop(shop_priority) # 商店 - cli.recruit() # 公招 - cli.mission() # 任务 - - -def schedule_task(): - """ - 定期运行任务 - """ - schedule.every().day.at('07:00').do(simulate) - schedule.every().day.at('19:00').do(simulate) + global ope_list + # 第一次执行任务 + # tasks = [{"plan": {'room_1_1': ['能天使','但书','龙舌兰']}, "time": datetime.now()}] + tasks =[] + reconnect_max_tries = 10 + reconnect_tries = 0 + base_scheduler = inialize(tasks) + while True: - schedule.run_pending() - time.sleep(60) + try: + if len(base_scheduler.tasks) > 0: + (base_scheduler.tasks.sort(key=lambda x: x["time"], reverse=False)) + sleep_time = (base_scheduler.tasks[0]["time"] - datetime.now()).total_seconds() + logger.info(base_scheduler.tasks) + base_scheduler.send_email(base_scheduler.tasks) + # 如果任务间隔时间超过9分钟则启动MAA + if sleep_time > 540: + base_scheduler.maa_plan_solver() + elif sleep_time > 0 : time.sleep(sleep_time) + base_scheduler.run() + reconnect_tries = 0 + except ConnectionError as e: + reconnect_tries +=1 + if reconnect_tries < reconnect_max_tries: + logger.warning(f'连接端口断开....正在重连....') + connected = False + while not connected: + try: + base_scheduler = inialize([],base_scheduler) + break + except Exception as ce: + logger.error(ce) + time.sleep(5) + continue + continue + else: + raise Exception(e) + except Exception as E: + logger.exception(f"程序出错--->{E}") + # cli.credit() # 信用 + # ope_lists = cli.ope(eliminate=True, plan=ope_lists) # 行动,返回未完成的作战计划 + # cli.shop(shop_priority) # 商店 + # cli.recruit() # 公招 + # cli.mission() # 任务 -debuglog() +# debuglog() savelog() simulate() -schedule_task() diff --git a/main.spec b/main.spec index da6c8b840..f4f43f59d 100644 --- a/main.spec +++ b/main.spec @@ -15,6 +15,7 @@ a = Analysis( ('arknights_mower/resources', 'arknights_mower/__init__/resources'), ('arknights_mower/data', 'arknights_mower/__init__/data'), ('arknights_mower/vendor', 'arknights_mower/__init__/vendor'), + ('arknights_mower/paddleocr', '.'), ('venv64/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_shared.dll', 'onnxruntime/capi/'), ('venv64/Lib/site-packages/shapely/DLLs/geos.dll', '.'), ('venv64/Lib/site-packages/shapely/DLLs/geos_c.dll', '.') diff --git a/menu.py b/menu.py new file mode 100644 index 000000000..7cf49559b --- /dev/null +++ b/menu.py @@ -0,0 +1,482 @@ +import importlib +import json +import sys +from multiprocessing import Pipe, Process, freeze_support +import time +import PySimpleGUI as sg +import os +from ruamel.yaml import YAML +from arknights_mower.utils.log import logger +from arknights_mower.data import agent_list +from arknights_mower.__main__ import main + +yaml = YAML() +confUrl = './conf.yml'; +conf = {} +plan = {} +global window +buffer = '' +line = 0 +half_line_index = 0 + + +# 读取写入配置文件 +def load_conf(): + global conf + global confUrl + if not os.path.isfile(confUrl): + open(confUrl, 'w') # 创建空配置文件 + else: + with open(confUrl, 'r', encoding='utf8') as c: + conf = yaml.load(c) + if conf is None: + conf = {} + conf['package_type'] = conf['package_type'] if 'package_type' in conf.keys() else 1 + conf['adb'] = conf['adb'] if 'adb' in conf.keys() else '' + conf['planFile'] = conf['planFile'] if 'planFile' in conf.keys() else './plan.json' # 默认排班表地址 + conf['free_blacklist'] = conf['free_blacklist'] if 'free_blacklist' in conf.keys() else '' + conf['run_mode'] = conf['run_mode'] if 'run_mode' in conf.keys() else 1 + conf['ling_xi'] = conf['ling_xi'] if 'ling_xi' in conf.keys() else 1 + conf['rest_in_full'] = conf['rest_in_full'] if 'rest_in_full' in conf.keys() else '' + conf['mail_enable'] = conf['mail_enable'] if 'mail_enable' in conf.keys() else 0 + conf['account'] = conf['account'] if 'account' in conf.keys() else '' + conf['pass_code'] = conf['pass_code'] if 'pass_code' in conf.keys() else '' + conf['maa_enable'] = conf['maa_enable'] if 'maa_enable' in conf.keys() else 0 + conf['maa_path'] = conf['maa_path'] if 'maa_path' in conf.keys() else '' + conf['maa_adb_path'] = conf['maa_adb_path'] if 'maa_adb_path' in conf.keys() else '' + conf['maa_weekly_plan'] = conf['maa_weekly_plan'] if 'maa_weekly_plan' in conf.keys() else [ + {"weekday": "周一", "stage": ['AP-5'], "medicine": 0}, + {"weekday": "周二", "stage": ['CE-6'], "medicine": 0}, + {"weekday": "周三", "stage": ['1-7'], "medicine": 0}, + {"weekday": "周四", "stage": ['AP-5'], "medicine": 0}, + {"weekday": "周五", "stage": ['1-7'], "medicine": 0}, + {"weekday": "周六", "stage": ['AP-5'], "medicine": 0}, + {"weekday": "周日", "stage": ['AP-5'], "medicine": 0} + ] + conf['max_resting_count'] = conf['max_resting_count'] if 'max_resting_count' in conf.keys() else 4 # 最大组人数 + conf['drone_count_limit'] = conf['drone_count_limit'] if 'drone_count_limit' in conf.keys() else 92 # 无人机阈值 + conf['run_order_delay'] = conf['run_order_delay'] if 'run_order_delay' in conf.keys() else 10 # 跑单提前10分钟运行 + conf['drone_room'] = conf['drone_room'] if 'drone_room' in conf.keys() else '' # 无人机使用房间 + + +def write_conf(): + global conf + global confUrl + with open(confUrl, 'w', encoding='utf8') as c: + yaml.default_flow_style = False + yaml.dump(conf, c) + + +# 读取排班表 +def load_plan(url): + global plan + if not os.path.isfile(url): + with open(url, 'w') as f: + json.dump(plan, f) # 创建空json文件 + return + try: + with open(url, 'r', encoding='utf8') as fp: + plan = json.loads(fp.read()) + conf['planFile'] = url + for i in range(1, 4): + for j in range(1, 4): + window[f'btn_room_{str(i)}_{str(j)}'].update('待建造', button_color=('white', '#4f4945')) + for key in plan: + if type(plan[key]).__name__ == 'list': # 兼容旧版格式 + plan[key] = {'plans': plan[key], 'name': ''} + elif plan[key]['name'] == '贸易站': + window['btn_' + key].update('贸易站', button_color=('#4f4945', '#33ccff')) + elif plan[key]['name'] == '制造站': + window['btn_' + key].update('制造站', button_color=('#4f4945', '#ffcc00')) + elif plan[key]['name'] == '发电站': + window['btn_' + key].update('发电站', button_color=('#4f4945', '#ccff66')) + except Exception as e: + logger.error(e) + println('json格式错误!') + + +# 写入排班表 +def write_plan(): + with open(conf['planFile'], 'w', encoding='utf8') as c: + json.dump(plan, c, ensure_ascii=False) + + +# 主页面 +def menu(): + global window + global buffer + load_conf() + sg.theme('LightBlue2') + # --------主页 + package_type_title = sg.Text('服务器:', size=10) + package_type_1 = sg.Radio('官服', 'package_type', default=conf['package_type'] == 1, + key='radio_package_type_1', enable_events=True) + package_type_2 = sg.Radio('BiliBili服', 'package_type', default=conf['package_type'] == 2, + key='radio_package_type_2', enable_events=True) + adb_title = sg.Text('adb连接地址:', size=10) + adb = sg.InputText(conf['adb'], size=60, key='conf_adb', enable_events=True) + # 黑名单 + free_blacklist_title = sg.Text('宿舍黑名单:', size=10) + free_blacklist = sg.InputText(conf['free_blacklist'], size=60, key='conf_free_blacklist', enable_events=True) + # 排班表json + plan_title = sg.Text('排班表:', size=10) + plan_file = sg.InputText(conf['planFile'], readonly=True, size=60, key='planFile', enable_events=True) + plan_select = sg.FileBrowse('...', size=(3, 1), file_types=(("JSON files", "*.json"),)) + # 总开关 + on_btn = sg.Button('开始执行', key='on') + off_btn = sg.Button('立即停止', key='off', visible=False, button_color='red') + # 日志栏 + output = sg.Output(size=(150, 25), key='log', text_color='#808069', font=('微软雅黑', 9)) + + # --------排班表设置页面 + # 宿舍区 + central = sg.Button('控制中枢', key='btn_central', size=(18, 3), button_color='#303030') + dormitory_1 = sg.Button('宿舍', key='btn_dormitory_1', size=(18, 2), button_color='#303030') + dormitory_2 = sg.Button('宿舍', key='btn_dormitory_2', size=(18, 2), button_color='#303030') + dormitory_3 = sg.Button('宿舍', key='btn_dormitory_3', size=(18, 2), button_color='#303030') + dormitory_4 = sg.Button('宿舍', key='btn_dormitory_4', size=(18, 2), button_color='#303030') + central_area = sg.Column([[central], [dormitory_1], [dormitory_2], [dormitory_3], [dormitory_4]]) + # 制造站区 + room_1_1 = sg.Button('待建造', key='btn_room_1_1', size=(12, 2), button_color='#4f4945') + room_1_2 = sg.Button('待建造', key='btn_room_1_2', size=(12, 2), button_color='#4f4945') + room_1_3 = sg.Button('待建造', key='btn_room_1_3', size=(12, 2), button_color='#4f4945') + room_2_1 = sg.Button('待建造', key='btn_room_2_1', size=(12, 2), button_color='#4f4945') + room_2_2 = sg.Button('待建造', key='btn_room_2_2', size=(12, 2), button_color='#4f4945') + room_2_3 = sg.Button('待建造', key='btn_room_2_3', size=(12, 2), button_color='#4f4945') + room_3_1 = sg.Button('待建造', key='btn_room_3_1', size=(12, 2), button_color='#4f4945') + room_3_2 = sg.Button('待建造', key='btn_room_3_2', size=(12, 2), button_color='#4f4945') + room_3_3 = sg.Button('待建造', key='btn_room_3_3', size=(12, 2), button_color='#4f4945') + left_area = sg.Column([[room_1_1, room_1_2, room_1_3], + [room_2_1, room_2_2, room_2_3], + [room_3_1, room_3_2, room_3_3]]) + # 功能区 + meeting = sg.Button('会客室', key='btn_meeting', size=(24, 2), button_color='#303030') + factory = sg.Button('加工站', key='btn_factory', size=(24, 2), button_color='#303030') + contact = sg.Button('办公室', key='btn_contact', size=(24, 2), button_color='#303030') + right_area = sg.Column([[meeting], [factory], [contact]]) + + setting_layout = [ + [sg.Column([[sg.Text('设施类别:'), sg.InputCombo(['贸易站', '制造站', '发电站'], size=12, key='station_type')]], + key='station_type_col', visible=False)]] + # 排班表设置标签 + for i in range(1, 6): + set_area = sg.Column([[sg.Text('干员:'), + sg.InputCombo(['Free'] + agent_list, size=20, key='agent' + str(i)), + sg.Text('组:'), + sg.InputText('', size=15, key='group' + str(i)), + sg.Text('替换:'), + sg.InputText('', size=30, key='replacement' + str(i)) + ]], key='setArea' + str(i), visible=False) + setting_layout.append([set_area]) + setting_layout.append([sg.Button('保存', key='savePlan', visible=False),sg.Button('清空', key='clearPlan', visible=False)]) + setting_area = sg.Column(setting_layout, element_justification="center", + vertical_alignment="bottom", + expand_x=True) + + # --------高级设置页面 + run_mode_title = sg.Text('运行模式:', size=25) + run_mode_1 = sg.Radio('换班模式', 'run_mode', default=conf['run_mode'] == 1, + key='radio_run_mode_1', enable_events=True) + run_mode_2 = sg.Radio('仅跑单模式', 'run_mode', default=conf['run_mode'] == 2, + key='radio_run_mode_2', enable_events=True) + ling_xi_title = sg.Text('令夕模式(令夕上班时起作用):', size=25) + ling_xi_1 = sg.Radio('感知信息', 'ling_xi', default=conf['ling_xi'] == 1, + key='radio_ling_xi_1', enable_events=True) + ling_xi_2 = sg.Radio('人间烟火', 'ling_xi', default=conf['ling_xi'] == 2, + key='radio_ling_xi_2', enable_events=True) + ling_xi_3 = sg.Radio('均衡模式', 'ling_xi', default=conf['ling_xi'] == 3, + key='radio_ling_xi_3', enable_events=True) + + max_resting_count_title = sg.Text('最大组人数:', size=25, key='max_resting_count_title') + max_resting_count = sg.InputText(conf['max_resting_count'], size=5, + key='int_max_resting_count', enable_events=True) + drone_count_limit_title = sg.Text('无人机使用阈值:', size=25, key='drone_count_limit_title') + drone_count_limit = sg.InputText(conf['drone_count_limit'], size=5, + key='int_drone_count_limit', enable_events=True) + run_order_delay_title = sg.Text('跑单前置延时(分钟):', size=25, key='run_order_delay_title') + run_order_delay = sg.InputText(conf['run_order_delay'], size=5, + key='int_run_order_delay', enable_events=True) + drone_room_title = sg.Text('无人机使用房间(room_X_X):', size=25, key='drone_room_title') + drone_room = sg.InputText(conf['drone_room'], size=15, + key='conf_drone_room', enable_events=True) + rest_in_full_title = sg.Text('需要回满心情的干员:', size=25) + rest_in_full = sg.InputText(conf['rest_in_full'], size=60, + key='conf_rest_in_full', enable_events=True) + + # --------外部调用设置页面 + # mail + mail_enable_1 = sg.Radio('启用', 'mail_enable', default=conf['mail_enable'] == 1, + key='radio_mail_enable_1', enable_events=True) + mail_enable_0 = sg.Radio('禁用', 'mail_enable', default=conf['mail_enable'] == 0, + key='radio_mail_enable_0', enable_events=True) + account_title = sg.Text('QQ邮箱', size=25) + account = sg.InputText(conf['account'], size=60, key='conf_account', enable_events=True) + pass_code_title = sg.Text('授权码', size=25) + pass_code = sg.Input(conf['pass_code'], size=60, key='conf_pass_code', enable_events=True, password_char='*') + mail_frame = sg.Frame('邮件提醒', + [[mail_enable_1, mail_enable_0], [account_title, account], [pass_code_title, pass_code]]) + # maa + + maa_enable_1 = sg.Radio('启用', 'maa_enable', default=conf['maa_enable'] == 1, + key='radio_maa_enable_1', enable_events=True) + maa_enable_0 = sg.Radio('禁用', 'maa_enable', default=conf['maa_enable'] == 0, + key='radio_maa_enable_0', enable_events=True) + maa_path_title = sg.Text('MAA地址', size=25) + maa_path = sg.InputText(conf['maa_path'], size=60, key='conf_maa_path', enable_events=True) + maa_adb_path_title = sg.Text('adb地址', size=25) + maa_adb_path = sg.InputText(conf['maa_adb_path'], size=60, key='conf_maa_adb_path', enable_events=True) + maa_weekly_plan_title = sg.Text('周计划', size=25) + maa_layout = [[maa_enable_1, maa_enable_0], [maa_path_title, maa_path], [maa_adb_path_title, maa_adb_path], + [maa_weekly_plan_title]] + for i, v in enumerate(conf['maa_weekly_plan']): + maa_layout.append([ + sg.Text(f"-- {v['weekday']}:", size=15), + sg.Text('关卡:', size=5), + sg.InputText(",".join(v['stage']), size=15, key='maa_weekly_plan_stage_' + str(i), enable_events=True), + sg.Text('理智药:', size=10), + sg.Spin([l for l in range(0, 999)], initial_value=v['medicine'], size=5, + key='maa_weekly_plan_medicine_' + str(i), enable_events=True, readonly=True) + ]) + + maa_frame = sg.Frame('MAA', maa_layout) + # --------组装页面 + main_tab = sg.Tab(' 主页 ', [[package_type_title, package_type_1, package_type_2], + [adb_title, adb], + [free_blacklist_title, free_blacklist], + [plan_title, plan_file, plan_select], + [output], + [on_btn, off_btn]]) + + plan_tab = sg.Tab(' 排班表 ', [[left_area, central_area, right_area], [setting_area]], element_justification="center") + setting_tab = sg.Tab(' 高级设置 ', + [[run_mode_title, run_mode_1, run_mode_2], [ling_xi_title, ling_xi_1, ling_xi_2, ling_xi_3], + [max_resting_count_title, max_resting_count, sg.Text('', size=16), run_order_delay_title, + run_order_delay], + [drone_room_title, drone_room, sg.Text('', size=7), drone_count_limit_title, + drone_count_limit], + [rest_in_full_title, rest_in_full]], pad=((10, 10), (10, 10))) + other_tab = sg.Tab(' 外部调用 ', + [[mail_frame], [maa_frame]], pad=((10, 10), (10, 10))) + window = sg.Window('Mower', [[sg.TabGroup([[main_tab, plan_tab, setting_tab, other_tab]], border_width=0, + tab_border_width=0, focus_color='#bcc8e5', + selected_background_color='#d4dae8', background_color='#aab6d3', + tab_background_color='#aab6d3')]], font='微软雅黑', finalize=True, + resizable=False) + + load_plan(conf['planFile']) + btn = None + bind_scirpt() # 为基建布局左边的站点排序绑定事件 + drag_task = DragTask() + while True: + event, value = window.Read() + if event == sg.WIN_CLOSED: + break + if event.endswith('-script'): # 触发事件,进行处理 + run_script(event[:event.rindex('-')], drag_task) + continue + drag_task.clear() # 拖拽事件连续不间断,若未触发事件,则初始化 + if event.startswith('conf_'): + key = event[5:] + conf[key] = window[event].get() + elif event.startswith('int_'): + key = event[4:] + try: + conf[key] = int(window[event].get()) + except ValueError: + println(f'[{window[key + "_title"].get()}]需为数字') + elif event.startswith('radio_'): + v_index = event.rindex('_') + conf[event[6:v_index]] = int(event[v_index + 1:]) + elif event == 'planFile' and plan_file.get() != conf['planFile']: # 排班表 + write_plan() + load_plan(plan_file.get()) + plan_file.update(conf['planFile']) + elif event.startswith('maa_weekly_plan_stage_'): # 关卡名 + v_index = event.rindex('_') + conf['maa_weekly_plan'][int(event[v_index + 1:])]['stage'] = [window[event].get()] + elif event.startswith('maa_weekly_plan_medicine_'): # 体力药 + v_index = event.rindex('_') + conf['maa_weekly_plan'][int(event[v_index + 1:])]['medicine'] = int(window[event].get()) + elif event.startswith('btn_'): # 设施按钮 + btn = event + init_btn(event) + elif event == 'savePlan': # 保存设施信息 + save_btn(btn) + elif event == 'clearPlan': # 清空当前设施信息 + clear_btn(btn) + elif event == 'on': + # if adb.get() == '': + # println('adb未设置!') + # continue + + on_btn.update(visible=False) + off_btn.update(visible=True) + clear() + parent_conn, child_conn = Pipe() + main_thread = Process(target=main, args=(conf, plan, child_conn), daemon=True) + main_thread.start() + window.perform_long_operation(lambda: log(parent_conn), 'log') + elif event == 'off': + println('停止运行') + child_conn.close() + main_thread.terminate() + on_btn.update(visible=True) + off_btn.update(visible=False) + + window.close() + write_conf() + write_plan() + + +def bind_scirpt(): + for i in range(3): + for j in range(3): + event = f'btn_room_{str(i + 1)}_{str(j + 1)}' + window[event].bind("", "-motion-script") + window[event].bind("", "-ButtonRelease-script") + window[event].bind("", "-Enter-script") + + +def run_script(event, drag_task): + # logger.info(f"{event}:{drag_task}") + if event.endswith('-motion'): # 拖拽事件,标志拖拽开始 + if drag_task.step == 0 or drag_task.step == 2: # 若为2说明拖拽结束未进入其他元素,则初始化 + drag_task.btn = event[:event.rindex('-')] # 记录初始按钮 + drag_task.step = 1 # 初始化键位,并推进任务步骤 + elif event.endswith('-ButtonRelease'): # 松开按钮事件,标志着拖拽结束 + if drag_task.step == 1 : + drag_task.step = 2 # 推进任务步骤 + elif event.endswith('-Enter'): # 进入元素事件,拖拽结束鼠标若在其他元素,会进入此事件 + if drag_task.step == 2: + drag_task.new_btn = event[:event.rindex('-')] # 记录需交换的按钮 + swtich_plan(drag_task) + drag_task.clear() + else: + drag_task.clear() + + +def swtich_plan(drag_task): + key1 = drag_task.btn[4:] + key2 = drag_task.new_btn[4:] + value1 = plan[key1] if key1 in plan else None; + value2 = plan[key2] if key2 in plan else None; + if value1 is not None: + plan[key2] = value1 + elif key2 in plan: + plan.pop(key2) + if value2 is not None: + plan[key1] = value2 + elif key1 in plan: + plan.pop(key1) + write_plan() + load_plan(conf['planFile']) + + +def init_btn(event): + room_key = event[4:] + station_name = plan[room_key]['name'] if room_key in plan.keys() else '' + plans = plan[room_key]['plans'] if room_key in plan.keys() else [] + if room_key.startswith('room'): + window['station_type_col'].update(visible=True) + window['station_type'].update(station_name) + visible_cnt = 3 # 设施干员需求数量 + else: + if room_key == 'meeting': + visible_cnt = 2 + elif room_key == 'factory' or room_key == 'contact': + visible_cnt = 1 + else: + visible_cnt = 5 + window['station_type_col'].update(visible=False) + window['station_type'].update('') + window['savePlan'].update(visible=True) + window['clearPlan'].update(visible=True) + for i in range(1, 6): + if i > visible_cnt: + window['setArea' + str(i)].update(visible=False) + window['agent' + str(i)].update('') + window['group' + str(i)].update('') + window['replacement' + str(i)].update('') + else: + window['setArea' + str(i)].update(visible=True) + window['agent' + str(i)].update(plans[i - 1]['agent'] if len(plans) >= i else '') + window['group' + str(i)].update(plans[i - 1]['group'] if len(plans) >= i else '') + window['replacement' + str(i)].update(','.join(plans[i - 1]['replacement']) if len(plans) >= i else '') + + +def save_btn(btn): + plan1 = {'name': window['station_type'].get(), 'plans': []} + for i in range(1, 6): + agent = window['agent' + str(i)].get() + group = window['group' + str(i)].get() + replacement = list(filter(None, window['replacement' + str(i)].get().replace(',', ',').split(','))) + if agent != '': + plan1['plans'].append({'agent': agent, 'group': group, 'replacement': replacement}) + elif btn.startswith('btn_dormitory'): # 宿舍 + plan1['plans'].append({'agent': 'Free', 'group': '', 'replacement': []}) + plan[btn[4:]] = plan1 + write_plan() + load_plan(conf['planFile']) + + +def clear_btn(btn): + if btn[4:] in plan: + plan.pop(btn[4:]) + init_btn(btn) + write_plan() + load_plan(conf['planFile']) + + +# 输出日志 +def log(pipe): + try: + while True: + msg = pipe.recv() + println(msg) + except EOFError: + pipe.close() + + +def println(msg): + global buffer + global line + global half_line_index + maxLen = 500 # 最大行数 + buffer = f'{buffer}\n{time.strftime("%m-%d %H:%M:%S")} {msg}'.strip() + window['log'].update(value=buffer) + if line == maxLen // 2: + half_line_index = len(buffer) + if line >= maxLen: + buffer = buffer[half_line_index:] + line = maxLen // 2 + else: + line += 1 + + +# 清空输出栏 +def clear(): + global buffer + global line + buffer = '' + window['log'].update(value=buffer) + line = 0 + + +class DragTask: + def __init__(self): + self.btn = None + self.new_btn = None + self.step = 0 + + def clear(self): + self.btn = None + self.new_btn = None + self.step = 0 + + def __repr__(self): + return f"btn:{self.btn},new_btn:{self.new_btn},step:{self.step}" + + +if __name__ == '__main__': + freeze_support() + menu() diff --git a/requirements.txt b/requirements.txt index d7e5cdade..4dee94da6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,11 @@ pyclipper==1.3.0 shapely==1.7.1 tornado==6.1 requests==2.22.0 -ruamel.yaml==0.17.17 schedule~=1.1.0 +setuptools~=60.2.0 +Pillow~=9.2.0 +pandas==1.5.2 +pyyaml==6.0 +PySimpleGUI~=4.60.4 +pytz~=2022.6 +paddleocr==2.6.1.3