Files

444 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Recorder;
using BetterGenshinImpact.Core.Script.Project;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.AutoPathing;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
using BetterGenshinImpact.GameTask.Shell;
using BetterGenshinImpact.ViewModel.Pages;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using BetterGenshinImpact.GameTask.AutoPathing.Model.Enum;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.FarmingPlan;
using BetterGenshinImpact.GameTask.LogParse;
using BetterGenshinImpact.Helpers;
using Microsoft.Extensions.Logging;
namespace BetterGenshinImpact.Core.Script.Group;
public partial class ScriptGroupProject : ObservableObject
{
[ObservableProperty]
private int _index;
/// <summary>
/// 理论上是文件名
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 理论上是当前类型脚本根目录到脚本文件所在目录的相对路径
/// 但是:
/// 1. JS 脚本的文件名是内部的名称,文件夹名脚本所在文件夹,这个也是唯一标识
/// 2. KeyMouse 脚本的文件名和文件夹名相同,文件夹名暂时无意义
/// 3. Pathing 文件名就是实际脚本的文件名,文件夹名是脚本所在的相对目录
/// </summary>
public string FolderName { get; set; } = string.Empty;
[ObservableProperty]
private string _type = string.Empty;
[JsonIgnore]
public string TypeDesc => ScriptGroupProjectExtensions.TypeDescriptions[Type];
[ObservableProperty]
private string _status = string.Empty;
[JsonIgnore]
public string StatusDesc => ScriptGroupProjectExtensions.StatusDescriptions[Status];
/// <summary>
/// 执行周期
/// 不在 ScheduleDescriptions 中则会被视为自定义Cron表达式
/// </summary>
[ObservableProperty]
private string _schedule = string.Empty;
[JsonIgnore]
public string ScheduleDesc => ScriptGroupProjectExtensions.ScheduleDescriptions.GetValueOrDefault(Schedule, "自定义周期");
[ObservableProperty]
private int _runNum = 1;
[JsonIgnore]
public ScriptProject? Project { get; set; }
public ExpandoObject? JsScriptSettingsObject { get; set; }
/// <summary>
/// 所属配置组
/// </summary>
[JsonIgnore]
public ScriptGroup? GroupInfo { get; set; }
private bool? _nextFlag = false;
private bool? _skipFlag = false;
/// <summary>
/// 下一个从此执行标志
/// </summary>
[JsonIgnore]
public bool? NextFlag
{
get => _nextFlag;
set => SetProperty(ref _nextFlag, value);
}
/// <summary>
/// 直接跳过标志
/// </summary>
[JsonIgnore]
public bool? SkipFlag
{
get => _skipFlag;
set => SetProperty(ref _skipFlag, value);
}
[ObservableProperty]
private bool? _allowJsNotification = true;
[ObservableProperty]
private string? _allowJsHTTPHash = "";
/// <summary>
/// 是否允许JS脚本发送HTTP请求通过验证Hash来控制
/// </summary>
[JsonIgnore]
public bool AllowJsHTTP
{
get
{
return GetHttpAllowedUrlsHash() == AllowJsHTTPHash;
}
}
public ScriptGroupProject()
{
}
public ScriptGroupProject(ScriptProject project)
{
Name = project.Manifest.Name;
FolderName = project.FolderName;
Status = "Enabled";
Schedule = "Daily";
Project = project;
Type = "Javascript";
}
/// <summary>
///
/// </summary>
/// <param name="name"></param>
/// <param name="folder"></param>
/// <param name="type">KeyMouse|Pathing</param>
public ScriptGroupProject(string name, string folder, string type)
{
Name = name;
FolderName = folder;
Status = "Enabled";
Schedule = "Daily";
Project = null; // 不是JS脚本
Type = type;
}
public static ScriptGroupProject BuildKeyMouseProject(string name)
{
return new ScriptGroupProject(name, name, "KeyMouse");
}
public static ScriptGroupProject BuildShellProject(string command)
{
return new ScriptGroupProject(command, "", "Shell");
}
public static ScriptGroupProject BuildPathingProject(string name, string folder)
{
return new ScriptGroupProject(name, folder, "Pathing");
}
/// <summary>
/// 通过 FolderName 查找 ScriptProject
/// </summary>
public void BuildScriptProjectRelation()
{
if (string.IsNullOrEmpty(FolderName))
{
throw new Exception("FolderName 为空");
}
Project = new ScriptProject(FolderName);
}
public string GetHttpAllowedUrlsHash()
{
if (Project == null)
{
BuildScriptProjectRelation();
}
if (Project == null)
{
return "";
}
return string.Join("|", Project.Manifest.HttpAllowedUrls);
}
public async Task Run()
{
//执行记录
ExecutionRecord executionRecord = new ExecutionRecord()
{
ServerStartTime =
GroupInfo?.Config.PathingConfig.TaskCompletionSkipRuleConfig.IsBoundaryTimeBasedOnServerTime ?? false
? ServerTimeHelper.GetServerTimeNow()
: DateTimeOffset.Now,
StartTime = DateTime.Now,
GroupName = GroupInfo?.Name ?? "",
FolderName = FolderName,
ProjectName = Name,
Type = Type
};
ExecutionRecordStorage.SaveExecutionRecord(executionRecord);
if (Type == "Javascript")
{
if (Project == null)
{
throw new Exception("JS脚本未初始化");
}
JsScriptSettingsObject ??= new ExpandoObject();
// 清理配置中的无效值
CleanInvalidSettingsValues();
var pathingPartyConfig = GroupInfo?.Config.PathingConfig;
await Project.ExecuteAsync(JsScriptSettingsObject, pathingPartyConfig);
}
else if (Type == "KeyMouse")
{
// 加载并执行
var json = await File.ReadAllTextAsync(Global.Absolute(@$"User\KeyMouseScript\{Name}"));
await KeyMouseMacroPlayer.PlayMacro(json, CancellationContext.Instance.Cts.Token, false);
}
else if (Type == "Pathing")
{
// 加载并执行
var task = PathingTask.BuildFromFilePath(Path.Combine(MapPathingViewModel.PathJsonPath, FolderName, Name));
if (task == null)
{
return;
}
var pathingTask = new PathExecutor(CancellationContext.Instance.Cts.Token);
pathingTask.PartyConfig = GroupInfo?.Config.PathingConfig;
if (pathingTask.PartyConfig is null || pathingTask.PartyConfig.AutoPickEnabled)
{
TaskTriggerDispatcher.Instance().AddTrigger("AutoPick", null);
}
await pathingTask.Pathing(task);
executionRecord.IsSuccessful = pathingTask.SuccessEnd;
OtherConfig.AutoRestart autoRestart = TaskContext.Instance().Config.OtherConfig.AutoRestartConfig;
if (!pathingTask.SuccessEnd)
{
TaskControl.Logger.LogWarning($"此追踪脚本未正常走完!");
if (autoRestart.Enabled && autoRestart.IsPathingFailureExceptional && !pathingTask.SuccessEnd)
{
throw new Exception($"路径追踪任务未完全走完,判定失败,触发异常!");
}
}
if (task.FarmingInfo.AllowFarmingCount)
{
var successFight = pathingTask.SuccessEnd;
var fightCount = 0;
//未走完完整路径下,才校验打架次数
if (!successFight)
{
fightCount = task.Positions.Count(pos => pos.Action == ActionEnum.Fight.Code);
successFight = pathingTask.SuccessFight >= fightCount;
//判断为锄地脚本
if (task.FarmingInfo.PrimaryTarget!="disable")
{
if (autoRestart.Enabled
&&autoRestart.IsFightFailureExceptional
&&!successFight)
{
throw new Exception($"实际战斗次数({pathingTask.SuccessFight})<预期战斗次数({fightCount}),判定失败,触发异常!");
}
}
}
if (successFight)
{
//每日锄地记录
FarmingStatsRecorder.RecordFarmingSession(task.FarmingInfo, new FarmingRouteInfo
{
GroupName = GroupInfo?.Name ?? "",
FolderName = FolderName,
ProjectName = Name
});
}
else
{
TaskControl.Logger.LogWarning($"实际战斗次数({pathingTask.SuccessFight})<预期战斗次数({fightCount}),判定失败,此次不纳入成功锄地规划的统计上限!");
}
}
}
else if (Type == "Shell")
{
ShellConfig? shellConfig = null;
if (GroupInfo?.Config.EnableShellConfig ?? false)
{
shellConfig = GroupInfo?.Config.ShellConfig;
}
var task = new ShellTask(ShellTaskParam.BuildFromConfig(Name, shellConfig ?? new ShellConfig()));
await task.Start(CancellationContext.Instance.Cts.Token);
}
if (Type != "Pathing")
{
executionRecord.IsSuccessful = true;
}
executionRecord.ServerEndTime =
GroupInfo?.Config.PathingConfig.TaskCompletionSkipRuleConfig.IsBoundaryTimeBasedOnServerTime ?? false
? ServerTimeHelper.GetServerTimeNow()
: DateTimeOffset.Now;
executionRecord.EndTime = DateTime.Now;
ExecutionRecordStorage.SaveExecutionRecord(executionRecord);
}
partial void OnTypeChanged(string value)
{
OnPropertyChanged(nameof(TypeDesc));
}
partial void OnStatusChanged(string value)
{
OnPropertyChanged(nameof(StatusDesc));
}
partial void OnScheduleChanged(string value)
{
OnPropertyChanged(nameof(ScheduleDesc));
}
/// <summary>
/// 清理 JsScriptSettingsObject 中不在 settings.json 定义的 Options 列表中的无效值
/// </summary>
private void CleanInvalidSettingsValues()
{
if (Project == null || JsScriptSettingsObject == null)
{
return;
}
try
{
// 加载 settings.json 中定义的配置项
var settingItems = Project.Manifest.LoadSettingItems(Project.ProjectPath);
if (settingItems.Count == 0)
{
return;
}
var settingsDict = JsScriptSettingsObject as IDictionary<string, object?>;
if (settingsDict == null)
{
return;
}
// 遍历所有 multi-checkbox 类型的配置项
foreach (var item in settingItems.Where(i => i.Type == "multi-checkbox"))
{
if (!settingsDict.ContainsKey(item.Name))
{
continue;
}
// 获取当前保存的值
var savedValue = settingsDict[item.Name];
List<string>? checkedValues = null;
if (savedValue is List<string> stringList)
{
checkedValues = stringList;
}
else if (savedValue is List<object> objectList)
{
checkedValues = objectList.Select(i => (string)i).ToList();
settingsDict[item.Name] = checkedValues;
}
// 清理不在 Options 列表中的无效值
if (checkedValues != null && item.Options != null)
{
checkedValues.RemoveAll(value => !item.Options.Contains(value));
}
}
}
catch (Exception ex)
{
// 清理失败不影响脚本执行,只记录日志
TaskControl.Logger.LogDebug(ex, "清理JS脚本配置中的无效值时发生异常");
}
}
}
public class ScriptGroupProjectExtensions
{
public static readonly Dictionary<string, string> TypeDescriptions = new()
{
{ "Javascript", "JS脚本" },
{ "KeyMouse", "键鼠脚本" },
{ "Pathing", "地图追踪" },
{ "Shell", "Shell" }
};
public static readonly Dictionary<string, string> StatusDescriptions = new()
{
{ "Enabled", "启用" },
{ "Disabled", "禁用" }
};
public Dictionary<string, string> GetStatusDescriptions()
{
return StatusDescriptions;
}
public static readonly Dictionary<string, string> ScheduleDescriptions = new()
{
{ "Daily", "每日" },
{ "EveryTwoDays", "间隔两天" },
{ "Monday", "每周一" },
{ "Tuesday", "每周二" },
{ "Wednesday", "每周三" },
{ "Thursday", "每周四" },
{ "Friday", "每周五" },
{ "Saturday", "每周六" },
{ "Sunday", "每周日" }
};
public Dictionary<string, string> GetScheduleDescriptions()
{
return ScheduleDescriptions;
}
}