// // MaaWpfGui - A part of the MaaCoreArknights project // Copyright (C) 2021 MistEO and 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.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; using System.Windows.Threading; using HandyControl.Controls; using MaaWpfGui.Constants; using MaaWpfGui.Helper; using MaaWpfGui.States; 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 RecognizerViewModel : Screen { private readonly RunningState _runningState; private static readonly ILogger _logger = Log.ForContext(); /// /// Initializes a new instance of the class. /// public RecognizerViewModel() { DisplayName = LocalizationHelper.GetString("Toolbox"); _runningState = RunningState.Instance; _runningState.IdleChanged += RunningState_IdleChanged; _peepImageTimer.Elapsed += RefreshPeepImageAsync; _peepImageTimer.Interval = 1000d / PeepTargetFps; _gachaTimer.Tick += RefreshGachaTip; } private void RunningState_IdleChanged(object? sender, bool e) { Idle = e; if (!Idle) { return; } Peeping = false; IsPeepInProgress = false; IsGachaInProgress = false; } private bool _idle; /// /// Gets or sets a value indicating whether it is idle. /// public bool Idle { get => _idle; set => SetAndNotify(ref _idle, 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 string _recruitResult = string.Empty; /// /// Gets or sets the recruit result. /// public string RecruitResult { get => _recruitResult; set => SetAndNotify(ref _recruitResult, value); } 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()); } } public static SettingsViewModel RecruitSettingDataContext => Instances.SettingsViewModel; private bool _recruitCaught; /// /// Starts calculation. /// // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public async void RecruitStartCalc() { string errMsg = string.Empty; RecruitInfo = LocalizationHelper.GetString("ConnectingToEmulator"); _recruitCaught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); if (!_recruitCaught) { RecruitInfo = errMsg; return; } RecruitInfo = LocalizationHelper.GetString("Identifying"); RecruitResult = string.Empty; var levelList = new List(); if (ChooseLevel3) { levelList.Add(3); } if (ChooseLevel4) { levelList.Add(4); } if (ChooseLevel5) { levelList.Add(5); } if (ChooseLevel6) { levelList.Add(6); } Instances.AsstProxy.AsstStartRecruitCalc(levelList.ToArray(), RecruitAutoSetTime, RecruitSettingDataContext.ChooseLevel3Time, RecruitSettingDataContext.ChooseLevel4Time, RecruitSettingDataContext.ChooseLevel5Time); } 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": { string resultContent = string.Empty; JArray? resultArray = (JArray?)subTaskDetails?["result"]; /* int level = (int)subTaskDetails["level"]; */ foreach (var combs in resultArray ?? []) { int tagLevel = (int)(combs["level"] ?? -1); resultContent += tagLevel + "★ Tags: "; resultContent = (((JArray?)combs["tags"]) ?? []).Aggregate(resultContent, (current, tag) => current + (tag + " ")); resultContent += "\n\t"; foreach (var oper in (JArray?)combs["opers"] ?? []) { int operLevel = (int)(oper["level"] ?? -1); var operId = oper["id"]?.ToString(); var operName = DataHelper.GetLocalizedCharacterName(oper["name"]?.ToString()); string potential = string.Empty; if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) { if (OperBoxPotential.ContainsKey(operId)) { potential = " ( " + OperBoxPotential[operId] + " )"; } else { potential = " ( !!! NEW !!! )"; } } resultContent += operLevel + "★ " + operName + potential + " "; } resultContent += "\n\n"; } RecruitResult = resultContent; } break; } } #endregion Recruit #region Depot private string _depotInfo = LocalizationHelper.GetString("DepotRecognitionTip"); /// /// Gets or sets the depot info. /// public string DepotInfo { get => _depotInfo; set => SetAndNotify(ref _depotInfo, value); } private ObservableCollection _depotResult = []; /// /// Gets or sets the depot result. /// public ObservableCollection DepotResult { get => _depotResult; set => SetAndNotify(ref _depotResult, value); } public class DepotResultDate { public string? Name { get; set; } public string Id { get; set; } = null!; public BitmapImage? Image { get; set; } public int Count { get; set; } } /// /// Gets or sets the ArkPlanner result. /// public string ArkPlannerResult { get; set; } = string.Empty; /// /// Gets or sets the Lolicon result. /// public string LoliconResult { get; set; } = string.Empty; /// /// parse of depot recognition result /// /// detailed json-style parameters /// Success or not public bool DepotParse(JObject? details) { if (details == null) { return false; } DepotResult.Clear(); foreach (var item in details["arkplanner"]?["object"]?["items"]?.Cast()!) { var id = (string?)item["id"]; if (id == null) { continue; } DepotResultDate result = new DepotResultDate() { Id = id, Name = ItemListHelper.GetItemName(id), Image = ItemListHelper.GetItemImage(id), Count = (int)(item["have"] ?? -1), }; DepotResult.Add(result); } ArkPlannerResult = details["arkplanner"]?["data"]?.ToString() ?? string.Empty; LoliconResult = details["lolicon"]?["data"]?.ToString() ?? string.Empty; bool done = (bool)(details["done"] ?? false); if (!done) { return true; } DepotInfo = LocalizationHelper.GetString("IdentificationCompleted") + "\n" + LocalizationHelper.GetString("DepotRecognitionTip"); return true; } /// /// Export depot info to ArkPlanner. /// // xaml 中用到了 // ReSharper disable once UnusedMember.Global public void ExportToArkplanner() { Clipboard.SetDataObject(ArkPlannerResult); DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); } /// /// Export depot info to Lolicon. /// // xaml 中用到了 // ReSharper disable once UnusedMember.Global public void ExportToLolicon() { Clipboard.SetDataObject(LoliconResult); DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); } private void DepotClear() { DepotResult.Clear(); ArkPlannerResult = string.Empty; LoliconResult = string.Empty; } /// /// Starts depot recognition. /// // xaml 中用到了 // ReSharper disable once UnusedMember.Global public async void 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 /// /// 未实装干员,但在battle_data中, /// private static readonly HashSet _virtuallyOpers = [ "char_504_rguard", "char_505_rcast", "char_506_rmedic", "char_507_rsnipe", "char_514_rdfend", "char_513_apionr", "char_511_asnipe", "char_510_amedic", "char_509_acast", "char_508_aguard", "char_1001_amiya2", "char_1037_amiya3" ]; private string _operBoxInfo = LocalizationHelper.GetString("OperBoxRecognitionTip"); public string OperBoxInfo { get => _operBoxInfo; set => SetAndNotify(ref _operBoxInfo, value); } public string OperBoxExportData { get; set; } = string.Empty; private JArray _operBoxDataArray = (JArray)(JsonConvert.DeserializeObject(ConfigurationHelper.GetValue(ConfigurationKeys.OperBoxData, new JArray().ToString())) ?? new JArray()); public JArray OperBoxDataArray { get => _operBoxDataArray; set { SetAndNotify(ref _operBoxDataArray, value); ConfigurationHelper.SetValue(ConfigurationKeys.OperBoxData, value.ToString()); _operBoxPotential = null; // reset } } private Dictionary? _operBoxPotential; public Dictionary? OperBoxPotential { get { if (_operBoxPotential != null) { return _operBoxPotential; } _operBoxPotential = new Dictionary(); foreach (JObject operBoxData in OperBoxDataArray.OfType()) { var id = (string?)operBoxData["id"]; var potential = (int)(operBoxData["potential"] ?? -1); if (id != null) { _operBoxPotential[id] = potential; } } return _operBoxPotential; } } 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); } } public bool OperBoxParse(JObject? details) { var operBoxes = (JArray?)details?["all_opers"]; if (operBoxes == null) { return false; } List> operHave = []; List> operNotHave = []; foreach (JObject operBox in operBoxes.Cast()) { var tuple = new Tuple(DataHelper.GetLocalizedCharacterName((string?)operBox["name"]) ?? "???", (int)(operBox["rarity"] ?? -1)); if (_virtuallyOpers.Contains((string?)operBox["id"])) { continue; } if ((bool)(operBox["own"] ?? false)) { /*已拥有干员*/ operHave.Add(tuple); } else { /*未拥有干员,包含预备干员等*/ operNotHave.Add(tuple); } } operHave.Sort((x, y) => y.Item2.CompareTo(x.Item2)); operNotHave.Sort((x, y) => y.Item2.CompareTo(x.Item2)); OperBoxHaveList = new ObservableCollection(operHave.Select(tuple => tuple.Item1)); OperBoxNotHaveList = new ObservableCollection(operNotHave.Select(tuple => tuple.Item1)); bool done = (bool)(details?["done"] ?? false); if (!done) { return true; } OperBoxInfo = LocalizationHelper.GetString("IdentificationCompleted") + "\n" + LocalizationHelper.GetString("OperBoxRecognitionTip"); OperBoxExportData = details?["own_opers"]?.ToString() ?? string.Empty; OperBoxDataArray = (JArray)(details?["own_opers"] ?? new JArray()); _runningState.SetIdle(true); return true; } /// /// 开始识别干员 /// /// xaml 中用到了 /// ReSharper disable once UnusedMember.Global public async void StartOperBox() { OperBoxExportData = string.Empty; _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"); } } // xaml 中用到了 // ReSharper disable once UnusedMember.Global public void ExportOperBox() { if (string.IsNullOrEmpty(OperBoxExportData)) { return; } Clipboard.SetDataObject(OperBoxExportData); OperBoxInfo = LocalizationHelper.GetString("CopiedToClipboard"); } #endregion OperBox #region Gacha private string _gachaInfo = LocalizationHelper.GetString("GachaInitTip"); public string GachaInfo { get => _gachaInfo; set => SetAndNotify(ref _gachaInfo, value); } // xaml 中用到了 // ReSharper disable once UnusedMember.Global public void GachaOnce() { StartGacha(); } // xaml 中用到了 // ReSharper disable once UnusedMember.Global public void GachaTenTimes() { StartGacha(false); } private bool _isGachaInProgress; public bool IsGachaInProgress { get => _isGachaInProgress; set { if (!SetAndNotify(ref _isGachaInProgress, value)) { return; } if (!value) { _gachaTimer.Stop(); } } } public async void 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()); } } // xaml 中用到了 // ReSharper disable once UnusedMember.Global 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; } 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 BitmapImage? _peepImage; public BitmapImage? 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 { > 60 => 60, < 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 RefreshPeepImageAsync(object? sender, EventArgs? e) { if (!await _peepImageSemaphore.WaitAsync(0)) { // 一秒内连续三次未能获取信号量,降低 FPS if (++_peepImageSemaphoreFailCount < 3) { return; } _peepImageSemaphoreFailCount = 0; if (_peepImageSemaphoreCurrentCount < PeepImageSemaphoreMaxCount) { _peepImageSemaphoreCurrentCount++; _peepImageSemaphore.Release(); return; } _logger.Warning($"Gacha Semaphore Full, Reduce fps count to {--PeepTargetFps}"); _ = Execute.OnUIThreadAsync(() => { Growl.Clear(); Growl.Warning($"Screenshot taking too long, reduce FPS to {PeepTargetFps}"); }); return; } try { var count = Interlocked.Increment(ref _peepImageCount); var cacheImage = await Instances.AsstProxy.AsstGetFreshImageAsync(); if (!Peeping || count <= _peepImageNewestCount || cacheImage is null) { return; } Interlocked.Exchange(ref _peepImageNewestCount, count); PeepImage = cacheImage; 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); } /// /// 获取或停止获取实时截图,在抽卡时额外停止抽卡 /// public async void Peep() { if (IsPeepTransitioning) { return; } IsPeepTransitioning = true; try { // 正在 Peep 时,点击按钮停止 Peep if (Peeping) { Peeping = false; _peepImageTimer.Stop(); // 由 Peep() 方法启动的 Peep 也需要停止,Block 不会自动停止 if (IsGachaInProgress || IsPeepInProgress) { await Instances.TaskQueueViewModel.Stop(); Instances.TaskQueueViewModel.SetStopped(); } IsPeepInProgress = false; return; } // 点击按钮开始 Peep Peeping = true; // 如果没任务在运行,需要先连接,并标记是由 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 } }