diff --git a/ArknightsGameResource b/ArknightsGameResource index 0e97bb29..4654ff7a 160000 --- a/ArknightsGameResource +++ b/ArknightsGameResource @@ -1 +1 @@ -Subproject commit 0e97bb2928f4b8debf7146d4151e47048d4b8318 +Subproject commit 4654ff7ab0813a566f8cfdf3ec3284aeaca28863 diff --git a/README.md b/README.md index 7e9f4f93..7c838b17 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,5 @@ # arknights-mower - - - - Mower 是为长期运行设计的、开源的明日方舟脚本。 ## 关于 Mower-NG @@ -36,10 +28,5 @@ Mower 是为长期运行设计的、开源的明日方舟脚本。 待更新…… - diff --git a/arknights_mower/__init__.py b/arknights_mower/__init__.py index 3d5fcfee..8d092469 100644 --- a/arknights_mower/__init__.py +++ b/arknights_mower/__init__.py @@ -2,7 +2,7 @@ import sys from pathlib import Path -__version__ = "2024.08" +__version__ = "2024.11.1.1" if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): __rootdir__ = Path(sys._MEIPASS).joinpath("arknights_mower").resolve() diff --git a/arknights_mower/__main__.py b/arknights_mower/__main__.py index 032734ac..a34a5128 100644 --- a/arknights_mower/__main__.py +++ b/arknights_mower/__main__.py @@ -1,37 +1,34 @@ -import json import os -import re -from datetime import datetime, timedelta +from datetime import datetime -from evalidate import Expr - -from arknights_mower import __version__ from arknights_mower.solvers.base_schedule import BaseSchedulerSolver from arknights_mower.solvers.reclamation_algorithm import ReclamationAlgorithm from arknights_mower.solvers.secret_front import SecretFront from arknights_mower.utils import config, path, rapidocr from arknights_mower.utils.csleep import MowerExit -from arknights_mower.utils.datetime import format_time +from arknights_mower.utils.datetime import format_time, get_server_time from arknights_mower.utils.depot import 创建csv, 创建json from arknights_mower.utils.device.adb_client.session import Session from arknights_mower.utils.device.scrcpy import Scrcpy -from arknights_mower.utils.email import send_message, task_template, version_template -from arknights_mower.utils.hot_update import get_listing +from arknights_mower.utils.email import send_message, task_template from arknights_mower.utils.log import logger from arknights_mower.utils.logic_expression import get_logic_exp +from arknights_mower.utils.operators import Operator from arknights_mower.utils.path import get_path from arknights_mower.utils.plan import Plan, PlanConfig, Room from arknights_mower.utils.simulator import restart_simulator base_scheduler = None -operators = config.operators # 执行自动排班 -def main(): +def main(saved_state): logger.info("开始运行Mower") rapidocr.initialize_ocr() - simulate() + data = None + if saved_state != {}: + data = saved_state + simulate(data) def initialize( @@ -55,6 +52,7 @@ def initialize( free_blacklist=conf.free_blacklist, resting_threshold=conf.resting_threshold, refresh_trading_config=config.plan.conf.refresh_trading, + refresh_drained=config.plan.conf.refresh_drained, free_room=conf.free_room, ) for room, obj in plan[plan["default"]].items(): @@ -82,6 +80,7 @@ def initialize( free_blacklist=i["conf"]["free_blacklist"], resting_threshold=conf.resting_threshold, refresh_trading_config=i["conf"]["refresh_trading"], + refresh_drained=i["conf"]["refresh_drained"], free_room=conf.free_room, ) backup_trigger = get_logic_exp(i["trigger"]) if "trigger" in i else None @@ -121,19 +120,19 @@ def initialize( return base_scheduler -def simulate(): +def simulate(saved): """ 具体调用方法可见各个函数的参数说明 """ logger.info(f"正在使用全局配置空间: {path.global_space}") - tasks = [] + tasks = saved["tasks"] if saved else [] reconnect_max_tries = 10 reconnect_tries = 0 global base_scheduler success = False while not success: try: - base_scheduler = initialize(tasks) + base_scheduler = initialize([]) success = True except MowerExit: return @@ -158,20 +157,6 @@ def simulate(): if validation_msg is not None: logger.error(validation_msg) return - if operators != {}: - for k, v in operators.items(): - if ( - k in base_scheduler.op_data.operators - and not base_scheduler.op_data.operators[k].room.startswith("dorm") - ): - # 只复制心情数据 - base_scheduler.op_data.operators[k].mood = v.mood - base_scheduler.op_data.operators[k].time_stamp = v.time_stamp - base_scheduler.op_data.operators[k].depletion_rate = v.depletion_rate - base_scheduler.op_data.operators[k].current_room = v.current_room - base_scheduler.op_data.operators[k].current_index = v.current_index - timezone_offset = 0 - if len(base_scheduler.op_data.backup_plans) > 0: conditions = base_scheduler.op_data.generate_conditions( len(base_scheduler.op_data.backup_plans) @@ -184,85 +169,93 @@ def simulate(): base_scheduler.op_data.swap_plan( [False] * len(base_scheduler.op_data.backup_plans), True ) + timezone_offset = 0 + if saved: + try: + for k, v in saved["operators"].items(): + if k not in base_scheduler.op_data.operators: + base_scheduler.op_data.add(Operator(k, "")) + # 只复制心情数据 + base_scheduler.op_data.operators[k].mood = v.mood + base_scheduler.op_data.operators[k].time_stamp = v.time_stamp + base_scheduler.op_data.operators[k].depletion_rate = v.depletion_rate + base_scheduler.op_data.operators[k].current_room = v.current_room + base_scheduler.op_data.operators[k].current_index = v.current_index + base_scheduler.op_data.dorm = saved["dorm"] + base_scheduler.party_time = saved["party_time"] + base_scheduler.daily_visit_friend = saved["daily_visit_friend"] + base_scheduler.daily_report = saved["daily_report"] + base_scheduler.daily_skland = saved["daily_skland"] + base_scheduler.daily_mail = saved["daily_mail"] + base_scheduler.task_count = saved["task_count"] + base_scheduler.op_data.skill_upgrade_supports = saved[ + "skill_upgrade_supports" + ] + except Exception as ex: + logger.exception(ex) + base_scheduler.tasks = tasks while True: try: if len(base_scheduler.tasks) > 0: (base_scheduler.tasks.sort(key=lambda x: x.time, reverse=False)) - logger.info("||".join([str(t) for t in base_scheduler.tasks])) remaining_time = ( base_scheduler.tasks[0].time - datetime.now() ).total_seconds() if remaining_time > 540: - if config.conf.check_for_updates: - logger.info("检查版本更新") - listing = get_listing() - version = __version__.replace("+", "-") - if not any(i.name.startswith(version) for i in listing): - stable = [] - testing = [] - for i in listing: - name = i.name - if re.fullmatch(r"[0-9]{4}\.[0-9]{2}\.[0-9]+/", name): - stable.append(name[:-1]) - elif re.fullmatch( - r"[0-9]{4}\.[0-9]{2}-[0-9a-z]{7}/", name - ): - testing.append(name[:-1]) - title = "Mower版本过旧,请及时更新" - logger.error(title) - body = version_template.render( - stable=stable, testing=testing, current=version - ) - send_message(body, title, "WARNING") + # if config.conf.check_for_updates: + # logger.info("检查版本更新") + # listing = get_listing() + # version = __version__.replace("+", "-") + # if not any(i.name.startswith(version) for i in listing): + # stable = [] + # testing = [] + # for i in listing: + # name = i.name + # if re.fullmatch(r"[0-9]{4}\.[0-9]{2}\.[0-9]+/", name): + # stable.append(name[:-1]) + # elif re.fullmatch( + # r"[0-9]{4}\.[0-9]{2}-[0-9a-z]{7}/", name + # ): + # testing.append(name[:-1]) + # title = "Mower版本过旧,请及时更新" + # logger.error(title) + # body = version_template.render( + # stable=stable, testing=testing, current=version + # ) + # send_message(body, title, "WARNING") # 刷新时间以鹰历为准 - if ( - base_scheduler.sign_in - < (datetime.now() - timedelta(hours=4)).date() - ): - if base_scheduler.sign_in_plan_solver(): - base_scheduler.sign_in = ( - datetime.now() - timedelta(hours=4) - ).date() - - if ( - base_scheduler.daily_visit_friend - < (datetime.now() - timedelta(hours=4)).date() - ): + # if ( + # base_scheduler.sign_in + # < get_server_time().date() + # ): + # if base_scheduler.sign_in_plan_solver(): + # base_scheduler.sign_in = ( + # datetime.now() - timedelta(hours=4) + # ).date() + + if base_scheduler.daily_visit_friend < get_server_time().date(): if base_scheduler.visit_friend_plan_solver(): - base_scheduler.daily_visit_friend = ( - datetime.now() - timedelta(hours=4) - ).date() + base_scheduler.daily_visit_friend = get_server_time().date() - if ( - base_scheduler.daily_report - < (datetime.now() - timedelta(hours=4)).date() - ): + if base_scheduler.daily_report < get_server_time().date(): if base_scheduler.report_plan_solver(): - base_scheduler.daily_report = ( - datetime.now() - timedelta(hours=4) - ).date() + base_scheduler.daily_report = get_server_time().date() if ( config.conf.skland_enable - and base_scheduler.daily_skland - < (datetime.now() - timedelta(hours=4)).date() + and base_scheduler.daily_skland < get_server_time().date() ): if base_scheduler.skland_plan_solover(): - base_scheduler.daily_skland = ( - datetime.now() - timedelta(hours=4) - ).date() + base_scheduler.daily_skland = get_server_time().date() if ( config.conf.check_mail_enable - and base_scheduler.daily_mail - < (datetime.now() - timedelta(hours=8)).date() + and base_scheduler.daily_mail < get_server_time().date() ): if base_scheduler.mail_plan_solver(): - base_scheduler.daily_mail = ( - datetime.now() - timedelta(hours=8) - ).date() + base_scheduler.daily_mail = get_server_time().date() if config.conf.recruit_enable: base_scheduler.recruit_plan_solver() @@ -443,29 +436,3 @@ def _is_depotscan(): except Exception as e: logger.exception(f"程序出错--->{e}") base_scheduler.recog.update() - - -def save_state(op_data, file="state.json"): - tmp_dir = get_path("@app/tmp") - if not tmp_dir.exists(): - tmp_dir.mkdir() - state_file = tmp_dir / file - with open(state_file, "w") as f: - if op_data is not None: - json.dump(vars(op_data), f, default=str) - - -def load_state(file="state.json"): - state_file = get_path("@app/tmp") / file - if not state_file.exists(): - return None - with open(state_file, "r") as f: - state = json.load(f) - operators = {k: Expr(v).eval() for k, v in state["operators"].items()} - for k, v in operators.items(): - if not v.time_stamp == "None": - v.time_stamp = datetime.strptime(v.time_stamp, "%Y-%m-%d %H:%M:%S.%f") - else: - v.time_stamp = None - logger.info("基建配置已加载!") - return operators diff --git a/arknights_mower/data/__init__.py b/arknights_mower/data/__init__.py index f7f66d37..9985c8a3 100644 --- a/arknights_mower/data/__init__.py +++ b/arknights_mower/data/__init__.py @@ -6,6 +6,10 @@ # agents list in Arknights agent_list = json.loads(Path(f"{__rootdir__}/data/agent.json").read_text("utf-8")) +agent_profession = json.loads( + Path(f"{__rootdir__}/data/agent_profession.json").read_text("utf-8") +) + # # agents base skills # agent_base_config = json.loads( # Path(f'{__rootdir__}/data/agent-base.json').read_text('utf-8')) diff --git a/arknights_mower/data/agent.json b/arknights_mower/data/agent.json index acc36f9b..b63030cf 100644 --- a/arknights_mower/data/agent.json +++ b/arknights_mower/data/agent.json @@ -1 +1 @@ -["芬", "梅", "宴", "砾", "孑", "吽", "红", "空", "黑", "W", "夕", "林", "令", "阿", "黍", "年", "山", "陈", "锏", "煌", "夜刀", "黑角", "杜林", "香草", "翎羽", "卡缇", "斑点", "炎熔", "芙蓉", "梓兰", "夜烟", "远山", "卡达", "深靛", "布丁", "流星", "红云", "白雪", "松果", "酸糖", "铅踝", "跃跃", "讯使", "红豆", "豆苗", "杜宾", "缠丸", "霜叶", "慕斯", "刻刀", "芳汀", "石英", "暗索", "末药", "清流", "褐果", "角峰", "泡泡", "露托", "古米", "坚雷", "地灵", "伊桑", "阿消", "维荻", "微风", "凛冬", "贾维", "青枳", "红隼", "苇草", "野鬃", "极境", "万顷", "夜半", "渡桥", "晓歌", "谜图", "鞭刃", "苍苔", "医生", "炎客", "摩根", "燧石", "断崖", "烈夏", "铎铃", "柏喙", "战车", "星极", "铸铁", "赤冬", "海沫", "奥达", "蓝毒", "白金", "灰喉", "四月", "隐现", "陨星", "慑砂", "截云", "苦艾", "雪绒", "天火", "惊蛰", "星源", "蜜蜡", "寒檀", "薄绿", "和弦", "蚀清", "耶拉", "洛洛", "至简", "折光", "温米", "梅尔", "稀音", "衡沙", "赫默", "亚叶", "锡兰", "絮雨", "图耶", "桑葚", "蜜莓", "刺玫", "明椒", "莎草", "临光", "深律", "槐琥", "乌有", "雷蛇", "深巡", "可颂", "拜松", "火神", "石棉", "暮落", "闪击", "暴雨", "灰毫", "火哨", "极光", "洋灰", "玫拉", "子月", "熔泉", "冰酿", "空构", "崖心", "雪雉", "杏仁", "初雪", "巫恋", "海霓", "真理", "但书", "小满", "掠风", "海蒂", "月禾", "夏栎", "凛视", "狮蝎", "绮良", "罗宾", "霜华", "贝娜", "风丸", "双月", "锡人", "空弦", "灰烬", "鸿雪", "远牙", "早露", "提丰", "莱伊", "风笛", "嵯峨", "琴柳", "焰尾", "伺夜", "异客", "澄闪", "黑键", "妮芙", "灵知", "铃兰", "魔王", "白铁", "塑心", "傀影", "老鲤", "温蒂", "水月", "艾拉", "闪灵", "夜莺", "流明", "星熊", "瑕光", "泥岩", "斥罪", "森蚺", "号角", "重岳", "银灰", "棘刺", "仇白", "佩佩", "左乐", "止颂", "暴行", "空爆", "猎蜂", "杰克", "夜魔", "巡林者", "12F", "玫兰莎", "泡普卡", "米格鲁", "克洛丝", "安赛尔", "格雷伊", "杰西卡", "安比尔", "清道夫", "桃金娘", "断罪者", "罗小黑", "休谟斯", "嘉维尔", "苏苏洛", "调香师", "蛇屠箱", "深海色", "波登可", "白面鸮", "诗怀雅", "芙兰卡", "因陀罗", "达格达", "幽灵鲨", "布洛卡", "导火索", "羽毛笔", "龙舌兰", "送葬人", "奥斯塔", "阿米娅", "特米米", "爱丽丝", "戴菲恩", "阿罗玛", "华法琳", "哈洛德", "卡夫卡", "车尔尼", "守林人", "安哲拉", "埃拉托", "九色鹿", "食铁兽", "见行者", "能天使", "迷迭香", "伊内丝", "刻俄柏", "逻各斯", "麦哲伦", "多萝西", "凯尔希", "塞雷娅", "赫德雷", "艾丽妮", "赫拉格", "帕拉斯", "玛恩纳", "月见夜", "格拉尼", "斯卡蒂", "安德切尔", "史都华德", "艾丝黛尔", "罗比菈塔", "德克萨斯", "拉普兰德", "莱恩哈特", "炎狱炎熔", "濯尘芙蓉", "普罗旺斯", "格劳克斯", "菲亚梅塔", "维什戴尔", "娜仁图亚", "推进之王", "缪尔赛思", "伊芙利特", "莫斯提马", "艾雅法拉", "霍尔海雅", "卡涅利安", "安洁莉娜", "淬羽赫默", "歌蕾蒂娅", "阿斯卡纶", "焰影苇草", "乌尔比安", "史尔特尔", "薇薇安娜", "正义骑士号", "历阵锐枪芬", "火龙S黑角", "寒芒克洛丝", "承曦格雷伊", "假日威龙陈", "浊心斯卡蒂", "麒麟R夜刀", "琳琅诗怀雅", "归溟幽灵鲨", "涤火杰西卡", "百炼嘉维尔", "耀骑士临光", "圣约送葬人", "缄默德克萨斯", "纯烬艾雅法拉", "THRM-EX", "泰拉大陆调查团", "Lancet-2", "Castle-3", "PhonoR-0", "Friston-3", "U-Official"] \ No newline at end of file +["芬", "梅", "宴", "砾", "孑", "吽", "红", "空", "黑", "W", "夕", "林", "令", "阿", "黍", "年", "山", "陈", "锏", "煌", "夜刀", "黑角", "杜林", "香草", "翎羽", "卡缇", "斑点", "炎熔", "芙蓉", "梓兰", "夜烟", "远山", "卡达", "深靛", "布丁", "流星", "红云", "白雪", "松果", "酸糖", "铅踝", "跃跃", "讯使", "红豆", "豆苗", "杜宾", "缠丸", "霜叶", "慕斯", "刻刀", "芳汀", "石英", "暗索", "末药", "清流", "褐果", "角峰", "泡泡", "露托", "古米", "坚雷", "地灵", "伊桑", "阿消", "维荻", "云迹", "微风", "凛冬", "贾维", "青枳", "红隼", "苇草", "野鬃", "极境", "万顷", "夜半", "渡桥", "晓歌", "谜图", "鞭刃", "苍苔", "医生", "炎客", "摩根", "燧石", "断崖", "烈夏", "铎铃", "柏喙", "战车", "星极", "铸铁", "赤冬", "海沫", "奥达", "蓝毒", "白金", "灰喉", "四月", "隐现", "陨星", "慑砂", "截云", "苦艾", "雪绒", "天火", "惊蛰", "星源", "蜜蜡", "寒檀", "薄绿", "和弦", "蚀清", "耶拉", "洛洛", "至简", "折光", "温米", "梅尔", "稀音", "衡沙", "赫默", "亚叶", "锡兰", "絮雨", "图耶", "桑葚", "蜜莓", "刺玫", "明椒", "莎草", "临光", "深律", "森西", "槐琥", "乌有", "裁度", "雷蛇", "深巡", "可颂", "拜松", "火神", "石棉", "暮落", "闪击", "暴雨", "灰毫", "火哨", "极光", "洋灰", "菲莱", "玫拉", "子月", "熔泉", "冰酿", "空构", "崖心", "雪雉", "杏仁", "初雪", "巫恋", "海霓", "真理", "但书", "小满", "掠风", "海蒂", "月禾", "夏栎", "凛视", "波卜", "狮蝎", "绮良", "罗宾", "霜华", "贝娜", "风丸", "双月", "锡人", "空弦", "灰烬", "鸿雪", "远牙", "早露", "提丰", "莱伊", "风笛", "嵯峨", "忍冬", "琴柳", "焰尾", "伺夜", "异客", "澄闪", "黑键", "妮芙", "灵知", "铃兰", "魔王", "白铁", "塑心", "傀影", "老鲤", "温蒂", "水月", "艾拉", "闪灵", "夜莺", "流明", "星熊", "瑕光", "泥岩", "斥罪", "森蚺", "号角", "重岳", "银灰", "棘刺", "仇白", "佩佩", "左乐", "止颂", "暴行", "空爆", "猎蜂", "杰克", "夜魔", "巡林者", "12F", "玫兰莎", "泡普卡", "米格鲁", "克洛丝", "安赛尔", "格雷伊", "杰西卡", "安比尔", "清道夫", "桃金娘", "断罪者", "罗小黑", "休谟斯", "嘉维尔", "苏苏洛", "调香师", "蛇屠箱", "深海色", "波登可", "白面鸮", "诗怀雅", "芙兰卡", "莱欧斯", "因陀罗", "达格达", "幽灵鲨", "布洛卡", "导火索", "羽毛笔", "龙舌兰", "送葬人", "奥斯塔", "阿米娅", "特米米", "爱丽丝", "戴菲恩", "阿罗玛", "华法琳", "哈洛德", "卡夫卡", "车尔尼", "守林人", "安哲拉", "埃拉托", "凯瑟琳", "九色鹿", "食铁兽", "见行者", "能天使", "迷迭香", "伊内丝", "刻俄柏", "逻各斯", "麦哲伦", "弑君者", "多萝西", "凯尔希", "塞雷娅", "赫德雷", "艾丽妮", "赫拉格", "帕拉斯", "玛恩纳", "月见夜", "格拉尼", "斯卡蒂", "安德切尔", "史都华德", "艾丝黛尔", "罗比菈塔", "德克萨斯", "齐尔查克", "拉普兰德", "莱恩哈特", "炎狱炎熔", "濯尘芙蓉", "普罗旺斯", "格劳克斯", "菲亚梅塔", "维什戴尔", "娜仁图亚", "推进之王", "缪尔赛思", "伊芙利特", "莫斯提马", "艾雅法拉", "霍尔海雅", "玛露西尔", "卡涅利安", "安洁莉娜", "淬羽赫默", "歌蕾蒂娅", "阿斯卡纶", "焰影苇草", "乌尔比安", "史尔特尔", "薇薇安娜", "正义骑士号", "历阵锐枪芬", "火龙S黑角", "寒芒克洛丝", "承曦格雷伊", "假日威龙陈", "浊心斯卡蒂", "麒麟R夜刀", "琳琅诗怀雅", "归溟幽灵鲨", "涤火杰西卡", "百炼嘉维尔", "耀骑士临光", "圣约送葬人", "荒芜拉普兰德", "缄默德克萨斯", "纯烬艾雅法拉", "THRM-EX", "泰拉大陆调查团", "维娜·维多利亚", "Lancet-2", "Castle-3", "PhonoR-0", "Friston-3", "U-Official"] \ No newline at end of file diff --git a/arknights_mower/data/agent_profession.json b/arknights_mower/data/agent_profession.json new file mode 100644 index 00000000..05a38289 --- /dev/null +++ b/arknights_mower/data/agent_profession.json @@ -0,0 +1 @@ +{"Lancet-2": "MEDIC", "Castle-3": "WARRIOR", "THRM-EX": "SPECIAL", "正义骑士号": "SNIPER", "泰拉大陆调查团": "SNIPER", "U-Official": "SUPPORT", "Friston-3": "TANK", "PhonoR-0": "SUPPORT", "夜刀": "PIONEER", "黑角": "TANK", "巡林者": "SNIPER", "杜林": "CASTER", "12F": "CASTER", "芬": "PIONEER", "香草": "PIONEER", "翎羽": "PIONEER", "玫兰莎": "WARRIOR", "泡普卡": "WARRIOR", "卡缇": "TANK", "米格鲁": "TANK", "斑点": "TANK", "克洛丝": "SNIPER", "安德切尔": "SNIPER", "炎熔": "CASTER", "芙蓉": "MEDIC", "安赛尔": "MEDIC", "史都华德": "CASTER", "梓兰": "SUPPORT", "夜烟": "CASTER", "远山": "CASTER", "格雷伊": "CASTER", "卡达": "CASTER", "深靛": "CASTER", "布丁": "CASTER", "杰西卡": "SNIPER", "流星": "SNIPER", "红云": "SNIPER", "梅": "SNIPER", "白雪": "SNIPER", "松果": "SNIPER", "安比尔": "SNIPER", "酸糖": "SNIPER", "铅踝": "SNIPER", "跃跃": "SNIPER", "讯使": "PIONEER", "清道夫": "PIONEER", "红豆": "PIONEER", "桃金娘": "PIONEER", "豆苗": "PIONEER", "杜宾": "WARRIOR", "缠丸": "WARRIOR", "断罪者": "WARRIOR", "霜叶": "WARRIOR", "艾丝黛尔": "WARRIOR", "慕斯": "WARRIOR", "刻刀": "WARRIOR", "宴": "WARRIOR", "芳汀": "WARRIOR", "罗小黑": "WARRIOR", "石英": "WARRIOR", "休谟斯": "WARRIOR", "砾": "SPECIAL", "孑": "SPECIAL", "暗索": "SPECIAL", "末药": "MEDIC", "嘉维尔": "MEDIC", "苏苏洛": "MEDIC", "调香师": "MEDIC", "清流": "MEDIC", "褐果": "MEDIC", "角峰": "TANK", "蛇屠箱": "TANK", "泡泡": "TANK", "露托": "TANK", "古米": "TANK", "坚雷": "TANK", "深海色": "SUPPORT", "地灵": "SUPPORT", "波登可": "SUPPORT", "罗比菈塔": "SUPPORT", "伊桑": "SPECIAL", "阿消": "SPECIAL", "维荻": "SPECIAL", "云迹": "SPECIAL", "白面鸮": "MEDIC", "微风": "MEDIC", "凛冬": "PIONEER", "德克萨斯": "PIONEER", "贾维": "PIONEER", "青枳": "PIONEER", "红隼": "PIONEER", "苇草": "PIONEER", "野鬃": "PIONEER", "历阵锐枪芬": "PIONEER", "极境": "PIONEER", "万顷": "PIONEER", "夜半": "PIONEER", "渡桥": "PIONEER", "晓歌": "PIONEER", "谜图": "PIONEER", "齐尔查克": "PIONEER", "诗怀雅": "WARRIOR", "鞭刃": "WARRIOR", "苍苔": "WARRIOR", "医生": "WARRIOR", "芙兰卡": "WARRIOR", "炎客": "WARRIOR", "摩根": "WARRIOR", "莱欧斯": "WARRIOR", "因陀罗": "WARRIOR", "燧石": "WARRIOR", "达格达": "WARRIOR", "拉普兰德": "WARRIOR", "断崖": "WARRIOR", "烈夏": "WARRIOR", "铎铃": "WARRIOR", "柏喙": "WARRIOR", "战车": "WARRIOR", "幽灵鲨": "WARRIOR", "布洛卡": "WARRIOR", "导火索": "WARRIOR", "星极": "WARRIOR", "铸铁": "WARRIOR", "赤冬": "WARRIOR", "火龙S黑角": "WARRIOR", "羽毛笔": "WARRIOR", "海沫": "WARRIOR", "龙舌兰": "WARRIOR", "奥达": "WARRIOR", "蓝毒": "SNIPER", "白金": "SNIPER", "灰喉": "SNIPER", "四月": "SNIPER", "寒芒克洛丝": "SNIPER", "隐现": "SNIPER", "陨星": "SNIPER", "慑砂": "SNIPER", "截云": "SNIPER", "送葬人": "SNIPER", "奥斯塔": "SNIPER", "阿米娅": "CASTER", "苦艾": "CASTER", "特米米": "CASTER", "雪绒": "CASTER", "天火": "CASTER", "惊蛰": "CASTER", "星源": "CASTER", "蜜蜡": "CASTER", "莱恩哈特": "CASTER", "寒檀": "CASTER", "薄绿": "CASTER", "爱丽丝": "CASTER", "和弦": "CASTER", "戴菲恩": "CASTER", "炎狱炎熔": "CASTER", "蚀清": "CASTER", "阿罗玛": "CASTER", "耶拉": "CASTER", "洛洛": "CASTER", "至简": "CASTER", "折光": "CASTER", "温米": "CASTER", "梅尔": "SUPPORT", "稀音": "SUPPORT", "衡沙": "SUPPORT", "赫默": "MEDIC", "华法琳": "MEDIC", "亚叶": "MEDIC", "锡兰": "MEDIC", "絮雨": "MEDIC", "图耶": "MEDIC", "桑葚": "MEDIC", "蜜莓": "MEDIC", "哈洛德": "MEDIC", "濯尘芙蓉": "MEDIC", "刺玫": "MEDIC", "明椒": "MEDIC", "莎草": "MEDIC", "临光": "TANK", "吽": "TANK", "深律": "TANK", "森西": "TANK", "红": "SPECIAL", "槐琥": "SPECIAL", "卡夫卡": "SPECIAL", "乌有": "SPECIAL", "裁度": "SPECIAL", "雷蛇": "TANK", "深巡": "TANK", "可颂": "TANK", "拜松": "TANK", "火神": "TANK", "石棉": "TANK", "暮落": "TANK", "车尔尼": "TANK", "闪击": "TANK", "暴雨": "TANK", "灰毫": "TANK", "火哨": "TANK", "极光": "TANK", "洋灰": "TANK", "菲莱": "TANK", "普罗旺斯": "SNIPER", "玫拉": "SNIPER", "守林人": "SNIPER", "安哲拉": "SNIPER", "子月": "SNIPER", "熔泉": "SNIPER", "埃拉托": "SNIPER", "承曦格雷伊": "SNIPER", "冰酿": "SNIPER", "空构": "SPECIAL", "崖心": "SPECIAL", "雪雉": "SPECIAL", "杏仁": "SPECIAL", "初雪": "SUPPORT", "巫恋": "SUPPORT", "海霓": "SUPPORT", "真理": "SUPPORT", "格劳克斯": "SUPPORT", "但书": "SUPPORT", "小满": "SUPPORT", "掠风": "SUPPORT", "凯瑟琳": "SUPPORT", "空": "SUPPORT", "海蒂": "SUPPORT", "月禾": "SUPPORT", "九色鹿": "SUPPORT", "夏栎": "SUPPORT", "凛视": "SUPPORT", "波卜": "SUPPORT", "狮蝎": "SPECIAL", "绮良": "SPECIAL", "食铁兽": "SPECIAL", "见行者": "SPECIAL", "罗宾": "SPECIAL", "霜华": "SPECIAL", "贝娜": "SPECIAL", "风丸": "SPECIAL", "双月": "SPECIAL", "锡人": "SPECIAL", "能天使": "SNIPER", "空弦": "SNIPER", "灰烬": "SNIPER", "黑": "SNIPER", "鸿雪": "SNIPER", "远牙": "SNIPER", "W": "SNIPER", "菲亚梅塔": "SNIPER", "早露": "SNIPER", "提丰": "SNIPER", "迷迭香": "SNIPER", "维什戴尔": "SNIPER", "假日威龙陈": "SNIPER", "莱伊": "SNIPER", "娜仁图亚": "SNIPER", "推进之王": "PIONEER", "风笛": "PIONEER", "嵯峨": "PIONEER", "忍冬": "PIONEER", "琴柳": "PIONEER", "焰尾": "PIONEER", "伺夜": "PIONEER", "缪尔赛思": "PIONEER", "伊内丝": "PIONEER", "伊芙利特": "CASTER", "莫斯提马": "CASTER", "艾雅法拉": "CASTER", "刻俄柏": "CASTER", "霍尔海雅": "CASTER", "逻各斯": "CASTER", "夕": "CASTER", "玛露西尔": "CASTER", "异客": "CASTER", "卡涅利安": "CASTER", "林": "CASTER", "澄闪": "CASTER", "荒芜拉普兰德": "CASTER", "黑键": "CASTER", "妮芙": "CASTER", "灵知": "SUPPORT", "安洁莉娜": "SUPPORT", "铃兰": "SUPPORT", "麦哲伦": "SUPPORT", "浊心斯卡蒂": "SUPPORT", "魔王": "SUPPORT", "淬羽赫默": "SUPPORT", "令": "SUPPORT", "白铁": "SUPPORT", "塑心": "SUPPORT", "傀影": "SPECIAL", "缄默德克萨斯": "SPECIAL", "麒麟R夜刀": "SPECIAL", "弑君者": "SPECIAL", "老鲤": "SPECIAL", "琳琅诗怀雅": "SPECIAL", "温蒂": "SPECIAL", "阿": "SPECIAL", "歌蕾蒂娅": "SPECIAL", "水月": "SPECIAL", "阿斯卡纶": "SPECIAL", "归溟幽灵鲨": "SPECIAL", "多萝西": "SPECIAL", "艾拉": "SPECIAL", "闪灵": "MEDIC", "夜莺": "MEDIC", "凯尔希": "MEDIC", "流明": "MEDIC", "纯烬艾雅法拉": "MEDIC", "焰影苇草": "MEDIC", "星熊": "TANK", "塞雷娅": "TANK", "瑕光": "TANK", "黍": "TANK", "年": "TANK", "泥岩": "TANK", "斥罪": "TANK", "森蚺": "TANK", "号角": "TANK", "涤火杰西卡": "TANK", "山": "WARRIOR", "重岳": "WARRIOR", "银灰": "WARRIOR", "棘刺": "WARRIOR", "仇白": "WARRIOR", "赫德雷": "WARRIOR", "乌尔比安": "WARRIOR", "佩佩": "WARRIOR", "陈": "WARRIOR", "艾丽妮": "WARRIOR", "锏": "WARRIOR", "煌": "WARRIOR", "百炼嘉维尔": "WARRIOR", "史尔特尔": "WARRIOR", "薇薇安娜": "WARRIOR", "维娜·维多利亚": "WARRIOR", "赫拉格": "WARRIOR", "左乐": "WARRIOR", "帕拉斯": "WARRIOR", "耀骑士临光": "WARRIOR", "止颂": "WARRIOR", "圣约送葬人": "WARRIOR", "玛恩纳": "WARRIOR", "暴行": "WARRIOR", "空爆": "SNIPER", "月见夜": "WARRIOR", "猎蜂": "WARRIOR", "杰克": "WARRIOR", "夜魔": "CASTER", "格拉尼": "PIONEER", "斯卡蒂": "WARRIOR"} \ No newline at end of file diff --git a/arknights_mower/data/arrange_order.json b/arknights_mower/data/arrange_order.json index d7825b46..8ee7b609 100644 --- a/arknights_mower/data/arrange_order.json +++ b/arknights_mower/data/arrange_order.json @@ -166,5 +166,26 @@ "泰拉大陆调查团": [ 2, "true" - ] + ], + "安哲拉": [ + 3, + "true" + ], + "斯卡蒂": [ + 3, + "true" + ], + "幽灵鲨": [ + 3, + "true" + ], + "乌尔比安": [ + 3, + "true" + ], + "推进之王": [ + 3, + "true" + ], + "职介选择开关": ["推进之王", "安哲拉", "斯卡蒂", "幽灵鲨", "乌尔比安"] } \ No newline at end of file diff --git a/arknights_mower/data/key_mapping.json b/arknights_mower/data/key_mapping.json index a1fb5159..0d574153 100644 --- a/arknights_mower/data/key_mapping.json +++ b/arknights_mower/data/key_mapping.json @@ -111,19 +111,19 @@ "NORMAL", 10008 ], - "LMTGS_COIN_5301": [ - "LMTGS_COIN_5301", - "LMTGS_COIN_5301", + "LMTGS_COIN_5601": [ + "LMTGS_COIN_5601", + "LMTGS_COIN_5601", "寻访数据契约", "NORMAL", - 10025 + 10026 ], "寻访数据契约": [ - "LMTGS_COIN_5301", - "LMTGS_COIN_5301", + "LMTGS_COIN_5601", + "LMTGS_COIN_5601", "寻访数据契约", "NORMAL", - 10025 + 10026 ], "EPGS_COIN": [ "EPGS_COIN", @@ -203,11 +203,11 @@ 20001 ], "ID信息更新卡": [ - "renamingCard_30_050", + "renamingCard_30_056", "renamingCard", "ID信息更新卡", "CONSUME", - 20022 + 20024 ], "renamingCard_0_018": [ "renamingCard_0_018", @@ -307,6 +307,20 @@ "CONSUME", 20022 ], + "renamingCard_0_056": [ + "renamingCard_0_056", + "renamingCard", + "ID信息更新卡", + "CONSUME", + 20023 + ], + "renamingCard_30_056": [ + "renamingCard_30_056", + "renamingCard", + "ID信息更新卡", + "CONSUME", + 20024 + ], "ap_supply_lt_100": [ "ap_supply_lt_100", "ap_supply_lt_100", @@ -391,17 +405,17 @@ "NORMAL", 40001 ], - "LIMITED_TKT_GACHA_10_5301": [ - "LIMITED_TKT_GACHA_10_5301", - "LIMITED_TKT_GACHA_10_5301", - "璀璨闪耀寻访凭证", + "LIMITED_TKT_GACHA_10_5601": [ + "LIMITED_TKT_GACHA_10_5601", + "LIMITED_TKT_GACHA_10_5601", + "荒芜探戈寻访凭证", "NORMAL", 40003 ], - "璀璨闪耀寻访凭证": [ - "LIMITED_TKT_GACHA_10_5301", - "LIMITED_TKT_GACHA_10_5301", - "璀璨闪耀寻访凭证", + "荒芜探戈寻访凭证": [ + "LIMITED_TKT_GACHA_10_5601", + "LIMITED_TKT_GACHA_10_5601", + "荒芜探戈寻访凭证", "NORMAL", 40003 ], @@ -518,11 +532,11 @@ 50006 ], "感谢庆典干员凭证": [ - "voucher_item_pick4401", + "voucher_item_pick5601", "voucher_recruitR6_g", "感谢庆典干员凭证", "CONSUME", - 50014 + 50018 ], "voucher_item_pick1803": [ "voucher_item_pick1803", @@ -629,6 +643,27 @@ "CONSUME", 50018 ], + "voucher_item_pick5601": [ + "voucher_item_pick5601", + "voucher_recruitR6_g", + "感谢庆典干员凭证", + "CONSUME", + 50018 + ], + "voucher_class_pick5601": [ + "voucher_class_pick5601", + "voucher_class_pick5601", + "中坚高级干员调用凭证", + "CONSUME", + 50018 + ], + "中坚高级干员调用凭证": [ + "voucher_class_pick5601", + "voucher_class_pick5601", + "中坚高级干员调用凭证", + "CONSUME", + 50018 + ], "voucher_levelmax_6": [ "voucher_levelmax_6", "voucher_levelmax_6", @@ -1189,6 +1224,20 @@ "CONSUME", 60110 ], + "randomMaterial_5d5": [ + "randomMaterial_5d5", + "randomMaterial_4", + "2024感谢庆典物资补给", + "CONSUME", + 60111 + ], + "2024感谢庆典物资补给": [ + "randomMaterial_5d5", + "randomMaterial_4", + "2024感谢庆典物资补给", + "CONSUME", + 60111 + ], "randomMaterial_rhine2": [ "randomMaterial_rhine2", "randomMaterial_rhine2", @@ -1208,56 +1257,70 @@ "randomDiamondShd_1", "罗德岛迎春红包", "CONSUME", - 60111 + 60112 ], "罗德岛迎春红包": [ "randomDiamondShd_1", "randomDiamondShd_1", "罗德岛迎春红包", "CONSUME", - 60111 + 60112 ], "randomDiamondShd_2": [ "randomDiamondShd_2", "randomDiamondShd_2", "庆典礼盒", "CONSUME", - 60112 + 60113 ], "庆典礼盒": [ "randomDiamondShd_2", "randomDiamondShd_2", "庆典礼盒", "CONSUME", - 60112 + 60113 ], "randomMaterial_siesta2": [ "randomMaterial_siesta2", "randomMaterial_siesta2", "峯联贸易物流补给", "CONSUME", - 60113 + 60114 ], "峯联贸易物流补给": [ "randomMaterial_siesta2", "randomMaterial_siesta2", "峯联贸易物流补给", "CONSUME", - 60113 + 60114 ], "randomMaterial_leith2": [ "randomMaterial_leith2", "randomMaterial_leith2", "查访补给", "CONSUME", - 60114 + 60115 ], "查访补给": [ "randomMaterial_leith2", "randomMaterial_leith2", "查访补给", "CONSUME", - 60114 + 60115 + ], + "randomMaterial_act38side": [ + "randomMaterial_act38side", + "randomMaterial_act38side", + "狂欢烟花桶", + "CONSUME", + 60115 + ], + "狂欢烟花桶": [ + "randomMaterial_act38side", + "randomMaterial_act38side", + "狂欢烟花桶", + "CONSUME", + 60115 ], "2001": [ "2001", @@ -4451,6 +4514,20 @@ "MATERIAL", 800055 ], + "p_char_4165_ctrail": [ + "p_char_4165_ctrail", + "p_char_4165_ctrail", + "云迹的信物", + "MATERIAL", + 800056 + ], + "云迹的信物": [ + "p_char_4165_ctrail", + "p_char_4165_ctrail", + "云迹的信物", + "MATERIAL", + 800056 + ], "p_char_128_plosis": [ "p_char_128_plosis", "p_char_128_plosis", @@ -6579,6 +6656,104 @@ "MATERIAL", 700151 ], + "p_char_4142_laios": [ + "p_char_4142_laios", + "p_char_4142_laios", + "莱欧斯的信物", + "MATERIAL", + 700152 + ], + "莱欧斯的信物": [ + "p_char_4142_laios", + "p_char_4142_laios", + "莱欧斯的信物", + "MATERIAL", + 700152 + ], + "p_char_4143_sensi": [ + "p_char_4143_sensi", + "p_char_4143_sensi", + "森西的信物", + "MATERIAL", + 700153 + ], + "森西的信物": [ + "p_char_4143_sensi", + "p_char_4143_sensi", + "森西的信物", + "MATERIAL", + 700153 + ], + "p_char_4144_chilc": [ + "p_char_4144_chilc", + "p_char_4144_chilc", + "齐尔查克的信物", + "MATERIAL", + 700154 + ], + "齐尔查克的信物": [ + "p_char_4144_chilc", + "p_char_4144_chilc", + "齐尔查克的信物", + "MATERIAL", + 700154 + ], + "p_char_487_bobb": [ + "p_char_487_bobb", + "p_char_487_bobb", + "波卜的信物", + "MATERIAL", + 700155 + ], + "波卜的信物": [ + "p_char_487_bobb", + "p_char_487_bobb", + "波卜的信物", + "MATERIAL", + 700155 + ], + "p_char_4162_cathy": [ + "p_char_4162_cathy", + "p_char_4162_cathy", + "凯瑟琳的信物", + "MATERIAL", + 700156 + ], + "凯瑟琳的信物": [ + "p_char_4162_cathy", + "p_char_4162_cathy", + "凯瑟琳的信物", + "MATERIAL", + 700156 + ], + "p_char_4148_philae": [ + "p_char_4148_philae", + "p_char_4148_philae", + "菲莱的信物", + "MATERIAL", + 700157 + ], + "菲莱的信物": [ + "p_char_4148_philae", + "p_char_4148_philae", + "菲莱的信物", + "MATERIAL", + 700157 + ], + "p_char_4155_talr": [ + "p_char_4155_talr", + "p_char_4155_talr", + "裁度的信物", + "MATERIAL", + 700158 + ], + "裁度的信物": [ + "p_char_4155_talr", + "p_char_4155_talr", + "裁度的信物", + "MATERIAL", + 700158 + ], "p_char_103_angel": [ "p_char_103_angel", "p_char_103_angel", @@ -7923,6 +8098,90 @@ "MATERIAL", 600096 ], + "p_char_4141_marcil": [ + "p_char_4141_marcil", + "p_char_4141_marcil", + "玛露西尔的信物", + "MATERIAL", + 600097 + ], + "玛露西尔的信物": [ + "p_char_4141_marcil", + "p_char_4141_marcil", + "玛露西尔的信物", + "MATERIAL", + 600097 + ], + "p_char_1019_siege2": [ + "p_char_1019_siege2", + "p_char_1019_siege2", + "维娜·维多利亚的信物", + "MATERIAL", + 600098 + ], + "维娜·维多利亚的信物": [ + "p_char_1019_siege2", + "p_char_1019_siege2", + "维娜·维多利亚的信物", + "MATERIAL", + 600098 + ], + "p_char_1038_whitw2": [ + "p_char_1038_whitw2", + "p_char_1038_whitw2", + "荒芜拉普兰德的信物", + "MATERIAL", + 600099 + ], + "荒芜拉普兰德的信物": [ + "p_char_1038_whitw2", + "p_char_1038_whitw2", + "荒芜拉普兰德的信物", + "MATERIAL", + 600099 + ], + "p_char_1502_crosly": [ + "p_char_1502_crosly", + "p_char_1502_crosly", + "弑君者的信物", + "MATERIAL", + 600100 + ], + "弑君者的信物": [ + "p_char_1502_crosly", + "p_char_1502_crosly", + "弑君者的信物", + "MATERIAL", + 600100 + ], + "p_char_4026_vulpis": [ + "p_char_4026_vulpis", + "p_char_4026_vulpis", + "忍冬的信物", + "MATERIAL", + 600101 + ], + "忍冬的信物": [ + "p_char_4026_vulpis", + "p_char_4026_vulpis", + "忍冬的信物", + "MATERIAL", + 600101 + ], + "p_char_4011_lessng": [ + "p_char_4011_lessng", + "p_char_4011_lessng", + "止颂的信物", + "MATERIAL", + 600102 + ], + "止颂的信物": [ + "p_char_4011_lessng", + "p_char_4011_lessng", + "止颂的信物", + "MATERIAL", + 600102 + ], "class_p_char_123_fang": [ "class_p_char_123_fang", "class_p_char_123_fang", diff --git a/arknights_mower/data/recruit.json b/arknights_mower/data/recruit.json index 032fb207..850c45c2 100644 --- a/arknights_mower/data/recruit.json +++ b/arknights_mower/data/recruit.json @@ -322,6 +322,16 @@ "狙击干员" ] }, + "char_440_pinecn": { + "name": "松果", + "stars": 4, + "tags": [ + "群攻", + "输出", + "远程位", + "狙击干员" + ] + }, "char_302_glaze": { "name": "安比尔", "stars": 4, @@ -922,6 +932,17 @@ "特种干员" ] }, + "char_214_kafka": { + "name": "卡夫卡", + "stars": 5, + "tags": [ + "快速复活", + "控场", + "资深干员", + "近战位", + "特种干员" + ] + }, "char_107_liskam": { "name": "雷蛇", "stars": 5, @@ -1312,6 +1333,17 @@ "重装干员" ] }, + "char_264_f12yin": { + "name": "山", + "stars": 6, + "tags": [ + "输出", + "生存", + "高级资深干员", + "近战位", + "近卫干员" + ] + }, "char_172_svrash": { "name": "银灰", "stars": 6, diff --git a/arknights_mower/data/recruit_result.json b/arknights_mower/data/recruit_result.json index 8446f968..89286cc6 100644 --- a/arknights_mower/data/recruit_result.json +++ b/arknights_mower/data/recruit_result.json @@ -37,6 +37,7 @@ "char_279_excu", "char_346_aosta", "char_171_bldsk", + "char_214_kafka", "char_158_milu", "char_218_cuttle", "char_241_panda", @@ -64,6 +65,7 @@ "char_126_shotst", "char_190_clour", "char_118_yuki", + "char_440_pinecn", "char_366_acdrop", "char_290_vigna", "char_130_doberm", @@ -138,6 +140,7 @@ "char_144_red", "char_340_shwaz", "char_225_haak", + "char_264_f12yin", "char_010_chen", "char_017_huang" ], diff --git a/arknights_mower/data/stage_data.json b/arknights_mower/data/stage_data.json index ed9ea4eb..b06a5c07 100644 --- a/arknights_mower/data/stage_data.json +++ b/arknights_mower/data/stage_data.json @@ -78,7 +78,7 @@ "周日": 1 }, { - "id": "SK-6", + "id": "SK-5", "name": "碳条", "drop": "", "end": -1, diff --git a/arknights_mower/models/CONSUME.pkl b/arknights_mower/models/CONSUME.pkl index d3bef00f..aeb1e490 100644 Binary files a/arknights_mower/models/CONSUME.pkl and b/arknights_mower/models/CONSUME.pkl differ diff --git a/arknights_mower/models/NORMAL.pkl b/arknights_mower/models/NORMAL.pkl index 5bf1bd24..84789876 100644 Binary files a/arknights_mower/models/NORMAL.pkl and b/arknights_mower/models/NORMAL.pkl differ diff --git a/arknights_mower/models/avatar.pkl b/arknights_mower/models/avatar.pkl index 1c00a03f..6797e383 100644 Binary files a/arknights_mower/models/avatar.pkl and b/arknights_mower/models/avatar.pkl differ diff --git a/arknights_mower/models/operator_room.model b/arknights_mower/models/operator_room.model index 0fcb12a7..81f46793 100644 Binary files a/arknights_mower/models/operator_room.model and b/arknights_mower/models/operator_room.model differ diff --git a/arknights_mower/models/operator_select.model b/arknights_mower/models/operator_select.model index 283b4cd9..547687cc 100644 Binary files a/arknights_mower/models/operator_select.model and b/arknights_mower/models/operator_select.model differ diff --git a/arknights_mower/models/recruit.pkl b/arknights_mower/models/recruit.pkl index 53d8bb6c..46b7281f 100644 Binary files a/arknights_mower/models/recruit.pkl and b/arknights_mower/models/recruit.pkl differ diff --git a/arknights_mower/models/recruit_result.pkl b/arknights_mower/models/recruit_result.pkl index e62757f5..a80a13d2 100644 Binary files a/arknights_mower/models/recruit_result.pkl and b/arknights_mower/models/recruit_result.pkl differ diff --git a/arknights_mower/resources/arrange_order_options.png b/arknights_mower/resources/arrange_order_options.png index ca81c4d0..e54039ae 100644 Binary files a/arknights_mower/resources/arrange_order_options.png and b/arknights_mower/resources/arrange_order_options.png differ diff --git a/arknights_mower/resources/infra_overview.png b/arknights_mower/resources/infra_overview.png index a56c65c2..5a59684c 100644 Binary files a/arknights_mower/resources/infra_overview.png and b/arknights_mower/resources/infra_overview.png differ diff --git a/arknights_mower/resources/recruit/choose_template/normal.png b/arknights_mower/resources/recruit/choose_template/normal.png new file mode 100644 index 00000000..c075be33 Binary files /dev/null and b/arknights_mower/resources/recruit/choose_template/normal.png differ diff --git a/arknights_mower/resources/recruit/choose_template/rare.png b/arknights_mower/resources/recruit/choose_template/rare.png new file mode 100644 index 00000000..bfaa6035 Binary files /dev/null and b/arknights_mower/resources/recruit/choose_template/rare.png differ diff --git a/arknights_mower/solvers/base_mixin.py b/arknights_mower/solvers/base_mixin.py index 4698fbaa..c3a35f2a 100644 --- a/arknights_mower/solvers/base_mixin.py +++ b/arknights_mower/solvers/base_mixin.py @@ -21,10 +21,10 @@ class BaseMixin: def detect_arrange_order(self): name_list = ["工作状态", "技能", "心情", "信赖值"] - x_list = (1196, 1320, 1445, 1572) + x_list = (1309, 1435, 1560, 1685) y = 70 hsv = cv2.cvtColor(self.recog.img, cv2.COLOR_RGB2HSV) - mask = cv2.inRange(hsv, (99, 200, 0), (100, 255, 255)) + mask = cv2.inRange(hsv, (95, 100, 100), (105, 255, 255)) for idx, x in enumerate(x_list): if np.count_nonzero(mask[y : y + 3, x : x + 5]): return (name_list[idx], True) @@ -32,7 +32,7 @@ def detect_arrange_order(self): return (name_list[idx], False) def switch_arrange_order(self, name, ascending=False): - name_x = {"工作状态": 1197, "技能": 1322, "心情": 1447, "信赖值": 1575} + name_x = {"工作状态": 1309, "技能": 1439, "心情": 1565, "信赖值": 1690} if isinstance(name, int): name = list(name_x.keys())[name - 1] if isinstance(ascending, str): @@ -104,73 +104,61 @@ def verify_agent(self, agent: list[str], error_count=0, max_agent_count=-1): raise e def swipe_left(self, right_swipe): - if right_swipe > 3: - self.detail_filter(控制中枢=True) - self.detail_filter(控制中枢=False) - else: - swipe_time = 2 if right_swipe == 3 else right_swipe - for i in range(swipe_time): - self.swipe_noinertia((650, 540), (1900, 0)) + # if right_swipe > 3: + # return right_swipe + # else: + # swipe_time = 2 if right_swipe == 3 else right_swipe + for i in range(right_swipe): + self.swipe_noinertia((650, 540), (1900, 0)) return 0 - def detail_filter(self, **kwargs): - if kwargs: - text = ",".join( - f"{'打开' if value else '关闭'}{label}筛选" - for label, value in kwargs.items() - ) - text += ",关闭其余筛选" - logger.info(text) + def profession_filter(self, profession=None): + retry = 0 + open_threshold = 1700 + if profession: + logger.info(f"打开 {profession} 筛选") else: - logger.info("关闭所有筛选") - + logger.info("关闭职业筛选") + while ( + confirm_btn := self.find("confirm_blue") + ) is not None and confirm_btn[0][0] < open_threshold: + self.tap((1860, 60), 0.1) + retry += 1 + if retry > 5: + raise Exception("关闭职业筛选失败") + return labels = [ - "未进驻", - "产出设施", - "功能设施", - "自定义设施", - "控制中枢", - "生产类后勤", - "功能类后勤", - "恢复类后勤", + "ALL", + "PIONEER", + "WARRIOR", + "TANK", + "SNIPER", + "CASTER", + "MEDIC", + "SUPPORT", + "SPECIAL", ] - label_x = (560, 815, 1070, 1330) - label_y = (540, 645) - - label_pos = [] - for y in label_y: - for x in label_x: - label_pos.append((x, y)) - + x = 1918 + label_pos = [(x, 60 + i * 120) for i in range(9)] label_pos_map = dict(zip(labels, label_pos)) - target_state = dict(zip(labels, [False] * len(labels))) - target_state.update(kwargs) - - filter_pos = (self.recog.w * 0.95, self.recog.h * 0.05) - self.tap(filter_pos) - - err_cnt = 0 - while not self.find("arrange_order_options_scene"): - self.ctap(filter_pos) - err_cnt += 1 - if err_cnt > 3: - raise Exception("未进入筛选页面") - - for label, pos in label_pos_map.items(): - current_state = self.get_color(pos)[2] > 100 - if target_state[label] != current_state: - self.tap(pos, interval=0.1) - - self.recog.update() - confirm_pos = (self.recog.w * 0.8, self.recog.h * 0.8) - self.tap(confirm_pos) - - err_cnt = 0 - while self.find("arrange_order_options_scene"): - self.ctap(confirm_pos) - err_cnt += 1 - if err_cnt > 3: - raise Exception("筛选确认失败") + if profession == "ALL": + self.tap(label_pos_map[profession], 0.1) + self.tap(label_pos_map[profession], 0.1) + return + while (confirm_btn := self.find("confirm_blue")) is not None and confirm_btn[0][ + 0 + ] > open_threshold: + self.tap((1860, 60), 0.1) + retry += 1 + if retry > 5: + raise Exception("打开职业筛选失败") + retry = 0 + while self.get_color(label_pos_map[profession])[2] < 250: + logger.debug(f"配色为: {self.get_color(label_pos_map[profession])[2]}") + self.tap(label_pos_map[profession], 0.1) + retry += 1 + if retry > 5: + raise Exception("打开职业筛选失败") def detect_room_number(self, img) -> int: score = [] @@ -339,6 +327,7 @@ def read_time(self, cord, upperlimit, error_count=0, use_digit_reader=False): time_str = self.digit_reader.get_time(self.recog.gray) else: time_str = self.read_screen(self.recog.img, type="time", cord=cord) + logger.debug(time_str) h, m, s = str(time_str).split(":") if int(m) > 60 or int(s) > 60: raise Exception("读取错误") diff --git a/arknights_mower/solvers/base_schedule.py b/arknights_mower/solvers/base_schedule.py index 07eb276f..43542d22 100644 --- a/arknights_mower/solvers/base_schedule.py +++ b/arknights_mower/solvers/base_schedule.py @@ -3,35 +3,32 @@ import os import pathlib import sys +import urllib from ctypes import CFUNCTYPE, c_char_p, c_int, c_void_p from datetime import datetime, timedelta from typing import Literal import cv2 -from arknights_mower.data import agent_list, base_room_list +from arknights_mower.data import agent_list, agent_profession, base_room_list from arknights_mower.solvers.base_mixin import BaseMixin from arknights_mower.solvers.credit import CreditSolver -from arknights_mower.solvers.credit_fight import CreditFight from arknights_mower.solvers.cultivate_depot import cultivate as cultivateDepotSolver from arknights_mower.solvers.depotREC import depotREC as DepotSolver from arknights_mower.solvers.mail import MailSolver -from arknights_mower.solvers.mission import MissionSolver -from arknights_mower.solvers.navigation import NavigationSolver -from arknights_mower.solvers.operation import OperationSolver from arknights_mower.solvers.reclamation_algorithm import ReclamationAlgorithm from arknights_mower.solvers.recruit import RecruitSolver from arknights_mower.solvers.report import ReportSolver from arknights_mower.solvers.secret_front import SecretFront from arknights_mower.solvers.shop import CreditShop from arknights_mower.solvers.skland import SKLand -from arknights_mower.utils import config, detector, hot_update, rapidocr +from arknights_mower.utils import config, detector, rapidocr from arknights_mower.utils import typealias as tp from arknights_mower.utils.csleep import MowerExit, csleep from arknights_mower.utils.datetime import format_time, get_server_weekday from arknights_mower.utils.device.device import Device from arknights_mower.utils.digit_reader import DigitReader -from arknights_mower.utils.email import send_message +from arknights_mower.utils.email import maa_template, send_message from arknights_mower.utils.graph import SceneGraphSolver from arknights_mower.utils.image import cropimg, loadres, thres2 from arknights_mower.utils.log import logger @@ -45,6 +42,7 @@ add_release_dorm, check_dorm_ordering, find_next_task, + merge_release_dorm, scheduling, try_add_release_dorm, ) @@ -77,11 +75,7 @@ def __init__(self, device: Device = None, recog: Recognizer = None) -> None: self.last_clue = None self.sleeping = False self.operators = {} - - self.last_execution = { - "maa": None, - "recruit": None, - } + self.last_execution = {"maa": None, "recruit": None, "todo": None} self.sign_in = (datetime.now() - timedelta(days=1, hours=4)).date() self.daily_report = (datetime.now() - timedelta(days=1, hours=4)).date() @@ -144,7 +138,6 @@ def run(self) -> None: self.free_clue = None if self.credit_fight is not None and self.credit_fight != get_server_weekday(): self.credit_fight = None - logger.debug(self.credit_fight) self.todo_task = False self.collect_notification = False self.planned = False @@ -152,6 +145,7 @@ def run(self) -> None: self.initialize_operators() self.op_data.correct_dorm() self.backup_plan_solver(PlanTriggerTiming.BEGINNING) + logger.debug("当前任务: " + ("||".join([str(t) for t in self.tasks]))) return super().run() def transition(self) -> None: @@ -328,7 +322,12 @@ def plan_fia(self): return # 肥鸭充能新模式:https://github.com/ArkMowers/arknights-mower/issues/551 target = None - fia_threshold = 0.9 + if not config.conf.fia_fool: + fia_threshold = config.conf.fia_threshold + logger.info(f"菲亚防呆设计未开启,菲亚阈值为{fia_threshold}") + else: + fia_threshold = 0.9 + logger.info(f"菲亚防呆设计已开启,菲亚阈值为{fia_threshold}") for operator in fia_plan: data = self.op_data.operators[operator] operator_morale = data.current_mood() @@ -362,10 +361,24 @@ def plan_fia(self): continue target = operator break + # 若全部跳过且关闭防呆则令目标干员为心情最低干员 + if target is None and not config.conf.fia_fool: + target = fia_plan[0] + op_mood = 24 + for op in fia_plan: + data = self.op_data.operators[op] + op_mood_t = data.current_mood() + if data.rest_in_full and data.exhaust_require and not data.is_resting(): + logger.debug(f"{operator}为暖机干员但不在宿舍,跳过充能") + continue + if op_mood_t < op_mood: + target = op + op_mood = op_mood_t if target: self.tasks.append( SchedulerTask( time=self.task.time, + task_type=TaskTypes.FIAMMETTA, task_plan={fia_room: [target, "菲亚梅塔"]}, ) ) @@ -389,10 +402,12 @@ def plan_fia(self): def plan_metadata(self): planned_index = [] + # 移除当前 SHIFT_ON 重新刷新 for t in self.tasks: if "dorm" in t.meta_data: planned_index.extend([int(w[4:]) for w in t.meta_data.split(",")]) _time = datetime.max + min_resting_time = datetime.max _plan = {} _type = [] # 第一个心情低的且小于3 则只休息半小时 @@ -419,6 +434,15 @@ def plan_metadata(self): is not None ): short_rest = True + if not short_rest: + for x in self.total_agent: + if ( + not x.workaholic + and not x.exhaust_require + and x.room not in ["factory", "train"] + ): + min_resting_time = min(min_resting_time, x.predict_exhaust()) + logger.debug(f"预测最低休息时间为:{min_resting_time}") low_priority = [] for idx, dorm in enumerate(self.op_data.dorm): logger.debug(f"开始计算{dorm}") @@ -431,6 +455,7 @@ def plan_metadata(self): # 如果是rest in full,则新增单独任务.. if ( _name in self.op_data.operators.keys() + and self.op_data.operators[_name].is_high() and self.op_data.operators[_name].rest_in_full ): __plan = {} @@ -446,7 +471,11 @@ def plan_metadata(self): __time = dorm.time else: __time = datetime.max + need_early = True for x in __rest_agent: + # 如果小组内没有耗尽,则提前8分钟上班 + if self.op_data.operators[x].exhaust_require: + need_early = False # 如果同小组也是rest_in_full则取最大休息时间 否则忽略 if x in low_priority: logger.debug("检测到回满组已经安排") @@ -465,6 +494,7 @@ def plan_metadata(self): if _idx is not None: __type.append("dorm" + str(_idx)) planned_index.append(_idx) + logger.debug(f"计划干员为{x}") __room = self.op_data.operators[x].room if __room not in __plan.keys(): __plan[__room] = ["Current"] * len(self.op_data.plan[__room]) @@ -472,6 +502,9 @@ def plan_metadata(self): if __time < datetime.now(): __time = datetime.now() if __time != datetime.max: + if need_early: + __time -= timedelta(minutes=8) + logger.info("全组无耗尽,提前8分钟上班") self.tasks.append( SchedulerTask( time=__time, @@ -542,6 +575,7 @@ def plan_metadata(self): # 生成单个任务 if len(_plan.items()) > 0: if _time != datetime.max: + _time = min(_time, min_resting_time) _time -= timedelta(minutes=8) if _time < datetime.now(): _time = datetime.now() @@ -567,6 +601,17 @@ def plan_metadata(self): def should_keep(task): if task.type != TaskTypes.RELEASE_DORM: return True + elif len(task.plan) == 0: + return False + name = task.meta_data + free_room = list(task.plan.keys())[0] + free_op = task.plan[free_room] + if ( + self.op_data.operators[name].current_room != free_room + or free_op[self.op_data.operators[name].current_index] != "Free" + ): + logger.info(f"检测到{task.meta_data}不在对应位置,移除相关任务") + return False i, d = self.op_data.get_dorm_by_name(task.meta_data) if i is None: logger.info(f"检测到{task.meta_data}不在宿舍,移除相关任务") @@ -574,6 +619,8 @@ def should_keep(task): return True self.tasks = [t for t in self.tasks if should_keep(t)] + merge_interval = config.conf.merge_interval + merge_release_dorm(self.tasks, merge_interval) def infra_main(self): """位于基建首页""" @@ -604,10 +651,12 @@ def infra_main(self): free_agent.name ) if idx is not None: - if update_task := self.find_next_task( + update_task = find_next_task( + self.tasks, task_type=TaskTypes.SHIFT_ON, meta_data="dorm" + str(idx), - ): + ) + if update_task: logger.debug("开始更新宿舍信息") dorm_list = update_task.meta_data.split( "," @@ -618,7 +667,12 @@ def infra_main(self): ) free_agent.mood = free_agent.upper_limit free_agent.time_stamp = dorm.time - self.task.plan = {} + else: + self.task.plan = {} + else: + self.task.plan = {} + else: + self.task.plan = {} if ( config.conf.run_order_grandet_mode.back_to_index and TaskTypes.RUN_ORDER == self.task.type @@ -632,14 +686,12 @@ def infra_main(self): self.refresh_connecting = False self.agent_arrange(self.task.plan, get_time) if get_time: - self.backup_plan_solver(PlanTriggerTiming.BEFORE_PLANNING) - if ( - self.find_next_task(datetime.now() + timedelta(seconds=15)) - is None + if not self.backup_plan_solver( + PlanTriggerTiming.BEFORE_PLANNING ): self.plan_metadata() else: - logger.info("检测到15秒内有额外任务,跳过plan") + logger.info("检测到排班表切换,跳过plan") if TaskTypes.RE_ORDER == self.task.type: self.skip() # 如果任务名称包含干员名,则为动态生成的 @@ -655,7 +707,7 @@ def infra_main(self): self.last_clue = None self.clue_new() self.last_clue = datetime.now() - self.skip(["planned", "collect_notification"]) + self.skip(["collect_notification"]) elif self.task.type == TaskTypes.REFRESH_TIME: if self.task.meta_data == "train": if upgrade := self.find_next_task( @@ -664,7 +716,7 @@ def infra_main(self): self.refresh_skill_time(upgrade) else: self.plan_run_order(self.task.meta_data) - self.skip(["planned", "todo_task", "collect_notification"]) + self.skip(["todo_task", "collect_notification"]) elif self.task.type == TaskTypes.SKILL_UPGRADE: self.skill_upgrade(self.task.meta_data) del self.tasks[0] @@ -719,10 +771,6 @@ def infra_main(self): ): self.clue_new() self.last_clue = datetime.now() - # if (self.party_time is None or self.free_clue is None) and self.enable_party: - # self.clue() - # if self.clue_count > self.clue_count_limit and self.enable_party: - # self.share_clue() if ( self.drone_room not in self.op_data.run_order_rooms and ( @@ -787,19 +835,25 @@ def agent_get_mood(self, skip_dorm=False, force=False): for room in need_read: error_count = 0 # 由于训练室不纠错,如果训练室有干员且时间读取过就跳过 - if room == "train": - first = next( - ( - (value) - for key, value in self.op_data.operators.items() - if value.current_room == "train" - ), - None, + current_working = [ + value + for key, value in self.op_data.operators.items() + if value.current_room == room + ] + + if current_working and all( + operator.time_stamp + and operator.time_stamp + > datetime.now() + - timedelta( + hours=0.5 if operator.name in ["歌蕾蒂娅", "见行者"] else 2.5 ) - if first is not None and first.time_stamp > datetime.now() - timedelta( - hours=2.5 - ): - continue + for operator in current_working + ): + for e in current_working: + logger.debug(e.time_stamp) + logger.debug(f"{room} 所有干员不满足扫描条件,跳过") + continue while True: try: self.enter_room(room) @@ -809,7 +863,6 @@ def agent_get_mood(self, skip_dorm=False, force=False): for item in _mood_data ] logger.info(f"房间 {self.translate_room(room)} {mood_info}") - # logger.info(f'房间 {room} 心情为:{_mood_data}') break except MowerExit: raise @@ -1196,7 +1249,7 @@ def plan_run_order(self, room): in_out_plan[room][idx] = x.replacement[0] self.tasks.append( SchedulerTask( - time=self.get_run_roder_time(room), + time=self.get_run_order_time(room), task_plan=in_out_plan, task_type=TaskTypes.RUN_ORDER, meta_data=room, @@ -1418,21 +1471,22 @@ def plan_solver(self): except Exception as e: logger.exception(e) # 如果下个 普通任务 >5 分钟则补全宿舍 - logger.debug("tasks:" + str(self.tasks)) if self.find_next_task(datetime.now() + timedelta(seconds=15)): logger.info("有其他任务,跳过宿舍纠错") return if self.agent_get_mood() is None: self.backup_plan_solver() + if not self.find_next_task(datetime.now() + timedelta(minutes=5)): + try_add_release_dorm({}, None, self.op_data, self.tasks) def backup_plan_solver(self, timing=None): if timing is None: timing = PlanTriggerTiming.END try: + new_task = False if self.op_data.backup_plans: con = copy.deepcopy(self.op_data.plan_condition) current_con = self.op_data.plan_condition - new_task = False for idx, bp in enumerate(self.op_data.backup_plans): func = str(bp.trigger) logger.debug(func) @@ -1459,10 +1513,12 @@ def backup_plan_solver(self, timing=None): self.op_data.swap_plan(con, refresh=True) if not new_task: self.tasks.append(SchedulerTask(task_plan={})) + return new_task except MowerExit: raise except Exception as e: logger.exception(e) + return False def rearrange_resting_priority(self, group): operators = self.op_data.groups[group] @@ -1525,10 +1581,15 @@ def get_resting_plan(self, agents, exist_replacement, plan, high_free, low_free) # 对于252可能需要进行额外判定,由于 low_free 性质等同于 high_free success = True if high_free - _high >= 0 and low_free - _low >= 0: + logger.debug(f"计算排班:{agents}") for agent in agents: if not success: break x = self.op_data.operators[agent] + if x.room not in base_room_list: + logger.debug(f"干员房间出错:{agent}") + success = False + break if self.op_data.get_dorm_by_name(x.name)[0] is not None: # 如果干员已经被安排了 success = False @@ -1649,7 +1710,7 @@ def check_fia(self): ].replacement, self.op_data.operators["菲亚梅塔"].room return None, None - def get_run_roder_time(self, room): + def get_run_order_time(self, room): logger.info("基建:读取插拔时间") # 点击进入该房间 self.enter_room(room) @@ -1678,15 +1739,19 @@ def todo_list(self) -> None: """处理基建 Todo 列表""" tapped = False collect = {"bill": "订单", "factory": "制造站产物", "trust": "信赖"} - for res, name in collect.items(): - tap_times = 0 - while pos := self.find(f"infra_collect_{res}"): - logger.info(f"收取{name}") - self.tap(pos) - tapped = True - tap_times += 1 - if tap_times > 5: - break + if self.last_execution["todo"] is None or self.last_execution[ + "todo" + ] < datetime.now() - timedelta(minutes=15): + for res, name in collect.items(): + tap_times = 0 + while pos := self.find(f"infra_collect_{res}"): + logger.info(f"收取{name}") + self.tap(pos) + tapped = True + tap_times += 1 + if tap_times > 0: + break + self.last_execution["todo"] = datetime.now() if not tapped: # 点击右上角的通知图标 # 可能被产物收取提示挡住,所以直接点位置 @@ -2151,6 +2216,12 @@ def drone( if not self.waiting_solver(): return self.recog.update() + wait = 0 + while self.find("order_ready", scope=((450, 675), (600, 750))) is None: + if wait > 6: + break + self.sleep(1) + self.recog.save_screencap("run_order") if not ( self.drone_room is None or ( @@ -2312,19 +2383,27 @@ def tap_confirm(self, room, new_plan=None): if wait_confirm > 0: logger.info(f"等待跑单 {str(wait_confirm)} 秒") self.sleep(wait_confirm) - self.tap_element("confirm_blue") - if self.find("arrange_confirm"): + retry_count = 0 + while self.find("confirm_blue") and retry_count < 4: + self.tap_element("confirm_blue") + self.sleep(0.5) + self.recog.update() + retry_count += 1 + retry_count = 0 + while self.find("arrange_confirm") and retry_count < 4: _x0 = self.recog.w // 3 * 2 # double confirm _y0 = self.recog.h - 10 self.tap((_x0, _y0)) + self.sleep(0.5) + self.recog.update() def choose_train_agent( self, current_room, agents, idx, error_count=0, fast_mode=False ): if current_room[idx] != agents[idx]: while ( - self.find("arrange_order_options") is None - and self.find("confirm_blue") is None + # self.find("arrange_order_options",scope=((1785, 0), (1920, 128))) is None + self.find("confirm_blue") is None ): if error_count > 3: raise Exception("未成功进入干员选择界面") @@ -2365,6 +2444,7 @@ def choose_agent( "dorm" ): agents[idx] = "Free" + __agent.depletion_rate = 0 logger.info("检测满心情释放休息位") elif agents[idx] == "Free" and self.task.type != TaskTypes.RE_ORDER: if self.op_data.config.free_room: @@ -2411,12 +2491,12 @@ def choose_agent( right_swipe = 0 retry_count = 0 selected = [] - logger.info(f"上次进入房间为:{self.last_room},本次房间为:{room}") - self.detail_filter() + logger.debug(f"上次进入房间为:{self.last_room},本次房间为:{room}") + self.profession_filter() if self.detect_arrange_order()[0] == "信赖值": self.switch_arrange_order("工作状态") siege = False # 推进之王 - last_special_filter = "" + last_special_filter = "ALL" while len(agent) > 0: if retry_count > 1: raise Exception("到达最大尝试次数 1次") @@ -2425,7 +2505,7 @@ def choose_agent( right_swipe = self.swipe_left(right_swipe) max_swipe = 50 retry_count += 1 - self.detail_filter() + self.profession_filter() if first_time: # 清空 if is_dorm: @@ -2463,24 +2543,25 @@ def choose_agent( not siege and not is_dorm and agent - and all( - element in ["推进之王", "安哲拉", "斯卡蒂", "幽灵鲨", "乌尔比安"] - for element in agent - ) + and all(element in self.op_data.profession_filter for element in agent) ): siege = True - - if agent[0] in ["推进之王", "安哲拉", "斯卡蒂", "幽灵鲨"]: - self.detail_filter(恢复类后勤=True) - if last_special_filter != "恢复类后勤": + if agent[0] in self.op_data.profession_filter: + profession = agent_profession[agent[0]] + self.profession_filter(profession) + if last_special_filter != profession: right_swipe = 0 - last_special_filter = "恢复类后勤" - else: - self.detail_filter(功能类后勤=True) - if last_special_filter != "功能类后勤": + last_special_filter = profession + elif agent and agent[0] in self.op_data.operators: + ag = self.op_data.operators[agent[0]] + if ag.is_resting() and room.startswith("drom") and ag.name != "阿米娅": + # 只有在宿舍中打开职介筛选 + profession = agent_profession[agent[0]] + self.profession_filter(profession) + if last_special_filter != profession: right_swipe = 0 - last_special_filter = "功能类后勤" - self.switch_arrange_order(3, "true") + last_special_filter = profession + self.switch_arrange_order(3, "true") changed, ret = self.scan_agent(agent) if changed: selected.extend(changed) @@ -2501,7 +2582,8 @@ def choose_agent( if len(agent) == 0: if siege: self.detail_filter() - right_swipe = 0 + if last_special_filter != "ALL": + right_swipe = 0 break # 安排空闲干员 @@ -2512,7 +2594,6 @@ def choose_agent( # 滑动到最左边 self.sleep(interval=0.5) right_swipe = self.swipe_left(right_swipe) - self.detail_filter(未进驻=True) self.switch_arrange_order(3, "true") # 只选择在列表里面的 # 替换组小于20才休息,防止进入就满心情进行网络连接 @@ -2580,6 +2661,7 @@ def choose_agent( self.swipe_left(right_swipe) self.switch_arrange_order(2) if not self.verify_agent(agents): + logger.debug(agents) raise Exception("检测到干员选择错误,重新选择") self.last_room = room @@ -2769,6 +2851,44 @@ def current_room_changed(self, instance): ) for ref_room in ref_rooms: self.refresh_run_order_time(ref_room) + if ( + instance.name in self.op_data.operators + and self.op_data.operators[instance.name].refresh_drained + ): + self.refresh_drained_time() + + def refresh_drained_time(self): + logger.debug("刷新用尽倒计时") + solved = [] + for agent in self.op_data.exhaust_agent: + if agent in solved: + continue + logger.debug(f"开始检查{agent}") + shift_off = self.find_next_task( + datetime.now() + timedelta(hours=24), + task_type=TaskTypes.EXHAUST_OFF, + meta_data=agent, + compare_type="<", + ) + if shift_off: + logger.info(f"移除 {shift_off.meta_data} 用尽下班任务以刷新时间") + exhausts = shift_off.meta_data.split(",") + solved.extend(exhausts) + self.tasks.remove(shift_off) + for o in exhausts: + self.op_data.operators[o].time_stamp = None + else: + self.op_data.operators[agent].time_stamp = None + if solved: + self.tasks.append( + ( + SchedulerTask( + time=datetime.now(), + task_plan={}, + task_type=TaskTypes.NOT_SPECIFIC, + ) + ) + ) def refresh_run_order_time(self, room): logger.debug("检测到插拔房间人员变动!") @@ -2788,14 +2908,12 @@ def refresh_run_order_time(self, room): compare_type="<", ) if run_order_task and run_order_task.time > datetime.now(): + task_time = datetime.now() + if len(self.tasks) > 0 and self.tasks[0].type != TaskTypes.FIAMMETTA: + task_time = self.tasks[0].time - timedelta(seconds=1) logger.info(f"移除{limit}分钟以内的跑单任务以强X刷新时间") self.tasks.remove(run_order_task) logger.info("新增强X刷新跑单时间任务") - task_time = ( - datetime.now() - if len(self.tasks) == 0 - else self.tasks[0].time - timedelta(seconds=1) - ) self.tasks.append( ( SchedulerTask( @@ -2891,7 +3009,7 @@ def agent_arrange_room( if room == "train": self.choose_train(plan[room], choose_error <= 0) else: - while self.find("arrange_order_options", score=0.5) is None: + while self.find("confirm_blue") is None: if error_count > 3: raise Exception("未成功进入干员选择界面") self.ctap((self.recog.w * 0.82, self.recog.h * 0.2)) @@ -2997,11 +3115,24 @@ def agent_arrange(self, plan: tp.BasePlan, get_time=False): if self.scene() in self.waiting_scene: if not self.waiting_solver(): return - self.recog.update() + else: + logger.info("检测到漏单") + send_message("检测到漏单!", level="WARNING") + wait = 0 + while self.find("order_ready", scope=((450, 675), (600, 750))) is None: + if wait > 6: + break + self.recog.update() + self.sleep(0.5) + wait += 1 # 接受当前订单 + not_take = True while ( self.find("order_ready", scope=((450, 675), (600, 750))) is not None ): + if not_take: + self.recog.save_screencap("run_order") + not_take = False self.tap((self.recog.w * 0.25, self.recog.h * 0.25), interval=0.5) if self.drone_room is None or ( self.drone_room == room and room in self.op_data.run_order_rooms @@ -3044,7 +3175,11 @@ def agent_arrange(self, plan: tp.BasePlan, get_time=False): self.skip() elif len(new_plan) > 1: self.tasks.append( - SchedulerTask(time=self.tasks[0].time, task_plan=new_plan) + SchedulerTask( + time=self.tasks[0].time, + task_plan=new_plan, + task_type=TaskTypes.FIAMMETTA, + ) ) # 急速换班 self.skip() @@ -3096,8 +3231,35 @@ def reload(self): @CFUNCTYPE(None, c_int, c_char_p, c_void_p) def log_maa(msg, details, arg): - logger.debug(json.loads(details.decode("utf-8"))) - logger.debug(Message(msg)) + m = Message(msg) + d = json.loads(details.decode("utf-8")) + logger.debug(d) + logger.debug(m) + logger.debug(arg) + if "what" in d and d["what"] == "StageDrops": + global stage_drop + stage_drop["details"].append(d["details"]["drops"]) + stage_drop["summary"] = d["details"]["stats"] + + elif "what" in d and d["what"] == "RecruitTagsSelected": + global recruit_tags_selected + recruit_tags_selected["tags"].append(d["details"]["tags"]) + + elif "what" in d and d["what"] == "RecruitResult": + global recruit_results + temp_dict = { + "tags": d["details"]["tags"], + "level": d["details"]["level"], + "result": d["details"]["result"], + } + recruit_results["results"].append(temp_dict) + + elif "what" in d and d["what"] == "RecruitSpecialTag": + global recruit_special_tags + recruit_special_tags["tags"].append(d["details"]["tags"]) + # elif d.get("what") == "DepotInfo" and d["details"].get("done") is True: + # logger.info(f"开始扫描仓库(MAA)") + # process_itemlist(d) def initialize_maa(self): config.stop_maa.clear() @@ -3117,9 +3279,25 @@ def initialize_maa(self): logger.exception(f"Maa Python模块导入失败:{str(e)}") raise Exception("Maa Python模块导入失败") + try: + logger.debug("开始更新Maa活动关卡导航……") + ota_tasks_url = ( + "https://ota.maa.plus/MaaAssistantArknights/api/resource/tasks.json" + ) + ota_tasks_path = path / "cache" / "resource" / "tasks.json" + ota_tasks_path.parent.mkdir(parents=True, exist_ok=True) + with urllib.request.urlopen(ota_tasks_url, timeout=60) as u: + res = u.read().decode("utf-8") + with open(ota_tasks_path, "w", encoding="utf-8") as f: + f.write(res) + logger.info("Maa活动关卡导航更新成功") + except Exception as e: + logger.error(f"Maa活动关卡导航更新失败:{str(e)}") + Asst.load(path=path, incremental_path=path / "cache") self.MAA = Asst(callback=self.log_maa) + self.stages = [] self.MAA.set_instance_option( InstanceOptionType.touch_type, conf.maa_touch_option ) @@ -3131,12 +3309,63 @@ def initialize_maa(self): logger.info("MAA 连接失败") raise Exception("MAA 连接失败") - def maa_plan_solver(self): + def append_maa_task(self, type): + if type in ["StartUp", "Visit", "Award"]: + self.MAA.append_task(type) + elif type == "Fight": + conf = config.conf + server_weekday = get_server_weekday() + _plan = conf.maa_weekly_plan[server_weekday] + logger.info(f"现在服务器是{_plan.weekday}") + use_medicine = False + if conf.maa_expiring_medicine: + if conf.exipring_medicine_on_weekend: + use_medicine = server_weekday >= 5 + else: + use_medicine = True + for stage in _plan.stage: + logger.info(f"添加关卡:{stage}") + self.MAA.append_task( + "Fight", + { + # 空值表示上一次 + # 'stage': '', + "stage": stage, + "medicine": _plan.medicine, + "stone": 999 if conf.maa_eat_stone else 0, + "times": 999, + "report_to_penguin": True, + "client_type": "", + "penguin_id": "", + "DrGrandet": False, + "server": "CN", + "expiring_medicine": 999 if use_medicine else 0, + }, + ) + self.stages.append(stage) + elif type == "Mall": + conf = config.conf + self.MAA.append_task( + "Mall", + { + "shopping": True, + "buy_first": conf.maa_mall_buy.split(","), + "blacklist": conf.maa_mall_blacklist.split(","), + "credit_fight": conf.maa_credit_fight + and "" not in self.stages + and self.credit_fight is None, + "select_formation": conf.credit_fight.squad, + "force_shopping_if_credit_full": conf.maa_mall_ignore_blacklist_when_full, + }, + ) + + def maa_plan_solver(self, tasks="All", one_time=False): """清日常""" try: conf = config.conf if ( - self.last_execution["maa"] is not None + not one_time + and self.last_execution["maa"] is not None and ( delta := ( timedelta(hours=conf.maa_gap) @@ -3148,37 +3377,74 @@ def maa_plan_solver(self): ): logger.info(f"{format_time(delta.total_seconds())}后开始做日常任务") else: - send_message("开始刷理智") - plan_today = conf.maa_weekly_plan[get_server_weekday()] - stage_today = plan_today.stage - nav_solver = NavigationSolver(self.device, self.recog) - ope_solver = OperationSolver(self.device, self.recog) - drain = True - for name in stage_today: - if self.tasks[0].time - datetime.now() < timedelta(minutes=5): - drain = False - break - if nav_solver.run(name): - drain = drain and ope_solver.run(self.tasks[0].time) - mission_solver = MissionSolver(self.device, self.recog) - mission_solver.run() - logger.debug(self.credit_fight) - if ( - config.conf.maa_credit_fight - and "" not in stage_today - and self.credit_fight is None - and self.tasks[0].time - datetime.now() > timedelta(minutes=3) - ): - credit_fight = CreditFight(self.device, self.recog) - credit_fight.run() - self.credit_fight = get_server_weekday() - logger.debug(self.credit_fight) - if drain: - self.last_execution["maa"] = datetime.now() - send_message("刷理智结束") + send_message("启动MAA") + self.back_to_index() + # 任务及参数请参考 docs/集成文档.md + self.initialize_maa() + if tasks == "All": + tasks = ["StartUp", "Fight", "Mall", "Award"] + for maa_task in tasks: + self.append_maa_task(maa_task) + self.MAA.start() + stop_time = None + if one_time: + stop_time = datetime.now() + timedelta(minutes=5) else: - send_message("理智没有刷完") + global stage_drop + stage_drop = {"details": [], "summary": {}} + + logger.info("MAA 启动") + hard_stop = False + while self.MAA.running(): + # 单次任务默认5分钟 + if one_time and stop_time < datetime.now(): + self.MAA.stop() + hard_stop = True + # 5分钟之前就停止 + elif ( + not one_time + and (self.tasks[0].time - datetime.now()).total_seconds() < 300 + ): + self.MAA.stop() + hard_stop = True + elif config.stop_maa.is_set(): + self.MAA.stop() + hard_stop = True + else: + self.sleep(5) + if hard_stop: + hard_stop_msg = "Maa任务未完成,等待3分钟关闭游戏" + logger.info(hard_stop_msg) + send_message(hard_stop_msg) + self.sleep(180) + self.device.exit() + if self.device.check_current_focus(): + self.recog.update() + elif not one_time: + logger.info("记录MAA 本次执行时间") + self.last_execution["maa"] = datetime.now() + logger.info(self.last_execution["maa"]) + if "Mall" in tasks and self.credit_fight is None: + self.credit_fight = get_server_weekday() + logger.info("记录首次信用作战") + logger.debug(stage_drop) + # 有掉落东西再发 + if stage_drop["details"]: + send_message( + maa_template.render(stage_drop=stage_drop), + "Maa停止", + ) + else: + send_message("Maa单次任务停止") + if ( + self.find_next_task(datetime.now() + timedelta(minutes=15)) + is None + ): + logger.debug( + "Maa单次任务结束15分钟内没有其他任务,新增单次任务防止漏单" + ) + self.tasks.insert(0, SchedulerTask(time=datetime.now())) conf = config.conf now_time = datetime.now().time() try: @@ -3331,7 +3597,7 @@ def visit_friend_plan_solver(self): def sign_in_plan_solver(self): if not config.conf.sign_in.enable: return - hot_update.update() + # hot_update.update() try: import sign_in diff --git a/arknights_mower/solvers/cultivate_depot.py b/arknights_mower/solvers/cultivate_depot.py index bbf847c5..da74a61c 100644 --- a/arknights_mower/solvers/cultivate_depot.py +++ b/arknights_mower/solvers/cultivate_depot.py @@ -1,166 +1,42 @@ -import hashlib -import hmac import json -import time -from urllib import parse import requests from arknights_mower.utils import config -from arknights_mower.utils.log import logger from arknights_mower.utils.path import get_path - -app_code = "4ca99fa6b56cc2ba" - -# 签到url -sign_url = "https://zonai.skland.com/api/v1/game/attendance" -# 绑定的角色url -binding_url = "https://zonai.skland.com/api/v1/game/player/binding" -# 验证码url -login_code_url = "https://as.hypergryph.com/general/v1/send_phone_code" -# 验证码登录 -token_phone_code_url = "https://as.hypergryph.com/user/auth/v2/token_by_phone_code" -# 密码登录 -token_password_url = "https://as.hypergryph.com/user/auth/v1/token_by_phone_password" -# 使用token获得认证代码 -grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant" -# 使用认证代码获得cred -cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code" +from arknights_mower.utils.skland import ( + get_binding_list, + get_cred_by_token, + get_sign_header, + header, + log, +) class cultivate: def __init__(self): self.record_path = get_path("@app/tmp/cultivate.json") - self.header = { - "cred": "", - "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0", - "Accept-Encoding": "gzip", - "Connection": "close", - } - self.header_login = { - "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0", - "Accept-Encoding": "gzip", - "Connection": "close", - } - self.reward = [] - - self.header_for_sign = {"platform": "", "timestamp": "", "dId": "", "vName": ""} self.sign_token = "" self.all_recorded = True def start(self): for item in config.conf.skland_info: if item.isCheck: - self.save_param(self.get_cred_by_token(self.log(item))) - for i in self.get_binding_list(): + self.save_param(get_cred_by_token(log(item))) + for i in get_binding_list(self.sign_token): if item.cultivate_select == i.get("isOfficial"): body = {"gameId": 1, "uid": i.get("uid")} ingame = f"https://zonai.skland.com/api/v1/game/cultivate/player?uid={i.get('uid')}" resp = requests.get( ingame, - headers=self.get_sign_header( - ingame, "get", body, self.header + headers=get_sign_header( + ingame, "get", body, self.sign_token ), ).json() with open(self.record_path, "w", encoding="utf-8") as file: json.dump(resp, file, ensure_ascii=False, indent=4) def save_param(self, cred_resp): - self.header["cred"] = cred_resp["cred"] + header["cred"] = cred_resp["cred"] self.sign_token = cred_resp["token"] - - def log(self, account): - r = requests.post( - token_password_url, - json={"phone": account.account, "password": account.password}, - headers=self.header_login, - ).json() - if r.get("status") != 0: - raise Exception(f'获得token失败:{r["msg"]}') - return r["data"]["token"] - - def get_cred_by_token(self, token): - return self.get_cred(self.get_grant_code(token)) - - def get_grant_code(self, token): - response = requests.post( - grant_code_url, - json={"appCode": app_code, "token": token, "type": 0}, - headers=self.header_login, - ) - resp = response.json() - if response.status_code != 200: - raise Exception(f"获得认证代码失败:{resp}") - if resp.get("status") != 0: - raise Exception(f'获得认证代码失败:{resp["msg"]}') - return resp["data"]["code"] - - def get_cred(self, grant): - resp = requests.post( - cred_code_url, json={"code": grant, "kind": 1}, headers=self.header_login - ).json() - if resp["code"] != 0: - raise Exception(f'获得cred失败:{resp["message"]}') - return resp["data"] - - def get_binding_list(self): - v = [] - resp = requests.get( - binding_url, - headers=self.get_sign_header(binding_url, "get", None, self.header), - ).json() - - if resp["code"] != 0: - logger.warning(f"请求角色列表出现问题:{resp['message']}") - if resp.get("message") == "用户未登录": - logger.warning("用户登录可能失效了,请重新运行此程序!") - return [] - for i in resp["data"]["list"]: - if i.get("appCode") != "arknights": - continue - v.extend(i.get("bindingList")) - return v - - def get_sign_header(self, url: str, method, body, old_header): - h = json.loads(json.dumps(old_header)) - p = parse.urlparse(url) - if method.lower() == "get": - h["sign"], header_ca = self.generate_signature( - self.sign_token, p.path, p.query - ) - else: - h["sign"], header_ca = self.generate_signature( - self.sign_token, p.path, json.dumps(body) - ) - for i in header_ca: - h[i] = header_ca[i] - return h - - def generate_signature(self, token: str, path, body_or_query): - """ - 获得签名头 - 接口地址+方法为Get请求?用query否则用body+时间戳+ 请求头的四个重要参数(dId,platform,timestamp,vName).toJSON() - 将此字符串做HMAC加密,算法为SHA-256,密钥token为请求cred接口会返回的一个token值 - 再将加密后的字符串做MD5即得到sign - :param token: 拿cred时候的token - :param path: 请求路径(不包括网址) - :param body_or_query: 如果是GET,则是它的query。POST则为它的body - :return: 计算完毕的sign - """ - # 总是说请勿修改设备时间,怕不是yj你的服务器有问题吧,所以这里特地-2 - - t = str(int(time.time()) - 2) - token = token.encode("utf-8") - header_ca = json.loads(json.dumps(self.header_for_sign)) - header_ca["timestamp"] = t - header_ca_str = json.dumps(header_ca, separators=(",", ":")) - s = path + body_or_query + t + header_ca_str - hex_s = hmac.new(token, s.encode("utf-8"), hashlib.sha256).hexdigest() - md5 = ( - hashlib.md5(hex_s.encode("utf-8")) - .hexdigest() - .encode("utf-8") - .decode("utf-8") - ) - return md5, header_ca diff --git a/arknights_mower/solvers/navigation.py b/arknights_mower/solvers/navigation.py index fa3d500b..1b5ccedd 100644 --- a/arknights_mower/solvers/navigation.py +++ b/arknights_mower/solvers/navigation.py @@ -3,7 +3,6 @@ import cv2 from arknights_mower.models import navigation -from arknights_mower.utils import hot_update from arknights_mower.utils.graph import SceneGraphSolver from arknights_mower.utils.image import thres2 from arknights_mower.utils.log import logger @@ -150,10 +149,10 @@ def run(self, name: str): self.success = False self.act = None - hot_update.update() - if name in hot_update.navigation.NavigationSolver.location: - hot_update.navigation.NavigationSolver(self.device, self.recog).run(name) - return True + # hot_update.update() + # if name in hot_update.navigation.NavigationSolver.location: + # hot_update.navigation.NavigationSolver(self.device, self.recog).run(name) + # return True self.name = name prefix = name.split("-")[0] diff --git a/arknights_mower/solvers/record.py b/arknights_mower/solvers/record.py index e968f927..38f0e9b6 100644 --- a/arknights_mower/solvers/record.py +++ b/arknights_mower/solvers/record.py @@ -1,4 +1,5 @@ # 用于记录Mower操作行为 +import pickle import sqlite3 from datetime import datetime @@ -73,6 +74,94 @@ def wrapper(self, name, mood, current_room, current_index, update_time=False): return wrapper +def save_state(func): + def save_wrapper(*args, **kwargs): + from arknights_mower.__main__ import base_scheduler + + saved_state = { + "dorm": base_scheduler.op_data.dorm, + "tasks": base_scheduler.tasks, + "party_time": base_scheduler.op_data.party_time, + "operators": base_scheduler.op_data.operators, + "daily_visit_friend": base_scheduler.daily_visit_friend, + "daily_report": base_scheduler.daily_report, + "daily_skland": base_scheduler.daily_skland, + "daily_mail": base_scheduler.daily_mail, + "task_count": base_scheduler.task_count, + "skill_upgrade_supports": base_scheduler.op_data.skill_upgrade_supports, + } + + # Call the original function + result = func(*args, **kwargs) + + # Save saved_state to database + current_time = datetime.now() + database_path = get_path("@app/tmp/data.db") + + try: + # Create 'tmp' directory if it doesn't exist + get_path("@app/tmp").mkdir(exist_ok=True) + + connection = sqlite3.connect(database_path) + cursor = connection.cursor() + + # Create a table if it doesn't exist + cursor.execute( + "CREATE TABLE IF NOT EXISTS saved_state (" "time TEXT," "state BLOB" ")" + ) + + # Delete the previous saved state + cursor.execute("DELETE FROM saved_state") + + # Insert data + cursor.execute( + "INSERT INTO saved_state VALUES (?, ?)", + ( + str(current_time), + sqlite3.Binary(pickle.dumps(saved_state)), # Serialize saved_state + ), + ) + + connection.commit() + connection.close() + + logger.info(f"储存缓存数据至数据库 {current_time}") + + except sqlite3.Error as e: + logger.error(f"SQLite error: {e}") + + return result + + return save_wrapper + + +def load_state(): + # Initialize an empty variable to hold the loaded state + loaded_state = None + database_path = get_path("@app/tmp/data.db") + + try: + connection = sqlite3.connect(database_path) + cursor = connection.cursor() + + # Query the last saved state + cursor.execute("SELECT state FROM saved_state ORDER BY time DESC LIMIT 1") + row = cursor.fetchone() + + if row is not None: + loaded_state = pickle.loads(row[0]) # Deserialize the state + logger.info("成功从数据库载入缓存数据") + else: + logger.debug("No saved state found in the database") + + connection.close() + + except sqlite3.Error as e: + logger.error(f"SQLite error: {e}") + + return loaded_state + + def get_work_rest_ratios(): # TODO 整理数据计算工休比 database_path = get_path("@app/tmp/data.db") @@ -93,6 +182,8 @@ def get_work_rest_ratios(): AND b.is_high = 1 AND b.current_room NOT LIKE 'dormitory%' UNION SELECT '菲亚梅塔' AS name + UNION + SELECT '歌蕾蒂娅' AS name ) AS subquery ON a.name = subquery.name WHERE DATE(a.current_time) >= DATE('now', '-1 month', 'localtime') ORDER BY a.current_time; @@ -163,6 +254,8 @@ def get_mood_ratios(): AND b.is_high = 1 AND b.current_room NOT LIKE 'dormitory%' UNION SELECT '菲亚梅塔' AS name + UNION + SELECT '歌蕾蒂娅' AS name ) AS subquery ON a.name = subquery.name WHERE DATE(a.current_time) >= DATE('now', '-7 day', 'localtime') ORDER BY a.agent_group DESC, a.current_time; diff --git a/arknights_mower/solvers/recruit.py b/arknights_mower/solvers/recruit.py index 150bb790..27191f20 100644 --- a/arknights_mower/solvers/recruit.py +++ b/arknights_mower/solvers/recruit.py @@ -3,23 +3,24 @@ from itertools import combinations import cv2 +import numpy as np from arknights_mower import __rootdir__ from arknights_mower.data import ( agent_with_tags, recruit_agent, ) +from arknights_mower.models import noto_sans, riic_base_digits from arknights_mower.utils import config from arknights_mower.utils.device.device import Device from arknights_mower.utils.email import recruit_rarity, recruit_template, send_message from arknights_mower.utils.graph import SceneGraphSolver -from arknights_mower.utils.image import cropimg, thres2 +from arknights_mower.utils.image import cmatch, cropimg, loadres, thres2 from arknights_mower.utils.log import logger from arknights_mower.utils.recognize import Recognizer, Scene from arknights_mower.utils.vector import va -with lzma.open(f"{__rootdir__}/models/riic_base_digits.pkl", "rb") as f: - number = pickle.load(f) +number = riic_base_digits with lzma.open(f"{__rootdir__}/models/recruit_result.pkl", "rb") as f: recruit_res_template = pickle.load(f) with lzma.open(f"{__rootdir__}/models/recruit.pkl", "rb") as f: @@ -63,39 +64,47 @@ def __init__(self, device: Device = None, recog: Recognizer = None) -> None: # 默认要支援机械 self.recruit_order_index = 2 self.recruit_order = [6, 5, 1, 4, 3, 2] - + self.refresh = False self.result_agent = {} + self.tags = {} self.ticket_number = None + self.normal = loadres("recruit/choose_template/normal") + self.rare = loadres("recruit/choose_template/rare") + def run(self): self.add_recruit_param() super().run() logger.info(self.result_agent) - + recruit_results = {} if self.agent_choose: - logger.info("开包汇总如下") for pos in self.agent_choose: + if self.agent_choose[pos]["choosed"] is False: + continue agent = [] - for item in self.agent_choose[pos]["result"]: - agent.append(item["name"]) - logger.info( - "{}:[".format(pos) - + ",".join(self.agent_choose[pos]["tags"]) - + "]:{}".format(",".join(agent)) - ) + if self.agent_choose[pos]["result"]: + for item in self.agent_choose[pos]["result"]: + agent.append(item["name"]) + recruit_results[pos] = self.agent_choose[pos] + logger.info( + "{}:[".format(pos) + + ",".join(self.agent_choose[pos]["tags"]) + + "]:{}".format(",".join(agent)) + ) + logger.info(recruit_results) if self.agent_choose or self.result_agent: logger.info("招募汇总如下") send_message( recruit_template.render( - recruit_results=self.agent_choose, + recruit_results=recruit_results, recruit_get_agent=self.result_agent, - permit_count=config.conf.recruitment_permit, + permit_count=self.ticket_number, title_text="公招汇总", ), "公招汇总通知", "INFO", ) - return self.agent_choose, self.result_agent + return recruit_results, self.result_agent def transition(self) -> bool: if (scene := self.scene()) == Scene.RECRUIT_MAIN: @@ -145,6 +154,8 @@ def transition(self) -> bool: if self.find("recruit/job_requirements", scope=job_requirements_scope): self.recruit_index = self.recruit_index + 1 + if self.recruit_index in self.agent_choose.keys(): + self.agent_choose[self.recruit_index]["choosed"] = True logger.debug(f"{self.recruit_index}正在招募") return elif pos := self.find("recruit/recruit_lock", scope=recruit_lock_scope): @@ -162,10 +173,95 @@ def transition(self) -> bool: return self.tap(pos) return + else: + self.sleep() elif scene == Scene.RECRUIT_TAGS: self.ticket_number = self.get_ticket_number() - return self.recruit_tags() + + if self.recruit_index not in self.tags.keys() or self.refresh: + tmp_tags = self.get_recruit_tag() + if tmp_tags is False: + self.back() + return + self.tags[self.recruit_index] = tmp_tags + self.refresh = False + logger.info( + f"{self.recruit_index}号位置的tag识别结果{self.tags[self.recruit_index]}" + ) + + if self.recruit_index in self.agent_choose.keys(): + if self.agent_choose[self.recruit_index]["level"] == 3: + if pos := self.find("recruit/refresh"): + self.tap(pos) + del self.tags[self.recruit_index] + del self.agent_choose[self.recruit_index] + self.refresh = True + return + + choose = self.agent_choose[self.recruit_index]["tags"] + tags = self.tags[self.recruit_index] + logger.info(f"选择标签:{choose}") + tag_all_choose = True + for x in choose: + h, w, _ = tag_template[x].shape + tag_img = cropimg(self.recog.img, [tags[x], va(tags[x], (w, h))]) + + if self.tag_not_choosed(tag_img): + tag_all_choose = False + self.tap(tags[x]) + + if tag_all_choose is False: + return + + if self.ticket_number == 0: + self.recruit_index = self.recruit_index + 1 + self.back() + return + + # 默认三星招募时长是9:00 + recruit_time_choose = 540 + recruit_result_level = self.agent_choose[self.recruit_index]["level"] + # 默认一星招募时长是3:50 + if recruit_result_level == 1: + recruit_time_choose = 230 + + if ( + self.ticket_number < config.conf.recruitment_permit + and recruit_result_level == 3 + ): + self.recruit_index = self.recruit_index + 1 + logger.info("没券 返回") + self.back() + return + + recruit_time = [9, 0] + if recruit_time_choose == 230: + recruit_time = [3, 50] + elif recruit_time_choose == 460: + recruit_time = [7, 40] + + now_time = [ + self.get_recruit_time("hour"), + self.get_recruit_time("minute"), + ] + + if now_time[1] != recruit_time[1]: + self.choose_time(now_time[1], recruit_time[1], mode="minute") + return + + if now_time[0] != recruit_time[0]: + self.choose_time(now_time[0], recruit_time[0], mode="hour") + return + + # # start recruit + self.tap_element("recruit/start_recruit") + self.agent_choose[self.recruit_index]["choosed"] = True + self.ticket_number = self.ticket_number - 1 + self.recruit_index = self.recruit_index + 1 + return + else: + self.recruit_tags(self.tags[self.recruit_index]) elif scene == Scene.REFRESH_TAGS: self.tap_element("recruit/refresh_comfirm") elif scene == Scene.RECRUIT_AGENT: @@ -207,10 +303,7 @@ def recruit_result(self): finally: self.tap((500, 500)) - def recruit_tags(self): - tags = self.get_recruit_tag() - logger.debug("tag识别结果{}".format(tags)) - + def recruit_tags(self, tags): tem_res = self.recruit_cal(sorted(tags)) recruit_cal_result = None recruit_result_level = -1 @@ -228,11 +321,7 @@ def recruit_tags(self): recruit_cal_result = [tem_res[recruit_result_level][-1]] else: recruit_cal_result = tem_res[recruit_result_level] - logger.debug(recruit_cal_result) - if recruit_result_level == 3: - if pos := self.find("recruit/refresh"): - self.tap(pos) - return + logger.debug(f"recruit_cal_result:{recruit_cal_result}") if self.recruit_order.index(recruit_result_level) <= self.recruit_order_index: logger.info("稀有tag,发送邮件") @@ -259,81 +348,22 @@ def recruit_tags(self): self.back() return - if self.ticket_number == 0: - self.recruit_index = self.recruit_index + 1 - self.back() + if recruit_result_level != 3: + self.agent_choose[self.recruit_index] = { + "tags": list(recruit_cal_result[-1]["tag"]), + "result": list(recruit_cal_result[-1]["result"]), + "level": recruit_result_level, + "choosed": False, + } return - if ( - self.ticket_number < config.conf.recruitment_permit - and recruit_result_level == 3 - ): - self.recruit_index = self.recruit_index + 1 - logger.info("没券 返回") - self.back() - return + self.agent_choose[self.recruit_index] = { + "tags": [], + "result": [{"id": "", "name": "随机三星干员", "star": 3}], + "level": recruit_result_level, + "choosed": False, + } - choose = [] - if recruit_result_level > 3: - choose = recruit_cal_result[0]["tag"] - - # tap selected tags - logger.info(f"选择标签:{list(choose)} ") - for x in choose: - # 存在choose为空但是进行标签选择的情况 - logger.debug(f"tap{x}:{tags[x]}") - self.tap(tags[x]) - - # 9h为True 3h50min为False - logger.debug("开始选择时长") - # 默认三星招募时长是9:00 - recruit_time_choose = 540 - # 默认一星招募时长是3:50 - if recruit_result_level == 1: - recruit_time_choose = 230 - - if recruit_time_choose == 540: - # 09:00 - logger.debug("时间9h") - self.tap_element("one_hour", 0.2, 0.8, 0.5) - elif recruit_time_choose == 230: - # 03:50 - logger.debug("时间3h50min") - [self.tap_element("one_hour", 0.2, 0.2, 0.5) for _ in range(2)] - [self.tap_element("one_hour", 0.5, 0.2, 0.5) for _ in range(5)] - # elif recruit_time_choose == 460: - # # 07:40 - # logger.debug("时间7h40min") - # [self.tap_element("one_hour", 0.2, 0.8, 0) for _ in range(2)] - # [self.tap_element("one_hour", 0.5, 0.8, 0) for _ in range(2)] - - # start recruit - self.tap_element("recruit/start_recruit") - self.ticket_number = self.ticket_number - 1 - - if recruit_result_level > 3: - self.agent_choose[str(self.recruit_index)] = { - "tags": list(choose), - "result": list(recruit_cal_result[0]["result"]), - } - tmp_res_list = [] - for i in list(recruit_cal_result[0]["result"]): - tmp_res_list.append(i["name"]) - logger.info( - "第{}个位置上的公招预测结果:{}".format( - self.recruit_index, - tmp_res_list, - ) - ) - else: - self.agent_choose[str(self.recruit_index)] = { - "tags": list(choose), - "result": [{"id": "", "name": "随机三星干员", "star": 3}], - } - logger.info( - f'第{self.recruit_index}个位置上的公招预测结果:{"随机三星干员"}' - ) - self.recruit_index = self.recruit_index + 1 return def all_same_res(self, recruit_cal_res, index): @@ -399,7 +429,7 @@ def recruit_cal(self, tags: list[str]): if max_star < 6 and agent["star"] == 6: continue result_dict[item[0]].append(agent) - logger.debug(item[0], agent) + try: for key in list(result_dict.keys()): if len(result_dict[key]) == 0: @@ -432,7 +462,7 @@ def recruit_cal(self, tags: list[str]): logger.debug("{}:{}".format(item, result[item])) return result - def get_recruit_tag(self): + def get_recruit_tag(self) -> dict | bool: up = 520 down = 740 left = 530 @@ -444,6 +474,8 @@ def get_recruit_tag(self): h, w, _ = img.shape for index, value in enumerate(tags_img): + if self.tag_not_choosed(value) is False: + return False max_v = -1 tag_res = None for key in tag_template: @@ -516,3 +548,82 @@ def add_recruit_param(self): if not config.conf.recruit_robot: self.recruit_order = [6, 5, 4, 3, 2, 1] self.recruit_order_index = 1 + + def tag_not_choosed(self, tag: np.ndarray): + if cmatch(tag, self.normal, thresh=80) or cmatch(tag, self.rare, thresh=80): + return False + + return True + + def get_recruit_time( + self, mode="hour" or "minute", height: int | None = 84, thres: int | None = 100 + ): + area = [] + if mode == "hour": + area = [(610, 280), (750, 400)] + elif mode == "minute": + area = [(850, 280), (980, 400)] + + img = cropimg(self.recog.gray, area) + templates = noto_sans + default_height = 28 + + if height and height != default_height: + scale = default_height / height + img = cv2.resize(img, None, None, scale, scale) + img = thres2(img, thres) + contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + rect = [cv2.boundingRect(c) for c in contours] + rect.sort(key=lambda c: c[0]) + + value = 0 + img = cv2.bitwise_not(img) + for x, y, w, h in rect: + digit = cropimg(img, ((x, y), (x + w, y + h))) + digit = cv2.copyMakeBorder( + digit, 10, 10, 10, 10, cv2.BORDER_CONSTANT, None, (0,) + ) + if digit.size < 900: + continue + score = [] + for i in range(10): + im = templates[i] + result = cv2.matchTemplate(digit, im, cv2.TM_SQDIFF_NORMED) + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) + score.append(min_val) + value = value * 10 + score.index(min(score)) + + return value + + def choose_time(self, now_time: int, to_time: int, mode="hour" or "minute"): + subtract_time = now_time - to_time + + click_time = 0 + max_click = 0 + tap_pos = 0 + if mode == "minute": + click_time = int(abs(subtract_time) / 10) + tap_pos = 0.5 + [self.tap_element("one_hour", tap_pos, 0.8, 0.1) for _ in range(click_time)] + return + + if mode == "hour": + click_time = abs(subtract_time) + max_click = 9 + tap_pos = 0.2 + + if abs(subtract_time) > (max_click / 2): + if subtract_time > 0: + [self.tap_element("one_hour", 0.2, 0.2, 0.1) for _ in range(click_time)] + else: + [ + self.tap_element("one_hour", 0.2, 0.8, 0.1) + for _ in range(max_click - click_time) + ] + else: + if subtract_time < 0: + [self.tap_element("one_hour", 0.2, 0.2, 0.1) for _ in range(click_time)] + else: + [self.tap_element("one_hour", 0.2, 0.8, 0.1) for _ in range(click_time)] + + return diff --git a/arknights_mower/solvers/report.py b/arknights_mower/solvers/report.py index 4b47e2b0..5615abae 100644 --- a/arknights_mower/solvers/report.py +++ b/arknights_mower/solvers/report.py @@ -1,10 +1,10 @@ -import datetime import os import cv2 import pandas as pd from arknights_mower.models import noto_sans +from arknights_mower.utils.datetime import get_server_time from arknights_mower.utils.device.device import Device from arknights_mower.utils.digit_reader import DigitReader from arknights_mower.utils.email import report_template, send_message @@ -35,9 +35,7 @@ def __init__( self.record_path = get_path("@app/tmp/report.csv") self.low_range_gray = (100, 100, 100) self.high_range_gray = (255, 255, 255) - self.date = ( - (datetime.datetime.now() - datetime.timedelta(hours=4)).date().__str__() - ) + self.date = get_server_time().date().__str__() self.digitReader = DigitReader() self.report_res = { "作战录像": None, diff --git a/arknights_mower/solvers/skland.py b/arknights_mower/solvers/skland.py index 36b791d2..96fcd228 100644 --- a/arknights_mower/solvers/skland.py +++ b/arknights_mower/solvers/skland.py @@ -1,10 +1,5 @@ import datetime -import hashlib -import hmac -import json import os -import time -from urllib import parse import pandas as pd import requests @@ -12,45 +7,24 @@ from arknights_mower.utils import config from arknights_mower.utils.log import logger from arknights_mower.utils.path import get_path - -app_code = "4ca99fa6b56cc2ba" - -# 签到url -sign_url = "https://zonai.skland.com/api/v1/game/attendance" -# 绑定的角色url -binding_url = "https://zonai.skland.com/api/v1/game/player/binding" -# 验证码url -login_code_url = "https://as.hypergryph.com/general/v1/send_phone_code" -# 验证码登录 -token_phone_code_url = "https://as.hypergryph.com/user/auth/v2/token_by_phone_code" -# 密码登录 -token_password_url = "https://as.hypergryph.com/user/auth/v1/token_by_phone_password" -# 使用token获得认证代码 -grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant" -# 使用认证代码获得cred -cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code" +from arknights_mower.utils.skland import ( + get_binding_list, + get_cred_by_token, + get_sign_header, + header, + header_login, + log, + sign_url, + token_password_url, +) class SKLand: def __init__(self): self.record_path = get_path("@app/tmp/skland.csv") - self.header = { - "cred": "", - "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0", - "Accept-Encoding": "gzip", - "Connection": "close", - } - self.header_login = { - "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0", - "Accept-Encoding": "gzip", - "Connection": "close", - } - self.reward = [] - # 签名请求头一定要这个顺序,否则失败 - # timestamp是必填的,其它三个随便填,不要为none即可 - self.header_for_sign = {"platform": "", "timestamp": "", "dId": "", "vName": ""} + self.sign_token = "" self.all_recorded = True @@ -59,13 +33,14 @@ def start(self): if self.has_record(item.account): continue self.all_recorded = False - self.save_param(self.get_cred_by_token(self.log(item))) - for i in self.get_binding_list(): + self.save_param(get_cred_by_token(log(item))) + for i in get_binding_list(self.sign_token): body = {"gameId": 1, "uid": i.get("uid")} - # list_awards(1, i.get('uid')) resp = requests.post( sign_url, - headers=self.get_sign_header(sign_url, "post", body, self.header), + headers=get_sign_header( + sign_url, "post", body, self.sign_token, header + ), json=body, ).json() if resp["code"] != 0: @@ -93,104 +68,19 @@ def start(self): return False def save_param(self, cred_resp): - self.header["cred"] = cred_resp["cred"] + header["cred"] = cred_resp["cred"] self.sign_token = cred_resp["token"] def log(self, account): r = requests.post( token_password_url, json={"phone": account.account, "password": account.password}, - headers=self.header_login, + headers=header_login, ).json() if r.get("status") != 0: raise Exception(f'获得token失败:{r["msg"]}') return r["data"]["token"] - def get_cred_by_token(self, token): - return self.get_cred(self.get_grant_code(token)) - - def get_grant_code(self, token): - response = requests.post( - grant_code_url, - json={"appCode": app_code, "token": token, "type": 0}, - headers=self.header_login, - ) - resp = response.json() - if response.status_code != 200: - raise Exception(f"获得认证代码失败:{resp}") - if resp.get("status") != 0: - raise Exception(f'获得认证代码失败:{resp["msg"]}') - return resp["data"]["code"] - - def get_cred(self, grant): - resp = requests.post( - cred_code_url, json={"code": grant, "kind": 1}, headers=self.header_login - ).json() - if resp["code"] != 0: - raise Exception(f'获得cred失败:{resp["message"]}') - return resp["data"] - - def get_binding_list(self): - v = [] - resp = requests.get( - binding_url, - headers=self.get_sign_header(binding_url, "get", None, self.header), - ).json() - - if resp["code"] != 0: - print(f"请求角色列表出现问题:{resp['message']}") - if resp.get("message") == "用户未登录": - print("用户登录可能失效了,请重新运行此程序!") - return [] - for i in resp["data"]["list"]: - if i.get("appCode") != "arknights": - continue - v.extend(i.get("bindingList")) - return v - - def get_sign_header(self, url: str, method, body, old_header): - h = json.loads(json.dumps(old_header)) - p = parse.urlparse(url) - if method.lower() == "get": - h["sign"], header_ca = self.generate_signature( - self.sign_token, p.path, p.query - ) - else: - h["sign"], header_ca = self.generate_signature( - self.sign_token, p.path, json.dumps(body) - ) - for i in header_ca: - h[i] = header_ca[i] - return h - - def generate_signature(self, token: str, path, body_or_query): - """ - 获得签名头 - 接口地址+方法为Get请求?用query否则用body+时间戳+ 请求头的四个重要参数(dId,platform,timestamp,vName).toJSON() - 将此字符串做HMAC加密,算法为SHA-256,密钥token为请求cred接口会返回的一个token值 - 再将加密后的字符串做MD5即得到sign - :param token: 拿cred时候的token - :param path: 请求路径(不包括网址) - :param body_or_query: 如果是GET,则是它的query。POST则为它的body - :return: 计算完毕的sign - """ - # 总是说请勿修改设备时间,怕不是yj你的服务器有问题吧,所以这里特地-2 - - t = str(int(time.time()) - 2) - token = token.encode("utf-8") - header_ca = json.loads(json.dumps(self.header_for_sign)) - header_ca["timestamp"] = t - header_ca_str = json.dumps(header_ca, separators=(",", ":")) - s = path + body_or_query + t + header_ca_str - hex_s = hmac.new(token, s.encode("utf-8"), hashlib.sha256).hexdigest() - md5 = ( - hashlib.md5(hex_s.encode("utf-8")) - .hexdigest() - .encode("utf-8") - .decode("utf-8") - ) - return md5, header_ca - def record_log(self): date_str = datetime.datetime.now().strftime("%Y/%m/%d") logger.info(f"存入{date_str}的数据{self.reward}") @@ -227,8 +117,8 @@ def test_connect(self): for item in config.conf.skland_info: if item.isCheck: try: - self.save_param(self.get_cred_by_token(self.log(item))) - for i in self.get_binding_list(): + self.save_param(get_cred_by_token(log(item))) + for i in get_binding_list(self.sign_token): if i["uid"]: res.append( "{}连接成功".format( diff --git a/arknights_mower/utils/SecuritySm.py b/arknights_mower/utils/SecuritySm.py new file mode 100644 index 00000000..2a2db254 --- /dev/null +++ b/arknights_mower/utils/SecuritySm.py @@ -0,0 +1,318 @@ +# from https://gitee.com/FancyCabbage/skyland-auto-sign + +import base64 +import gzip +import hashlib + +# 数美加密方法类 +import json +import time +import uuid + +import requests +from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.ciphers.algorithms import AES +from cryptography.hazmat.primitives.ciphers.base import Cipher +from cryptography.hazmat.primitives.ciphers.modes import CBC, ECB + +# 查询dId请求头 +devices_info_url = "https://fp-it.portal101.cn/deviceprofile/v4" + +# 数美配置 +SM_CONFIG = { + "organization": "UWXspnCCJN4sfYlNfqps", + "appId": "default", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmxMNr7n8ZeT0tE1R9j/mPixoinPkeM+k4VGIn/s0k7N5rJAfnZ0eMER+QhwFvshzo0LNmeUkpR8uIlU/GEVr8mN28sKmwd2gpygqj0ePnBmOW4v0ZVwbSYK+izkhVFk2V/doLoMbWy6b+UnA8mkjvg0iYWRByfRsK2gdl7llqCwIDAQAB", + "protocol": "https", + "apiHost": "fp-it.portal101.cn", +} + +PK = serialization.load_der_public_key(base64.b64decode(SM_CONFIG["publicKey"])) + +DES_RULE = { + "appId": { + "cipher": "DES", + "is_encrypt": 1, + "key": "uy7mzc4h", + "obfuscated_name": "xx", + }, + "box": {"is_encrypt": 0, "obfuscated_name": "jf"}, + "canvas": { + "cipher": "DES", + "is_encrypt": 1, + "key": "snrn887t", + "obfuscated_name": "yk", + }, + "clientSize": { + "cipher": "DES", + "is_encrypt": 1, + "key": "cpmjjgsu", + "obfuscated_name": "zx", + }, + "organization": { + "cipher": "DES", + "is_encrypt": 1, + "key": "78moqjfc", + "obfuscated_name": "dp", + }, + "os": { + "cipher": "DES", + "is_encrypt": 1, + "key": "je6vk6t4", + "obfuscated_name": "pj", + }, + "platform": { + "cipher": "DES", + "is_encrypt": 1, + "key": "pakxhcd2", + "obfuscated_name": "gm", + }, + "plugins": { + "cipher": "DES", + "is_encrypt": 1, + "key": "v51m3pzl", + "obfuscated_name": "kq", + }, + "pmf": { + "cipher": "DES", + "is_encrypt": 1, + "key": "2mdeslu3", + "obfuscated_name": "vw", + }, + "protocol": {"is_encrypt": 0, "obfuscated_name": "protocol"}, + "referer": { + "cipher": "DES", + "is_encrypt": 1, + "key": "y7bmrjlc", + "obfuscated_name": "ab", + }, + "res": { + "cipher": "DES", + "is_encrypt": 1, + "key": "whxqm2a7", + "obfuscated_name": "hf", + }, + "rtype": { + "cipher": "DES", + "is_encrypt": 1, + "key": "x8o2h2bl", + "obfuscated_name": "lo", + }, + "sdkver": { + "cipher": "DES", + "is_encrypt": 1, + "key": "9q3dcxp2", + "obfuscated_name": "sc", + }, + "status": { + "cipher": "DES", + "is_encrypt": 1, + "key": "2jbrxxw4", + "obfuscated_name": "an", + }, + "subVersion": { + "cipher": "DES", + "is_encrypt": 1, + "key": "eo3i2puh", + "obfuscated_name": "ns", + }, + "svm": { + "cipher": "DES", + "is_encrypt": 1, + "key": "fzj3kaeh", + "obfuscated_name": "qr", + }, + "time": { + "cipher": "DES", + "is_encrypt": 1, + "key": "q2t3odsk", + "obfuscated_name": "nb", + }, + "timezone": { + "cipher": "DES", + "is_encrypt": 1, + "key": "1uv05lj5", + "obfuscated_name": "as", + }, + "tn": { + "cipher": "DES", + "is_encrypt": 1, + "key": "x9nzj1bp", + "obfuscated_name": "py", + }, + "trees": { + "cipher": "DES", + "is_encrypt": 1, + "key": "acfs0xo4", + "obfuscated_name": "pi", + }, + "ua": { + "cipher": "DES", + "is_encrypt": 1, + "key": "k92crp1t", + "obfuscated_name": "bj", + }, + "url": { + "cipher": "DES", + "is_encrypt": 1, + "key": "y95hjkoo", + "obfuscated_name": "cf", + }, + "version": {"is_encrypt": 0, "obfuscated_name": "version"}, + "vpw": { + "cipher": "DES", + "is_encrypt": 1, + "key": "r9924ab5", + "obfuscated_name": "ca", + }, +} + +BROWSER_ENV = { + "plugins": "MicrosoftEdgePDFPluginPortableDocumentFormatinternal-pdf-viewer1,MicrosoftEdgePDFViewermhjfbmdgcfjbbpaeojofohoefgiehjai1", + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0", + "canvas": "259ffe69", # 基于浏览器的canvas获得的值,不知道复用行不行 + "timezone": -480, # 时区,应该是固定值吧 + "platform": "Win32", + "url": "https://www.skland.com/", # 固定值 + "referer": "", + "res": "1920_1080_24_1.25", # 屏幕宽度_高度_色深_window.devicePixelRatio + "clientSize": "0_0_1080_1920_1920_1080_1920_1080", + "status": "0011", # 不知道在干啥 +} + + +# // 将浏览器环境对象的key全部排序,然后对其所有的值及其子对象的值加入数字并字符串相加。若值为数字,则乘以10000(0x2710)再将其转成字符串存入数组,最后再做md5,存入tn变量(tn变量要做加密) +# //把这个对象用加密规则进行加密,然后对结果做GZIP压缩(结果是对象,应该有序列化),最后做AES加密(加密细节目前不清除),密钥为变量priId +# //加密规则:新对象的key使用相对应加解密规则的obfuscated_name值,value为字符串化后进行进行DES加密,再进行btoa加密 + + +# 通过测试 +def _DES(o: dict): + result = {} + for i in o.keys(): + if i in DES_RULE.keys(): + rule = DES_RULE[i] + res = o[i] + if rule["is_encrypt"] == 1: + c = Cipher(TripleDES(rule["key"].encode("utf-8")), ECB()) + data = str(res).encode("utf-8") + # 补足字节 + data += b"\x00" * 8 + res = base64.b64encode(c.encryptor().update(data)).decode("utf-8") + result[rule["obfuscated_name"]] = res + else: + result[i] = o[i] + return result + + +# 通过测试 +def _AES(v: bytes, k: bytes): + iv = "0102030405060708" + key = AES(k) + c = Cipher(key, CBC(iv.encode("utf-8"))) + c.encryptor() + # 填充明文 + v += b"\x00" + while len(v) % 16 != 0: + v += b"\x00" + return c.encryptor().update(v).hex() + + +def GZIP(o: dict): + # 这个压缩结果似乎和前台不太一样,不清楚是否会影响 + json_str = json.dumps(o, ensure_ascii=False) + stream = gzip.compress(json_str.encode("utf-8"), 2, mtime=0) + return base64.b64encode(stream) + + +# 获得tn的值,后续做DES加密用 +# 通过测试 +def get_tn(o: dict): + sorted_keys = sorted(o.keys()) + + result_list = [] + + for i in sorted_keys: + v = o[i] + if isinstance(v, (int, float)): + v = str(v * 10000) + elif isinstance(v, dict): + v = get_tn(v) + result_list.append(v) + return "".join(result_list) + + +def get_smid(): + t = time.localtime() + _time = "{}{:0>2d}{:0>2d}{:0>2d}{:0>2d}{:0>2d}".format( + t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec + ) + uid = str(uuid.uuid4()) + v = _time + hashlib.md5(uid.encode("utf-8")).hexdigest() + "00" + smsk_web = hashlib.md5(("smsk_web_" + v).encode("utf-8")).hexdigest()[0:14] + return v + smsk_web + "0" + + +def get_d_id(): + # storageName = '.thumbcache_' + md5(SM_CONFIG['organization']) // 用于从本地存储获得值 + # uid = uuid() + # priId=md5(uid)[0:16] + # ep=rsa(uid,publicKey) + # SMID = localStorage.get(storageName);// 获得本地存储存的值 + # _0x30b2eb为递归md5 + + uid = str(uuid.uuid4()).encode("utf-8") + priId = hashlib.md5(uid).hexdigest()[0:16] + # ep不一定对,先走走看 + ep = PK.encrypt(uid, padding.PKCS1v15()) + ep = base64.b64encode(ep).decode("utf-8") + + browser = BROWSER_ENV.copy() + current_time = int(time.time() * 1000) + browser.update( + { + "vpw": str(uuid.uuid4()), + "svm": current_time, + "trees": str(uuid.uuid4()), + "pmf": current_time, + } + ) + + des_target = { + **browser, + "protocol": 102, + "organization": SM_CONFIG["organization"], + "appId": SM_CONFIG["appId"], + "os": "web", + "version": "3.0.0", + "sdkver": "3.0.0", + "box": "", # 似乎是个SMID,但是第一次的时候是空,不过不影响结果 + "rtype": "all", + "smid": get_smid(), + "subVersion": "1.0.0", + "time": 0, + } + des_target["tn"] = hashlib.md5(get_tn(des_target).encode()).hexdigest() + + des_result = _AES(GZIP(_DES(des_target)), priId.encode("utf-8")) + + response = requests.post( + devices_info_url, + json={ + "appId": "default", + "compress": 2, + "data": des_result, + "encode": 5, + "ep": ep, + "organization": SM_CONFIG["organization"], + "os": "web", # 固定值 + }, + ) + + resp = response.json() + if resp["code"] != 1100: + raise Exception("did计算失败,请联系作者") + # 开头必须是B + return "B" + resp["detail"]["deviceId"] diff --git a/arknights_mower/utils/character_recognize.py b/arknights_mower/utils/character_recognize.py index 11e7bcb6..3e088c6f 100644 --- a/arknights_mower/utils/character_recognize.py +++ b/arknights_mower/utils/character_recognize.py @@ -16,7 +16,7 @@ def operator_list(img, draw=False): name_y = ((488, 520), (909, 941)) - line1 = cropimg(img, tuple(zip((600, 1920), name_y[0]))) + line1 = cropimg(img, tuple(zip((600, 1860), name_y[0]))) hsv = cv2.cvtColor(line1, cv2.COLOR_RGB2HSV) mask = cv2.inRange(hsv, (98, 140, 200), (102, 255, 255)) line1 = cv2.cvtColor(line1, cv2.COLOR_RGB2GRAY) diff --git a/arknights_mower/utils/config/conf.py b/arknights_mower/utils/config/conf.py index 6fedc2b5..7567bfb4 100644 --- a/arknights_mower/utils/config/conf.py +++ b/arknights_mower/utils/config/conf.py @@ -102,7 +102,7 @@ class WaitingSceneConf(ConfModel): "界面主题" screenshot_interval: int = 500 "截图最短间隔(毫秒)" - screenshot: float = 24 + screenshot: float = 0.02 "截图保留时长(小时)" check_for_updates: bool = True "检查更新" @@ -235,7 +235,7 @@ class MaaDailyPlan1(BaseModel): "周计划" maa_weekly_plan1: list[MaaDailyPlan1] = [ { - "stage": ["点x删除"], + "stage": ["Annihilation"], "周一": 1, "周二": 1, "周三": 1, @@ -245,7 +245,7 @@ class MaaDailyPlan1(BaseModel): "周日": 1, }, { - "stage": ["把鼠标放到问号上查看帮助"], + "stage": ["1-7"], "周一": 1, "周二": 1, "周三": 1, @@ -255,7 +255,7 @@ class MaaDailyPlan1(BaseModel): "周日": 1, }, { - "stage": ["自定义关卡3"], + "stage": ["点x删除"], "周一": 1, "周二": 1, "周三": 1, @@ -265,7 +265,7 @@ class MaaDailyPlan1(BaseModel): "周日": 1, }, { - "stage": ["Annihilation"], + "stage": ["把鼠标放到问号上查看帮助"], "周一": 1, "周二": 1, "周三": 1, @@ -275,7 +275,7 @@ class MaaDailyPlan1(BaseModel): "周日": 1, }, { - "stage": ["1-7"], + "stage": ["自定义关卡3"], "周一": 1, "周二": 1, "周三": 1, @@ -315,7 +315,7 @@ class MaaDailyPlan1(BaseModel): "周日": 1, }, { - "stage": ["SK-6"], + "stage": ["SK-5"], "周一": 1, "周二": 0, "周三": 1, @@ -345,7 +345,7 @@ class MaaDailyPlan1(BaseModel): "周日": 1, }, { - "stage": ["PR-A-2"], + "stage": ["PR-A-1"], "周一": 1, "周二": 0, "周三": 0, @@ -451,6 +451,12 @@ class RunOrderGrandetModeConf(ConfModel): "葛朗台跑单" free_room: bool = False "宿舍不养闲人模式" + fia_fool: bool = True + "菲亚防呆" + fia_threshold: float = 0.9 + "菲亚阈值" + merge_interval: float = 10 + "不养闲人合并间隔" class SimulatorPart(ConfModel): diff --git a/arknights_mower/utils/config/plan.py b/arknights_mower/utils/config/plan.py index 61e96e26..d76508bb 100644 --- a/arknights_mower/utils/config/plan.py +++ b/arknights_mower/utils/config/plan.py @@ -20,6 +20,8 @@ class PlanConf(BaseModel): "0心情工作(主力宿舍黑名单)" refresh_trading: str = "" "跑单时间刷新干员" + refresh_drained: str = "" + "用尽时间刷新干员" class BackupPlanConf(PlanConf): @@ -103,6 +105,7 @@ class BackupPlan(BaseModel): task: Task = {} trigger: Trigger = {} trigger_timing: str = "AFTER_PLANNING" + name: str = "plan" class PlanModel(BaseModel): diff --git a/arknights_mower/utils/datetime.py b/arknights_mower/utils/datetime.py index 5cbb711b..9dcd9f72 100644 --- a/arknights_mower/utils/datetime.py +++ b/arknights_mower/utils/datetime.py @@ -19,6 +19,10 @@ def get_server_weekday(): return datetime.now(pytz.timezone("Asia/Dubai")).weekday() +def get_server_time(): + return datetime.now(pytz.timezone("Asia/Dubai")) + + # newbing说用这个来定义休息时间省事 def format_time(seconds): if seconds < 0: # 权宜之计 配合刷生息演算 diff --git a/arknights_mower/utils/email.py b/arknights_mower/utils/email.py index 6cd90a5c..b76859a9 100644 --- a/arknights_mower/utils/email.py +++ b/arknights_mower/utils/email.py @@ -1,4 +1,7 @@ +import os import smtplib +from email import encoders +from email.mime.base import MIMEBase from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -26,7 +29,7 @@ class Email: - def __init__(self, body, subject, attach_image): + def __init__(self, body, subject, attach_image, attach_files=None): conf = config.conf msg = MIMEMultipart() msg.attach(MIMEText(body, "html")) @@ -41,6 +44,20 @@ def __init__(self, body, subject, attach_image): "Content-Disposition", "attachment", filename="image.jpg" ) msg.attach(image_content) + + if attach_files: + for file_path in attach_files: + if os.path.isfile(file_path): + with open(file_path, "rb") as file: + part = MIMEBase("application", "octet-stream") + part.set_payload(file.read()) + encoders.encode_base64(part) + part.add_header( + "Content-Disposition", + f"attachment; filename={os.path.basename(file_path)}", + ) + msg.attach(part) + self.msg = msg if conf.custom_smtp_server.enable: @@ -52,7 +69,7 @@ def __init__(self, body, subject, attach_image): self.port = 465 self.encryption = "tls" - def send(self): + def send(self, recipient=None): if self.encryption == "starttls": s = smtplib.SMTP(self.smtp_server, self.port, timeout=10) s.starttls() @@ -60,7 +77,7 @@ def send(self): s = smtplib.SMTP_SSL(self.smtp_server, self.port, timeout=10) conf = config.conf s.login(conf.account, conf.pass_code) - recipient = conf.recipient or [conf.account] + recipient = (conf.recipient or [conf.account]) if not recipient else recipient s.send_message(self.msg, conf.account, recipient) s.quit() diff --git a/arknights_mower/utils/graph.py b/arknights_mower/utils/graph.py index 7a243113..47b53d1f 100644 --- a/arknights_mower/utils/graph.py +++ b/arknights_mower/utils/graph.py @@ -431,7 +431,9 @@ class SceneGraphSolver(BaseSolver): def scene_graph_navigation(self, scene: int): if scene not in DG.nodes: logger.error(f"{SceneComment[scene]}不在场景图中") - return False + return + + error_count = 0 while (current := self.scene()) != scene: if current in self.waiting_scene: @@ -453,7 +455,7 @@ def scene_graph_navigation(self, scene: int): self.device.start_droidcast() if config.conf.touch_method == "scrcpy": self.device.control.scrcpy = Scrcpy(self.device.client) - return False + return logger.debug(sp) @@ -462,11 +464,12 @@ def scene_graph_navigation(self, scene: int): try: transition(self) + error_count = 0 except MowerExit: raise except Exception as e: logger.exception(f"场景转移异常:{e}") - restart_simulator() + """restart_simulator() self.device.client.check_server_alive() Session().connect(config.conf.adb) if config.conf.droidcast.enable: @@ -475,7 +478,22 @@ def scene_graph_navigation(self, scene: int): self.device.control.scrcpy = Scrcpy(self.device.client) self.check_current_focus() return False - return True + return True""" + if error_count <= 5: + self.sleep() + error_count += 1 + continue + if restart_simulator(): + self.device.client.check_server_alive() + Session().connect(config.conf.adb) + if config.conf.droidcast.enable: + self.device.start_droidcast() + if config.conf.touch_method == "scrcpy": + self.device.control.scrcpy = Scrcpy(self.device.client) + self.check_current_focus() + else: + self.restart_game() + error_count = 0 def back_to_index(self): logger.info("场景图导航:back_to_index") diff --git a/arknights_mower/utils/log.py b/arknights_mower/utils/log.py index d59dfa1f..c708845c 100644 --- a/arknights_mower/utils/log.py +++ b/arknights_mower/utils/log.py @@ -3,15 +3,20 @@ import sys import time import traceback -from logging.handlers import RotatingFileHandler +from datetime import datetime, timedelta +from logging.handlers import QueueHandler, QueueListener, TimedRotatingFileHandler from pathlib import Path +from queue import Queue +from threading import Thread import colorlog from arknights_mower.utils import config from arknights_mower.utils.path import get_path -BASIC_FORMAT = "%(asctime)s %(relativepath)s:%(lineno)d %(levelname)s %(message)s" +BASIC_FORMAT = ( + "%(asctime)s %(relativepath)s:%(lineno)d %(levelname)s %(funcName)s: %(message)s" +) COLOR_FORMAT = f"%(log_color)s{BASIC_FORMAT}" DATE_FORMAT = None basic_formatter = logging.Formatter(BASIC_FORMAT, DATE_FORMAT) @@ -31,28 +36,24 @@ def filter(self, record: logging.LogRecord) -> bool: filter = PackagePathFilter() - logger = logging.getLogger(__name__) -logger.setLevel("DEBUG") +logger.setLevel(logging.DEBUG) +# d(ebug)hlr: 终端输出 dhlr = logging.StreamHandler(stream=sys.stdout) dhlr.setFormatter(color_formatter) -dhlr.setLevel("DEBUG") +dhlr.setLevel(logging.DEBUG) dhlr.addFilter(filter) -logger.addHandler(dhlr) +# f(ile)hlr: 文件记录 folder = Path(get_path("@app/log")) folder.mkdir(exist_ok=True, parents=True) -fhlr = RotatingFileHandler( - folder.joinpath("runtime.log"), - encoding="utf8", - maxBytes=10 * 1024 * 1024, - backupCount=20, +fhlr = TimedRotatingFileHandler( + folder.joinpath("runtime.log"), encoding="utf8", backupCount=168 ) fhlr.setFormatter(basic_formatter) fhlr.setLevel("DEBUG") fhlr.addFilter(filter) -logger.addHandler(fhlr) class Handler(logging.StreamHandler): @@ -63,24 +64,81 @@ def emit(self, record: logging.LogRecord): config.log_queue.put(msg) +# w(ebsocket)hlr: WebSocket whlr = Handler() whlr.setLevel(logging.INFO) -logger.addHandler(whlr) + +log_queue = Queue() +queue_handler = QueueHandler(log_queue) +logger.addHandler(queue_handler) +listener = QueueListener(log_queue, dhlr, fhlr, whlr, respect_handler_level=True) +listener.start() + +screenshot_folder = get_path("@app/screenshot") +screenshot_folder.mkdir(exist_ok=True, parents=True) +screenshot_queue = Queue() +cleanup_time = datetime.now() -def save_screenshot(img: bytes) -> None: - folder = get_path("@app/screenshot") - folder.mkdir(exist_ok=True, parents=True) - time_ns = time.time_ns() - start_time_ns = time_ns - config.conf.screenshot * 3600 * 10**9 - for i in folder.iterdir(): +def screenshot_cleanup(): + logger.info("清理过期截图") + start_time_ns = time.time_ns() - config.conf.screenshot * 3600 * 10**9 + for i in screenshot_folder.iterdir(): if i.is_dir(): + if i.name == "run_order": + continue shutil.rmtree(i) elif not i.stem.isnumeric(): i.unlink() elif int(i.stem) < start_time_ns: i.unlink() - filename = f"{time_ns}.jpg" - with folder.joinpath(filename).open("wb") as f: - f.write(img) - logger.debug(f"save screenshot: {filename}") + global cleanup_time + cleanup_time = datetime.now() + + +def screenshot_worker(): + screenshot_cleanup() + while True: + now = datetime.now() + if now - cleanup_time > timedelta(hours=1): + screenshot_cleanup() + img, filename = screenshot_queue.get() + with screenshot_folder.joinpath(filename).open("wb") as f: + f.write(img) + + +Thread(target=screenshot_worker, daemon=True).start() + + +def save_screenshot(img: bytes, sub_folder=None) -> None: + filename = f"{time.time_ns()}.jpg" + logger.debug(filename) + if sub_folder: + sub_folder_path = Path(screenshot_folder) / sub_folder + sub_folder_path.mkdir(parents=True, exist_ok=True) + filename = f"{sub_folder}/{datetime.now().strftime('%Y%m%d%H%M%S')}.jpg" + screenshot_queue.put((img, filename)) + + +def get_log_by_time(target_time, time_range=1): + folder = Path(get_path("@app/log")) + time_points = [ + target_time - timedelta(hours=time_range), + target_time, + target_time + timedelta(hours=time_range), + ] + valid_suffixes = [tp.strftime("%Y-%m-%d_%H") for tp in time_points] + matching_files = [] + for file_path in folder.iterdir(): + if file_path.is_file(): + try: + if any(suffix in file_path.name for suffix in valid_suffixes): + matching_files.append(file_path) + elif ( + file_path.name == "runtime.log" + and (datetime.now() - target_time).total_seconds() <= 3600 + ): + matching_files.append(file_path) + except Exception as e: + logger.exception(f"Error processing file {file_path}: {e}") + return matching_files diff --git a/arknights_mower/utils/operators.py b/arknights_mower/utils/operators.py index 93da13e0..ef69e27b 100644 --- a/arknights_mower/utils/operators.py +++ b/arknights_mower/utils/operators.py @@ -26,8 +26,6 @@ def __init__(self, name, skill_level, efficiency, match, swap_name="艾丽妮"): self.level = skill_level self.efficiency = efficiency self.match = match - if self.level > 1: - self.half_off = True self.swap_name = swap_name @@ -63,7 +61,7 @@ def __init__(self, plan): self.clues = [] self.current_room_changed_callback = None self.party_time = None - + self.profession_filter = agent_arrange_order["职介选择开关"] self.eval_model = base_eval_model.clone() self.eval_model.nodes.extend(["Call", "Attribute"]) self.eval_model.attributes.extend( @@ -94,7 +92,7 @@ def calculate_switch_time(self, support: SkillUpgradeSupport): # 基本5% basic = 5 if support.add_on: - # 阿斯卡伦 + # 阿斯卡纶 basic += 5 if hour == 0: hour = level * 8 @@ -368,7 +366,9 @@ def init_mood_limit(self): def evaluate_expression(self, expression): try: - result = Expr(expression, self.eval_model).eval({"op_data": self}) + model = {e: e for e in base_room_list} + model["op_data"] = self + result = Expr(expression, self.eval_model).eval(model) return result except Exception as e: logger.exception(f"Error evaluating expression: {e}") @@ -422,11 +422,18 @@ def update_detail(self, name, mood, current_room, current_index, update_time=Fal agent = self.operators[name] if update_time: if agent.time_stamp is not None and agent.mood > mood: - agent.depletion_rate = ( - (agent.mood - mood) - * 3600 - / ((datetime.now() - agent.time_stamp).total_seconds()) - ) + time_difference = datetime.now() - agent.time_stamp + if time_difference > timedelta(minutes=29): + logger.debug("开始计算心情掉率") + logger.debug( + f"当前心情:{mood},上次{agent.mood},上次时间{agent.time_stamp}" + ) + agent.depletion_rate = ( + (agent.mood - mood) * 3600 / time_difference.total_seconds() + ) + logger.debug( + f"更新 {agent.name} 心情掉率为:{agent.depletion_rate}" + ) agent.time_stamp = datetime.now() # 如果移出宿舍,则清除对应宿舍数据 且重新记录高效组心情(如果有备用班,则跳过高效组判定) if ( @@ -472,15 +479,9 @@ def update_detail(self, name, mood, current_room, current_index, update_time=Fal def refresh_dorm_time(self, room, index, agent): for idx, dorm in enumerate(self.dorm): - # Filter out resting priority low - # if idx >= self.config.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() or self.config.free_room - ): + if _name in self.operators.keys() or _name in agent_list: dorm.name = _name _agent = self.operators[_name] # 如果干员有心情上限,则按比例修改休息时间 @@ -493,9 +494,6 @@ def refresh_dorm_time(self, room, index, agent): dorm.time = _agent.time_stamp + timedelta(seconds=sec_remaining) else: dorm.time = agent["time"] - elif _name in agent_list: - dorm.name = _name - dorm.time = agent["time"] break def correct_dorm(self): @@ -515,6 +513,7 @@ def correct_dorm(self): ): op.mood = op.upper_limit op.time_stamp = self.dorm[idx].time + op.depletion_rate = 0 logger.debug( f"检测到{op.name}心情恢复满,设置心情至{op.upper_limit}" ) @@ -564,6 +563,7 @@ def add(self, operator): operator.rest_in_full = self.config.is_rest_in_full(operator.name) operator.workaholic = self.config.is_workaholic(operator.name) operator.refresh_order_room = self.config.is_refresh_trading(operator.name) + operator.refresh_drained = self.config.is_refresh_drained(operator.name) if operator.name in agent_arrange_order: operator.arrange_order = agent_arrange_order[operator.name] # 复制基建数据 @@ -591,6 +591,18 @@ def add(self, operator): if operator.workaholic and operator.name not in self.workaholic_agent: self.workaholic_agent.append(operator.name) + def average_mood(self): + total_mood = 0 + current_mood = 0 + count = 0 + for k, v in self.operators().items(): + if not v.is_resting() and v.operator_type != "low" and not v.workaholic: + current_mood += v.current_mood() - v.lower_limit + total_mood += v.upper_limit - v.lower_limit + count += 1 + logger.debug(f"{count}, {current_mood / total_mood}") + return current_mood / total_mood + def available_free(self, free_type="high"): ret = 0 freeName = [] @@ -646,10 +658,18 @@ def assign_dorm(self, name): _room = None for i in range(self.config.max_resting_count, len(self.dorm)): _name = self.dorm[i].name - if _name == "" or not self.operators[_name].is_high(): + if ( + _name == "" + or not self.operators[_name].is_high() + or ( + self.dorm[i].time is not None + and self.dorm[i].time < datetime.now() + ) + ): _room = self.dorm[i] break _room.name = name + _room.time = None return _room def get_current_operator(self, room, index): @@ -684,11 +704,6 @@ def __repr__(self): class Operator: - time_stamp = None - depletion_rate = 0 - workaholic = False - arrange_order = [2, "false"] - def __init__( self, name, @@ -729,6 +744,9 @@ def __init__( self.lower_limit = lower_limit self.depletion_rate = depletion_rate self.time_stamp = time_stamp + self.workaholic = False + self.arrange_order = [2, "false"] + self.refresh_drained = False @property def current_room(self): @@ -752,6 +770,8 @@ def is_working(self): def need_to_refresh(self, h=2, r=""): # 是否需要读取心情 + if self.name in ["歌蕾蒂娅", "见行者"]: + h = 0.5 if ( self.time_stamp is None or ( @@ -798,5 +818,16 @@ def current_mood(self): else: return self.mood + def predict_exhaust(self): + remaining_mood = self.mood - self.lower_limit # 剩余心情 + depletion_rate = self.depletion_rate # 心情掉率,小时单位 + # 计算到心情归零所需时间(小时),再加上当前时间戳 + if depletion_rate > 0: + return self.time_stamp + timedelta( + hours=((remaining_mood / depletion_rate) - 0.5) + ) + else: + return datetime.now() + timedelta(hours=24) + def __repr__(self): return f"Operator(name='{self.name}', room='{self.room}', index={self.index}, group='{self.group}', replacement={self.replacement}, resting_priority='{self.resting_priority}', current_room='{self.current_room}',exhaust_require={self.exhaust_require},mood={self.mood}, upper_limit={self.upper_limit}, rest_in_full={self.rest_in_full}, current_index={self.current_index}, lower_limit={self.lower_limit}, operator_type='{self.operator_type}',depletion_rate={self.depletion_rate},time_stamp='{self.time_stamp}',refresh_order_room = {self.refresh_order_room})" diff --git a/arknights_mower/utils/plan.py b/arknights_mower/utils/plan.py index f095ffec..1bf63d92 100644 --- a/arknights_mower/utils/plan.py +++ b/arknights_mower/utils/plan.py @@ -36,6 +36,7 @@ def __init__( resting_threshold: float = 0.5, refresh_trading_config: str = "", free_room: bool = False, + refresh_drained: str = "", ): """排班的设置 @@ -68,6 +69,7 @@ def __init__( # example: 阿米娅,夕,令 # 夕(room_3_1,room_1_3),令(room_3_1) self.refresh_trading_config = to_list(refresh_trading_config) + self.refresh_drained = to_list(refresh_drained) def is_rest_in_full(self, agent_name) -> bool: return agent_name in self.rest_in_full @@ -84,6 +86,9 @@ def is_resting_priority(self, agent_name) -> bool: def is_free_blacklist(self, agent_name) -> bool: return agent_name in self.free_blacklist + def is_refresh_drained(self, agent_name) -> bool: + return agent_name in self.refresh_drained + def is_refresh_trading(self, agent_name) -> list[bool, list[str]]: match = next( (e for e in self.refresh_trading_config if agent_name in e.lower()), @@ -106,6 +111,7 @@ def merge_config(self, target: Self) -> Self: "resting_priority", "free_blacklist", "refresh_trading_config", + "refresh_drained", ]: p_dict = set(getattr(n, p)) target_p = set(getattr(target, p)) diff --git a/arknights_mower/utils/recognize.py b/arknights_mower/utils/recognize.py index c3e5aaff..d22d482a 100644 --- a/arknights_mower/utils/recognize.py +++ b/arknights_mower/utils/recognize.py @@ -9,7 +9,6 @@ from arknights_mower.utils import config from arknights_mower.utils import typealias as tp from arknights_mower.utils.csleep import MowerExit -from arknights_mower.utils.deprecated import deprecated from arknights_mower.utils.device.device import Device from arknights_mower.utils.image import bytes2img, cmatch, cropimg, loadres, thres2 from arknights_mower.utils.log import logger, save_screenshot @@ -95,10 +94,9 @@ def color(self, x: int, y: int) -> tp.Pixel: """get the color of the pixel""" return self.img[y][x] - @deprecated def save_screencap(self, folder): - del folder # 兼容2024.05旧版接口 - save_screenshot(self.screencap) + # del folder # 兼容2024.05旧版接口 + save_screenshot(self.screencap, folder) def detect_index_scene(self) -> bool: res = loadres("index_nav", True) @@ -174,7 +172,7 @@ def get_scene(self) -> int: self.scene = Scene.CTRLCENTER_ASSISTANT elif self.find("infra_overview"): self.scene = Scene.INFRA_MAIN - elif self.find("infra_todo"): + elif self.find("infra_todo", scope=((0, 1013), (241, 1080))): self.scene = Scene.INFRA_TODOLIST elif self.find("clue"): self.scene = Scene.INFRA_CONFIDENTIAL @@ -672,9 +670,9 @@ def find( "factory_collect": (1542, 886), "fight/refresh": (1639, 22), "hypergryph": (0, 961), - "infra_overview": (54, 135), + # "infra_overview": (54, 135), "infra_overview_in": (64, 705), - "infra_todo": (13, 1013), + # "infra_todo": (13, 1013), "loading2": (620, 247), "loading7": (106, 635), "login_account": (622, 703), @@ -722,6 +720,28 @@ def find( "terminal_pre2": (1459, 797), } + template_matching_score = { + "connecting": 0.7, + "navigation/ope_hard": 0.7, + "navigation/ope_hard_small": 0.7, + "navigation/ope_normal": 0.7, + "navigation/ope_normal_small": 0.7, + "recruit/agent_token": 0.8, + "recruit/agent_token_first": 0.8, + "recruit/lmb": 0.7, + "recruit/riic_res/CASTER": 0.7, + "recruit/riic_res/MEDIC": 0.7, + "recruit/riic_res/PIONEER": 0.7, + "recruit/riic_res/SPECIAL": 0.7, + "recruit/riic_res/SNIPER": 0.7, + "recruit/riic_res/SUPPORT": 0.7, + "recruit/riic_res/TANK": 0.7, + "recruit/riic_res/WARRIOR": 0.7, + "recruit/time": 0.8, + "recruit/stone": 0.7, + "arrange_confirm": 0.85, + } + if res in color: res_img = loadres(res) h, w, _ = res_img.shape @@ -737,7 +757,10 @@ def find( res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2GRAY) ssim = structural_similarity(gray, res_img) logger.debug(f"{ssim=}") - if ssim >= 0.9: + threshold = 0.9 + if res in template_matching_score: + threshold = template_matching_score[res] + if ssim >= threshold: return scope return None @@ -814,27 +837,6 @@ def find( "upgrade": (997, 501), } - template_matching_score = { - "connecting": 0.7, - "navigation/ope_hard": 0.7, - "navigation/ope_hard_small": 0.7, - "navigation/ope_normal": 0.7, - "navigation/ope_normal_small": 0.7, - "recruit/agent_token": 0.8, - "recruit/agent_token_first": 0.8, - "recruit/lmb": 0.7, - "recruit/riic_res/CASTER": 0.7, - "recruit/riic_res/MEDIC": 0.7, - "recruit/riic_res/PIONEER": 0.7, - "recruit/riic_res/SPECIAL": 0.7, - "recruit/riic_res/SNIPER": 0.7, - "recruit/riic_res/SUPPORT": 0.7, - "recruit/riic_res/TANK": 0.7, - "recruit/riic_res/WARRIOR": 0.7, - "recruit/time": 0.8, - "recruit/stone": 0.7, - } - if res in template_matching: threshold = 0.9 if res in template_matching_score: diff --git a/arknights_mower/utils/scheduler_task.py b/arknights_mower/utils/scheduler_task.py index 2b1aca2a..eb4c731a 100644 --- a/arknights_mower/utils/scheduler_task.py +++ b/arknights_mower/utils/scheduler_task.py @@ -1,4 +1,5 @@ import copy +import heapq from datetime import datetime, timedelta from enum import Enum from typing import Literal @@ -171,12 +172,61 @@ def scheduling(tasks, run_order_delay=5, execution_time=0.75, time_now=None): def try_add_release_dorm(plan, time, op_data, tasks): if not op_data.config.free_room: return + # 有plan 的情况 for k, v in plan.items(): for name in v: if name != "Current": _idx, __dorm = op_data.get_dorm_by_name(name) if __dorm and __dorm.time < time: add_release_dorm(tasks, op_data, name) + # 普通情况 + if not plan: + try: + # 查看是否有未满心情的人 + logger.info("启动不养闲人安排空余宿舍位") + waiting_list = [] + for k, v in op_data.operators.items(): + if ( + not v.is_high() + and v.current_mood() < v.upper_limit + and v.current_room == "" + and v.name not in op_data.config.free_blacklist + ): + heapq.heappush( + waiting_list, + ( + 1 if k in ["九色鹿", "年"] else 0, + (v.current_mood() - v.lower_limit) + / (v.upper_limit - v.lower_limit), + k, + ), + ) + logger.debug(f"{k}:心情:{v.current_mood()}") + if not waiting_list: + return + logger.debug(f"有{len(waiting_list)}个干员心情未满") + plan = {} + for idx, value in enumerate(op_data.dorm): + if value.name in op_data.operators: + if not waiting_list: + break + agent = op_data.operators[value.name] + logger.debug(str(value)) + if not v.is_high() and ( + agent.current_mood() >= agent.upper_limit + or (value.time is not None and value.time < datetime.now()) + ): + rest = heapq.heappop(waiting_list) + if value.position[0] not in plan: + plan[value.position[0]] = ["Current"] * 5 + plan[value.position[0]][value.position[1]] = rest[2] + if plan: + logger.debug(f"不养闲人任务:{plan}") + logger.info("添加不养闲人任务完成") + task = SchedulerTask(task_plan=plan) + tasks.append(task) + except Exception as ex: + logger.exception(ex) def add_release_dorm(tasks, op_data, name): @@ -273,6 +323,30 @@ def set_type_enum(value): return TaskTypes.NOT_SPECIFIC +def merge_release_dorm(tasks, merge_interval): + for idx in range(1, len(tasks) + 1): + if idx == 1: + continue + task = tasks[-idx] + last_not_release = None + if task.type != TaskTypes.RELEASE_DORM: + continue + for index_last_not_release in range(idx + 1, len(tasks) + 1): + if tasks[-index_last_not_release].type != TaskTypes.RELEASE_DORM and tasks[ + -index_last_not_release + ].time > task.time - timedelta(minutes=1): + last_not_release = tasks[-index_last_not_release] + if last_not_release is not None: + continue + elif task.time + timedelta(minutes=merge_interval) > tasks[-idx + 1].time: + tasks[-idx].time = tasks[-idx + 1].time + timedelta(seconds=1) + tasks[-idx], tasks[-idx + 1] = ( + tasks[-idx + 1], + tasks[-idx], + ) + logger.info(f"自动合并{merge_interval}分钟以内任务") + + class SchedulerTask: time = None type = "" diff --git a/arknights_mower/utils/simulator.py b/arknights_mower/utils/simulator.py index 7f474f11..f38010f4 100644 --- a/arknights_mower/utils/simulator.py +++ b/arknights_mower/utils/simulator.py @@ -28,10 +28,10 @@ def restart_simulator(stop=True, start=True): cmd = "" blocking = False - if simulator_type not in Simulator_Type: + if simulator_type not in [types.value for types in Simulator_Type]: logger.warning(f"尚未支持{simulator_type}重启/自动启动") csleep(10) - return + return False if simulator_type == Simulator_Type.Nox.value: cmd = "Nox.exe" @@ -95,6 +95,7 @@ def restart_simulator(stop=True, start=True): pyautogui.FAILSAFE = False pyautogui.hotkey(*hotkey) + return True def exec_cmd(cmd, folder_path, wait_time, blocking): diff --git a/arknights_mower/utils/skland.py b/arknights_mower/utils/skland.py new file mode 100644 index 00000000..23fccf87 --- /dev/null +++ b/arknights_mower/utils/skland.py @@ -0,0 +1,148 @@ +import hashlib +import hmac +import json +import time +from urllib import parse + +import requests + +from arknights_mower.utils.log import logger +from arknights_mower.utils.SecuritySm import get_d_id + +app_code = "4ca99fa6b56cc2ba" + +# 签到url +sign_url = "https://zonai.skland.com/api/v1/game/attendance" +# 绑定的角色url +binding_url = "https://zonai.skland.com/api/v1/game/player/binding" +# 验证码url +login_code_url = "https://as.hypergryph.com/general/v1/send_phone_code" +# 验证码登录 +token_phone_code_url = "https://as.hypergryph.com/user/auth/v2/token_by_phone_code" +# 密码登录 +token_password_url = "https://as.hypergryph.com/user/auth/v1/token_by_phone_password" +# 使用token获得认证代码 +grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant" +# 使用认证代码获得cred +cred_code_url = "https://zonai.skland.com/web/v1/user/auth/generate_cred_by_code" +header = { + "cred": "", + "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0", + "Accept-Encoding": "gzip", + "Connection": "close", +} +header_login = { + "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0", + "Accept-Encoding": "gzip", + "Connection": "close", + "dId": get_d_id(), +} +header_for_sign = {"platform": "", "timestamp": "", "dId": "", "vName": ""} + + +def generate_signature(token: str, path, body_or_query): + """ + 获得签名头 + 接口地址+方法为Get请求?用query否则用body+时间戳+ 请求头的四个重要参数(dId,platform,timestamp,vName).toJSON() + 将此字符串做HMAC加密,算法为SHA-256,密钥token为请求cred接口会返回的一个token值 + 再将加密后的字符串做MD5即得到sign + :param token: 拿cred时候的token + :param path: 请求路径(不包括网址) + :param body_or_query: 如果是GET,则是它的query。POST则为它的body + :return: 计算完毕的sign + """ + # 总是说请勿修改设备时间,怕不是yj你的服务器有问题吧,所以这里特地-2 + + t = str(int(time.time()) - 2) + token = token.encode("utf-8") + header_ca = json.loads(json.dumps(header_for_sign)) + header_ca["timestamp"] = t + header_ca_str = json.dumps(header_ca, separators=(",", ":")) + s = path + body_or_query + t + header_ca_str + hex_s = hmac.new(token, s.encode("utf-8"), hashlib.sha256).hexdigest() + md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest().encode("utf-8").decode("utf-8") + return md5, header_ca + + +def get_sign_header(url: str, method, body, sign_token, old_header=header): + h = json.loads(json.dumps(old_header)) + p = parse.urlparse(url) + if method.lower() == "get": + h["sign"], header_ca = generate_signature(sign_token, p.path, p.query) + else: + h["sign"], header_ca = generate_signature(sign_token, p.path, json.dumps(body)) + for i in header_ca: + h[i] = header_ca[i] + return h + + +def get_grant_code(token): + response = requests.post( + grant_code_url, + json={"appCode": app_code, "token": token, "type": 0}, + headers=header_login, + ) + resp = response.json() + if response.status_code != 200: + raise Exception(f"获得认证代码失败:{resp}") + if resp.get("status") != 0: + raise Exception(f'获得认证代码失败:{resp["msg"]}') + return resp["data"]["code"] + + +def get_cred(grant): + """ + 获取cred + :param cred_code_url: 获取cred的URL + :param grant: 授权代码 + :param header_login: 登录请求头 + :return: cred + """ + resp = requests.post( + cred_code_url, json={"code": grant, "kind": 1}, headers=header_login + ).json() + + if resp["code"] != 0: + raise Exception(f'获得cred失败:{resp["message"]}') + + return resp["data"] + + +def get_binding_list(sign_token): + v = [] + resp = requests.get( + binding_url, + headers=get_sign_header( + binding_url, + "get", + None, + sign_token, + ), + ).json() + + if resp["code"] != 0: + logger.info(f"请求角色列表出现问题:{resp['message']}") + if resp.get("message") == "用户未登录": + logger.warning("用户登录可能失效了,请重新运行此程序!") + return [] + for i in resp["data"]["list"]: + if i.get("appCode") != "arknights": + continue + v.extend(i.get("bindingList")) + return v + + +def get_cred_by_token(token): + return get_cred(get_grant_code(token)) + + +def log(account): + r = requests.post( + token_password_url, + json={"phone": account.account, "password": account.password}, + headers=header_login, + ).json() + if r.get("status") != 0: + raise Exception(f'获得token失败:{r["msg"]}') + logger.info("森空岛登陆成功") + return r["data"]["token"] diff --git a/arknights_mower/utils/solver.py b/arknights_mower/utils/solver.py index 60625808..d0cc77a5 100644 --- a/arknights_mower/utils/solver.py +++ b/arknights_mower/utils/solver.py @@ -78,8 +78,17 @@ def run(self) -> None: self.check_current_focus() retry_times = config.MAX_RETRYTIME result = None + start_time = datetime.now() + recruit_timeout_limit = 300 # 获取超时限制 300s + from arknights_mower.solvers.recruit import RecruitSolver + while retry_times > 0: try: + if isinstance(self, RecruitSolver): + if datetime.now() - start_time > timedelta( + seconds=recruit_timeout_limit + ): + raise Exception("任务超时,强制停止") result = self.transition() if result: return result @@ -190,6 +199,11 @@ def ctap(self, pos: tp.Location, max_seconds: int = 10): def check_current_focus(self): self.recog.check_current_focus() + def restart_game(self): + self.device.exit() + self.device.launch() + self.recog.update() + def tap_element( self, element_name: tp.Res, @@ -228,7 +242,7 @@ def tap_index_element( ): pos = { "friend": (544, 862), # 好友 - "infrastructure": (1545, 948), # 基建 + "infrastructure": (1410, 870), # 基建 "mission": (1201, 904), # 任务 "recruit": (1507, 774), # 公开招募 "shop": (1251, 727), # 采购中心 diff --git a/auto_get_res_new.py b/auto_get_res_new.py index 72192e88..b60efaa6 100644 --- a/auto_get_res_new.py +++ b/auto_get_res_new.py @@ -121,20 +121,27 @@ def 检查图标代码匹配(目标图标代码, 物品类型): def 添加干员(self): 干员_名称列表 = [] - + 干员_职业列表 = {} for 干员代码, 干员数据 in self.干员表.items(): if not 干员数据["itemObtainApproach"]: continue - 干员名 = 干员数据["name"] 干员_名称列表.append(干员名) + 干员_职业列表[干员名] = 干员数据["profession"] 干员头像路径 = f"./ArknightsGameResource/avatar/{干员代码}.png" 目标路径 = f"./ui/public/avatar/{干员数据['name']}.webp" print(f"{干员名}: {干员代码}") - - png_image = Image.open(干员头像路径) - png_image.save(目标路径, "WEBP") + try: + png_image = Image.open(干员头像路径) + png_image.save(目标路径, "WEBP") + except Exception as ex: + print("头像读取失败") + print(ex) 干员_名称列表.sort(key=len) + with open( + "./arknights_mower/data/agent_profession.json", "w", encoding="utf-8" + ) as f: + json.dump(干员_职业列表, f, ensure_ascii=False) with open("./arknights_mower/data/agent.json", "w", encoding="utf-8") as f: json.dump(干员_名称列表, f, ensure_ascii=False) print() @@ -488,6 +495,10 @@ def 训练选中的干员名的模型(self): "arknights_mower/fonts/SourceHanSansCN-Medium.otf", 25 ) + font27 = ImageFont.truetype( + "arknights_mower/fonts/SourceHanSansCN-Medium.otf", 27 + ) + data = {} kernel = np.ones((10, 10), np.uint8) @@ -498,12 +509,34 @@ def 训练选中的干员名的模型(self): font = font31 if not operator[0].encode().isalpha(): if len(operator) == 7: - font = font25 + if "·" in operator: + # 维娜·维多利亚 识别的临时解决办法 + font = font27 + else: + font = font25 elif len(operator) == 6: font = font30 img = Image.new(mode="L", size=(400, 100)) draw = ImageDraw.Draw(img) - draw.text((50, 20), operator, fill=(255,), font=font) + if "·" in operator: + x, y = 50, 20 + char_index = { + i: False for i, char in enumerate(operator) if char == "·" + } + for i, char in enumerate(operator): + if i in char_index and not char_index[i]: + x -= 8 + char_index[i] = True + if i + 1 not in char_index and char == "·": + char_index[i + 1] = False + draw.text((x, y), char, fill=(255,), font=font) # 绘制每个字符 + char_width, char_height = font.getbbox(char)[ + 2:4 + ] # getbbox 返回 (x1, y1, x2, y2) + x += char_width + else: + draw.text((50, 20), operator, fill=(255,), font=font) + img = np.array(img, dtype=np.uint8) img = thres2(img, 140) dilation = cv2.dilate(img, kernel, iterations=1) @@ -583,7 +616,7 @@ def 获得干员基建描述(self): 干员技能详情["skill_level"] = skill_level skill_level += 1 干员技能详情["phase_level"] = ( - f"精{item2["cond"]["phase"]} {item2["cond"]["level"]}级" + f'精{item2["cond"]["phase"]} {item2["cond"]["level"]}级' ) 干员技能详情["skillname"] = buff_table[item2["buffId"]][0] text = buff_table[item2["buffId"]][1] diff --git a/manager.py b/manager.py index 23bb7176..6175920c 100755 --- a/manager.py +++ b/manager.py @@ -68,7 +68,7 @@ def jump_to_index(window): api = Api() window = webview.create_window( title="多开管理器", - url="dist/index.html", + url="ui/dist/index.html", js_api=api, min_size=(400, 500), width=400, diff --git a/requirements.in b/requirements.in index b1fe93c8..61464f7c 100644 --- a/requirements.in +++ b/requirements.in @@ -56,3 +56,6 @@ Jinja2==3.1.4 # 启动模拟器后按快捷键 PyAutoGUI==0.9.54 + +# 森空岛检测 +cryptography==43.0.1 diff --git a/requirements.txt b/requirements.txt index d21400d1..e4175926 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,8 @@ coloredlogs==15.0.1 # via onnxruntime colorlog==6.8.2 # via -r requirements.in +cryptography==43.0.1 + # via -r requirements.in evalidate==2.0.2 # via -r requirements.in flask==3.0.3 diff --git a/server.py b/server.py index da15133c..0992b547 100755 --- a/server.py +++ b/server.py @@ -18,15 +18,16 @@ from werkzeug.exceptions import NotFound from arknights_mower import __system__ +from arknights_mower.solvers.record import load_state, save_state from arknights_mower.utils import config -from arknights_mower.utils.log import logger +from arknights_mower.utils.log import get_log_by_time, logger from arknights_mower.utils.path import get_path mimetypes.add_type("text/html", ".html") mimetypes.add_type("text/css", ".css") mimetypes.add_type("application/javascript", ".js") -app = Flask(__name__, static_folder="dist", static_url_path="") +app = Flask(__name__, static_folder="ui/dist", static_url_path="") sock = Sock(app) CORS(app) @@ -62,17 +63,17 @@ def decorated_function(*args, **kwargs): @app.route("/") def serve_index(path): - return send_from_directory("dist", path) + return send_from_directory("ui/dist", path) @app.errorhandler(404) def not_found(e): if (path := request.path).startswith("/docs"): try: - return send_from_directory("dist" + path, "index.html") + return send_from_directory("ui/dist" + path, "index.html") except NotFound: return "

