mower-ng/mower/__main__.py

430 lines
18 KiB
Python

import json
from datetime import datetime, timedelta
from evalidate import Expr
from mower.solvers.base_schedule import BaseSchedulerSolver
from mower.solvers.depot_reader import DepotManager
from mower.solvers.reclamation_algorithm import ReclamationAlgorithm
from mower.solvers.secret_front import SecretFront
from mower.utils import config, path
from mower.utils.csleep import MowerExit
from mower.utils.datetime import format_time
from mower.utils.device.adb_client.session import Session
from mower.utils.device.scrcpy import Scrcpy
from mower.utils.email import send_message, task_template
from mower.utils.log import logger
from mower.utils.logic_expression import get_logic_exp
from mower.utils.path import get_path
from mower.utils.plan import Plan, PlanConfig, Room
from mower.utils.simulator import restart_simulator
base_scheduler = None
operators = config.operators
# 执行自动排班
def main():
logger.info("开始运行Mower")
simulate()
def initialize(
tasks: list, scheduler: BaseSchedulerSolver | None = None
) -> BaseSchedulerSolver:
if scheduler:
scheduler.handle_error(True)
return scheduler
base_scheduler = BaseSchedulerSolver()
plan1 = {}
plan = config.plan.model_dump(exclude_none=True)
conf = config.conf
plan_config = PlanConfig(
rest_in_full=config.plan.conf.rest_in_full,
exhaust_require=config.plan.conf.exhaust_require,
resting_priority=config.plan.conf.resting_priority,
ling_xi=config.plan.conf.ling_xi,
workaholic=config.plan.conf.workaholic,
max_resting_count=config.plan.conf.max_resting_count,
free_blacklist=conf.free_blacklist,
resting_threshold=conf.resting_threshold,
refresh_trading_config=config.plan.conf.refresh_trading,
free_room=conf.free_room,
)
for room, obj in plan[plan["default"]].items():
plan1[room] = [
Room(op["agent"], op["group"], op["replacement"]) for op in obj["plans"]
]
# 默认任务
plan["default_plan"] = Plan(plan1, plan_config)
# 备用自定义任务
backup_plans: list[Plan] = []
for i in plan["backup_plans"]:
backup_plan: dict[str, Room] = {}
for room, obj in i["plan"].items():
backup_plan[room] = [
Room(op["agent"], op["group"], op["replacement"]) for op in obj["plans"]
]
backup_config = PlanConfig(
i["conf"]["rest_in_full"],
i["conf"]["exhaust_require"],
i["conf"]["resting_priority"],
ling_xi=i["conf"]["ling_xi"],
workaholic=i["conf"]["workaholic"],
max_resting_count=i["conf"]["max_resting_count"],
free_blacklist=i["conf"]["free_blacklist"],
resting_threshold=conf.resting_threshold,
refresh_trading_config=i["conf"]["refresh_trading"],
free_room=conf.free_room,
)
backup_trigger = get_logic_exp(i["trigger"]) if "trigger" in i else None
backup_task = i.get("task")
backup_trigger_timing = i.get("trigger_timing")
backup_plans.append(
Plan(
backup_plan,
backup_config,
trigger=backup_trigger,
task=backup_task,
trigger_timing=backup_trigger_timing,
)
)
plan["backup_plans"] = backup_plans
logger.debug(plan)
base_scheduler.global_plan = plan
base_scheduler.tasks = tasks
base_scheduler.enable_party = conf.enable_party == 1 # 是否使用线索
base_scheduler.leifeng_mode = conf.leifeng_mode == 1 # 是否有额外线索就送出
# 干员宿舍回复阈值
# 高效组心情低于 UpperLimit * 阈值 (向下取整)的时候才会会安排休息
base_scheduler.last_room = ""
# logger.info("宿舍黑名单:" + str(plan_config.free_blacklist))
# 估计没用了
base_scheduler.MAA = None
base_scheduler.error = False
base_scheduler.drone_room = None if conf.drone_room == "" else conf.drone_room
base_scheduler.reload_room = list(
filter(None, conf.reload_room.replace("", ",").split(","))
)
# 关闭游戏次数计数器
base_scheduler.task_count = 0
return base_scheduler
def simulate():
"""
具体调用方法可见各个函数的参数说明
"""
logger.info(f"正在使用全局配置空间: {path.global_space}")
tasks = []
reconnect_max_tries = 10
reconnect_tries = 0
global base_scheduler
success = False
while not success:
try:
base_scheduler = initialize(tasks)
success = True
except MowerExit:
return
except Exception as e:
logger.exception(e)
reconnect_tries += 1
if reconnect_tries < 3:
restart_simulator()
config.device.client.check_server_alive()
Session().connect()
if config.conf.droidcast.enable:
config.device.start_droidcast()
if config.conf.touch_method == "scrcpy":
config.device.control.scrcpy = Scrcpy(config.device.client)
continue
else:
raise e
# base_scheduler.仓库扫描() #别删了 方便我找
validation_msg = base_scheduler.initialize_operators()
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)
)
for con in conditions:
validation_msg = base_scheduler.op_data.swap_plan(con, True)
if validation_msg is not None:
logger.error(f"替换排班验证错误:{validation_msg}, 附表条件为 {con}")
return
base_scheduler.op_data.swap_plan(
[False] * len(base_scheduler.op_data.backup_plans), True
)
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 (
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.visit_friend_plan_solver():
base_scheduler.daily_visit_friend = (
datetime.now() - timedelta(hours=4)
).date()
if (
base_scheduler.daily_report
< (datetime.now() - timedelta(hours=4)).date()
):
if base_scheduler.report_plan_solver():
base_scheduler.daily_report = (
datetime.now() - timedelta(hours=4)
).date()
if (
base_scheduler.switch_assistants
< (datetime.now() - timedelta(hours=4)).date()
):
if base_scheduler.switch_assistants_plan_solver():
base_scheduler.switch_assistants = (
datetime.now() - timedelta(hours=4)
).date()
if (
config.conf.skland_enable
and base_scheduler.daily_skland
< (datetime.now() - timedelta(hours=4)).date()
):
if base_scheduler.skland_plan_solover():
base_scheduler.daily_skland = (
datetime.now() - timedelta(hours=4)
).date()
if (
config.conf.check_mail_enable
and base_scheduler.daily_mail
< (datetime.now() - timedelta(hours=8)).date()
):
if base_scheduler.mail_plan_solver():
base_scheduler.daily_mail = (
datetime.now() - timedelta(hours=8)
).date()
if config.conf.recruit_enable:
base_scheduler.recruit_plan_solver()
# 应该在maa任务之后
def _is_depotscan():
depot_manager = DepotManager()
return depot_manager.读取仓库()[-1]
if config.conf.maa_depot_enable:
dt = int(datetime.now().timestamp()) - _is_depotscan()
if dt >= config.conf.maa_gap * 3600:
base_scheduler.仓库扫描()
else:
logger.info(
f"仓库扫描未到时间,将在 {config.conf.maa_gap - dt // 3600}小时之内开始扫描"
)
if config.conf.maa_enable == 1:
subject = f"下次任务在{base_scheduler.tasks[0].time.strftime('%H:%M:%S')}"
context = f"下一次任务:{base_scheduler.tasks[0].plan}"
logger.info(context)
logger.info(subject)
body = task_template.render(
tasks=[
obj.format(timezone_offset)
for obj in base_scheduler.tasks
],
base_scheduler=base_scheduler,
)
send_message(body, subject)
base_scheduler.maa_plan_solver()
else:
remaining_time = (
base_scheduler.tasks[0].time - datetime.now()
).total_seconds()
subject = f"休息 {format_time(remaining_time)},到{base_scheduler.tasks[0].time.strftime('%H:%M:%S')}开始工作"
context = f"下一次任务:{base_scheduler.tasks[0].plan if len(base_scheduler.tasks[0].plan) != 0 else '空任务' if base_scheduler.tasks[0].type == '' else base_scheduler.tasks[0].type}"
logger.info(context)
logger.info(subject)
base_scheduler.task_count += 1
logger.info(f"{base_scheduler.task_count}次任务结束")
if remaining_time > 0:
if remaining_time > 300:
if config.conf.close_simulator_when_idle:
restart_simulator(start=False)
elif config.conf.exit_game_when_idle:
config.device.exit()
body = task_template.render(
tasks=[
obj.format(timezone_offset)
for obj in base_scheduler.tasks
],
base_scheduler=base_scheduler,
)
send_message(body, subject)
config.idle = True
base_scheduler.sleep(remaining_time)
config.idle = False
base_scheduler.check_current_focus()
elif remaining_time > 0:
now_time = datetime.now().time()
try:
min_time = datetime.strptime(
config.conf.maa_rg_sleep_min, "%H:%M"
).time()
max_time = datetime.strptime(
config.conf.maa_rg_sleep_max, "%H:%M"
).time()
if max_time < min_time:
rg_sleep = now_time > min_time or now_time < max_time
else:
rg_sleep = min_time < now_time < max_time
except ValueError:
rg_sleep = False
if not rg_sleep:
if config.conf.RA:
config.recog.update()
base_scheduler.back_to_index()
ra_solver = ReclamationAlgorithm()
ra_solver.scheduler_stop_time = base_scheduler.tasks[0].time
ra_solver.run()
remaining_time = (
base_scheduler.tasks[0].time - datetime.now()
).total_seconds()
elif config.conf.SF:
config.recog.update()
base_scheduler.back_to_index()
sf_solver = SecretFront()
sf_solver.scheduler_stop_time = base_scheduler.tasks[0].time
sf_solver.run()
remaining_time = (
base_scheduler.tasks[0].time - datetime.now()
).total_seconds()
subject = f"休息 {format_time(remaining_time)},到{base_scheduler.tasks[0].time.strftime('%H:%M:%S')}开始工作"
context = f"下一次任务:{base_scheduler.tasks[0].plan}"
logger.info(context)
logger.info(subject)
base_scheduler.task_count += 1
logger.info(f"{base_scheduler.task_count}次任务结束")
if remaining_time > 300:
if config.conf.close_simulator_when_idle:
restart_simulator(start=False)
elif config.conf.exit_game_when_idle:
config.device.exit()
body = task_template.render(
tasks=[
obj.format(timezone_offset) for obj in base_scheduler.tasks
],
base_scheduler=base_scheduler,
)
send_message(body, subject)
config.idle = True
base_scheduler.sleep(remaining_time)
config.idle = False
base_scheduler.check_current_focus()
base_scheduler.run()
reconnect_tries = 0
except MowerExit:
return
except (ConnectionError, ConnectionAbortedError, AttributeError) as e:
logger.exception(e)
reconnect_tries += 1
if reconnect_tries < reconnect_max_tries:
logger.warning("出现错误.尝试重启Mower")
connected = False
while not connected:
try:
base_scheduler = initialize([], base_scheduler)
break
except MowerExit:
raise
except Exception as e:
logger.exception(e)
restart_simulator()
config.device.client.check_server_alive()
Session().connect()
if config.conf.droidcast.enable:
config.device.start_droidcast()
if config.conf.touch_method == "scrcpy":
config.device.control.scrcpy = Scrcpy(config.device.client)
continue
continue
else:
raise e
except RuntimeError as e:
logger.exception(f"程序出错-尝试重启模拟器->{e}")
restart_simulator()
config.device.client.check_server_alive()
Session().connect()
if config.conf.droidcast.enable:
config.device.start_droidcast()
if config.conf.touch_method == "scrcpy":
config.device.control.scrcpy = Scrcpy(config.device.client)
except Exception as e:
logger.exception(f"程序出错--->{e}")
config.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