// // 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.Buffers; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Documents; using System.Windows.Media.Imaging; using System.Windows.Threading; using HandyControl.Controls; using JetBrains.Annotations; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Models.AsstTasks; using MaaWpfGui.States; using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using Stylet; using Timer = System.Timers.Timer; namespace MaaWpfGui.ViewModels.UI; /// /// The view model of recruit. /// public class ToolboxViewModel : Screen { private readonly RunningState _runningState; private static readonly ILogger _logger = Log.ForContext(); /// /// Initializes a new instance of the class. /// public ToolboxViewModel() { PropertyDependsOnUtility.InitializePropertyDependencies(this); DisplayName = LocalizationHelper.GetString("Toolbox"); _runningState = RunningState.Instance; _runningState.StateChanged += (__, e) => { Idle = e.Idle; Inited = e.Inited; Stopping = e.Stopping; if (e.Stopping && Peeping && !IsPeepTransitioning) { _ = Peep(); } }; _peepImageTimer.Elapsed += PeepImageTimerElapsed; _peepImageTimer.Interval = 1000d / PeepTargetFps; _gachaTimer.Tick += RefreshGachaTip; LoadDepotDetails(); LoadOperBoxDetails(); OperBoxSelectedIndex = OperBoxNotHaveList.Count > 0 ? 0 : 1; UpdateMiniGameTaskList(); } private bool _idle; /// /// Gets or sets a value indicating whether it is idle. /// public bool Idle { get => _idle; set => SetAndNotify(ref _idle, value); } private bool _inited; public bool Inited { get => _inited; set => SetAndNotify(ref _inited, value); } private bool _stopping; public bool Stopping { get => _stopping; set => SetAndNotify(ref _stopping, value); } #region Recruit private string _recruitInfo = LocalizationHelper.GetString("RecruitmentRecognitionTip"); /// /// Gets or sets the recruit info. /// public string RecruitInfo { get => _recruitInfo; set => SetAndNotify(ref _recruitInfo, value); } private ObservableCollection _recruitResultInlines = []; public ObservableCollection RecruitResultInlines { get => _recruitResultInlines; set => SetAndNotify(ref _recruitResultInlines, value); } public void UpdateRecruitResult(JArray? resultArray) { ObservableCollection recruitResultInlines = []; foreach (var combs in resultArray ?? []) { int tagLevel = (int)(combs["level"] ?? -1); var tagStr = $"{tagLevel}★ Tags: "; tagStr = ((JArray?)combs["tags"] ?? []).Aggregate(tagStr, (current, tag) => current + $"{tag} "); var tagRun = new Run(tagStr); tagRun.SetResourceReference(TextElement.ForegroundProperty, UiLogColor.Text); tagRun.Tag = UiLogColor.Text; recruitResultInlines.Add(tagRun); recruitResultInlines.Add(new LineBreak()); var opersArray = (JArray?)combs["opers"] ?? []; var opersWithPotential = opersArray.Select(oper => { int operLevel = (int)(oper["level"] ?? -1); var operId = oper["id"]?.ToString(); int pot = -1; if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) { if (OperBoxPotential.TryGetValue(operId, out var potentialValue)) { pot = potentialValue; } } return new { Oper = oper, Potential = pot, OperLevel = operLevel }; }) .OrderByDescending(x => x.OperLevel) .ThenBy(x => x.Potential) .ToList(); foreach (var x in opersWithPotential) { var oper = x.Oper; int operLevel = x.OperLevel; var operId = oper["id"]?.ToString(); var operName = DataHelper.GetLocalizedCharacterName(oper["name"]?.ToString()); bool isMaxPot = false; string potentialText = string.Empty; if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) { if (OperBoxPotential.TryGetValue(operId, out var pot)) { potentialText = $" ( {pot} )"; if (pot == 6) { isMaxPot = true; potentialText = " ( MAX )"; } } else { potentialText = " ( !!! NEW !!! )"; } } var run = new Run($"{operName}{potentialText} "); var brushKey = GetBrushKeyByStar(operLevel, isMaxPot); run.SetResourceReference(TextElement.ForegroundProperty, brushKey); run.Tag = brushKey; recruitResultInlines.Add(run); } recruitResultInlines.Add(new LineBreak()); recruitResultInlines.Add(new LineBreak()); } RecruitResultInlines = recruitResultInlines; return; string GetBrushKeyByStar(int level, bool isMax) { return (level, isMax) switch { (6, true) => UiLogColor.Star6OperatorPotentialFull, (6, false) => UiLogColor.Star6Operator, (5, true) => UiLogColor.Star5OperatorPotentialFull, (5, false) => UiLogColor.Star5Operator, (4, true) => UiLogColor.Star4OperatorPotentialFull, (4, false) => UiLogColor.Star4Operator, (3, true) => UiLogColor.Star3OperatorPotentialFull, (3, false) => UiLogColor.Star3Operator, (2, true) => UiLogColor.Star2OperatorPotentialFull, (2, false) => UiLogColor.Star2Operator, (1, true) => UiLogColor.Star1OperatorPotentialFull, (1, false) => UiLogColor.Star1Operator, _ => UiLogColor.Text, }; } } private bool _chooseLevel3 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel3, bool.FalseString)); /// /// Gets or sets a value indicating whether to choose level 3. /// public bool ChooseLevel3 { get => _chooseLevel3; set { SetAndNotify(ref _chooseLevel3, value); ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel3, value.ToString()); } } private bool _chooseLevel4 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel4, bool.TrueString)); /// /// Gets or sets a value indicating whether to choose level 4. /// public bool ChooseLevel4 { get => _chooseLevel4; set { SetAndNotify(ref _chooseLevel4, value); ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel4, value.ToString()); } } private bool _chooseLevel5 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel5, bool.TrueString)); /// /// Gets or sets a value indicating whether to choose level 5. /// public bool ChooseLevel5 { get => _chooseLevel5; set { SetAndNotify(ref _chooseLevel5, value); ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel5, value.ToString()); } } private bool _chooseLevel6 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel6, bool.TrueString)); /// /// Gets or sets a value indicating whether to choose level 6. /// public bool ChooseLevel6 { get => _chooseLevel6; set { SetAndNotify(ref _chooseLevel6, value); ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel6, value.ToString()); } } private bool _autoSetTime = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.AutoSetTime, bool.TrueString)); /// /// Gets or sets a value indicating whether to set time automatically. /// public bool RecruitAutoSetTime { get => _autoSetTime; set { SetAndNotify(ref _autoSetTime, value); ConfigurationHelper.SetValue(ConfigurationKeys.AutoSetTime, value.ToString()); } } /// /// Starts calculation. /// UI 绑定的方法 /// /// Task [UsedImplicitly] public async Task RecruitStartCalc() { string errMsg = string.Empty; RecruitInfo = LocalizationHelper.GetString("ConnectingToEmulator"); _runningState.SetIdle(false); var recruitCaught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (!recruitCaught) { RecruitInfo = errMsg; _runningState.SetIdle(true); return; } RecruitInfo = LocalizationHelper.GetString("Identifying"); var levelList = new List(); if (ChooseLevel3) { levelList.Add(3); } if (ChooseLevel4) { levelList.Add(4); } if (ChooseLevel5) { levelList.Add(5); } if (ChooseLevel6) { levelList.Add(6); } var task = new AsstRecruitTask() { SelectList = levelList, ConfirmList = [-1], // 仅公招识别时将-1加入comfirm_level SetRecruitTime = RecruitAutoSetTime, ChooseLevel3Time = TaskQueueViewModel.RecruitTask.ChooseLevel3Time, ChooseLevel4Time = TaskQueueViewModel.RecruitTask.ChooseLevel4Time, ChooseLevel5Time = TaskQueueViewModel.RecruitTask.ChooseLevel5Time, ServerType = Instances.SettingsViewModel.ServerType, }; var (type, taskParams) = task.Serialize(); bool ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.RecruitCalc, type, taskParams); ret &= Instances.AsstProxy.AsstStart(); } private bool _recruitmentShowPotential = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.RecruitmentShowPotential, bool.TrueString)); public bool RecruitmentShowPotential { get => _recruitmentShowPotential; set { SetAndNotify(ref _recruitmentShowPotential, value); ConfigurationHelper.SetValue(ConfigurationKeys.RecruitmentShowPotential, value.ToString()); } } public void ProcRecruitMsg(JObject details) { string? what = details["what"]?.ToString(); var subTaskDetails = details["details"]; switch (what) { case "RecruitTagsDetected": { JArray? tags = (JArray?)subTaskDetails?["tags"]; string infoContent = LocalizationHelper.GetString("RecruitTagsDetected"); tags ??= []; infoContent = tags.Select(tagName => tagName.ToString()).Aggregate(infoContent, (current, tagStr) => current + (tagStr + " ")); RecruitInfo = infoContent; } break; case "RecruitResult": { JArray? resultArray = (JArray?)subTaskDetails?["result"]; UpdateRecruitResult(resultArray); } break; } } #endregion Recruit #region Depot private string _depotInfo = string.Empty; /// /// Gets or sets the depot info. /// public string DepotInfo { get => _depotInfo; set => SetAndNotify(ref _depotInfo, value); } private DateTime? _lastDepotSyncTime; /// /// Gets or sets 上次仓库同步时间(UTC 时间) /// public DateTime? LastDepotSyncTime { get => _lastDepotSyncTime; set => SetAndNotify(ref _lastDepotSyncTime, value); } /// /// Gets 上次仓库同步时间的显示文本(本地时间) /// [PropertyDependsOn(nameof(LastDepotSyncTime))] public string LastDepotSyncTimeText { get { if (LastDepotSyncTime == null) { return string.Empty; } // 将 UTC 时间转换为本地时间显示 var localTime = LastDepotSyncTime.Value.ToLocalTime(); return localTime.ToString(); } } private ObservableCollection _depotResult = []; /// /// Gets or sets the depot result. /// public ObservableCollection DepotResult { get => _depotResult; set { SetAndNotify(ref _depotResult, value); InvalidateDepotCache(); } } // 缓存相关字段 private bool _depotCacheInvalid = true; private string? _cachedArkPlannerResult; private string? _cachedLoliconResult; /// /// 标记仓库缓存失效 /// private void InvalidateDepotCache() { _depotCacheInvalid = true; _cachedArkPlannerResult = null; _cachedLoliconResult = null; } public class DepotResultDate { public string? Name { get; set; } public string Id { get; set; } = null!; public BitmapSource? Image { get; set; } /// /// Gets or sets 物品数量(原始数值) /// public int Count { get; set; } /// /// Gets 格式化后的显示数量(用于 UI 绑定) /// public string DisplayCount => Count >= 0 ? Count.FormatNumber(false) : "-1"; } /// /// 保存仓库详情数据 /// private void SaveDepotDetails() { // 构建简化格式:{"itemId": count} var depotData = new JObject(); foreach (var item in DepotResult) { if (item.Count >= 0) { depotData[item.Id] = item.Count; } } var details = new JObject { ["done"] = true, ["data"] = depotData.ToString(Formatting.None), }; // 保存同步时间为 UTC(如果有) if (LastDepotSyncTime.HasValue) { details["syncTime"] = LastDepotSyncTime.Value.ToString("o"); // ISO 8601 格式 } JsonDataHelper.Set(JsonDataKey.DepotData, details); } private void LoadDepotDetails() { var json = JsonDataHelper.Get(JsonDataKey.DepotData, string.Empty); if (string.IsNullOrWhiteSpace(json)) { return; } var details = JObject.Parse(json); DepotParse(details); } /// /// Gets 获取 ArkPlanner 导出格式(带缓存) /// public string ArkPlannerResult { get { if (DepotResult.Count == 0) { return string.Empty; } // 使用缓存 if (!_depotCacheInvalid && _cachedArkPlannerResult != null) { return _cachedArkPlannerResult; } // 重新计算 var items = DepotResult .Where(item => item.Count >= 0) .Select(item => new JObject { ["id"] = item.Id, ["have"] = item.Count, ["name"] = item.Name ?? string.Empty, }); var result = new JObject { ["@type"] = "@penguin-statistics/depot", ["items"] = new JArray(items), }; _cachedArkPlannerResult = result.ToString(Formatting.None); _depotCacheInvalid = false; // 标记缓存已更新 return _cachedArkPlannerResult; } } /// /// Gets 获取 工具箱 导出格式(带缓存) /// public string LoliconResult { get { if (DepotResult.Count == 0) { return string.Empty; } // 使用缓存 if (!_depotCacheInvalid && _cachedLoliconResult != null) { return _cachedLoliconResult; } // 重新计算 var depotData = new JObject(); foreach (var item in DepotResult) { if (item.Count >= 0) { depotData[item.Id] = item.Count; } } _cachedLoliconResult = depotData.ToString(Formatting.None); _depotCacheInvalid = false; // 标记缓存已更新 return _cachedLoliconResult; } } /// /// 解析仓库识别结果(兼容新旧格式) /// /// 详细的 JSON 参数 /// 是否更新同步时间为当前时间(从 Core 获取新数据时为 true,从本地加载时为 false) /// 是否成功 public bool DepotParse(JObject? details, bool updateSyncTime = false) { if (details == null) { return false; } DepotResult.Clear(); Dictionary depotItems = []; // 尝试解析新格式 var dataStr = details["data"]?.ToString(); if (!string.IsNullOrEmpty(dataStr)) { try { var dataObj = JObject.Parse(dataStr); foreach (var prop in dataObj.Properties()) { if (int.TryParse(prop.Value.ToString(), out var count)) { depotItems[prop.Name] = count; } } } catch (Exception ex) { _logger.Warning(ex, "Failed to parse depot data format"); } } // 如果新格式解析失败,尝试旧格式 if (depotItems.Count == 0) { if (depotItems.Count == 0) { var arkplannerItems = details["arkplanner"]?["object"]?["items"] ?.Cast() ?? []; foreach (var item in arkplannerItems) { var id = (string?)item["id"]; if (string.IsNullOrEmpty(id)) { continue; } if (item["have"] != null && int.TryParse(item["have"]?.ToString(), out var count)) { depotItems[id] = count; } } } } // 转换为 DepotResult,按 ID 排序 foreach (var kvp in depotItems.OrderBy(x => x.Key)) { var id = kvp.Key; var count = kvp.Value; DepotResultDate result = new() { Id = id, Name = ItemListHelper.GetItemName(id), Image = ItemListHelper.GetItemImage(id), Count = count, }; if (count > 0 && count > AchievementTrackerHelper.Instance.GetProgress(AchievementIds.WarehouseMiser)) { AchievementTrackerHelper.Instance.SetProgress(AchievementIds.WarehouseMiser, count); } DepotResult.Add(result); } // 标记缓存失效 InvalidateDepotCache(); bool done = (bool)(details["done"] ?? false); if (!done) { return true; } if (updateSyncTime) { // 从 Core 获取新数据,更新为当前 UTC 时间 LastDepotSyncTime = DateTime.UtcNow; } else { // 从本地加载,读取保存的时间 var syncTimeStr = details["syncTime"]?.ToString(Formatting.None)?.Trim('"'); if (!string.IsNullOrEmpty(syncTimeStr) && DateTime.TryParseExact(syncTimeStr, "O", null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var lastDepotSyncTime)) { LastDepotSyncTime = lastDepotSyncTime; } } DepotInfo = LocalizationHelper.GetString("IdentificationCompleted"); SaveDepotDetails(); Instances.TaskQueueViewModel.UpdateDatePrompt(); return true; } /// /// Export depot info to ArkPlanner. /// UI 绑定的方法 /// [UsedImplicitly] public void ExportToArkplanner() { System.Windows.Forms.Clipboard.Clear(); System.Windows.Forms.Clipboard.SetDataObject(ArkPlannerResult); DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); } /// /// Export depot info to Lolicon. /// UI 绑定的方法 /// [UsedImplicitly] public void ExportToLolicon() { System.Windows.Forms.Clipboard.Clear(); System.Windows.Forms.Clipboard.SetDataObject(LoliconResult); DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); } private void DepotClear() { DepotResult.Clear(); InvalidateDepotCache(); } // 需要排除的物品 ID(不统计到仓库) private static readonly HashSet ExcludedItemIds = [ "3401", // 家具 "3112", "3113", "3114", // 碳 "4001", // 龙门币 "4003", // 合成玉 "4006", // 红票 "5001", // 经验 ]; /// /// 检查物品 ID 是否应该被排除(不统计到仓库) /// /// 物品 ID /// true 表示应该排除 private static bool ShouldExcludeItem(string itemId) { // 排除特定 ID if (ExcludedItemIds.Contains(itemId)) { return true; } // 排除非纯数字的 ID if (!int.TryParse(itemId, out _)) { return true; } return false; } /// /// 根据 StageDrops 数据更新仓库 /// /// 关卡掉落数据列表 (ItemId, ItemName, Total, Add) public void UpdateDepotFromDrops(List<(string ItemId, string ItemName, int Total, int Add)> drops) { if (drops == null || drops.Count == 0) { return; } bool hasUpdates = false; foreach (var (itemId, _, total, add) in drops) { if (string.IsNullOrEmpty(itemId) || add <= 0) { continue; } // 过滤不需要统计的物品 if (ShouldExcludeItem(itemId)) { continue; } // 查找现有仓库项 var existingItem = DepotResult.FirstOrDefault(x => x.Id == itemId); if (existingItem != null) { // 更新现有物品数量 if (existingItem.Count >= 0) { var newCount = existingItem.Count + add; existingItem.Count = newCount; hasUpdates = true; // 更新成就进度 if (newCount > AchievementTrackerHelper.Instance.GetProgress(AchievementIds.WarehouseMiser)) { AchievementTrackerHelper.Instance.SetProgress(AchievementIds.WarehouseMiser, newCount); } } } else { // 添加新物品 var newItem = new DepotResultDate { Id = itemId, Name = ItemListHelper.GetItemName(itemId), Image = ItemListHelper.GetItemImage(itemId), Count = add, }; DepotResult.Add(newItem); hasUpdates = true; } } // 如果有更新,重新排序并保存 if (hasUpdates) { // 按 ID 排序(与 DepotParse 保持一致) var sortedItems = DepotResult.OrderBy(x => x.Id).ToList(); DepotResult.Clear(); foreach (var item in sortedItems) { DepotResult.Add(item); } // 标记缓存失效 InvalidateDepotCache(); // 保存更新后的数据 SaveDepotDetails(); Instances.TaskQueueViewModel.UpdateDatePrompt(); _logger.Information("Depot updated from stage drops, {Count} items processed", drops.Count); } } /// /// Starts depot recognition. /// UI 绑定的方法 /// /// Task [UsedImplicitly] public async Task StartDepot() { _runningState.SetIdle(false); string errMsg = string.Empty; DepotInfo = LocalizationHelper.GetString("ConnectingToEmulator"); bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (!caught) { DepotInfo = errMsg; _runningState.SetIdle(true); return; } DepotInfo = LocalizationHelper.GetString("Identifying"); DepotClear(); Instances.AsstProxy.AsstStartDepot(); } #endregion Depot #region OperBox private int _operBoxSelectedIndex = 0; public int OperBoxSelectedIndex { get => _operBoxSelectedIndex; set => SetAndNotify(ref _operBoxSelectedIndex, value); } private string _operBoxInfo = LocalizationHelper.GetString("OperBoxRecognitionTip"); public string OperBoxInfo { get => _operBoxInfo; set => SetAndNotify(ref _operBoxInfo, value); } private List _operBoxDataArray = []; public List OperBoxDataArray { get => _operBoxDataArray; set { SetAndNotify(ref _operBoxDataArray, value); _operBoxPotential = null; // reset } } private Dictionary? _operBoxPotential; public Dictionary? OperBoxPotential { get { if (_operBoxPotential != null) { return _operBoxPotential; } _operBoxPotential = OperBoxDataArray.ToDictionary(oper => oper.Id, oper => oper.Potential); return _operBoxPotential; } } public class Operator(string id, string name, int rarity) { [JsonProperty("id")] public string Id { get; } = id; [JsonProperty("name")] public string Name { get; } = name; [JsonProperty("rarity")] public int Rarity { get; } = rarity; public bool Equals(Operator? other) => other != null && Name == other.Name && Rarity == other.Rarity; public override bool Equals(object? obj) => obj is Operator other && Equals(other); public override int GetHashCode() => HashCode.Combine(Id, Name, Rarity); public override string ToString() => $"{Name} (★{Rarity})"; } private ObservableCollection _operBoxHaveList = []; public ObservableCollection OperBoxHaveList { get => _operBoxHaveList; set => SetAndNotify(ref _operBoxHaveList, value); } private ObservableCollection _operBoxNotHaveList = []; public ObservableCollection OperBoxNotHaveList { get => _operBoxNotHaveList; set => SetAndNotify(ref _operBoxNotHaveList, value); } private static void SaveOperBoxDetails(List details) { // var json = details.ToString(Formatting.None); // ConfigurationHelper.SetValue(ConfigurationKeys.OperBoxData, json); JsonDataHelper.Set(JsonDataKey.OperBoxData, JArray.FromObject(details)); } private void LoadOperBoxDetails() { // TODO: 删除老数据节省 gui.json 的大小,后续版本可以删除 // var json = ConfigurationHelper.GetValue(ConfigurationKeys.OperBoxData, string.Empty); ConfigurationHelper.DeleteValue(ConfigurationKeys.OperBoxData); var json = JsonDataHelper.Get(JsonDataKey.OperBoxData, string.Empty); if (string.IsNullOrWhiteSpace(json)) { return; } try { var ownOpers = JsonConvert.DeserializeObject>(json)?.Where(i => !string.IsNullOrEmpty(i.Id)).ToList(); if (ownOpers is null) { return; } OperBoxDataArray = ownOpers; var ids = ownOpers.Select(o => o.Id).ToHashSet(); foreach (var (id, oper) in DataHelper.Operators) { if (DataHelper.IsCharacterAvailableInClient(oper, SettingsViewModel.GameSettings.ClientType)) { var name = DataHelper.GetLocalizedCharacterName(oper) ?? "???"; if (ids.Contains(id)) { OperBoxHaveList.Add(new Operator(id, name, oper.Rarity)); if (id == "char_485_pallas") { AchievementTrackerHelper.Instance.Unlock(AchievementIds.WarehouseKeeper); } } else { OperBoxNotHaveList.Add(new Operator(id, name, oper.Rarity)); } } } } catch { // 兼容老数据或异常时忽略 } } /// /// 每次传进来的都是完整数据, 临时缓存去重 /// private HashSet _tempOperHaveSet = []; public bool OperBoxParse(JObject? details) { if (details == null) { return false; } var ownOpers = (details["own_opers"] as JArray)?.ToObject>()?.Where(o => !string.IsNullOrEmpty(o.Id)).ToList(); if (ownOpers is null) { return false; } foreach (var oper in ownOpers) { if (_tempOperHaveSet.Add(oper.Id)) { var name = DataHelper.GetLocalizedCharacterName(DataHelper.Operators.FirstOrDefault(i => i.Key == oper.Id).Value) ?? "???"; OperBoxHaveList.Add(new Operator(oper.Id, name, oper.Rarity)); if (oper.Id == "char_485_pallas") { AchievementTrackerHelper.Instance.Unlock(AchievementIds.WarehouseKeeper); } } } bool done = (bool)(details["done"] ?? false); if (!done) { return true; } foreach (var (id, oper) in DataHelper.Operators) { if (!_tempOperHaveSet.Contains(id) && DataHelper.IsCharacterAvailableInClient(oper, SettingsViewModel.GameSettings.ClientType)) { var name = DataHelper.GetLocalizedCharacterName(oper) ?? "???"; OperBoxNotHaveList.Add(new Operator(id, name, oper.Rarity)); } } if (OperBoxNotHaveList.Count > 0) { OperBoxSelectedIndex = 0; } OperBoxInfo = $"{LocalizationHelper.GetString("IdentificationCompleted")}\n{LocalizationHelper.GetString("OperBoxRecognitionTip")}"; OperBoxDataArray = ownOpers; SaveOperBoxDetails(ownOpers); _tempOperHaveSet = []; return true; } /// /// 开始识别干员 /// UI 绑定的方法 /// /// Task [UsedImplicitly] public async Task StartOperBox() { OperBoxSelectedIndex = 1; _tempOperHaveSet = []; OperBoxHaveList = []; OperBoxNotHaveList = []; _runningState.SetIdle(false); string errMsg = string.Empty; OperBoxInfo = LocalizationHelper.GetString("ConnectingToEmulator"); bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (!caught) { OperBoxInfo = errMsg; _runningState.SetIdle(true); return; } if (Instances.AsstProxy.AsstStartOperBox()) { OperBoxInfo = LocalizationHelper.GetString("Identifying"); } } // UI 绑定的方法 [UsedImplicitly] public void ExportOperBox() { if (OperBoxDataArray.Count == 0) { return; } var exportList = new List(); var userOperMap = OperBoxDataArray.ToDictionary(op => op.Id); foreach (var (operId, operInfo) in DataHelper.Operators) { if (!DataHelper.IsCharacterAvailableInClient(operInfo, SettingsViewModel.GameSettings.ClientType)) { continue; } var operName = DataHelper.GetLocalizedCharacterName(operInfo) ?? "???"; if (userOperMap.TryGetValue(operId, out var value)) { exportList.Add(value); } else { exportList.Add(new OperBoxData.OperData() { Id = operId, Name = operName, Rarity = operInfo.Rarity, }); } } System.Windows.Forms.Clipboard.Clear(); System.Windows.Forms.Clipboard.SetDataObject(JsonConvert.SerializeObject(exportList, Formatting.Indented)); OperBoxInfo = LocalizationHelper.GetString("CopiedToClipboard"); } #endregion OperBox #region Gacha private string _gachaInfo = LocalizationHelper.GetString("GachaInitTip"); public string GachaInfo { get => _gachaInfo; set => SetAndNotify(ref _gachaInfo, value); } // UI 绑定的方法 public async Task GachaOnce() { await StartGacha(); } // UI 绑定的方法 public async Task GachaTenTimes() { await StartGacha(false); } private bool _isGachaInProgress; public bool IsGachaInProgress { get => _isGachaInProgress; set { if (!SetAndNotify(ref _isGachaInProgress, value)) { return; } if (!value) { _gachaTimer.Stop(); } } } public async Task StartGacha(bool once = true) { _runningState.SetIdle(false); string errMsg = string.Empty; GachaInfo = LocalizationHelper.GetString("ConnectingToEmulator"); bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg) && Instances.AsstProxy.AsstStartGacha(once)); if (!caught) { GachaInfo = errMsg; _runningState.SetIdle(true); return; } _gachaTimer.Interval = TimeSpan.FromSeconds(5); _gachaTimer.Start(); RefreshGachaTip(null, null); IsGachaInProgress = true; _ = Peep(); } private void RefreshGachaTip(object? sender, EventArgs? e) { var rd = new Random(); GachaInfo = LocalizationHelper.GetString("GachaTip" + rd.Next(1, 18)); } // DO NOT CHANGE // 请勿更改 // 請勿更改 // このコードを変更しないでください // 변경하지 마십시오 private bool _gachaShowDisclaimer = true; // !Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ShowDisclaimerNoMore, bool.FalseString)); public bool GachaShowDisclaimer { get => _gachaShowDisclaimer; set { SetAndNotify(ref _gachaShowDisclaimer, value); } } private bool _gachaShowDisclaimerNoMore = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.GachaShowDisclaimerNoMore, bool.FalseString)); public bool GachaShowDisclaimerNoMore { get => _gachaShowDisclaimerNoMore; set { SetAndNotify(ref _gachaShowDisclaimerNoMore, value); ConfigurationHelper.SetValue(ConfigurationKeys.GachaShowDisclaimerNoMore, value.ToString()); } } // UI 绑定的方法 [UsedImplicitly] public void GachaAgreeDisclaimer() { var result = MessageBoxHelper.Show( LocalizationHelper.GetString("GachaWarning"), LocalizationHelper.GetString("Warning"), MessageBoxButton.YesNo, MessageBoxImage.Warning, no: LocalizationHelper.GetString("Confirm"), yes: LocalizationHelper.GetString("Cancel"), iconBrushKey: "DangerBrush"); if (result != MessageBoxResult.No) { return; } AchievementTrackerHelper.Instance.Unlock(AchievementIds.RealGacha); GachaShowDisclaimer = false; } #endregion Gacha #region Peep private bool _peeping; public bool Peeping { get => _peeping; set { if (!SetAndNotify(ref _peeping, value)) { return; } if (!value) { _peepImageTimer.Stop(); } } } private bool _isPeepInProgress; /// /// Gets or sets a value indicating whether由 Peep 方法启动的 Peep /// public bool IsPeepInProgress { get => _isPeepInProgress; set { if (!SetAndNotify(ref _isPeepInProgress, value)) { return; } if (!value) { _peepImageTimer.Stop(); } } } private WriteableBitmap? _peepImage; public WriteableBitmap? PeepImage { get => _peepImage; set => SetAndNotify(ref _peepImage, value); } private double _peepScreenFpf; public double PeepScreenFpf { get => _peepScreenFpf; set => SetAndNotify(ref _peepScreenFpf, value); } private int _peepTargetFps = int.Parse(ConfigurationHelper.GetValue(ConfigurationKeys.PeepTargetFps, "20")); public int PeepTargetFps { get { return _peepTargetFps; } set { value = value switch { > 600 => 600, < 1 => 1, _ => value, }; SetAndNotify(ref _peepTargetFps, value); _peepImageTimer.Interval = 1000d / _peepTargetFps; ConfigurationHelper.SetValue(ConfigurationKeys.PeepTargetFps, value.ToString()); } } private DateTime _lastFpsUpdateTime = DateTime.MinValue; private int _frameCount; private readonly Timer _peepImageTimer = new(); private readonly DispatcherTimer _gachaTimer = new() { Interval = TimeSpan.FromSeconds(5) }; private int _peepImageCount; private int _peepImageNewestCount; private static int _peepImageSemaphoreCurrentCount = 2; private const int PeepImageSemaphoreMaxCount = 5; private static int _peepImageSemaphoreFailCount = 0; private static readonly SemaphoreSlim _peepImageSemaphore = new(_peepImageSemaphoreCurrentCount, PeepImageSemaphoreMaxCount); private async void PeepImageTimerElapsed(object? sender, EventArgs? e) { try { await RefreshPeepImageAsync(); } catch { // ignored } } private readonly WriteableBitmap?[] _peepImageCache = new WriteableBitmap?[PeepImageSemaphoreMaxCount]; private async Task RefreshPeepImageAsync() { if (!await _peepImageSemaphore.WaitAsync(0)) { // 一秒内连续三次未能获取信号量,降低 FPS if (++_peepImageSemaphoreFailCount < 3) { return; } _peepImageSemaphoreFailCount = 0; if (_peepImageSemaphoreCurrentCount < PeepImageSemaphoreMaxCount) { _peepImageSemaphoreCurrentCount++; _peepImageSemaphore.Release(); _logger.Information("Screenshot Semaphore Full, increase semaphore count to {PeepImageSemaphoreCurrentCount}", _peepImageSemaphoreCurrentCount); return; } _logger.Warning("Screenshot Semaphore Full, Reduce Target FPS count to {PeepTargetFps}", --PeepTargetFps); _ = Execute.OnUIThreadAsync(() => { Growl.Clear(); Growl.Warning($"Screenshot taking too long, reduce Target FPS to {PeepTargetFps}"); }); return; } try { var count = Interlocked.Increment(ref _peepImageCount); var index = count % _peepImageCache.Length; var frameData = await Instances.AsstProxy.AsstGetImageBgrDataAsync(forceScreencap: true); if (frameData is null || frameData.Length == 0) { _logger.Warning("Peep image data is null or empty."); return; } // 若不满足条件,提前释放 frameData 避免内存泄露 if (!Peeping || count <= _peepImageNewestCount) { _logger.Debug("Peep image count {Count} is not the newest, skip updating image.", count); ArrayPool.Shared.Return(frameData); return; } await Execute.OnUIThreadAsync(() => { _peepImageCache[index] = AsstProxy.WriteBgrToBitmap(frameData, _peepImageCache[index]); }); PeepImage = _peepImageCache[index]; ArrayPool.Shared.Return(frameData); Interlocked.Exchange(ref _peepImageNewestCount, count); var now = DateTime.Now; Interlocked.Increment(ref _frameCount); var totalSeconds = (now - _lastFpsUpdateTime).TotalSeconds; if (totalSeconds < 1) { return; } var frameCount = Interlocked.Exchange(ref _frameCount, 0); _lastFpsUpdateTime = now; PeepScreenFpf = frameCount / totalSeconds; _peepImageSemaphoreFailCount = 0; } finally { _peepImageSemaphore.Release(); } } private bool _isPeepTransitioning; public bool IsPeepTransitioning { get => _isPeepTransitioning; set => SetAndNotify(ref _isPeepTransitioning, value); } /// /// 获取或停止获取实时截图,在抽卡时额外停止抽卡 /// /// Task public async Task Peep() { if (IsPeepTransitioning) { return; } IsPeepTransitioning = true; try { // 正在 Peep 时,点击按钮停止 Peep if (Peeping) { Peeping = false; _peepImageTimer.Stop(); Array.Fill(_peepImageCache, null); // 由 Peep() 方法启动的 Peep 也需要停止,Block 不会自动停止 if (IsGachaInProgress || IsPeepInProgress) { await Instances.TaskQueueViewModel.Stop(); Instances.TaskQueueViewModel.SetStopped(); } IsPeepInProgress = false; return; } // 点击按钮开始 Peep Peeping = true; AchievementTrackerHelper.Instance.Unlock(AchievementIds.PeekScreen); // 如果没任务在运行,需要先连接,并标记是由 Peep() 方法启动的 Peep if (Idle) { _runningState.SetIdle(false); string errMsg = string.Empty; bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (!caught) { GachaInfo = errMsg; _runningState.SetIdle(true); return; } IsPeepInProgress = true; } PeepScreenFpf = 0; _peepImageCount = 0; _peepImageNewestCount = 0; _peepImageTimer.Start(); } finally { IsPeepTransitioning = false; } } #endregion #region MiniGame public static ObservableCollection MiniGameTaskList { get; } = []; public static void UpdateMiniGameTaskList() { MiniGameTaskList.Clear(); var tasks = Instances.StageManager.MiniGameEntries; foreach (var t in tasks) { MiniGameTaskList.Add(new MiniGameEntry { Display = t.Display, DisplayKey = t.DisplayKey, Value = t.Value, Tip = t.Tip, TipKey = t.TipKey }); } } private string _miniGameTaskName = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MiniGameTaskName, "SS@Store@Begin"); public string MiniGameTaskName { get => _miniGameTaskName; set { SetAndNotify(ref _miniGameTaskName, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.MiniGameTaskName, value); MiniGameTip = GetMiniGameTip(value); } } public string GetMiniGameTask() { return MiniGameTaskName switch { "MiniGame@SecretFront" => $"{MiniGameTaskName}@Begin@Ending{SecretFrontEnding}{(string.IsNullOrEmpty(SecretFrontEvent) ? string.Empty : $"@{SecretFrontEvent}")}", _ => MiniGameTaskName, }; } private string? _miniGameTip; public string MiniGameTip { get { _miniGameTip ??= GetMiniGameTip(MiniGameTaskName); return _miniGameTip; } set => SetAndNotify(ref _miniGameTip, value); } private static string GetMiniGameTip(string name) { if (string.IsNullOrEmpty(name)) { return LocalizationHelper.GetString("MiniGameNameEmptyTip"); } var entry = Instances.StageManager.MiniGameEntries.FirstOrDefault(e => e.Value == name); if (entry == null) { return LocalizationHelper.GetString("MiniGameNameEmptyTip"); } // 优先使用 TipKey 的本地化 if (!string.IsNullOrEmpty(entry.TipKey) && LocalizationHelper.TryGetString(entry.TipKey, out var tipFromKey)) { return tipFromKey; } // 然后使用 explicit Tip if (!string.IsNullOrEmpty(entry.Tip)) { return entry.Tip; } // 若不存在 Tip,再尝试使用 DisplayKey + "Tip" 的约定键 if (!string.IsNullOrEmpty(entry.DisplayKey)) { var displayTipKey = entry.DisplayKey + "Tip"; if (LocalizationHelper.TryGetString(displayTipKey, out var displayTip)) { return displayTip; } // 最后回退为 Display 的本地化或原始 Display if (LocalizationHelper.TryGetString(entry.DisplayKey, out var displayLoc)) { return displayLoc; } } if (!string.IsNullOrEmpty(entry.Display)) { return entry.Display; } return string.Empty; } public List SecretFrontEndingList { get; set; } = ["A", "B", "C", "D", "E"]; private string _secretFrontEnding = ConfigurationHelper.GetValue(ConfigurationKeys.MiniGameSecretFrontEnding, "A"); public string SecretFrontEnding { get => _secretFrontEnding; set { SetAndNotify(ref _secretFrontEnding, value); ConfigurationHelper.SetValue(ConfigurationKeys.MiniGameSecretFrontEnding, value); } } public List> SecretFrontEventList { get; set; } = [ new GenericCombinedData { Display = LocalizationHelper.GetString("NotSelected"), Value = string.Empty }, new GenericCombinedData { Display = LocalizationHelper.GetString("MiniGame@SecretFront@Event1"), Value = "支援作战平台" }, new GenericCombinedData { Display = LocalizationHelper.GetString("MiniGame@SecretFront@Event2"), Value = "游侠" }, new GenericCombinedData { Display = LocalizationHelper.GetString("MiniGame@SecretFront@Event3"), Value = "诡影迷踪" }, ]; private string _secretFrontEvent = ConfigurationHelper.GetValue(ConfigurationKeys.MiniGameSecretFrontEvent, string.Empty); public string SecretFrontEvent { get => _secretFrontEvent; set { SetAndNotify(ref _secretFrontEvent, value); ConfigurationHelper.SetValue(ConfigurationKeys.MiniGameSecretFrontEvent, value); } } public void StartMiniGame() { _ = StartMiniGameAsync(); } private async Task StartMiniGameAsync() { if (!Idle) { await Instances.TaskQueueViewModel.Stop(); return; } _runningState.SetIdle(false); string errMsg = string.Empty; bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (!caught) { _runningState.SetIdle(true); } if (_runningState.GetStopping()) { Instances.TaskQueueViewModel.SetStopped(); return; } caught = Instances.AsstProxy.AsstMiniGame(GetMiniGameTask()); if (!caught) { _runningState.SetIdle(true); } } #endregion }