// // Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team) // Copyright (C) 2021-2025 MaaAssistantArknights Contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License v3.0 only as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY // #nullable enable using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using JetBrains.Annotations; using MaaWpfGui.Constants; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Models.AsstTasks; using MaaWpfGui.Models.Copilot; using MaaWpfGui.Services; using MaaWpfGui.States; using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.Items; using Microsoft.Win32; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using Stylet; using static MaaWpfGui.Helper.CopilotHelper; using static MaaWpfGui.Helper.PathsHelper; using static MaaWpfGui.Models.AsstTasks.AsstCopilotTask; using DataFormats = System.Windows.Forms.DataFormats; using Task = System.Threading.Tasks.Task; namespace MaaWpfGui.ViewModels.UI; /// /// The view model of copilot. /// // 通过 container.Get(); 实例化或获取实例 // ReSharper disable once ClassNeverInstantiated.Global public partial class CopilotViewModel : Screen { private readonly RunningState _runningState; private static readonly ILogger _logger = Log.ForContext(); private static readonly SemaphoreSlim _semaphore = new(1, 1); private readonly List _copilotIdList = []; // 用于保存作业列表中的作业的Id,对于同一个作业,只有都执行成功才点赞 private readonly List _recentlyRatedCopilotId = []; // TODO: 可能考虑加个持久化 private AsstTaskType _taskType = AsstTaskType.Copilot; private readonly Dictionary _copilotJsonPathMap = []; // 下拉框与实际作业 json 档案路径对照表 /// /// 缓存的已解析作业,非即时添加的作业会使用该缓存 /// private CopilotBase? _copilotCache; private const string CopilotIdPrefix = "maa://"; private static readonly string TempCopilotFile = Path.Combine(CacheDir, "_temp_copilot.json"); // VideoRecognition 已不支持:仅保留 json 作业 private static readonly string[] _supportExt = [".json"]; private static readonly string CopilotJsonDir = Path.Combine(ConfigDir, "copilot"); private const string StageNameRegex = @"(?:[a-z]{0,3})(?:\d{0,2})-(?:(?:A|B|C|D|EX|S|TR|MO)-?)?(?:\d{1,2})"; private const string InvalidStageNameChars = @"[:',\.\(\)\|\[\]\?,。【】{};:]"; // 无效字符 [GeneratedRegex(InvalidStageNameChars)] private static partial Regex InvalidStageNameRegex(); [GeneratedRegex(@"^(act\d+(side|mini)|a00\d+)_")] private static partial Regex SideStoryStageIdRegex(); [GeneratedRegex(@"^(main|sub|tough|hard)_")] private static partial Regex MainStageIdRegex(); /// /// Gets the view models of log items. /// public ObservableCollection LogItemViewModels { get; } = []; /// /// Gets the file items for TreeView. /// public ObservableCollection FileItems { get; } = []; /// /// Gets or sets a value indicating whether gets or sets whether the file dropdown popup is open. /// public bool IsFilePopupOpen { get => field; set => SetAndNotify(ref field, value); } /// /// Gets or private sets the view models of Copilot items. /// public ObservableCollection CopilotItemViewModels { get; } = []; /// /// Initializes a new instance of the class. /// public CopilotViewModel() { PropertyDependsOnUtility.InitializePropertyDependencies(this); DisplayName = LocalizationHelper.GetString("Copilot"); AddLog(LocalizationHelper.GetString("CopilotTip"), showTime: false); _runningState = RunningState.Instance; _runningState.StateChanged += (_, e) => { Idle = e.NewState.Idle; Inited = e.NewState.Inited; Stopping = e.NewState.Stopping; }; UserAdditionalItems.CollectionChanged += (_, _) => { NotifyOfPropertyChange(nameof(UserAdditionalGridHeight)); NotifyOfPropertyChange(nameof(UserAdditionalPopupVerticalOffset)); }; var copilotTaskList = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotTaskList, string.Empty); if (string.IsNullOrEmpty(copilotTaskList)) { return; } var list = JsonConvert.DeserializeObject>(copilotTaskList) ?? []; for (int i = 0; i < list.Count; i++) { list[i].Index = i; CopilotItemViewModels.Add(list[i]); } SaveCopilotTask(); CopilotItemViewModels.CollectionChanged += (_, e) => { _logger.Information("Copilot item collection changed: {Action}", e.Action); if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Move) { for (int i = 0; i < CopilotItemViewModels.Count; i++) { CopilotItemViewModels[i].Index = i; } SaveCopilotTask(); } }; } #region UI绑定及操作 #region Log /// /// Adds log. /// /// The content. /// The font color. /// The font weight. /// Whether show time. public void AddLog(string? content, string color = UiLogColor.Trace, string weight = "Regular", bool showTime = true) { if (string.IsNullOrEmpty(content)) { return; } Execute.OnUIThread(() => { LogItemViewModels.Add(new LogItemViewModel(content, color, weight, "HH':'mm':'ss", showTime: showTime)); if (showTime) { switch (color) { case UiLogColor.Error: _logger.Error("{Content}", content); break; case UiLogColor.Warning: _logger.Warning("{Content}", content); break; default: _logger.Information("{Content}", content); break; } } }); // LogItemViewModels.Insert(0, new LogItemViewModel(time + content, color, weight)); } /// /// Clears log. /// private void ClearLog() { Execute.OnUIThread(() => { foreach (var log in LogItemViewModels) { if (log.ToolTip is ToolTip t) { t.IsOpen = false; t.Content = null; ToolTipService.SetPlacementTarget(t, null); t.PlacementTarget = null; } } LogItemViewModels.Clear(); AddLog(LocalizationHelper.GetString("CopilotTip"), showTime: false); }); } #endregion Log #region 属性 /// /// Gets a value indicating whether it is idle. /// public bool Idle { get => field; private set => SetAndNotify(ref field, value); } public bool Inited { get => field; set => SetAndNotify(ref field, value); } public bool Stopping { get => field; set => SetAndNotify(ref field, value); } /// /// Gets or sets a value indicating whether the start button is enabled. /// public bool StartEnabled { get => field; set => SetAndNotify(ref field, value); } = true; private int _copilotTabIndex = 0; /// /// Gets or sets 作业类型,0:主线/故事集/SS 1:保全派驻 2:悖论模拟 3:其他活动 /// public int CopilotTabIndex { get => _copilotTabIndex; set { if (!Idle) { return; } if (value == 1 || value == 3) { UseCopilotList = false; } if (!SetAndNotify(ref _copilotTabIndex, value)) { return; } } } private string _displayFilename = string.Empty; /// /// Gets or sets the display filename (relative path). /// public string DisplayFilename { get => _displayFilename; set { SetAndNotify(ref _displayFilename, value); if (string.IsNullOrEmpty(value)) { Filename = string.Empty; return; } var copilotRoot = Path.Combine(ResourceDir, "copilot"); var fullPath = Path.IsPathRooted(value) ? value : Path.Combine(copilotRoot, value); /* 相对/绝对路径 */ if (File.Exists(fullPath)) { Filename = fullPath; } /* copilot 文件夹下的文件名 */ else if (_copilotJsonPathMap.TryGetValue(Path.GetFileName(value), out var mappedPath)) { Filename = mappedPath; } /* maybe 是神秘代码,交给 FileName 处理 */ else { Filename = value; } } } /// /// Gets or sets the filename. /// public string Filename { get => field; set { var processedValue = ProcessFilePath(value); SetAndNotify(ref field, processedValue); UpdateDisplayFilename(processedValue); ClearLog(); UpdateCopilotUrl(processedValue); _ = UpdateFilename(processedValue); } } = string.Empty; private string ProcessFilePath(string value) { if (string.IsNullOrWhiteSpace(value) || File.Exists(value)) { return value; } // 从对照表取得完整 json 档案路径 if (_copilotJsonPathMap.TryGetValue(value, out var fullPath)) { return fullPath; } var resourceFile = Path.Combine(ResourceDir, "copilot", Path.GetFileName(value)); return File.Exists(resourceFile) ? resourceFile : value; } private void UpdateDisplayFilename(string filename) { if (string.IsNullOrEmpty(filename)) { _displayFilename = string.Empty; } else { var copilotRoot = Path.Combine(ResourceDir, "copilot"); _displayFilename = filename.StartsWith(copilotRoot, StringComparison.OrdinalIgnoreCase) ? Path.GetRelativePath(copilotRoot, filename) : filename; } NotifyOfPropertyChange(nameof(DisplayFilename)); } private void UpdateCopilotUrl(string filename) { CopilotUrl = string.IsNullOrWhiteSpace(filename) ? CopilotUiUrl : CopilotUrl; } private bool _form; /// /// Gets or sets a value indicating whether to use auto-formation. /// [PropertyDependsOn(nameof(CopilotTabIndex))] public bool Form { get { // Tab=1/2 不支持自动编队,根据 CopilotTabIndex 综合判断返回值 if (CopilotTabIndex is 1 or 2) { return false; } return _form; } set => SetAndNotify(ref _form, value); } /// /// Gets or sets a value indicating whether to use auto-formation. /// public bool AddTrust { get => field; set => SetAndNotify(ref field, value); } /// /// Gets or sets a value indicating whether to use auto-formation. /// public bool IgnoreRequirements { get => field; set => SetAndNotify(ref field, value); } /// /// Gets or sets a value indicating whether 真正有干员被忽略了要求 /// public bool HasRequirementIgnored { get; set; } = false; public int CurrentCopilotId { get; set; } = -1; public bool UseSanityPotion { get => field; set => SetAndNotify(ref field, value); } /// /// Gets or sets a value indicating whether to use auto-formation. /// public bool AddUserAdditional { get => field; set { SetAndNotify(ref field, value); ConfigurationHelper.SetValue(ConfigurationKeys.CopilotAddUserAdditional, value.ToString()); } } = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotAddUserAdditional, false); private string _userAdditional = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotUserAdditional, string.Empty).Trim(); /// /// Gets or sets a value indicating whether to use auto-formation. /// public string UserAdditional { get => _userAdditional; set { value = value.Trim(); SetAndNotify(ref _userAdditional, value); ConfigurationHelper.SetValue(ConfigurationKeys.CopilotUserAdditional, value); } } [PropertyDependsOn(nameof(UserAdditional))] public string UserAdditionalPrettyJson { get { if (string.IsNullOrWhiteSpace(UserAdditional)) { return string.Empty; } try { return JToken.Parse(UserAdditional).ToString(Formatting.None).Replace("},", "},\n"); } catch { return UserAdditional; // 解析失败时返回原始字符串 } } } /// /// Gets or sets a value indicating whether the UserAdditional popup is open. /// public bool IsUserAdditionalPopupOpen { get => field; set => SetAndNotify(ref field, value); } /// /// Gets the view models of UserAdditional items. /// public ObservableCollection UserAdditionalItems { get; } = []; private const int MaxVisibleUserAdditionalRows = 7; public double UserAdditionalRowHeight => 40; public double UserAdditionalGridMaxHeight => 350; public double UserAdditionalGridHeight { get { var rows = Math.Clamp(UserAdditionalItems.Count, 1, MaxVisibleUserAdditionalRows); return UserAdditionalGridMaxHeight - ((MaxVisibleUserAdditionalRows - rows) * UserAdditionalRowHeight); } } public double UserAdditionalPopupVerticalOffset => (UserAdditionalGridHeight - UserAdditionalGridMaxHeight) / 2; /// /// Opens the UserAdditional popup for editing. /// public void OpenUserAdditionalPopup() { // 清空列表 UserAdditionalItems.Clear(); // 优先按 JSON 反序列化 if (!string.IsNullOrWhiteSpace(UserAdditional)) { try { var list = JsonConvert.DeserializeObject>(UserAdditional) ?? []; foreach (var op in list) { if (string.IsNullOrWhiteSpace(op.Name)) { continue; } int skill = op.Skill; if (skill < 0) { skill = 0; } else if (skill > 3) { skill = 3; } var item = new UserAdditionalItemViewModel { Name = op.Name, Skill = skill, Module = op.Module, }; UserAdditionalItems.Add(item); } } catch { // 兼容旧格式:以 ; 和 , 解析 "name,skill;name,skill" 形式 try { Regex regex = new(@"(?<=;)(?[^,;]+)(?:, *(?\d))?(?=;)", RegexOptions.Compiled); var matches = regex.Matches(";" + UserAdditional + ";"); foreach (Match match in matches) { var name = match.Groups["name"].Value.Trim(); var skillStr = match.Groups["skill"].Value; int skill = string.IsNullOrEmpty(skillStr) ? 0 : int.Parse(skillStr); if (skill < 0) { skill = 0; } else if (skill > 3) { skill = 3; } var item = new UserAdditionalItemViewModel { Name = name, Skill = skill, Module = 0, }; UserAdditionalItems.Add(item); } } catch { // ignore } } } // 如果列表为空,添加一行空行 if (UserAdditionalItems.Count == 0) { var newItem = new UserAdditionalItemViewModel { Name = string.Empty, Skill = 0, Module = 0 }; UserAdditionalItems.Add(newItem); } IsUserAdditionalPopupOpen = true; } /// /// Saves the UserAdditional value from the popup. /// public void SaveUserAdditional() { // 将列表转换为 UserAdditional 并序列化为 JSON var list = new List(); foreach (var item in UserAdditionalItems) { if (string.IsNullOrWhiteSpace(item.Name)) { continue; // 跳过空行 } int skill = item.Skill; if (skill < 0) { skill = 0; } else if (skill > 3) { skill = 3; } list.Add(new UserAdditional { Name = item.Name.Trim(), Skill = skill, Module = item.Module, }); } UserAdditional = list.Count > 0 ? JsonConvert.SerializeObject(list, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore }) : string.Empty; IsUserAdditionalPopupOpen = false; } /// /// Cancels editing UserAdditional and closes the popup. /// public void CancelUserAdditionalEdit() { IsUserAdditionalPopupOpen = false; } /// /// Gets a value indicating whether can add a new UserAdditional item. /// public bool CanAddUserAdditionalItem => UserAdditionalItems.All(item => !string.IsNullOrWhiteSpace(item.Name)); /// /// Adds a new UserAdditional item. /// public void AddUserAdditionalItem() { if (!CanAddUserAdditionalItem) { return; } var newItem = new UserAdditionalItemViewModel { Name = string.Empty, Skill = 0, Module = 0 }; UserAdditionalItems.Add(newItem); } /// /// Removes a UserAdditional item. /// /// The item to remove. public void RemoveUserAdditionalItem(UserAdditionalItemViewModel item) { if (item != null && UserAdditionalItems.Contains(item)) { UserAdditionalItems.Remove(item); } } public static Dictionary ModuleMapping { get; } = new() { { LocalizationHelper.GetString("CopilotWithoutModule"), 0 }, { "χ", 1 }, { "γ", 2 }, { "α", 3 }, { "Δ", 4 }, }; public class UserAdditionalItemViewModel : PropertyChangedBase { private string _name = string.Empty; /// /// Gets or sets the operator name. /// public string Name { get => _name; set => SetAndNotify(ref _name, value); } private int _skill; /// /// Gets or sets the skill number. /// public int Skill { get => _skill; set => SetAndNotify(ref _skill, value); } private int _module; /// /// Gets or sets the module number. /// -1: 不切换模组 / 无要求, 0: 不使用模组, 1-4: 不同模组 /// public int Module { get => _module; set => SetAndNotify(ref _module, value); } } private bool _useFormation; public bool UseFormation { get => _useFormation; set => SetAndNotify(ref _useFormation, value); } public List> FormationSelectList { get; } = [ new() { Display = "1", Value = 1 }, new() { Display = "2", Value = 2 }, new() { Display = "3", Value = 3 }, new() { Display = "4", Value = 4 }, ]; private int _formationIndex = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotSelectFormation, 1); public int FormationIndex { get => _formationIndex; set { SetAndNotify(ref _formationIndex, value); ConfigurationHelper.SetValue(ConfigurationKeys.CopilotSelectFormation, value.ToString()); } } private bool _useSupportUnitUsage; public bool UseSupportUnitUsage { get => _useSupportUnitUsage; set => SetAndNotify(ref _useSupportUnitUsage, value); } private int _supportUnitUsage = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotSupportUnitUsage, 1); public int SupportUnitUsage { get => _supportUnitUsage; set { SetAndNotify(ref _supportUnitUsage, value); ConfigurationHelper.SetValue(ConfigurationKeys.CopilotSupportUnitUsage, value.ToString()); } } public List> SupportUnitUsageList { get; } = [ /* new() { Display = LocalizationHelper.GetString("SupportUnitUsage.None"), Value = 0 }, */ new() { Display = LocalizationHelper.GetString("SupportUnitUsage.WhenNeeded"), Value = 1 }, new() { Display = LocalizationHelper.GetString("SupportUnitUsage.Random"), Value = 3 }, ]; private bool _useCopilotList; /// /// Gets or sets a value indicating whether 自动编队. /// [PropertyDependsOn(nameof(CopilotTabIndex))] public bool UseCopilotList { get => _useCopilotList; set { if (value) { // _taskType 应该只由选择的作业文件决定,不在此强制修改 Form = true; } SetAndNotify(ref _useCopilotList, value); } } /// /// Gets or sets a value indicating whether to use auto-formation. /// private string? _copilotTaskName = string.Empty; public string? CopilotTaskName { get => _copilotTaskName; set { value = InvalidStageNameRegex().Replace(value ?? string.Empty, string.Empty).Trim(); SetAndNotify(ref _copilotTaskName, value); } } public bool Loop { get; set; } private int _loopTimes = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotLoopTimes, 1); public int LoopTimes { get => _loopTimes; set { SetAndNotify(ref _loopTimes, value); ConfigurationHelper.SetValue(ConfigurationKeys.CopilotLoopTimes, value.ToString()); } } private const string CopilotUiUrl = MaaUrls.PrtsPlus; private string _copilotUrl = CopilotUiUrl; /// /// Gets or private sets the copilot URL. /// public string CopilotUrl { get => _copilotUrl; private set { SetAndNotify(ref _copilotUrl, value); } } private string _videoUrl = string.Empty; /// /// Gets or private sets the video URL. /// public string VideoUrl { get => _videoUrl; private set => SetAndNotify(ref _videoUrl, value); } /// /// Gets a value indicating whether there is a video URL. /// [PropertyDependsOn(nameof(VideoUrl))] public bool HasVideoUrl => !string.IsNullOrEmpty(VideoUrl); private const string MapUiUrl = MaaUrls.MapPrts; private string _mapUrl = MapUiUrl; public string MapUrl { get => _mapUrl; private set => SetAndNotify(ref _mapUrl, value); } private bool _couldLikeWebJson; public bool CouldLikeWebJson { get => _couldLikeWebJson; set => SetAndNotify(ref _couldLikeWebJson, value); } #endregion 属性 #region 方法 /// /// Selects file. /// UI 绑定的方法 /// [UsedImplicitly] public void SelectFile() { var dialog = new OpenFileDialog { Filter = "JSON|*.json", }; if (dialog.ShowDialog() == true) { Filename = dialog.FileName; } } /// /// Paste clipboard contents. /// UI 绑定的方法 /// [UsedImplicitly] public void PasteClipboard() { if (Clipboard.ContainsText()) { Filename = Clipboard.GetText().Trim(); } else if (Clipboard.ContainsFileDropList()) { DropFile(Clipboard.GetFileDropList()[0]); } } /// /// Paste clipboard contents. /// UI 绑定的方法 /// /// Task [UsedImplicitly] public async Task PasteClipboardCopilotSet() { StartEnabled = false; ClearLog(); if (Clipboard.ContainsText()) { await GetCopilotSetAsync(Clipboard.GetText().Trim()); CopilotUrl = CopilotUiUrl; } StartEnabled = true; } /// /// 批量导入作业 /// UI 绑定的方法 /// /// Task [UsedImplicitly] public async Task ImportFiles() { var dialog = new OpenFileDialog { Filter = "JSON|*.json", Multiselect = true, }; if (dialog.ShowDialog() != true) { return; } CopilotId = 0; _copilotCache = null; foreach (var file in dialog.FileNames) { var fileInfo = new FileInfo(file); if (!fileInfo.Exists) { AddLog(LocalizationHelper.GetString("CopilotNoFound") + file, showTime: false); return; } try { using var reader = new StreamReader(File.OpenRead(file)); var str = await reader.ReadToEndAsync(); var payload = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); if (payload is CopilotModel copilot) { var difficulty = copilot.Difficulty; if (difficulty == CopilotModel.DifficultyFlags.None) { difficulty = CopilotModel.DifficultyFlags.Normal; } await AddCopilotTaskToList(copilot, difficulty); } else if (payload is SSSCopilotModel sss) { await AddSSSCopilotTaskToList(sss); } } catch { AddLog(LocalizationHelper.GetString("CopilotFileReadError"), UiLogColor.Error, showTime: false); return; } } } // UI 绑定的方法 [UsedImplicitly] public async Task AddCopilotTask() { await AddCopilotTaskToList(CopilotTaskName, false); } // UI 绑定的方法 [UsedImplicitly] public async Task AddCopilotTask_Adverse() { await AddCopilotTaskToList(CopilotTaskName, true); } // UI 绑定的方法 [UsedImplicitly] public void SelectCopilotTask(object? sender, MouseButtonEventArgs? e = null) { if (e?.Source is FrameworkElement element && element.Tag is int index) { Filename = CopilotItemViewModels[index].FilePath; // 假设原方法接受int参数 if (e.ChangedButton == MouseButton.Right) { UseCopilotList = false; } } } // UI 绑定的方法 [UsedImplicitly] public void DeleteCopilotTask(int index) { CopilotItemViewModels.RemoveAt(index); CopilotItemIndexChanged(); } // UI 绑定的方法 [UsedImplicitly] public void CleanUnableCopilotTask() { foreach (var item in CopilotItemViewModels.Where(model => !model.IsChecked).ToList()) { CopilotItemViewModels.Remove(item); } CopilotItemIndexChanged(); } // UI 绑定的方法 [UsedImplicitly] public void ClearCopilotTask() { CopilotItemViewModels.Clear(); SaveCopilotTask(); try { Directory.Delete(CopilotJsonDir, true); } catch { // ignored } } // UI 绑定的方法 [UsedImplicitly] public async Task LikeWebJson() { CouldLikeWebJson = false; if (await RateCopilot(CopilotId) == PrtsStatus.Success) { AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.CopilotLikeGroup); } } // UI 绑定的方法 [UsedImplicitly] public void DislikeWebJson() { CouldLikeWebJson = false; _ = RateCopilot(CopilotId, false); } #endregion 方法 #endregion UI绑定及操作 private async Task UpdateFilename(string filename) { StartEnabled = false; await UpdateFileDoc(filename); StartEnabled = true; } private async Task UpdateFileDoc(string filename) { ClearLog(); CopilotUrl = CopilotUiUrl; VideoUrl = string.Empty; MapUrl = MapUiUrl; IsDataFromWeb = false; CopilotId = 0; _copilotCache = null; int copilotId = 0; bool writeToCache = false; object? payload; if (string.IsNullOrEmpty(filename)) { return; } if (File.Exists(filename)) { var fileSize = new FileInfo(filename).Length; bool isJsonFile = filename.ToLower().EndsWith(".json") || fileSize < 4 * 1024 * 1024; if (!isJsonFile) { AddLog(LocalizationHelper.GetString("NotCopilotJson"), UiLogColor.Error, showTime: false); return; } try { using var reader = new StreamReader(File.OpenRead(filename)); var str = await reader.ReadToEndAsync(); payload = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); } catch (Exception e) { AddLog(LocalizationHelper.GetString("CopilotFileReadError") + $"\n{e.Message}", UiLogColor.Error, showTime: false); return; } } else if (filename.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase) || int.TryParse(filename, out _)) { (copilotId, payload) = await GetCopilotAsync(filename); if (payload is not null) { IsDataFromWeb = true; writeToCache = true; CopilotId = copilotId; } } else { payload = null; } switch (payload) { case CopilotModel copilot: await ParseCopilotAsync(copilot, writeToCache, UseCopilotList, copilotId); return; case SSSCopilotModel sss: await ParseSSSCopilot(sss, writeToCache); return; default: AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); return; } } /// /// 为自动战斗列表匹配名字 /// /// 用于匹配的名字 /// 关卡名 or string.Empty private static string? FindStageName(params string[] names) { names = names.Where(str => !string.IsNullOrEmpty(str)).ToArray(); if (names.Length == 0) { return string.Empty; } // 一旦有由小写字母、数字、'-'组成的name则视为关卡名直接使用 var directName = names.FirstOrDefault(name => Regex.IsMatch(name.ToLower(), @"^[0-9a-z\-]+$")); if (!string.IsNullOrEmpty(directName)) { return directName; } var regex = new Regex(StageNameRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase); return names.Select(str => regex.Match(str)).FirstOrDefault(result => result.Success)?.Value ?? string.Empty; } #region 作业解析 private async Task<(int CopilotId, CopilotBase? Payload)> GetCopilotAsync(string copilotCodeString) { if (copilotCodeString.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase)) { copilotCodeString = copilotCodeString[CopilotIdPrefix.Length..]; } if (int.TryParse(copilotCodeString, out var copilotCode)) { return await GetCopilotAsync(copilotCode); } AddLog(LocalizationHelper.GetString("CopilotNoFound") + $":{copilotCodeString}", UiLogColor.Error, showTime: false); return (0, null); } private async Task<(int CopilotId, CopilotBase? Payload)> GetCopilotAsync(int copilotId) { var (status, copilotset) = await RequestCopilotAsync(copilotId); if (status == PrtsStatus.NetworkError) { return (0, null); } else if (status == PrtsStatus.Success && copilotset is PrtsCopilotModel { StatusCode: 200 }) { if (copilotset.Data?.Content is CopilotModel { } copilot) { return (copilotId, copilot); // await ParseCopilotAsync(copilot, true, copilotList, copilotId)); } else if (copilotset.Data?.Content is SSSCopilotModel { } sss) { return (copilotId, sss); // await ParseSSSCopilot(sss, true)); } AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); return (0, null); } AddLog(LocalizationHelper.GetString("CopilotNoFound") + $":{copilotId}", UiLogColor.Error, showTime: false); return (0, null); } private async Task ParseCopilotAsync(CopilotModel copilot, bool writeToCache, bool copilotList, int copilotId, bool printInfo = true) { if (string.IsNullOrEmpty(copilot.StageName)) { AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); return false; } _taskType = AsstTaskType.Copilot; _copilotCache = copilot; VideoUrl = string.Empty; if (copilot.Documentation?.Details is not null) { var linkParser = BVRegex(); var match = linkParser.Match(copilot.Documentation.Details); if (match.Success) { VideoUrl = MaaUrls.BilibiliVideo + match.Value; // 视频链接 } } var list = copilot.Opers.Concat(copilot.Groups.SelectMany(g => g.Opers)).ToList(); foreach (var oper in list) { int rarity = DataHelper.GetCharacterByNameOrAlias(oper.Name)?.Rarity ?? -1; if (oper.Skill == 3 && rarity < 6) { AddLog(LocalizationHelper.GetStringFormat("UnsupportedSkill", DataHelper.GetLocalizedCharacterName(oper.Name) ?? oper.Name, oper.Skill), UiLogColor.Warning, showTime: false); oper.Skill = 0; } else if (oper.Skill == 2 && rarity < 4) { AddLog(LocalizationHelper.GetStringFormat("UnsupportedSkill", DataHelper.GetLocalizedCharacterName(oper.Name) ?? oper.Name, oper.Skill), UiLogColor.Warning, showTime: false); oper.Skill = 0; } else if (oper.Skill == 1 && rarity < 3) { AddLog(LocalizationHelper.GetStringFormat("UnsupportedSkill", DataHelper.GetLocalizedCharacterName(oper.Name) ?? oper.Name, oper.Skill), UiLogColor.Warning, showTime: false); oper.Skill = 0; } } if (printInfo) { foreach (var (output, color) in copilot.Output()) { AddLog(output, color ?? UiLogColor.Message, showTime: false); // 作业信息输出 } } MapUrl = MapUiUrl.Replace("areas", "map/" + copilot.StageName); var mapInfo = DataHelper.FindMap(copilot.StageName); var navigateName = mapInfo?.Code; if (navigateName is null) { // 不支持的关卡 AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {copilot.StageName}", UiLogColor.Error, showTime: false); navigateName = FindStageName(copilot.Documentation?.Title ?? string.Empty); _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); } CopilotTaskName = navigateName; if (mapInfo?.StageId is { } stageId) { if (GetCopilotType(stageId) is CopilotType type and not CopilotType.Unknown) { CopilotTabIndex = (int)type; if (copilotList) { UseCopilotList = CopilotTabIndex is 0 or 2; } } } if (!writeToCache) {// 现在是暂时将所有本地作业不添加到列表 } else if (copilotList) { switch (copilot.Difficulty) { case CopilotModel.DifficultyFlags.None: await AddCopilotTaskToList(copilot, CopilotModel.DifficultyFlags.Normal, navigateName, copilotId); break; default: await AddCopilotTaskToList(copilot, copilot.Difficulty, navigateName, copilotId); break; } } else { try { await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(copilot, Formatting.Indented)); } catch { _logger.Error("Could not save copilot task to file: " + TempCopilotFile); return false; } } return true; } private async Task ParseSSSCopilot(SSSCopilotModel copilot, bool writeToCache) { if (string.IsNullOrEmpty(copilot.StageName) || copilot.Type != new SSSCopilotModel().Type) { return false; } _taskType = AsstTaskType.SSSCopilot; CopilotTabIndex = 1; _copilotCache = copilot; MapUrl = MapUiUrl.Replace("areas", "map/" + copilot.StageName); VideoUrl = string.Empty; if (copilot.Documentation?.Details is not null) { var linkParser = new Regex(@"(?:av\d+|bv[a-z0-9]{10})(?:\/\?p=\d+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); var match = linkParser.Match(copilot.Documentation.Details); if (match.Success) { VideoUrl = MaaUrls.BilibiliVideo + match.Value; // 视频链接 } } foreach (var (output, color) in copilot.Output()) { AddLog(output, color ?? UiLogColor.Message, showTime: false); } // 不支持的关卡 var stages = copilot.Stages?.Select(copilot => DataHelper.FindMap(copilot.StageName)); if (stages?.Any(i => i is null) is null or true) { AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {copilot.StageName}", UiLogColor.Error, showTime: false); _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); } if (writeToCache) { try { await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(copilot, Formatting.Indented)); } catch { _logger.Error("Could not save copilot task to file: " + TempCopilotFile); return false; } } await AddSSSCopilotTaskToList(copilot, CopilotId); return true; } #endregion 作业解析 #region 作业集解析 private async Task GetCopilotSetAsync(string copilotCodeString) { if (copilotCodeString.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase)) { copilotCodeString = copilotCodeString[CopilotIdPrefix.Length..]; } if (int.TryParse(copilotCodeString, out var copilotCode)) { await GetCopilotSetAsync(copilotCode); return; } AddLog(LocalizationHelper.GetString("CopilotNoFound") + $" {copilotCodeString}", UiLogColor.Error, showTime: false); } private async Task GetCopilotSetAsync(int copilotCode) { var (status, copilotset) = await RequestCopilotSetAsync(copilotCode); if (status == PrtsStatus.NetworkError) { return; } else if (status == PrtsStatus.Success && copilotset is PrtsCopilotSetModel { StatusCode: 200 }) { await ParseCopilotSetAsync(copilotset.Data); return; } AddLog(LocalizationHelper.GetString("CopilotNoFound") + $" {copilotCode}", UiLogColor.Error, showTime: false); return; } private async Task ParseCopilotSetAsync(PrtsCopilotSetModel.CopilotSetData? copilotSet) { CopilotId = 0; _copilotCache = null; ClearLog(); if (copilotSet?.CopilotIds is null) { AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); return; } else if (copilotSet.CopilotIds.Count == 0) { Log(copilotSet.Name, copilotSet.Description); return; } var list = copilotSet.CopilotIds.Select(async (copilotId) => await GetCopilotAsync(copilotId)).ToList(); foreach (var task in list) { var (copilotId, payload) = await task; if (payload is CopilotModel copilot) { if (!await ParseCopilotAsync(copilot, true, true, copilotId, false)) { AddLog(LocalizationHelper.GetString("CopilotJsonError") + $", copilotId: {copilotId}", UiLogColor.Error, showTime: false); continue; } var opers = JArray.FromObject(copilot.Opers.Select(i => i.Name)); opers = JArray.FromObject(opers.Union(JArray.FromObject(copilot.Groups.Select(i => i.Opers.Select(op => op.Name))))); AddLog(opers.ToString(Formatting.None), UiLogColor.Message, showTime: false); } else if (payload is SSSCopilotModel sss) { CopilotTabIndex = 1; await AddSSSCopilotTaskToList(sss, copilotId); } } Log(copilotSet.Name, copilotSet.Description); _copilotCache = null; return; void Log(string? name, string? description) { if (!string.IsNullOrWhiteSpace(name)) { AddLog(name, UiLogColor.Message, showTime: false); } if (!string.IsNullOrWhiteSpace(description)) { AddLog(description, UiLogColor.Message, showTime: false); } } } #endregion 作业集解析 /// /// Drops file. /// /// The sender. /// The event arguments. /// TODO: 不知道为啥现在拖放不用了,之后瞅瞅 [UsedImplicitly] public void DropFile(object sender, DragEventArgs e) { if (!e.Data.GetDataPresent(DataFormats.FileDrop)) { return; } var filename = ((Array?)e.Data.GetData(DataFormats.FileDrop))?.GetValue(0)?.ToString(); DropFile(filename); } private void DropFile(string? filename) { if (string.IsNullOrEmpty(filename)) { return; } var filenameLower = filename.ToLower(); bool support = _supportExt.Any(ext => filenameLower.EndsWith(ext)); if (support) { Filename = filename; } else { Filename = string.Empty; ClearLog(); AddLog(LocalizationHelper.GetString("NotCopilotJson"), UiLogColor.Error, showTime: false); } } /// /// Load file items for TreeView. /// [UsedImplicitly] public void LoadFileItems() { try { _copilotJsonPathMap.Clear(); FileItems.Clear(); var copilotRoot = Path.Combine(ResourceDir, "copilot"); // 获取根目录下的所有目录和文件 var directories = Directory.GetDirectories(copilotRoot); var rootFiles = Directory.GetFiles(copilotRoot, "*.json"); // 添加根目录下的文件 foreach (var file in rootFiles) { var fileName = Path.GetFileName(file); var relativePath = Path.GetRelativePath(copilotRoot, file); _copilotJsonPathMap[fileName] = file; FileItems.Add(new CopilotFileItem { Name = fileName, FullPath = file, RelativePath = relativePath, IsFolder = false, }); } // 添加根目录下的文件夹(支持嵌套,old 文件夹放在最后) var oldFolderItem = (CopilotFileItem?)null; foreach (var dir in directories) { var dirName = Path.GetFileName(dir); var folderItem = LoadFolderItem(dir, copilotRoot); if (folderItem != null) { if (dirName.Equals("old", StringComparison.OrdinalIgnoreCase)) { oldFolderItem = folderItem; } else { FileItems.Add(folderItem); } } } // 将 old 文件夹添加到最后 if (oldFolderItem != null) { FileItems.Add(oldFolderItem); } } catch (Exception exception) { FileItems.Clear(); AddLog(exception.Message, UiLogColor.Error, showTime: false); } } /// /// 递归加载文件夹项(支持嵌套子文件夹) /// /// 文件夹路径 /// copilot 根目录路径 /// 文件项,如果文件夹为空则返回 null private CopilotFileItem? LoadFolderItem(string dirPath, string copilotRoot) { var dirName = Path.GetFileName(dirPath); var folderItem = new CopilotFileItem { Name = dirName, IsFolder = true, }; // 获取文件夹下的所有文件 var folderFiles = Directory.GetFiles(dirPath, "*.json"); foreach (var file in folderFiles) { var fileName = Path.GetFileName(file); var relativePath = Path.GetRelativePath(copilotRoot, file); _copilotJsonPathMap[fileName] = file; folderItem.Children.Add(new CopilotFileItem { Name = fileName, FullPath = file, RelativePath = relativePath, IsFolder = false, }); } // 获取文件夹下的所有子文件夹(递归加载) var subDirectories = Directory.GetDirectories(dirPath); foreach (var subDir in subDirectories) { var subFolderItem = LoadFolderItem(subDir, copilotRoot); if (subFolderItem != null) { folderItem.Children.Add(subFolderItem); } } // 如果文件夹为空(既没有文件也没有子文件夹),返回 null if (folderItem.Children.Count == 0) { return null; } return folderItem; } /// /// Handle file selection from TreeView. /// /// The selected file item. [UsedImplicitly] public void OnFileSelected(CopilotFileItem? fileItem) { if (fileItem == null || fileItem.IsFolder || string.IsNullOrEmpty(fileItem.FullPath)) { return; } Filename = fileItem.FullPath; IsFilePopupOpen = false; } /// /// Toggle file popup. /// [UsedImplicitly] public void ToggleFilePopup() { if (!IsFilePopupOpen) { LoadFileItems(); } IsFilePopupOpen = !IsFilePopupOpen; } private async Task AddCopilotTaskToList(string? stageName, bool isRaid) { if (string.IsNullOrEmpty(stageName) || InvalidStageNameRegex().IsMatch(stageName)) { AddLog(LocalizationHelper.GetString("CopilotInvalidStageNameForNavigation"), UiLogColor.Error, showTime: false); return; } try { if (_copilotCache is null) { } else if (_copilotCache is CopilotModel { } copilot) { await AddCopilotTaskToList(copilot, !isRaid ? CopilotModel.DifficultyFlags.Normal : CopilotModel.DifficultyFlags.Raid, stageName, CopilotId); } else if (_copilotCache is SSSCopilotModel { } sss) { await AddSSSCopilotTaskToList(sss, CopilotId); } } catch (Exception ex) { AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); _logger.Error(ex, "Exception caught"); } } /// /// 将作业添加到列表 /// /// 作业 /// 难度等级 /// 关卡 code,用于导航 /// 作业站 id /// 是否添加了作业 private async Task AddCopilotTaskToList(CopilotModel copilot, CopilotModel.DifficultyFlags flags, string? navigateName = null, int copilotId = 0) { if (string.IsNullOrEmpty(copilot.StageName)) { _logger.Error("Could not add copilot task with empty stage"); return false; } if (!Path.Exists(CopilotJsonDir)) { try { Directory.CreateDirectory(CopilotJsonDir); } catch { return false; } } var mapInfo = DataHelper.FindMap(copilot.StageName); var stageCode = mapInfo?.Code; var stageId = mapInfo?.StageId; if (mapInfo is null) { AddLog(LocalizationHelper.GetStringFormat("CopilotStageNameNotFound", stageCode), UiLogColor.Error, showTime: false); return false; } navigateName = string.IsNullOrEmpty(navigateName) ? stageCode : navigateName; if (stageCode != navigateName) { stageCode = navigateName; AddLog(LocalizationHelper.GetString("CopilotStageNameNotEqualWithNavigateName"), UiLogColor.Warning, showTime: false); } if (stageId is null || stageCode is null || navigateName is null) { return false; } var fileName = !string.IsNullOrEmpty(stageCode) ? stageCode : DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); var cachePath = Path.GetRelativePath(BaseDir, $"{CopilotJsonDir}/{fileName}.json"); await _semaphore.WaitAsync(); if (File.Exists(cachePath) && CopilotItemViewModels.Any(i => i.FilePath == cachePath)) { cachePath = Path.GetRelativePath(BaseDir, $"{CopilotJsonDir}/{fileName}_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}.json"); if (CopilotItemViewModels.Any(i => i.FilePath == cachePath)) { _logger.Error("Could not add copilot task with duplicate stage name: {StageName}", copilot.StageName); _semaphore.Release(); return false; } } try { await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(copilot, Formatting.Indented)); } catch { AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + cachePath, UiLogColor.Error, showTime: false); _semaphore.Release(); return false; } if (CopilotTabIndex == 2) { string? name = null; if (stageId?.Length > 6) { var codeName = stageId[4..^2]; var characterInfo = DataHelper.GetCharacterByCodeName(codeName); name = DataHelper.GetLocalizedCharacterName(characterInfo); } name ??= stageCode; var item = new CopilotItemViewModel(name, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; CopilotItemViewModels.Add(item); } else { if (flags.HasFlag(CopilotModel.DifficultyFlags.Normal) && !copilot.IsSandbox) { var item = new CopilotItemViewModel(stageCode, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; CopilotItemViewModels.Add(item); } if (flags.HasFlag(CopilotModel.DifficultyFlags.Raid) || copilot.IsSandbox) { var item = new CopilotItemViewModel(stageCode, cachePath, true, copilotId) { Index = CopilotItemViewModels.Count, }; CopilotItemViewModels.Add(item); } } _semaphore.Release(); SaveCopilotTask(); return true; } private async Task AddSSSCopilotTaskToList(SSSCopilotModel copilot, int copilotId = 0) { if (string.IsNullOrEmpty(copilot.StageName) || copilot.Type != new SSSCopilotModel().Type) { _logger.Error("Could not add SSS copilot task with empty stage"); return false; } if (!Path.Exists(CopilotJsonDir)) { try { Directory.CreateDirectory(CopilotJsonDir); } catch { return false; } } var fileName = string.Concat(copilot.StageName.Split(Path.GetInvalidFileNameChars())).Trim(); if (string.IsNullOrWhiteSpace(fileName)) { fileName = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); } var cachePath = Path.GetRelativePath(BaseDir, $"{CopilotJsonDir}/{fileName}.json"); await _semaphore.WaitAsync(); if (File.Exists(cachePath) && CopilotItemViewModels.Any(i => i.FilePath == cachePath)) { cachePath = Path.GetRelativePath(BaseDir, $"{CopilotJsonDir}/{fileName}_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}.json"); if (CopilotItemViewModels.Any(i => i.FilePath == cachePath)) { _logger.Error("Could not add SSS copilot task with duplicate stage name: {StageName}", copilot.StageName); _semaphore.Release(); return false; } } try { await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(copilot, Formatting.Indented)); } catch { AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + cachePath, UiLogColor.Error, showTime: false); _semaphore.Release(); return false; } var item = new CopilotItemViewModel(copilot.StageName, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; CopilotItemViewModels.Add(item); _semaphore.Release(); SaveCopilotTask(); return true; } public void SaveCopilotTask() { var task = JsonConvert.SerializeObject(CopilotItemViewModels, new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore }); ConfigurationHelper.SetValue(ConfigurationKeys.CopilotTaskList, task); } /// /// 战斗列表的当前战斗任务成功 /// public void CopilotTaskSuccess() { Execute.OnUIThread(() => { foreach (var model in CopilotItemViewModels) { if (!model.IsChecked || (CurrentCopilotId != -1 && model.Index != CurrentCopilotId)) { continue; } model.IsChecked = false; if (model.CopilotId > 0 && _copilotIdList.Remove(model.CopilotId) && _copilotIdList.IndexOf(model.CopilotId) == -1 && !HasRequirementIgnored) { _ = RateCopilot(model.CopilotId); } break; } SaveCopilotTask(); }); } /// /// 更新任务顺序 /// public void CopilotItemIndexChanged() { for (int i = 0; i < CopilotItemViewModels.Count; i++) { CopilotItemViewModels[i].Index = i; } SaveCopilotTask(); } /// /// Starts copilot. /// UI 绑定的方法 /// /// Task [UsedImplicitly] public async Task Start() { /* if (_form) { AddLog(Localization.GetString("AutoSquadTip"), LogColor.Message); }*/ _runningState.SetIdle(false); Instances.OverlayViewModel.LogItemsSource = LogItemViewModels; // if (_taskType == AsstTaskType.VideoRecognition) // { // _ = StartVideoTask(); // return; // } // 统一前置校验:先按 CopilotTabIndex 分发,再判断对应选项(UseCopilotList 等) if (!await ValidateStartAsync()) { _runningState.SetIdle(true); return; } await RunStartsWithScriptAsync(); if (!await ConnectToEmulatorAsync()) { Stop(); return; } var userAdditional = ParseUserAdditionals(); bool ret; try { ret = await AppendAndStartCopilotAsync(userAdditional); } catch (Exception ex) { _logger.Error(ex, "Failed to start copilot task"); AddLog(LocalizationHelper.GetString("CopilotStartError") + ex.Message, UiLogColor.Error, showTime: false); ret = false; } if (ret) { AddLog(LocalizationHelper.GetString("Running")); } else { if (!Instances.AsstProxy.AsstStop()) { _logger.Warning("Failed to stop Asst"); } _runningState.SetIdle(true); AddLog(LocalizationHelper.GetString("CopilotFileReadError"), UiLogColor.Error, showTime: false); } } private async Task ValidateStartAsync() { if (UseCopilotList) { // 列表模式:只校验列表本身,不检查输入框里的单文件作业类型 return await ValidateTaskListStrictAsync(tabIndex: CopilotTabIndex); } // 非列表模式必须有当前作业 if (_copilotCache is null) { AddLog(LocalizationHelper.GetString("CopilotEmptyError"), UiLogColor.Error, showTime: false); return false; } // 非列表模式:检查单文件作业的 _taskType 与 CopilotTabIndex 是否匹配 if ((_taskType == AsstTaskType.SSSCopilot && CopilotTabIndex != 1) || (_taskType != AsstTaskType.SSSCopilot && CopilotTabIndex == 1)) { AddLog(LocalizationHelper.GetString("CopilotTaskTypeMismatch"), UiLogColor.Error, showTime: false); return false; } return true; } private async Task ValidateTaskListStrictAsync(int tabIndex) { var selected = CopilotItemViewModels.Where(i => i.IsChecked).ToArray(); // 空列表:提示并失败 if (selected.Length == 0) { AddLog(LocalizationHelper.GetString("CopilotStartWithEmptyList"), UiLogColor.Error, showTime: false); return false; } if (tabIndex != 3) { var types = new HashSet(await Task.WhenAll(selected.Select(item => GetCopilotTypeAsync(item.FilePath)))); var concreteTypes = types.Where(type => type != CopilotType.Unknown).ToArray(); if (types.Contains(CopilotType.Unknown)) { AddLog(LocalizationHelper.GetString("CopilotTaskTypeParseFailedSkipCheck"), UiLogColor.Error, showTime: false); } if (concreteTypes.Length > 1) { AddLog(LocalizationHelper.GetString("CopilotTaskListMixedModeNotAllowed") + "\n" + string.Join(", ", concreteTypes.Select(GetCopilotTabName)), UiLogColor.Error, showTime: false); return false; } } // 先判断 CopilotTabIndex,再判断对应选项 if (tabIndex == 2) { return VerifyParadoxTasks(selected); } return await VerifyCopilotListTask(selected); } private async Task RunStartsWithScriptAsync() { if (!SettingsViewModel.GameSettings.CopilotWithScript) { return; } await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript", showLog: false)); if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.StartsWithScript)) { AddLog(LocalizationHelper.GetString("StartsWithScript")); } } private async Task ConnectToEmulatorAsync() { AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); string errMsg = string.Empty; bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (caught) { return true; } AddLog(errMsg, UiLogColor.Error); return false; } private IEnumerable ParseUserAdditionals() { // 反序列化自定义追加干员列表(JSON),兼容旧格式 if (string.IsNullOrWhiteSpace(UserAdditional)) { return []; } try { var list = JsonConvert.DeserializeObject>(UserAdditional) ?? []; foreach (var op in list) { if (string.IsNullOrWhiteSpace(op.Name)) { continue; } int skill = op.Skill; if (skill < 0) { skill = 0; } else if (skill > 3) { skill = 3; } op.Skill = skill; } return list.Where(op => !string.IsNullOrWhiteSpace(op.Name)); } catch { // 兼容旧格式:以 ; 和 , 解析 "name,skill;name,skill" 形式 try { Regex regex = new(@"(?<=;)(?[^,;]+)(?:, *(?\d))?(?=;)", RegexOptions.Compiled); var matches = regex.Matches(";" + UserAdditional + ";").ToList(); return matches.Select(match => { var name = match.Groups[1].Value.Trim(); var skillStr = match.Groups[2].Value; int skill = string.IsNullOrEmpty(skillStr) ? 0 : int.Parse(skillStr); if (skill < 0) { skill = 0; } else if (skill > 3) { skill = 3; } return new UserAdditional { Name = name, Skill = skill, Module = 0, }; }).Where(op => !string.IsNullOrWhiteSpace(op.Name)); } catch { return []; } } } private async Task AppendAndStartCopilotAsync(IEnumerable userAdditional) { if (!UseCopilotList) { } else if (CopilotTabIndex == 0) { _copilotIdList.Clear(); var t = CopilotItemViewModels.Where(i => i.IsChecked).Select(i => { _copilotIdList.Add(i.CopilotId); return new MultiTask { Index = i.Index, FileName = i.FilePath, IsRaid = i.IsRaid, StageName = i.Name, }; }); var task = new AsstCopilotTask() { MultiTasks = [.. t], Formation = Form, SupportUnitUsage = UseSupportUnitUsage ? SupportUnitUsage : 0, AddTrust = AddTrust, IgnoreRequirements = IgnoreRequirements, UserAdditionals = AddUserAdditional ? [.. userAdditional] : [], UseSanityPotion = UseSanityPotion, FormationIndex = UseFormation ? FormationIndex : 0, }; // 能用列表的是主线/ss/故事集/悖论,都是 Copilot 类型 var ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, task).IsSuccess; return ret && Instances.AsstProxy.AsstStart(); } else if (CopilotTabIndex == 2) { _copilotIdList.Clear(); var t = CopilotItemViewModels.Where(i => i.IsChecked).Select(i => { _copilotIdList.Add(i.CopilotId); return new AsstParadoxCopilotTask.MultiTask(i.Index, i.FilePath); }); var task = new AsstParadoxCopilotTask() { MultiTasks = [.. t], }; var ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, task).IsSuccess; return ret && Instances.AsstProxy.AsstStart(); } else { return false; } if (IsDataFromWeb) { try { await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(_copilotCache, Formatting.Indented)); } catch { AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + TempCopilotFile, UiLogColor.Error); return false; } } bool appended; if (CopilotTabIndex == 2) { var singleTask = new AsstParadoxCopilotTask() { FileName = TempCopilotFile }; appended = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, singleTask).IsSuccess; } else { var singleTask = new AsstCopilotTask() { FileName = IsDataFromWeb ? TempCopilotFile : Filename, Formation = Form, SupportUnitUsage = UseSupportUnitUsage ? SupportUnitUsage : 0, AddTrust = AddTrust, IgnoreRequirements = IgnoreRequirements, UserAdditionals = AddUserAdditional ? [.. userAdditional] : [], LoopTimes = Loop ? LoopTimes : 1, UseSanityPotion = false, FormationIndex = UseFormation ? FormationIndex : 0, }; // 单作业需要区分 Copilot / SSSCopilot appended = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, _taskType, singleTask.Serialize().Params); } return appended && Instances.AsstProxy.AsstStart(); } // private bool StartVideoTask() // { // return Instances.AsstProxy.AsstStartVideoRec(Filename); // } /// /// Stops copilot. /// UI 绑定的方法 /// public void Stop() { if (SettingsViewModel.GameSettings.CopilotWithScript && SettingsViewModel.GameSettings.ManualStopWithScript) { Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript", showLog: false)); if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.EndsWithScript)) { Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("EndsWithScript")); } } if (!Instances.AsstProxy.AsstStop()) { _logger.Warning("Failed to stop Asst"); } _runningState.SetIdle(true); } private bool IsDataFromWeb { get => field; set => SetAndNotify(ref field, value); } private int _copilotId; private int CopilotId { get => _copilotId; set { SetAndNotify(ref _copilotId, value); CouldLikeWebJson = value > 0; } } private async Task RateCopilot(int copilotId, bool isLike = true) { if (copilotId <= 0 || _recentlyRatedCopilotId.Contains(copilotId)) { return PrtsStatus.NotFound; } var result = await RateWebJsonAsync(copilotId, isLike ? "Like" : "Dislike"); switch (result) { case PrtsStatus.Success: _recentlyRatedCopilotId.Add(copilotId); AddLog(LocalizationHelper.GetString("ThanksForLikeWebJson"), UiLogColor.Info, showTime: false); break; case PrtsStatus.NetworkError: AddLog(LocalizationHelper.GetString("FailedToLikeWebJson"), UiLogColor.Error, showTime: false); break; } return result; } private async Task VerifyCopilotListTask() { return await VerifyCopilotListTask(null); } private async Task VerifyCopilotListTask(IEnumerable? items) { var copilotItemViewModels = (items ?? CopilotItemViewModels.Where(i => i.IsChecked)).ToArray(); switch (copilotItemViewModels.Length) { case 0: AddLog(LocalizationHelper.GetString("CopilotStartWithEmptyList"), UiLogColor.Error, showTime: false); return false; case 1: AddLog(LocalizationHelper.GetString("CopilotSingleTaskWarning"), UiLogColor.Warning, showTime: false); break; // 降级为警告, 有用户炸就派uuu } if (copilotItemViewModels.Any(i => string.IsNullOrEmpty(i.Name?.Trim()))) { AddLog(LocalizationHelper.GetString("CopilotTasksWithEmptyName"), UiLogColor.Error, showTime: false); return false; } var stageNames = copilotItemViewModels.Select(i => i.FilePath).ToHashSet().Select(async path => { if (!File.Exists(path)) { AddLog(LocalizationHelper.GetString("CopilotNoFound") + path, UiLogColor.Error, showTime: false); return null; } try { var str = await File.ReadAllTextAsync(path); return JsonConvert.DeserializeObject(str)?.StageName; } catch (Exception ex) { _logger.Error(ex, "could not read & parse copilot file: {Path}", path); return null; } }); foreach (var stageName in stageNames) { var name = await stageName; if (!string.IsNullOrEmpty(name) && DataHelper.FindMap(name) is not null) { continue; } AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {name}", UiLogColor.Error, showTime: false); _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); return false; } return true; } private bool VerifyParadoxTasks(IEnumerable? items = null) { var ok = true; foreach (var task in items ?? CopilotItemViewModels.Where(i => i.IsChecked)) { if (!DataHelper.Operators.Any(op => op.Value.Name == DataHelper.GetLocalizedCharacterName(task.Name, "zh-cn"))) { AddLog(LocalizationHelper.GetStringFormat("CopilotIllegalOperName", task.Name), UiLogColor.Error, showTime: false); _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); ok = false; } } return ok; } private static string GetCopilotTabName(CopilotType type) => type switch { CopilotType.MainStageAndSideStory => LocalizationHelper.GetString("MainStageStoryCollectionSideStory"), CopilotType.SSS => LocalizationHelper.GetString("SSS"), CopilotType.Paradox => LocalizationHelper.GetString("ParadoxSimulation"), CopilotType.Other => LocalizationHelper.GetString("OtherActivityStage"), _ => type.ToString(), }; /// /// 点击后移除界面中元素焦点 /// /// 点击事件发送者 /// 点击事件 [UsedImplicitly] public void MouseDown(object sender, MouseButtonEventArgs e) { if (sender is not UIElement element) { return; } DependencyObject scope = FocusManager.GetFocusScope(element); FocusManager.SetFocusedElement(scope, element); Keyboard.ClearFocus(); } /// /// 回车键点击后移除界面中元素焦点 /// /// 点击事件发送者 /// 点击事件 [UsedImplicitly] public void KeyDown(object sender, KeyEventArgs e) { if (e.Key != Key.Enter) { return; } if (sender is not UIElement element) { return; } DependencyObject scope = FocusManager.GetFocusScope(element); FocusManager.SetFocusedElement(scope, element); Keyboard.ClearFocus(); } private static async Task GetCopilotTypeAsync(string filePath) { try { if (!File.Exists(filePath)) { _logger.Error("file not found: {Path}, return: {Return}", filePath, CopilotType.Unknown); return CopilotType.Unknown; } var str = await File.ReadAllTextAsync(filePath); var job = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); return GetCopilotType(job); } catch (Exception ex) { _logger.Error(ex, "could not read & parse copilot file: {Path}", filePath); return CopilotType.Unknown; } } private static CopilotType GetCopilotType(CopilotBase? @base) { if (@base is null) { return CopilotType.Unknown; } else if (@base is SSSCopilotModel) { return CopilotType.SSS; } else if (@base is CopilotModel copilot) { if (string.IsNullOrEmpty(copilot.StageName)) { return CopilotType.Unknown; } var mapInfo = DataHelper.FindMap(copilot.StageName); if (mapInfo is null) { return CopilotType.Unknown; } return GetCopilotType(mapInfo.StageId); } return CopilotType.Unknown; } private static CopilotType GetCopilotType(string? stageId) { if (stageId?.StartsWith("mem_") is true) { return CopilotType.Paradox; } else if (stageId?.StartsWith("lt_") is true) { return CopilotType.SSS; } else if (!string.IsNullOrEmpty(stageId)) { if (MainStageIdRegex().IsMatch(stageId)) { return CopilotType.MainStageAndSideStory; } if (SideStoryStageIdRegex().IsMatch(stageId)) { return CopilotType.MainStageAndSideStory; } } return CopilotType.Unknown; } private enum CopilotType { Unknown = -1, /// 主线, 故事集, 支线作业 MainStageAndSideStory = 0, /// 保全 SSS, /// 悖论模拟 Paradox, /// 其他 Other, } [GeneratedRegex(@"(?:av\d+|bv[a-z0-9]{10})(?:\/\?p=\d+)?", RegexOptions.IgnoreCase | RegexOptions.Compiled, "zh-CN")] private static partial Regex BVRegex(); }