404 Not Found

", 404 - return send_from_directory("dist", "index.html") + return send_from_directory("ui/dist", "index.html") @app.route("/conf", methods=["GET", "POST"]) @@ -123,25 +124,27 @@ def running(): return "true" if mower_thread and mower_thread.is_alive() else "false" -@app.route("/start") +@app.route("/start/") @require_token -def start(): +def start(start_type): global mower_thread global log_lines if mower_thread and mower_thread.is_alive(): return "false" - # 创建 tmp 文件夹 tmp_dir = get_path("@app/tmp") tmp_dir.mkdir(exist_ok=True) config.stop_mower.clear() - config.operators = {} - + saved_state = load_state() + if saved_state is None or start_type == "2": + saved_state = {} + if start_type == "1": + saved_state["tasks"] = [] from arknights_mower.__main__ import main - mower_thread = Thread(target=main, daemon=True) + mower_thread = Thread(target=main, args=(saved_state,), daemon=True) mower_thread.start() log_lines = [] @@ -151,6 +154,7 @@ def start(): @app.route("/stop") @require_token +@save_state def stop(): global mower_thread @@ -576,19 +580,19 @@ def get_count(): or s["swap_name"] not in agent_list ): raise Exception("干员名不正确") - supports.append( - SkillUpgradeSupport( - name=s["name"], - skill_level=s["skill_level"], - efficiency=s["efficiency"], - match=s["match"], - swap_name=s["swap_name"], - ) + sup = SkillUpgradeSupport( + name=s["name"], + skill_level=s["skill_level"], + efficiency=s["efficiency"], + match=s["match"], + swap_name=s["swap_name"], ) + sup.half_off = s["half_off"] + supports.append(sup) if len(supports) == 0: raise Exception("请添加专精工具人") base_scheduler.op_data.skill_upgrade_supports = supports - logger.error("更新专精工具人完毕") + logger.info("更新专精工具人完毕") base_scheduler.tasks.append(new_task) logger.debug(f"成功:{str(new_task)}") return "添加任务成功!" @@ -611,3 +615,39 @@ def get_count(): ] else: return [] + + +@app.route("/submit_feedback", methods=["POST"]) +@require_token +def submit_feedback(): + from arknights_mower.utils.email import Email + + req = request.json + logger.debug(f"收到反馈务请求:{req}") + try: + log_files = [] + if req["type"] == "Bug": + dt = datetime.datetime.fromtimestamp(req["endTime"] / 1000.0) + logger.info(dt) + log_files = get_log_by_time(dt) + logger.info("log 文件发送中,请等待") + if not log_files: + raise ValueError("对应时间log 文件无法找到") + body = f"

Bug 发生时间区间:{datetime.datetime.fromtimestamp(req['startTime']/ 1000.0)}--{dt}


{req['description']}

" + else: + body = req["description"] + email = Email( + body, + "Mower " + req["type"], + None, + attach_files=None if req["type"] != "Bug" else log_files, + ) + email.send(["354013233@qq.com"]) + except ValueError as v: + logger.exception(v) + return str(v) + except Exception as e: + msg = "反馈发送失败,请确保邮箱功能正常使用\n" + str(e) + logger.exception(msg) + return msg + return "邮件发送成功!" diff --git a/ui/README.md b/ui/README.md index 1eb166db..32e95aec 100644 --- a/ui/README.md +++ b/ui/README.md @@ -59,7 +59,7 @@ VITE_HTTP_URL="http://localhost:5000" npm run build ``` -将生成的 `dist` 文件夹复制到 `arknights-mower` 的目录中。此时运行后端: +此时运行后端: ```运行 flask --app server run --port=8000 @@ -78,7 +78,7 @@ pip install pyinstaller 使用 `pyinstaller` 打包: ```bash -pyinstaller menu.spec +pyinstaller webui_zip.spec ``` -生成的 `mower.exe` 在 `dist` 文件夹中。 +生成的 `mower.exe` 在 `dist` 文件夹中,打包后在依赖文件夹`_internal`中新建`ui`文件夹,并将构建的前端`dist`文件夹复制到该`ui`文件夹中即可使用。 diff --git a/ui/components.d.ts b/ui/components.d.ts index 9b0280b1..1d4dc6d1 100644 --- a/ui/components.d.ts +++ b/ui/components.d.ts @@ -85,6 +85,7 @@ declare module 'vue' { PlanEditor: typeof import('./src/components/PlanEditor.vue')['default'] ReclamationAlgorithm: typeof import('./src/components/ReclamationAlgorithm.vue')['default'] Recruit: typeof import('./src/components/Recruit.vue')['default'] + RenameDialog: typeof import('./src/components/RenameDialog.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SecretFront: typeof import('./src/components/SecretFront.vue')['default'] diff --git "a/ui/public/avatar/\344\272\221\350\277\271.webp" "b/ui/public/avatar/\344\272\221\350\277\271.webp" new file mode 100644 index 00000000..84ca1265 Binary files /dev/null and "b/ui/public/avatar/\344\272\221\350\277\271.webp" differ diff --git "a/ui/public/avatar/\345\207\257\347\221\237\347\220\263.webp" "b/ui/public/avatar/\345\207\257\347\221\237\347\220\263.webp" new file mode 100644 index 00000000..96c3065f Binary files /dev/null and "b/ui/public/avatar/\345\207\257\347\221\237\347\220\263.webp" differ diff --git "a/ui/public/avatar/\345\274\221\345\220\233\350\200\205.webp" "b/ui/public/avatar/\345\274\221\345\220\233\350\200\205.webp" new file mode 100644 index 00000000..6d8b82cc Binary files /dev/null and "b/ui/public/avatar/\345\274\221\345\220\233\350\200\205.webp" differ diff --git "a/ui/public/avatar/\345\277\215\345\206\254.webp" "b/ui/public/avatar/\345\277\215\345\206\254.webp" new file mode 100644 index 00000000..34155ca6 Binary files /dev/null and "b/ui/public/avatar/\345\277\215\345\206\254.webp" differ diff --git "a/ui/public/avatar/\346\243\256\350\245\277.webp" "b/ui/public/avatar/\346\243\256\350\245\277.webp" new file mode 100644 index 00000000..0783b14d Binary files /dev/null and "b/ui/public/avatar/\346\243\256\350\245\277.webp" differ diff --git "a/ui/public/avatar/\346\263\242\345\215\234.webp" "b/ui/public/avatar/\346\263\242\345\215\234.webp" new file mode 100644 index 00000000..f74d3d39 Binary files /dev/null and "b/ui/public/avatar/\346\263\242\345\215\234.webp" differ diff --git "a/ui/public/avatar/\347\216\233\351\234\262\350\245\277\345\260\224.webp" "b/ui/public/avatar/\347\216\233\351\234\262\350\245\277\345\260\224.webp" new file mode 100644 index 00000000..101811ee Binary files /dev/null and "b/ui/public/avatar/\347\216\233\351\234\262\350\245\277\345\260\224.webp" differ diff --git "a/ui/public/avatar/\347\273\264\345\250\234\302\267\347\273\264\345\244\232\345\210\251\344\272\232.webp" "b/ui/public/avatar/\347\273\264\345\250\234\302\267\347\273\264\345\244\232\345\210\251\344\272\232.webp" new file mode 100644 index 00000000..b5ce4ce4 Binary files /dev/null and "b/ui/public/avatar/\347\273\264\345\250\234\302\267\347\273\264\345\244\232\345\210\251\344\272\232.webp" differ diff --git "a/ui/public/avatar/\350\215\222\350\212\234\346\213\211\346\231\256\345\205\260\345\276\267.webp" "b/ui/public/avatar/\350\215\222\350\212\234\346\213\211\346\231\256\345\205\260\345\276\267.webp" new file mode 100644 index 00000000..425d30a3 Binary files /dev/null and "b/ui/public/avatar/\350\215\222\350\212\234\346\213\211\346\231\256\345\205\260\345\276\267.webp" differ diff --git "a/ui/public/avatar/\350\216\261\346\254\247\346\226\257.webp" "b/ui/public/avatar/\350\216\261\346\254\247\346\226\257.webp" new file mode 100644 index 00000000..1f7298b2 Binary files /dev/null and "b/ui/public/avatar/\350\216\261\346\254\247\346\226\257.webp" differ diff --git "a/ui/public/avatar/\350\217\262\350\216\261.webp" "b/ui/public/avatar/\350\217\262\350\216\261.webp" new file mode 100644 index 00000000..7c5c21cb Binary files /dev/null and "b/ui/public/avatar/\350\217\262\350\216\261.webp" differ diff --git "a/ui/public/avatar/\350\243\201\345\272\246.webp" "b/ui/public/avatar/\350\243\201\345\272\246.webp" new file mode 100644 index 00000000..2dc6d82e Binary files /dev/null and "b/ui/public/avatar/\350\243\201\345\272\246.webp" differ diff --git "a/ui/public/avatar/\351\275\220\345\260\224\346\237\245\345\205\213.webp" "b/ui/public/avatar/\351\275\220\345\260\224\346\237\245\345\205\213.webp" new file mode 100644 index 00000000..3b0607be Binary files /dev/null and "b/ui/public/avatar/\351\275\220\345\260\224\346\237\245\345\205\213.webp" differ diff --git a/ui/public/building_skill/bskill_dorm_bd_dungeon.webp b/ui/public/building_skill/bskill_dorm_bd_dungeon.webp new file mode 100644 index 00000000..121df4e2 Binary files /dev/null and b/ui/public/building_skill/bskill_dorm_bd_dungeon.webp differ diff --git a/ui/public/building_skill/bskill_dorm_senshi.webp b/ui/public/building_skill/bskill_dorm_senshi.webp new file mode 100644 index 00000000..691e930c Binary files /dev/null and b/ui/public/building_skill/bskill_dorm_senshi.webp differ diff --git a/ui/public/building_skill/bskill_dorm_unfull.webp b/ui/public/building_skill/bskill_dorm_unfull.webp new file mode 100644 index 00000000..a50835bb Binary files /dev/null and b/ui/public/building_skill/bskill_dorm_unfull.webp differ diff --git a/ui/public/building_skill/bskill_man_marcille1.webp b/ui/public/building_skill/bskill_man_marcille1.webp new file mode 100644 index 00000000..e590b910 Binary files /dev/null and b/ui/public/building_skill/bskill_man_marcille1.webp differ diff --git a/ui/public/building_skill/bskill_man_marcille2.webp b/ui/public/building_skill/bskill_man_marcille2.webp new file mode 100644 index 00000000..389ccad2 Binary files /dev/null and b/ui/public/building_skill/bskill_man_marcille2.webp differ diff --git a/ui/public/building_skill/bskill_man_spd&cost1.webp b/ui/public/building_skill/bskill_man_spd&cost1.webp new file mode 100644 index 00000000..0e5c54bd Binary files /dev/null and b/ui/public/building_skill/bskill_man_spd&cost1.webp differ diff --git a/ui/public/building_skill/bskill_man_spd&limit5.webp b/ui/public/building_skill/bskill_man_spd&limit5.webp new file mode 100644 index 00000000..81fb14cc Binary files /dev/null and b/ui/public/building_skill/bskill_man_spd&limit5.webp differ diff --git a/ui/public/building_skill/bskill_man_spd_bd_dungeon.webp b/ui/public/building_skill/bskill_man_spd_bd_dungeon.webp new file mode 100644 index 00000000..24b24192 Binary files /dev/null and b/ui/public/building_skill/bskill_man_spd_bd_dungeon.webp differ diff --git a/ui/public/building_skill/bskill_meet_bd_dungeon.webp b/ui/public/building_skill/bskill_meet_bd_dungeon.webp new file mode 100644 index 00000000..4b666636 Binary files /dev/null and b/ui/public/building_skill/bskill_meet_bd_dungeon.webp differ diff --git a/ui/public/building_skill/bskill_meet_spd&bd2.webp b/ui/public/building_skill/bskill_meet_spd&bd2.webp new file mode 100644 index 00000000..c0c3308c Binary files /dev/null and b/ui/public/building_skill/bskill_meet_spd&bd2.webp differ diff --git a/ui/public/building_skill/bskill_power_rec_spd&dorm&lv.webp b/ui/public/building_skill/bskill_power_rec_spd&dorm&lv.webp new file mode 100644 index 00000000..667f96ae Binary files /dev/null and b/ui/public/building_skill/bskill_power_rec_spd&dorm&lv.webp differ diff --git a/ui/public/building_skill/bskill_tra_spd_bd_dungeon.webp b/ui/public/building_skill/bskill_tra_spd_bd_dungeon.webp new file mode 100644 index 00000000..c2355cc3 Binary files /dev/null and b/ui/public/building_skill/bskill_tra_spd_bd_dungeon.webp differ diff --git a/ui/public/building_skill/bskill_train_caster&vanguard1.webp b/ui/public/building_skill/bskill_train_caster&vanguard1.webp new file mode 100644 index 00000000..a4f51bbb Binary files /dev/null and b/ui/public/building_skill/bskill_train_caster&vanguard1.webp differ diff --git a/ui/public/building_skill/bskill_train_siracusa.webp b/ui/public/building_skill/bskill_train_siracusa.webp new file mode 100644 index 00000000..8be94f6b Binary files /dev/null and b/ui/public/building_skill/bskill_train_siracusa.webp differ diff --git a/ui/public/building_skill/bskill_ws_cost.webp b/ui/public/building_skill/bskill_ws_cost.webp new file mode 100644 index 00000000..d2ba798f Binary files /dev/null and b/ui/public/building_skill/bskill_ws_cost.webp differ diff --git a/ui/public/building_skill/bskill_ws_p7.webp b/ui/public/building_skill/bskill_ws_p7.webp new file mode 100644 index 00000000..bb4d8518 Binary files /dev/null and b/ui/public/building_skill/bskill_ws_p7.webp differ diff --git a/ui/public/building_skill/trade_ord_spd&par1.webp b/ui/public/building_skill/trade_ord_spd&par1.webp new file mode 100644 index 00000000..ac736c6e Binary files /dev/null and b/ui/public/building_skill/trade_ord_spd&par1.webp differ diff --git a/ui/public/building_skill/trade_ord_spd&par2.webp b/ui/public/building_skill/trade_ord_spd&par2.webp new file mode 100644 index 00000000..0e14bc6b Binary files /dev/null and b/ui/public/building_skill/trade_ord_spd&par2.webp differ diff --git "a/ui/public/depot/2024\346\204\237\350\260\242\345\272\206\345\205\270\347\211\251\350\265\204\350\241\245\347\273\231.webp" "b/ui/public/depot/2024\346\204\237\350\260\242\345\272\206\345\205\270\347\211\251\350\265\204\350\241\245\347\273\231.webp" new file mode 100644 index 00000000..a75ea3ee Binary files /dev/null and "b/ui/public/depot/2024\346\204\237\350\260\242\345\272\206\345\205\270\347\211\251\350\265\204\350\241\245\347\273\231.webp" differ diff --git "a/ui/public/depot/\344\270\255\345\235\232\351\253\230\347\272\247\345\271\262\345\221\230\350\260\203\347\224\250\345\207\255\350\257\201.webp" "b/ui/public/depot/\344\270\255\345\235\232\351\253\230\347\272\247\345\271\262\345\221\230\350\260\203\347\224\250\345\207\255\350\257\201.webp" new file mode 100644 index 00000000..1576e05c Binary files /dev/null and "b/ui/public/depot/\344\270\255\345\235\232\351\253\230\347\272\247\345\271\262\345\221\230\350\260\203\347\224\250\345\207\255\350\257\201.webp" differ diff --git "a/ui/public/depot/\344\272\221\350\277\271\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\344\272\221\350\277\271\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..4690e335 Binary files /dev/null and "b/ui/public/depot/\344\272\221\350\277\271\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\345\207\257\347\221\237\347\220\263\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\345\207\257\347\221\237\347\220\263\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..a3405b80 Binary files /dev/null and "b/ui/public/depot/\345\207\257\347\221\237\347\220\263\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\345\245\275\345\245\275\345\220\203\351\245\255\345\257\273\350\256\277\345\207\255\350\257\201.webp" "b/ui/public/depot/\345\245\275\345\245\275\345\220\203\351\245\255\345\257\273\350\256\277\345\207\255\350\257\201.webp" new file mode 100644 index 00000000..1cf65887 Binary files /dev/null and "b/ui/public/depot/\345\245\275\345\245\275\345\220\203\351\245\255\345\257\273\350\256\277\345\207\255\350\257\201.webp" differ diff --git "a/ui/public/depot/\345\274\221\345\220\233\350\200\205\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\345\274\221\345\220\233\350\200\205\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..198b2fcd Binary files /dev/null and "b/ui/public/depot/\345\274\221\345\220\233\350\200\205\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\345\277\215\345\206\254\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\345\277\215\345\206\254\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..e5e6e1aa Binary files /dev/null and "b/ui/public/depot/\345\277\215\345\206\254\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\346\234\252\350\207\264\350\222\231\345\260\230\345\257\273\350\256\277\345\207\255\350\257\201.webp" "b/ui/public/depot/\346\234\252\350\207\264\350\222\231\345\260\230\345\257\273\350\256\277\345\207\255\350\257\201.webp" new file mode 100644 index 00000000..80acbe69 Binary files /dev/null and "b/ui/public/depot/\346\234\252\350\207\264\350\222\231\345\260\230\345\257\273\350\256\277\345\207\255\350\257\201.webp" differ diff --git "a/ui/public/depot/\346\243\256\350\245\277\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\346\243\256\350\245\277\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..6a6c76df Binary files /dev/null and "b/ui/public/depot/\346\243\256\350\245\277\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\346\255\242\351\242\202\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\346\255\242\351\242\202\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..7483e688 Binary files /dev/null and "b/ui/public/depot/\346\255\242\351\242\202\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\346\263\242\345\215\234\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\346\263\242\345\215\234\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..ea5c4125 Binary files /dev/null and "b/ui/public/depot/\346\263\242\345\215\234\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\347\213\202\346\254\242\347\203\237\350\212\261\346\241\266.webp" "b/ui/public/depot/\347\213\202\346\254\242\347\203\237\350\212\261\346\241\266.webp" new file mode 100644 index 00000000..4ca94956 Binary files /dev/null and "b/ui/public/depot/\347\213\202\346\254\242\347\203\237\350\212\261\346\241\266.webp" differ diff --git "a/ui/public/depot/\347\216\233\351\234\262\350\245\277\345\260\224\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\347\216\233\351\234\262\350\245\277\345\260\224\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..f43e152b Binary files /dev/null and "b/ui/public/depot/\347\216\233\351\234\262\350\245\277\345\260\224\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\347\273\264\345\250\234\302\267\347\273\264\345\244\232\345\210\251\344\272\232\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\347\273\264\345\250\234\302\267\347\273\264\345\244\232\345\210\251\344\272\232\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..a39e4ec2 Binary files /dev/null and "b/ui/public/depot/\347\273\264\345\250\234\302\267\347\273\264\345\244\232\345\210\251\344\272\232\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\350\215\222\350\212\234\346\213\211\346\231\256\345\205\260\345\276\267\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\350\215\222\350\212\234\346\213\211\346\231\256\345\205\260\345\276\267\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..9b52b0bf Binary files /dev/null and "b/ui/public/depot/\350\215\222\350\212\234\346\213\211\346\231\256\345\205\260\345\276\267\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\350\215\222\350\212\234\346\216\242\346\210\210\345\257\273\350\256\277\345\207\255\350\257\201.webp" "b/ui/public/depot/\350\215\222\350\212\234\346\216\242\346\210\210\345\257\273\350\256\277\345\207\255\350\257\201.webp" new file mode 100644 index 00000000..001c671b Binary files /dev/null and "b/ui/public/depot/\350\215\222\350\212\234\346\216\242\346\210\210\345\257\273\350\256\277\345\207\255\350\257\201.webp" differ diff --git "a/ui/public/depot/\350\216\261\346\254\247\346\226\257\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\350\216\261\346\254\247\346\226\257\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..eb619b27 Binary files /dev/null and "b/ui/public/depot/\350\216\261\346\254\247\346\226\257\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\350\217\262\350\216\261\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\350\217\262\350\216\261\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..10d6796c Binary files /dev/null and "b/ui/public/depot/\350\217\262\350\216\261\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\350\243\201\345\272\246\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\350\243\201\345\272\246\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..44e62eaf Binary files /dev/null and "b/ui/public/depot/\350\243\201\345\272\246\347\232\204\344\277\241\347\211\251.webp" differ diff --git "a/ui/public/depot/\351\275\220\345\260\224\346\237\245\345\205\213\347\232\204\344\277\241\347\211\251.webp" "b/ui/public/depot/\351\275\220\345\260\224\346\237\245\345\205\213\347\232\204\344\277\241\347\211\251.webp" new file mode 100644 index 00000000..c898cf89 Binary files /dev/null and "b/ui/public/depot/\351\275\220\345\260\224\346\237\245\345\205\213\347\232\204\344\277\241\347\211\251.webp" differ diff --git a/ui/src/App.vue b/ui/src/App.vue index b9051ec2..79938889 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -275,7 +275,7 @@ const axios = inject('axios') function start() { running.value = true log_lines.value = [] - axios.get(`${import.meta.env.VITE_HTTP_URL}/start`) + axios.get(`${import.meta.env.VITE_HTTP_URL}/start/0`) } function actions_on_resize() { diff --git a/ui/src/components/DailyMission.vue b/ui/src/components/DailyMission.vue index 603586e8..44cafa57 100644 --- a/ui/src/components/DailyMission.vue +++ b/ui/src/components/DailyMission.vue @@ -42,12 +42,12 @@ const { check_mail_enable, report_enable, sign_in, visit_friend, skland_info, sk - + diff --git a/ui/src/components/Feedback.vue b/ui/src/components/Feedback.vue new file mode 100644 index 00000000..c8837857 --- /dev/null +++ b/ui/src/components/Feedback.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/ui/src/components/MaaWeekly.vue b/ui/src/components/MaaWeekly.vue index 1ffd26bf..a904b74f 100644 --- a/ui/src/components/MaaWeekly.vue +++ b/ui/src/components/MaaWeekly.vue @@ -118,11 +118,11 @@ function create_tag(label) { - + - +
{{ plan.weekday }} @@ -137,11 +137,11 @@ function create_tag(label) { :on-create="create_tag" />
diff --git a/ui/src/components/MaaWeeklyNew.vue b/ui/src/components/MaaWeeklyNew.vue index e73f26f0..a642302a 100644 --- a/ui/src/components/MaaWeeklyNew.vue +++ b/ui/src/components/MaaWeeklyNew.vue @@ -386,7 +386,7 @@ function clear() { {{ day[1] }}{{ currentDay === (index + 1) % 7 ? ' (今天)' : '' }} - + diff --git a/ui/src/components/PlanEditor.vue b/ui/src/components/PlanEditor.vue index 9075ae9f..8eddba63 100644 --- a/ui/src/components/PlanEditor.vue +++ b/ui/src/components/PlanEditor.vue @@ -6,14 +6,8 @@ import { swap } from '@/utils/common' import { ref, computed, nextTick, watch, inject } from 'vue' const config_store = useConfigStore() const plan_store = usePlanStore() -const { - operators, - groups, - current_plan: plan, - workaholic, - sub_plan, - backup_plans -} = storeToRefs(plan_store) +const { operators, groups, current_plan, plan, workaholic, sub_plan, backup_plans } = + storeToRefs(plan_store) const { facility_operator_limit } = plan_store const { theme } = storeToRefs(config_store) @@ -34,14 +28,14 @@ const button_type = { } const operator_limit = computed(() => { - if (facility.value.startsWith('room') && plan.value[facility.value].name == '发电站') { + if (facility.value.startsWith('room') && current_plan.value[facility.value].name == '发电站') { return 1 } return facility_operator_limit[facility.value] || 0 }) function clear() { - plan.value[facility.value].name = '' + current_plan.value[facility.value].name = '' nextTick(() => { const plans = [] for (let i = 0; i < operator_limit.value; ++i) { @@ -51,25 +45,25 @@ function clear() { replacement: [] }) } - plan.value[facility.value].plans = plans + current_plan.value[facility.value].plans = plans }) } watch( () => { if (facility.value.startsWith('room')) { - return plan.value[facility.value].name + return current_plan.value[facility.value].name } return '' }, (new_name, old_name) => { if (new_name == '发电站') { - const plans = plan.value[facility.value].plans + const plans = current_plan.value[facility.value].plans while (plans.length > operator_limit.value) { plans.pop() } } else if (old_name == '发电站') { - const plans = plan.value[facility.value].plans + const plans = current_plan.value[facility.value].plans while (plans.length < operator_limit.value) { plans.push({ agent: '', group: '', replacement: [] }) } @@ -127,7 +121,7 @@ const right_side_facility_name = computed(() => { const facility_empty = computed(() => { let empty = true - for (const i of plan.value[facility.value].plans) { + for (const i of current_plan.value[facility.value].plans) { if (i.agent) { empty = false break @@ -151,16 +145,69 @@ function drag_facility(room, event) { event.dataTransfer.dropEffect = 'move' } +function updateTrigger(trigger, source, target) { + for (const key in trigger) { + if (key === 'left' || key === 'right') { + if (typeof trigger[key] === 'string') { + trigger[key] = swapSubstrings(trigger[key], source, target) + } else if (typeof trigger[key] === 'object' && trigger[key] !== null) { + updateTrigger(trigger[key], source, target) + } + } + } +} + +function swapSubstrings(str, source, target) { + const placeholder = '__PLACEHOLDER__' + let newStr = str.replace(new RegExp(source, 'g'), placeholder) + newStr = newStr.replace(new RegExp(target, 'g'), source) + newStr = newStr.replace(new RegExp(placeholder, 'g'), target) + return newStr +} + +function swapTask(tasks, source, target) { + if (tasks) { + const placeholder = '__PLACEHOLDER__' + if (tasks.hasOwnProperty(source)) { + tasks[placeholder] = tasks[source] + delete tasks[source] + } + if (tasks.hasOwnProperty(target)) { + tasks[source] = tasks[target] + delete tasks[target] + } + if (tasks.hasOwnProperty(placeholder)) { + tasks[target] = tasks[placeholder] + delete tasks[placeholder] + } + } +} + function drop_facility(target, event) { const source = event.dataTransfer.getData('text/plain') - swap(source, target, plan.value) - // 移动主表设施时, 同步移动副表对应设施 - if (sub_plan.value == 'main') { - backup_plans.value.forEach((item) => { + // 1. 更新当前 current_plan 表 + swap(source, target, current_plan.value) + + // 2. 更新所有副表和主表(除当前表以外) + const allPlans = ['main', ...backup_plans.value] + + allPlans.forEach((item, index) => { + if ((sub_plan.value === 'main' && item === 'main') || sub_plan.value + 1 === index) { + return + } + // 执行更新操作 + if (item !== 'main') { swap(source, target, item.plan) - }) - } + // 副表才需要更新trigger 和 task + swapTask(item.task, source, target) + updateTrigger(item.trigger, source, target) + } else { + // plan 是主表 + swap(source, target, plan.value) + } + }) + event.preventDefault() } @@ -177,8 +224,8 @@ import { pinyin_match } from '@/utils/common' function fill_with_free() { for (let i = 0; i < operator_limit.value; ++i) { - if (plan.value[facility.value].plans[i].agent == '') { - plan.value[facility.value].plans[i].agent = 'Free' + if (current_plan.value[facility.value].plans[i].agent == '') { + current_plan.value[facility.value].plans[i].agent = 'Free' } } } @@ -224,8 +271,8 @@ const product_bg_opacity = computed(() => { const fia_list = computed(() => { for (let i = 1; i <= 4; ++i) { for (let j = 0; j < 5; ++j) { - if (plan.value[`dormitory_${i}`].plans[j].agent == '菲亚梅塔') { - return plan.value[`dormitory_${i}`].plans[j].replacement + if (current_plan.value[`dormitory_${i}`].plans[j].agent == '菲亚梅塔') { + return current_plan.value[`dormitory_${i}`].plans[j].replacement } } } @@ -251,17 +298,17 @@ function set_facility(e) { v-for="r in [`room_${row}_1`, `room_${row}_2`, `room_${row}_3`]" :key="r" @click="set_facility(r)" - :class="[button_type[plan[r].name], r === facility ? 'true' : 'false']" + :class="[button_type[current_plan[r].name], r === facility ? 'true' : 'false']" >
- {{ plan[r].name }} + {{ current_plan[r].name }}
-