// // 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 // using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Services; using MaaWpfGui.States; using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using Stylet; using StyletIoC; using Application = System.Windows.Application; using ComboBox = System.Windows.Controls.ComboBox; using Screen = Stylet.Screen; namespace MaaWpfGui.ViewModels.UI { /// /// The view model of task queue. /// // 通过 container.Get(); 实例化或获取实例 // ReSharper disable once ClassNeverInstantiated.Global public class TaskQueueViewModel : Screen { private readonly IContainer _container; private StageManager _stageManager; private readonly RunningState _runningState; private static readonly ILogger _logger = Log.ForContext(); /// /// Gets or private sets the view models of task items. /// public ObservableCollection TaskItemViewModels { get; private set; } /// /// Gets the visibility of task setting views. /// public static TaskSettingVisibilityInfo TaskSettingVisibilities => TaskSettingVisibilityInfo.Current; public static SettingsViewModel TaskSettingDataContext => Instances.SettingsViewModel; /// /// 实时更新任务顺序 /// // UI 绑定的方法 // ReSharper disable once MemberCanBePrivate.Global public void TaskItemSelectionChanged() { Execute.OnUIThread(() => { int index = 0; foreach (var item in TaskItemViewModels) { ConfigurationHelper.SetTaskOrder(item.OriginalName, index.ToString()); ++index; } }); } /// /// Gets or private sets the view models of log items. /// public ObservableCollection LogItemViewModels { get; private set; } #region ActionAfterTasks private bool _enableAfterActionSetting; /// /// Gets or sets a value indicating whether to show after task queue actions /// public bool EnableAfterActionSetting { get => _enableAfterActionSetting; set { SetAndNotify(ref _enableAfterActionSetting, value); TaskSettingVisibilityInfo.Current.Set("AfterAction", value); } } /// /// Checks after completion. /// public async void CheckAfterCompleted() { await Task.Run(() => Instances.SettingsViewModel.RunScript("EndsWithScript")); var actions = TaskSettingDataContext.PostActionSetting; if (actions.BackToAndroidHome) { Instances.AsstProxy.AsstBackToHome(); await Task.Delay(1000); } if (actions.ExitArknights) { var mode = Instances.SettingsViewModel.ClientType; if (!Instances.AsstProxy.AsstStartCloseDown(mode)) { AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); } await Task.Delay(1000); } if (actions.ExitEmulator) { DoKillEmulator(); await Task.Delay(1000); } if (actions.ExitSelf && !(actions.Hibernate || actions.Shutdown || actions.Sleep)) { Bootstrapper.Shutdown(); } if (actions.Hibernate) { if (actions.IfNoOtherMaa && HasOtherMaa()) { Bootstrapper.Shutdown(); } else { await DoHibernate(); } } if (actions.Shutdown) { if (actions.IfNoOtherMaa && HasOtherMaa()) { Bootstrapper.Shutdown(); } else { await DoShutDown(); } } if (actions.Sleep) { if (actions.IfNoOtherMaa && HasOtherMaa()) { Bootstrapper.Shutdown(); } else { await DoSleep(); } } if (actions.ExitSelf) { Bootstrapper.Shutdown(); } actions.LoadPostActions(); return; bool HasOtherMaa() { var processesCount = Process.GetProcessesByName("MAA").Length; _logger.Information($"MAA processes count: {processesCount}"); return processesCount > 1; } void DoKillEmulator() { if (!KillEmulatorModeSwitcher()) { AddLog(LocalizationHelper.GetString("ExitEmulatorFailed"), UiLogColor.Error); } } async Task DoHibernate() { actions.LoadPostActions(); // 休眠提示 AddLog(LocalizationHelper.GetString("HibernatePrompt"), UiLogColor.Error); await Task.Delay(10000); PowerManagement.Hibernate(); } async Task DoShutDown() { Process.Start("shutdown.exe", "-s -t 70"); if (await TimerCanceledAsync( LocalizationHelper.GetString("Shutdown"), LocalizationHelper.GetString("AboutToShutdown"), LocalizationHelper.GetString("Cancel"), 60)) { Process.Start("shutdown.exe", "-a"); return; } Bootstrapper.Shutdown(); } async Task DoSleep() { actions.LoadPostActions(); // 休眠提示 AddLog(LocalizationHelper.GetString("SleepPrompt"), UiLogColor.Error); await Task.Delay(10000); PowerManagement.Sleep(); } } #endregion /// /// Initializes a new instance of the class. /// /// The IoC container. public TaskQueueViewModel(IContainer container) { _container = container; _runningState = RunningState.Instance; _runningState.IdleChanged += RunningState_IdleChanged; } private void RunningState_IdleChanged(object sender, bool e) { Idle = e; TaskSettingDataContext.Idle = e; if (!e) { Instances.Data.ClearCache(); } } protected override void OnInitialActivate() { base.OnInitialActivate(); _stageManager = _container.Get(); DisplayName = LocalizationHelper.GetString("Farming"); LogItemViewModels = new ObservableCollection(); InitializeItems(); InitTimer(); } /* public void ShowButton() { Visible = Visibility.Visible; Hibernate = true; } private Visibility _visible = Visibility.Collapsed; public Visibility Visible { get => _visible; set => SetAndNotify(ref _visible, value); } */ public bool Running { get; set; } public bool Closing { get; set; } private readonly DispatcherTimer _timer = new(); public bool ConfirmExit() { if (Closing) { return false; } Closing = true; if (Application.Current.IsShuttingDown()) { // allow close if application is shutting down return true; } if (!Running) { // no need to confirm if no running task return true; } if (!Instances.RecognizerViewModel.GachaDone) { // no need to confirm if Gacha running return true; } var result = MessageBoxHelper.Show( LocalizationHelper.GetString("ConfirmExitText"), LocalizationHelper.GetString("ConfirmExitTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question); Closing = false; return result == MessageBoxResult.Yes; } public override Task CanCloseAsync() { return Task.FromResult(this.ConfirmExit()); } private void InitTimer() { _timer.Interval = TimeSpan.FromSeconds(59); _timer.Tick += Timer1_Elapsed; _timer.Start(); } private async void Timer1_Elapsed(object sender, EventArgs e) { // 提前记录时间,避免等待超过定时时间 DateTime currentTime = DateTime.Now; currentTime = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day, currentTime.Hour, currentTime.Minute, 0); HandleDatePromptUpdate(); HandleCheckForUpdates(); RefreshCustomInfrastPlanIndexByPeriod(); await HandleTimerLogic(currentTime); } private static int CalculateRandomDelay() { Random random = new Random(); int delayTime = random.Next(0, 60 * 60 * 1000); return delayTime; } private bool _isUpdatingDatePrompt; private void HandleDatePromptUpdate() { if (!NeedToUpdateDatePrompt() || _isUpdatingDatePrompt) { return; } _isUpdatingDatePrompt = true; UpdateDatePrompt(); UpdateStageList(false); var delayTime = CalculateRandomDelay(); _ = Task.Run(async () => { await Task.Delay(delayTime); await _runningState.UntilIdleAsync(60000); await _stageManager.UpdateStageWeb(); UpdateDatePrompt(); UpdateStageList(false); _isUpdatingDatePrompt = false; }); } private bool _isCheckingForUpdates; private void HandleCheckForUpdates() { if (!NeedToCheckForUpdates() || _isCheckingForUpdates) { return; } if (!Instances.SettingsViewModel.UpdateAutoCheck) { return; } _isCheckingForUpdates = true; var delayTime = CalculateRandomDelay(); _ = Task.Run(async () => { await Task.Delay(delayTime); await Instances.SettingsViewModel.ManualUpdate(); _isCheckingForUpdates = false; }); } private static (bool timeToStart, bool timeToChangeConfig, int configIndex) CheckTimers(DateTime currentTime) { bool timeToStart = false; bool timeToChangeConfig = false; int configIndex = 0; for (int i = 0; i < 8; ++i) { if (!Instances.SettingsViewModel.TimerModels.Timers[i].IsOn) { continue; } DateTime startTime = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day, Instances.SettingsViewModel.TimerModels.Timers[i].Hour, Instances.SettingsViewModel.TimerModels.Timers[i].Min, 0); DateTime restartDateTime = startTime.AddMinutes(-2); // 确保0点的定时会在当日的23:58重启 if (restartDateTime.Day != startTime.Day) { restartDateTime = restartDateTime.AddDays(1); } if (currentTime == restartDateTime && Instances.SettingsViewModel.CurrentConfiguration != Instances.SettingsViewModel.TimerModels.Timers[i].TimerConfig) { timeToChangeConfig = true; configIndex = i; break; } // ReSharper disable once InvertIf if (currentTime == startTime) { timeToStart = true; configIndex = i; break; } } return (timeToStart, timeToChangeConfig, configIndex); } private async Task HandleTimerLogic(DateTime currentTime) { if (!_runningState.GetIdle() && !Instances.SettingsViewModel.ForceScheduledStart) { return; } var (timeToStart, timeToChangeConfig, configIndex) = CheckTimers(currentTime); if (timeToChangeConfig) { HandleConfigChange(configIndex); return; } if (timeToStart) { await HandleScheduledStart(configIndex); } } private void HandleConfigChange(int configIndex) { if (Instances.SettingsViewModel.CustomConfig && (_runningState.GetIdle() || Instances.SettingsViewModel.ForceScheduledStart)) { Instances.SettingsViewModel.CurrentConfiguration = Instances.SettingsViewModel.TimerModels.Timers[configIndex].TimerConfig; } } private async Task HandleScheduledStart(int configIndex) { if (Instances.SettingsViewModel.ForceScheduledStart) { if (Instances.SettingsViewModel.CustomConfig && Instances.SettingsViewModel.CurrentConfiguration != Instances.SettingsViewModel.TimerModels.Timers[configIndex].TimerConfig) { _logger.Warning($"Scheduled start skipped: Custom configuration is enabled, but the current configuration does not match the scheduled timer configuration (Timer Index: {configIndex}). Current Configuration: {Instances.SettingsViewModel.CurrentConfiguration}, Scheduled Configuration: {Instances.SettingsViewModel.TimerModels.Timers[configIndex].TimerConfig}"); return; } if (Instances.SettingsViewModel.ShowWindowBeforeForceScheduledStart) { Instances.MainWindowManager?.Show(); } if (await TimerCanceledAsync( LocalizationHelper.GetString("ForceScheduledStart"), LocalizationHelper.GetString("ForceScheduledStartTip"), LocalizationHelper.GetString("Cancel"), 10)) { return; } if (!_runningState.GetIdle()) { await Stop(); } var mode = Instances.SettingsViewModel.ClientType; if (!Instances.AsstProxy.AsstAppendCloseDown(mode)) { AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); } ResetFightVariables(); ResetTaskSelection(); RefreshCustomInfrastPlanIndexByPeriod(); } LinkStart(); } private static async Task TimerCanceledAsync(string content = "", string tipContent = "", string buttonContent = "", int seconds = 10) { var delay = TimeSpan.FromSeconds(seconds); var dialogUserControl = new Views.UserControl.TextDialogWithTimerUserControl( content, tipContent, buttonContent, delay.TotalMilliseconds); var dialog = HandyControl.Controls.Dialog.Show(dialogUserControl, nameof(Views.UI.RootView)); var canceled = false; dialogUserControl.Click += (_, _) => { canceled = true; dialog.Close(); }; await Task.Delay(delay); dialog.Close(); return canceled; } /// /// Initializes items. /// private void InitializeItems() { List taskList = [ "WakeUp", "Recruiting", "Base", "Combat", "Mall", "Mission", "AutoRoguelike", ]; if (Instances.SettingsViewModel.ClientType is not "txwy") { taskList.Add("Reclamation"); } var tempOrderList = new List(new DragItemViewModel[taskList.Count]); var nonOrderList = new List(); for (int i = 0; i != taskList.Count; ++i) { var task = taskList[i]; bool parsed = int.TryParse(ConfigurationHelper.GetTaskOrder(task, "-1"), out var order); DragItemViewModel vm = new DragItemViewModel( LocalizationHelper.GetString(task), task, "TaskQueue.", task is not ("AutoRoguelike" or "Reclamation")); if (task == TaskSettingVisibilityInfo.DefaultVisibleTaskSetting) { vm.EnableSetting = true; } if (!parsed || order < 0 || order >= tempOrderList.Count) { nonOrderList.Add(vm); } else { tempOrderList[order] = vm; } } foreach (var newVm in nonOrderList) { int i = 0; while (tempOrderList[i] != null) { ++i; } tempOrderList[i] = newVm; } TaskItemViewModels = new ObservableCollection(tempOrderList); InitDrops(); NeedToUpdateDatePrompt(); UpdateDatePrompt(); UpdateStageList(true); RefreshCustomInfrastPlan(); if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay()) { AddLog(LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), UiLogColor.Info); } } private DayOfWeek _curDayOfWeek; /// /// Determine whether the specified stage is open /// /// stage name /// Whether the specified stage is open public bool IsStageOpen(string name) { return _stageManager.IsStageOpen(name, _curDayOfWeek); } /// /// Updates stage list. /// /// Whether to update the stage list for selection forcibly // FIXME: 被注入对象只能在private函数内使用,只有Model显示之后才会被注入。如果Model还没有触发OnInitialActivate时调用函数会NullPointerException // 这个函数被列为public可见,意味着他注入对象前被调用 public void UpdateStageList(bool forceUpdate) { var hideUnavailableStage = Instances.SettingsViewModel.HideUnavailableStage; // forceUpdate: initializing or actions changing, update stage list forcibly if (!forceUpdate && !hideUnavailableStage) { return; } EnableSetFightParams = false; var stage1 = Stage1 ?? string.Empty; var stage2 = Stage2 ?? string.Empty; var stage3 = Stage3 ?? string.Empty; var rss = RemainingSanityStage ?? string.Empty; StageList = hideUnavailableStage ? new ObservableCollection(_stageManager.GetStageList(_curDayOfWeek)) : new ObservableCollection(_stageManager.GetStageList()); AlternateStageList = new ObservableCollection(_stageManager.GetStageList()); RemainingSanityStageList = new ObservableCollection(_stageManager.GetStageList()) { [0] = new() { Display = LocalizationHelper.GetString("NoUse"), Value = string.Empty }, }; // reset closed stages to "Last/Current" if (!CustomStageCode) { stage1 = StageList.Any(x => x.Value == stage1) ? stage1 : string.Empty; stage2 = AlternateStageList.Any(x => x.Value == stage2) ? stage2 : string.Empty; stage3 = AlternateStageList.Any(x => x.Value == stage3) ? stage3 : string.Empty; rss = RemainingSanityStageList.Any(x => x.Value == rss) ? rss : string.Empty; } else if (hideUnavailableStage) { stage1 = IsStageOpen(stage1) ? stage1 : string.Empty; stage2 = IsStageOpen(stage2) ? stage2 : string.Empty; stage3 = IsStageOpen(stage3) ? stage3 : string.Empty; rss = IsStageOpen(rss) ? rss : string.Empty; } _stage1Fallback = stage1; Stage1 = stage1; Stage2 = stage2; Stage3 = stage3; RemainingSanityStage = rss; EnableSetFightParams = true; } private bool NeedToUpdateDatePrompt() { var now = DateTime.UtcNow.ToYjDateTime(); var hour = now.Hour; var min = now.Minute; // yj历的4/16点 if (min == 0 && hour == 12) { return true; } if (_curDayOfWeek == now.DayOfWeek) { return false; } _curDayOfWeek = now.DayOfWeek; return true; } private static bool NeedToCheckForUpdates() { var now = DateTime.UtcNow.ToYjDateTime(); var hour = now.Hour; var min = now.Minute; // yj历的4/22点 return min == 0 && hour == 18; } /// /// Updates date prompt. /// // FIXME: 被注入对象只能在private函数内使用,只有Model显示之后才会被注入。如果Model还没有触发OnInitialActivate时调用函数会NullPointerException // 这个函数被列为public可见,意味着他注入对象前被调用 public void UpdateDatePrompt() { var builder = new StringBuilder(LocalizationHelper.GetString("TodaysStageTip") + "\n"); // Closed activity stages var stages = new[] { Stage1, Stage2, Stage3 }; foreach (var stage in stages) { if (stage == null || _stageManager.GetStageInfo(stage)?.IsActivityClosed() != true) { continue; } builder.Append(stage).Append(": ").AppendLine(LocalizationHelper.GetString("ClosedStage")); break; } // Open stages today var openStages = _stageManager.GetStageTips(_curDayOfWeek); if (!string.IsNullOrEmpty(openStages)) { builder.Append(openStages); } var prompt = builder.ToString(); if (StagesOfToday == prompt) { return; } StagesOfToday = prompt; } private string _stagesOfToday = string.Empty; /// /// Gets or private sets the stages of today. /// public string StagesOfToday { get => _stagesOfToday; private set => SetAndNotify(ref _stagesOfToday, value); } /// /// Adds log. /// /// The content. /// The font color. /// The font weight. public void AddLog(string content, string color = UiLogColor.Trace, string weight = "Regular") { Execute.OnUIThread(() => { var log = new LogItemViewModel(content, color, weight); LogItemViewModels.Add(log); _logger.Information(content); }); } /// /// Clears log. /// private void ClearLog() { LogItemViewModels.Clear(); _logger.Information("Main windows log clear."); _logger.Information(string.Empty); } /// /// Selects all. /// // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void SelectedAll() { foreach (var item in TaskItemViewModels) { switch (item.OriginalName) { case "AutoRoguelike": case "Reclamation": continue; } item.IsChecked = true; } } private bool _inverseMode = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)); /// /// Gets or sets a value indicating whether to use inverse mode. /// public bool InverseMode { get => _inverseMode; set { SetAndNotify(ref _inverseMode, value); InverseShowText = value ? LocalizationHelper.GetString("Inverse") : LocalizationHelper.GetString("Clear"); InverseMenuText = value ? LocalizationHelper.GetString("Clear") : LocalizationHelper.GetString("Inverse"); ConfigurationHelper.SetValue(ConfigurationKeys.MainFunctionInverseMode, value.ToString()); } } /// /// The width of "Select All" when both. /// public const int SelectedAllWidthWhenBoth = 80; private int _selectedAllWidth = ConfigurationHelper.GetValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse" ? SelectedAllWidthWhenBoth : 85; /// /// Gets or sets the width of "Select All". /// public int SelectedAllWidth { get => _selectedAllWidth; set => SetAndNotify(ref _selectedAllWidth, value); } private bool _showInverse = ConfigurationHelper.GetValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse"; /// /// Gets or sets a value indicating whether "Select inversely" is visible. /// public bool ShowInverse { get => _showInverse; set => SetAndNotify(ref _showInverse, value); } private string _inverseShowText = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)) ? LocalizationHelper.GetString("Inverse") : LocalizationHelper.GetString("Clear"); /// /// Gets or private Sets the text to be displayed for "Select inversely". /// public string InverseShowText { get => _inverseShowText; private set => SetAndNotify(ref _inverseShowText, value); } private string _inverseMenuText = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)) ? LocalizationHelper.GetString("Clear") : LocalizationHelper.GetString("Inverse"); /// /// Gets or private sets the text of inversion menu. /// public string InverseMenuText { get => _inverseMenuText; private set => SetAndNotify(ref _inverseMenuText, value); } /// /// Changes inversion mode. /// // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void ChangeInverseMode() { InverseMode = !InverseMode; } /// /// Selects inversely. /// // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void InverseSelected() { if (InverseMode) { foreach (var item in TaskItemViewModels) { switch (item.OriginalName) { case "AutoRoguelike": case "Reclamation": item.IsChecked = false; continue; } item.IsChecked = !item.IsChecked; } } else { foreach (var item in TaskItemViewModels) { item.IsChecked = false; } } } /// /// Reset unsaved task selection. /// public void ResetTaskSelection() { foreach (var item in TaskItemViewModels) { if (item.IsCheckedWithNull == null) { item.IsChecked = false; } } } private async Task ConnectToEmulator() { string errMsg = string.Empty; bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); // 尝试启动模拟器 if (!connected && Instances.SettingsViewModel.RetryOnDisconnected) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToStartEmulator")); await Task.Run(() => Instances.SettingsViewModel.TryToStartEmulator(true)); if (Stopping) { SetStopped(); return false; } connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); } // 尝试断开连接, 然后重新连接 if (!connected) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToReconnectByAdb")); await Task.Run(() => Instances.SettingsViewModel.ReconnectByAdb()); if (Stopping) { SetStopped(); return false; } Instances.AsstProxy.Connected = false; connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); } // 尝试重启 ADB if (!connected && Instances.SettingsViewModel.AllowAdbRestart) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("RestartAdb")); await Task.Run(() => Instances.SettingsViewModel.RestartAdb()); if (Stopping) { SetStopped(); return false; } connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); } // 尝试杀掉 ADB 进程 if (!connected && Instances.SettingsViewModel.AllowAdbHardRestart) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("HardRestartAdb")); await Task.Run(() => Instances.SettingsViewModel.HardRestartAdb()); if (Stopping) { SetStopped(); return false; } connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); } if (connected) { return true; } AddLog(errMsg, UiLogColor.Error); _runningState.SetIdle(true); SetStopped(); return false; } /// /// Starts. /// public async void LinkStart() { if (!_runningState.GetIdle()) { return; } _runningState.SetIdle(false); // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 TaskItemSelectionChanged(); Instances.SettingsViewModel.InfrastOrderSelectionChanged(); InfrastTaskRunning = true; ClearLog(); var buildDateTimeLong = SettingsViewModel.BuildDateTimeCurrentCultureString; var resourceDateTimeLong = Instances.SettingsViewModel.ResourceDateTimeCurrentCultureString; AddLog($"Build Time:\n{buildDateTimeLong}\nResource Time:\n{resourceDateTimeLong}"); var uiVersion = SettingsViewModel.UiVersion; var coreVersion = SettingsViewModel.CoreVersion; if (uiVersion != coreVersion && Instances.VersionUpdateViewModel.IsStdVersion(uiVersion) && Instances.VersionUpdateViewModel.IsStdVersion(coreVersion)) { AddLog(string.Format(LocalizationHelper.GetString("VersionMismatch"), uiVersion, coreVersion), UiLogColor.Error); return; } await Task.Run(() => Instances.SettingsViewModel.RunScript("StartsWithScript")); AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); /* // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) { AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); } */ // 一般是点了“停止”按钮了 if (Stopping) { SetStopped(); return; } if (!await ConnectToEmulator()) { return; } // 一般是点了“停止”按钮了 if (Stopping) { SetStopped(); return; } bool taskRet = true; // 直接遍历TaskItemViewModels里面的内容,是排序后的 int count = 0; foreach (var item in TaskItemViewModels) { if (item.IsChecked == false) { continue; } ++count; switch (item.OriginalName) { case "Base": taskRet &= AppendInfrast(); break; case "WakeUp": taskRet &= AppendStart(); break; case "Combat": taskRet &= AppendFight(); break; case "Recruiting": taskRet &= AppendRecruit(); break; case "Mall": taskRet &= AppendMall(); break; case "Mission": taskRet &= AppendAward(); break; case "AutoRoguelike": taskRet &= AppendRoguelike(); break; case "Reclamation": taskRet &= AppendReclamation(); break; default: --count; _logger.Error("Unknown task: " + item.OriginalName); break; } if (taskRet) { continue; } AddLog(item.OriginalName + "Error", UiLogColor.Error); taskRet = true; --count; } if (count == 0) { AddLog(LocalizationHelper.GetString("UnselectedTask")); _runningState.SetIdle(true); Instances.AsstProxy.AsstStop(); SetStopped(); return; } taskRet &= Instances.AsstProxy.AsstStart(); if (taskRet) { AddLog(LocalizationHelper.GetString("Running")); } else { AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); await Stop(); SetStopped(); } } /// /// 通常要和 一起使用,除非能保证回调消息能收到 `AsstMsg.TaskChainStopped` /// This is usually done with Unless you are guaranteed to receive the callback message `AsstMsg.TaskChainStopped` /// /// Timeout millisecond /// A /// 尝试等待 core 成功停止运行,默认超时时间一分钟 /// Try to wait for the core to stop running, the default timeout is one minute /// public async Task Stop(int timeout = 60 * 1000) { Stopping = true; AddLog(LocalizationHelper.GetString("Stopping")); await Task.Run(() => { if (!Instances.AsstProxy.AsstStop()) { _logger.Warning("Failed to stop Asst"); } }); int count = 0; while (Instances.AsstProxy.AsstRunning() && count <= timeout / 100) { await Task.Delay(100); count++; } return !Instances.AsstProxy.AsstRunning(); } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public async void WaitAndStop() { Waiting = true; AddLog(LocalizationHelper.GetString("Waiting")); if (Instances.SettingsViewModel.RoguelikeDelayAbortUntilCombatComplete) { await WaitUntilRoguelikeCombatComplete(); if (Instances.AsstProxy.AsstRunning() && !Stopping) { await Stop(); } } } /// /// 等待肉鸽战斗结束,10分钟强制退出 /// private async Task WaitUntilRoguelikeCombatComplete() { int time = 0; while (Instances.SettingsViewModel.RoguelikeDelayAbortUntilCombatComplete && RoguelikeInCombatAndShowWait && time < 600 && !Stopping) { await Task.Delay(1000); ++time; } } private bool _roguelikeInCombatAndShowWait; public bool RoguelikeInCombatAndShowWait { get => _roguelikeInCombatAndShowWait; set => SetAndNotify(ref _roguelikeInCombatAndShowWait, value); } public void SetStopped() { SleepManagement.AllowSleep(); if (Instances.SettingsViewModel.ManualStopWithScript) { Task.Run(() => Instances.SettingsViewModel.RunScript("EndsWithScript")); } if (!_runningState.GetIdle() || Stopping) { AddLog(LocalizationHelper.GetString("Stopped")); } Waiting = false; Stopping = false; _runningState.SetIdle(true); } public async void QuickSwitchAccount() { if (!_runningState.GetIdle()) { return; } _runningState.SetIdle(false); // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 TaskItemSelectionChanged(); Instances.SettingsViewModel.InfrastOrderSelectionChanged(); ClearLog(); await Task.Run(() => Instances.SettingsViewModel.RunScript("StartsWithScript")); AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); /* // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) { AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); } */ // 一般是点了“停止”按钮了 if (Stopping) { SetStopped(); return; } if (!await ConnectToEmulator()) { return; } // 一般是点了“停止”按钮了 if (Stopping) { SetStopped(); return; } bool taskRet = true; taskRet &= AppendStart(); taskRet &= Instances.AsstProxy.AsstStart(); if (taskRet) { AddLog(LocalizationHelper.GetString("Running")); } else { AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); await Stop(); SetStopped(); } } private static bool AppendStart() { var mode = Instances.SettingsViewModel.ClientType; var enable = mode.Length != 0; Instances.SettingsViewModel.AccountName = Instances.SettingsViewModel.AccountName.Trim(); var accountName = Instances.SettingsViewModel.AccountName; return Instances.AsstProxy.AsstAppendStartUp(mode, enable, accountName); } private bool AppendFight() { int medicine = 0; if (UseMedicine) { if (!int.TryParse(MedicineNumber, out medicine)) { medicine = 0; } } int stone = 0; if (UseStone) { if (!int.TryParse(StoneNumber, out stone)) { stone = 0; } } int times = int.MaxValue; if (HasTimesLimited) { if (!int.TryParse(MaxTimes, out times)) { times = 0; } } if (!int.TryParse(Series, out var series)) { series = 1; } int dropsQuantity = 0; if (IsSpecifiedDrops) { if (!int.TryParse(DropsQuantity, out dropsQuantity)) { dropsQuantity = 0; } } string curStage = Stage; bool mainFightRet = Instances.AsstProxy.AsstAppendFight(curStage, medicine, stone, times, series, DropsItemId, dropsQuantity); if (!mainFightRet) { AddLog(LocalizationHelper.GetString("UnsupportedStages") + ": " + curStage, UiLogColor.Error); return false; } if ((curStage == "Annihilation") && Instances.SettingsViewModel.UseAlternateStage) { foreach (var stage in new[] { Stage1, Stage2, Stage3 }) { if (!IsStageOpen(stage) || (stage == curStage)) { continue; } AddLog(LocalizationHelper.GetString("AnnihilationTaskTip"), UiLogColor.Info); mainFightRet = Instances.AsstProxy.AsstAppendFight(stage, medicine, 0, int.MaxValue, series, string.Empty, 0); break; } } if (mainFightRet && UseRemainingSanityStage && !string.IsNullOrEmpty(RemainingSanityStage)) { return Instances.AsstProxy.AsstAppendFight(RemainingSanityStage, 0, 0, int.MaxValue, 1, string.Empty, 0, false); } return mainFightRet; } private bool EnableSetFightParams { get; set; } = true; /// /// Sets parameters. /// public void SetFightParams() { if (!EnableSetFightParams) { return; } int medicine = 0; if (UseMedicine) { if (!int.TryParse(MedicineNumber, out medicine)) { medicine = 0; } } int stone = 0; if (UseStone) { if (!int.TryParse(StoneNumber, out stone)) { stone = 0; } } int times = int.MaxValue; if (HasTimesLimited) { if (!int.TryParse(MaxTimes, out times)) { times = 0; } } if (!int.TryParse(Series, out var series)) { series = 1; } int dropsQuantity = 0; if (IsSpecifiedDrops) { if (!int.TryParse(DropsQuantity, out dropsQuantity)) { dropsQuantity = 0; } } Instances.AsstProxy.AsstSetFightTaskParams(Stage, medicine, stone, times, series, DropsItemId, dropsQuantity); } private void SetFightRemainingSanityParams() { Instances.AsstProxy.AsstSetFightTaskParams(RemainingSanityStage, 0, 0, int.MaxValue, 1, string.Empty, 0, false); } private void SetInfrastParams() { var order = Instances.SettingsViewModel.GetInfrastOrderList(); Instances.AsstProxy.AsstSetInfrastTaskParams( order.ToArray(), Instances.SettingsViewModel.UsesOfDrones, Instances.SettingsViewModel.ContinueTraining, Instances.SettingsViewModel.DormThreshold / 100.0, Instances.SettingsViewModel.DormFilterNotStationedEnabled, Instances.SettingsViewModel.DormTrustEnabled, Instances.SettingsViewModel.OriginiumShardAutoReplenishment, Instances.SettingsViewModel.CustomInfrastEnabled, Instances.SettingsViewModel.CustomInfrastFile, CustomInfrastPlanIndex); } private bool AppendInfrast() { if (Instances.SettingsViewModel.CustomInfrastEnabled && (!File.Exists(Instances.SettingsViewModel.CustomInfrastFile) || CustomInfrastPlanInfoList.Count == 0)) { AddLog(LocalizationHelper.GetString("CustomizeInfrastSelectionEmpty"), UiLogColor.Error); return false; } var order = Instances.SettingsViewModel.GetInfrastOrderList(); return Instances.AsstProxy.AsstAppendInfrast( order.ToArray(), Instances.SettingsViewModel.UsesOfDrones, Instances.SettingsViewModel.ContinueTraining, Instances.SettingsViewModel.DormThreshold / 100.0, Instances.SettingsViewModel.DormFilterNotStationedEnabled, Instances.SettingsViewModel.DormTrustEnabled, Instances.SettingsViewModel.OriginiumShardAutoReplenishment, Instances.SettingsViewModel.CustomInfrastEnabled, Instances.SettingsViewModel.CustomInfrastFile, CustomInfrastPlanIndex); } private readonly Dictionary> _blackCharacterListMapping = new() { { string.Empty, new[] { "讯使", "嘉维尔", "坚雷" } }, { "Official", new[] { "讯使", "嘉维尔", "坚雷" } }, { "Bilibili", new[] { "讯使", "嘉维尔", "坚雷" } }, { "YoStarEN", new[] { "Courier", "Gavial", "Dur-nar" } }, { "YoStarJP", new[] { "クーリエ", "ガヴィル", "ジュナー" } }, { "YoStarKR", new[] { "쿠리어", "가비알", "듀나" } }, { "txwy", new[] { "訊使", "嘉維爾", "堅雷" } }, }; private bool AppendMall() { var buyFirst = Instances.SettingsViewModel.CreditFirstList.Split(';', ';') .Select(s => s.Trim()); var blackList = Instances.SettingsViewModel.CreditBlackList.Split(';', ';') .Select(s => s.Trim()); blackList = blackList.Union(_blackCharacterListMapping[Instances.SettingsViewModel.ClientType]); return Instances.AsstProxy.AsstAppendMall( !string.IsNullOrEmpty(this.Stage) && Instances.SettingsViewModel.CreditFightTaskEnabled, Instances.SettingsViewModel.CreditFightSelectFormation, Instances.SettingsViewModel.CreditVisitFriendsEnabled, Instances.SettingsViewModel.CreditShopping, buyFirst.ToArray(), blackList.ToArray(), Instances.SettingsViewModel.CreditForceShoppingIfCreditFull, Instances.SettingsViewModel.CreditOnlyBuyDiscount, Instances.SettingsViewModel.CreditReserveMaxCredit); } private static bool AppendAward() { var receiveAward = Instances.SettingsViewModel.ReceiveAward; var receiveMail = Instances.SettingsViewModel.ReceiveMail; var receiveFreeRecruit = Instances.SettingsViewModel.ReceiveFreeRecruit; var receiveOrundum = Instances.SettingsViewModel.ReceiveOrundum; var receiveMining = Instances.SettingsViewModel.ReceiveMining; var receiveSpecialAccess = Instances.SettingsViewModel.ReceiveSpecialAccess; return Instances.AsstProxy.AsstAppendAward(receiveAward, receiveMail, receiveFreeRecruit, receiveOrundum, receiveMining, receiveSpecialAccess); } private static bool AppendRecruit() { // for debug if (!int.TryParse(Instances.SettingsViewModel.RecruitMaxTimes, out var maxTimes)) { maxTimes = 0; } var firstList = Instances.SettingsViewModel.AutoRecruitFirstList.Split(';', ';') .Select(s => s.Trim()); var reqList = new List(); var cfmList = new List(); if (Instances.SettingsViewModel.ChooseLevel3) { cfmList.Add(3); } if (Instances.SettingsViewModel.ChooseLevel4) { reqList.Add(4); cfmList.Add(4); } // ReSharper disable once InvertIf if (Instances.SettingsViewModel.ChooseLevel5) { reqList.Add(5); cfmList.Add(5); } _ = int.TryParse(Instances.SettingsViewModel.SelectExtraTags, out var selectExtra); return Instances.AsstProxy.AsstAppendRecruit( maxTimes, firstList.ToArray(), [.. reqList], [.. cfmList], Instances.SettingsViewModel.RefreshLevel3, Instances.SettingsViewModel.ForceRefresh, Instances.SettingsViewModel.UseExpedited, selectExtra, Instances.SettingsViewModel.NotChooseLevel1, Instances.SettingsViewModel.IsLevel3UseShortTime, Instances.SettingsViewModel.IsLevel3UseShortTime2); } private static bool AppendRoguelike() { _ = int.TryParse(Instances.SettingsViewModel.RoguelikeMode, out var mode); return Instances.AsstProxy.AsstAppendRoguelike( mode, Instances.SettingsViewModel.RoguelikeStartsCount, Instances.SettingsViewModel.RoguelikeInvestmentEnabled, Instances.SettingsViewModel.RoguelikeInvestmentWithMoreScore, Instances.SettingsViewModel.RoguelikeInvestsCount, Instances.SettingsViewModel.RoguelikeStopWhenInvestmentFull, Instances.SettingsViewModel.RoguelikeSquad, Instances.SettingsViewModel.RoguelikeRoles, DataHelper.GetCharacterByNameOrAlias(Instances.SettingsViewModel.RoguelikeCoreChar)?.Name ?? Instances.SettingsViewModel.RoguelikeCoreChar, Instances.SettingsViewModel.RoguelikeStartWithEliteTwo, Instances.SettingsViewModel.RoguelikeOnlyStartWithEliteTwo, Instances.SettingsViewModel.Roguelike3FirstFloorFoldartal, Instances.SettingsViewModel.Roguelike3StartFloorFoldartal, Instances.SettingsViewModel.Roguelike3NewSquad2StartingFoldartal, Instances.SettingsViewModel.Roguelike3NewSquad2StartingFoldartals, Instances.SettingsViewModel.RoguelikeUseSupportUnit, Instances.SettingsViewModel.RoguelikeEnableNonfriendSupport, Instances.SettingsViewModel.RoguelikeTheme, Instances.SettingsViewModel.RoguelikeRefreshTraderWithDice, Instances.SettingsViewModel.RoguelikeStopAtFinalBoss); } private static bool AppendReclamation() { _ = int.TryParse(Instances.SettingsViewModel.ReclamationMode, out var mode); return Instances.AsstProxy.AsstAppendReclamation( Instances.SettingsViewModel.ReclamationTheme, mode, Instances.SettingsViewModel.ReclamationToolToCraft, Instances.SettingsViewModel.ReclamationIncrementMode, Instances.SettingsViewModel.ReclamationMaxCraftCountPerRound); } [DllImport("User32.dll", EntryPoint = "FindWindow")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("User32.dll", CharSet = CharSet.Auto)] private static extern int GetWindowThreadProcessId(IntPtr hwnd, out int id); /// /// 一个根据连接配置判断使用关闭模拟器的方式的方法 /// /// 是否关闭成功 private static bool KillEmulatorModeSwitcher() { try { string emulatorMode = Instances.SettingsViewModel.ConnectConfig; Instances.AsstProxy.Connected = false; return emulatorMode switch { "Nox" => KillEmulatorNox(), "LDPlayer" => KillEmulatorLdPlayer(), "XYAZ" => KillEmulatorXyaz(), "BlueStacks" => KillEmulatorBlueStacks(), "MuMuEmulator12" => KillEmulatorMuMuEmulator12(), _ => KillEmulatorByWindow(), }; } catch (Exception e) { _logger.Error("Failed to close emulator: " + e.Message); return false; } } /// /// 一个用于调用 MuMu12 模拟器控制台关闭 MuMu12 的方法 /// /// 是否关闭成功 private static bool KillEmulatorMuMuEmulator12() { string address = Instances.SettingsViewModel.ConnectAddress; int emuIndex; if (address == "127.0.0.1:16384") { emuIndex = 0; } else { string portStr = address.Split(':')[1]; int port = int.Parse(portStr); emuIndex = (port - 16384) / 32; } Process[] processes = Process.GetProcessesByName("MuMuPlayer"); if (processes.Length <= 0) { return false; } ProcessModule processModule; try { processModule = processes[0].MainModule; } catch (Exception e) { _logger.Error("Error: Failed to get the main module of the emulator process."); _logger.Error(e.Message); return false; } if (processModule == null) { return false; } string emuLocation = processModule.FileName; emuLocation = Path.GetDirectoryName(emuLocation); if (emuLocation == null) { return false; } string consolePath = Path.Combine(emuLocation, "MuMuManager.exe"); if (File.Exists(consolePath)) { ProcessStartInfo startInfo = new ProcessStartInfo(consolePath) { Arguments = $"api -v {emuIndex} shutdown_player", CreateNoWindow = true, UseShellExecute = false, }; var process = Process.Start(startInfo); if (process != null && process.WaitForExit(5000)) { _logger.Information($"Emulator at index {emuIndex} closed through console. Console path: {consolePath}"); return KillEmulator(); } _logger.Warning($"Console process at index {emuIndex} did not exit within the specified timeout. Killing emulator by window. Console path: {consolePath}"); return KillEmulatorByWindow(); } _logger.Error($"Error: `{consolePath}` not found, try to kill emulator by window."); return KillEmulatorByWindow(); } /// /// 一个用于调用雷电模拟器控制台关闭雷电模拟器的方法 /// /// 是否关闭成功 private static bool KillEmulatorLdPlayer() { string address = Instances.SettingsViewModel.ConnectAddress; int emuIndex; if (address.Contains(":")) { string portStr = address.Split(':')[1]; int port = int.Parse(portStr); emuIndex = (port - 5555) / 2; } else { string portStr = address.Split('-')[1]; int port = int.Parse(portStr); emuIndex = (port - 5554) / 2; } Process[] processes = Process.GetProcessesByName("dnplayer"); if (processes.Length <= 0) { return false; } ProcessModule processModule; try { processModule = processes[0].MainModule; } catch (Exception e) { _logger.Error("Error: Failed to get the main module of the emulator process."); _logger.Error(e.Message); return false; } if (processModule == null) { return false; } string emuLocation = processModule.FileName; emuLocation = Path.GetDirectoryName(emuLocation); if (emuLocation == null) { return false; } string consolePath = Path.Combine(emuLocation, "ldconsole.exe"); if (File.Exists(consolePath)) { ProcessStartInfo startInfo = new ProcessStartInfo(consolePath) { Arguments = $"quit --index {emuIndex}", CreateNoWindow = true, UseShellExecute = false, }; var process = Process.Start(startInfo); if (process != null && process.WaitForExit(5000)) { _logger.Information($"Emulator at index {emuIndex} closed through console. Console path: {consolePath}"); return KillEmulator(); } _logger.Warning($"Console process at index {emuIndex} did not exit within the specified timeout. Killing emulator by window. Console path: {consolePath}"); return KillEmulatorByWindow(); } _logger.Information($"Error: `{consolePath}` not found, try to kill emulator by window."); return KillEmulatorByWindow(); } /// /// 一个用于调用夜神模拟器控制台关闭夜神模拟器的方法 /// /// 是否关闭成功 private static bool KillEmulatorNox() { string address = Instances.SettingsViewModel.ConnectAddress; int emuIndex; if (address == "127.0.0.1:62001") { emuIndex = 0; } else { string portStr = address.Split(':')[1]; int port = int.Parse(portStr); emuIndex = port - 62024; } Process[] processes = Process.GetProcessesByName("Nox"); if (processes.Length <= 0) { return false; } ProcessModule processModule; try { processModule = processes[0].MainModule; } catch (Exception e) { _logger.Error("Error: Failed to get the main module of the emulator process."); _logger.Error(e.Message); return false; } if (processModule == null) { return false; } string emuLocation = processModule.FileName; emuLocation = Path.GetDirectoryName(emuLocation); if (emuLocation == null) { return false; } string consolePath = Path.Combine(emuLocation, "NoxConsole.exe"); if (File.Exists(consolePath)) { ProcessStartInfo startInfo = new ProcessStartInfo(consolePath) { Arguments = $"quit -index:{emuIndex}", CreateNoWindow = true, UseShellExecute = false, }; var process = Process.Start(startInfo); if (process != null && process.WaitForExit(5000)) { _logger.Information($"Emulator at index {emuIndex} closed through console. Console path: {consolePath}"); return KillEmulator(); } _logger.Warning($"Console process at index {emuIndex} did not exit within the specified timeout. Killing emulator by window. Console path: {consolePath}"); return KillEmulatorByWindow(); } _logger.Information($"Error: `{consolePath}` not found, try to kill emulator by window."); return KillEmulatorByWindow(); } /// /// 一个用于调用逍遥模拟器控制台关闭逍遥模拟器的方法 /// /// 是否关闭成功 private static bool KillEmulatorXyaz() { string address = Instances.SettingsViewModel.ConnectAddress; string portStr = address.Split(':')[1]; int port = int.Parse(portStr); var emuIndex = (port - 21503) / 10; Process[] processes = Process.GetProcessesByName("MEmu"); if (processes.Length <= 0) { return false; } ProcessModule processModule; try { processModule = processes[0].MainModule; } catch (Exception e) { _logger.Error("Error: Failed to get the main module of the emulator process."); _logger.Error(e.Message); return false; } if (processModule == null) { return false; } string emuLocation = processModule.FileName; emuLocation = Path.GetDirectoryName(emuLocation); if (emuLocation == null) { return false; } string consolePath = Path.Combine(emuLocation, "memuc.exe"); if (File.Exists(consolePath)) { ProcessStartInfo startInfo = new ProcessStartInfo(consolePath) { Arguments = $"stop -i {emuIndex}", CreateNoWindow = true, UseShellExecute = false, }; var process = Process.Start(startInfo); if (process != null && process.WaitForExit(5000)) { _logger.Information($"Emulator at index {emuIndex} closed through console. Console path: {consolePath}"); return KillEmulator(); } _logger.Warning($"Console process at index {emuIndex} did not exit within the specified timeout. Killing emulator by window. Console path: {consolePath}"); return KillEmulatorByWindow(); } _logger.Information($"Error: `{consolePath}` not found, try to kill emulator by window."); return KillEmulatorByWindow(); } /// /// 一个用于关闭蓝叠模拟器的方法 /// /// 是否关闭成功 private static bool KillEmulatorBlueStacks() { Process[] processes = Process.GetProcessesByName("HD-Player"); if (processes.Length <= 0) { return false; } ProcessModule processModule; try { processModule = processes[0].MainModule; } catch (Exception e) { _logger.Error("Error: Failed to get the main module of the emulator process."); _logger.Error(e.Message); return false; } if (processModule == null) { return false; } string emuLocation = processModule.FileName; emuLocation = Path.GetDirectoryName(emuLocation); if (emuLocation == null) { return false; } string consolePath = Path.Combine(emuLocation, "bsconsole.exe"); if (File.Exists(consolePath)) { _logger.Information($"Info: `{consolePath}` has been found. This may be the BlueStacks China emulator, try to kill the emulator by window."); return KillEmulatorByWindow(); } _logger.Information($"Info: `{consolePath}` not found. This may be the BlueStacks International emulator, try to kill the emulator by the port."); if (KillEmulator()) { return true; } _logger.Information("Info: Failed to kill emulator by the port, try to kill emulator process with PID."); if (processes.Length > 1) { _logger.Warning("Warning: The number of elements in processes exceeds one, abort closing the emulator"); return false; } try { processes[0].Kill(); return processes[0].WaitForExit(20000); } catch (Exception ex) { _logger.Error($"Error: Failed to kill emulator process with PID {processes[0].Id}. Exception: {ex.Message}"); } return false; } /// /// Kills emulator by Window hwnd. /// /// Whether the operation is successful. private static bool KillEmulatorByWindow() { int pid = 0; var windowName = new[] { "明日方舟", "明日方舟 - MuMu模拟器", "BlueStacks App Player", "BlueStacks", }; foreach (string i in windowName) { var hwnd = FindWindow(null, i); if (hwnd == IntPtr.Zero) { continue; } GetWindowThreadProcessId(hwnd, out pid); break; } if (pid == 0) { return KillEmulator(); } try { var emulator = Process.GetProcessById(pid); emulator.CloseMainWindow(); if (!emulator.WaitForExit(5000)) { emulator.Kill(); if (emulator.WaitForExit(5000)) { _logger.Information($"Emulator with process ID {pid} killed successfully."); KillEmulator(); return true; } _logger.Error($"Failed to kill emulator with process ID {pid}."); return false; } // 尽管已经成功 CloseMainWindow(),再次尝试 killEmulator() // Refer to https://github.com/MaaAssistantArknights/MaaAssistantArknights/pull/1878 KillEmulator(); // 已经成功 CloseMainWindow(),所以不管 killEmulator() 的结果如何,都返回 true return true; } catch { _logger.Error("Kill emulator by window error"); } return KillEmulator(); } /// /// Kills emulator. /// /// Whether the operation is successful. public static bool KillEmulator() { int pid = 0; string address = ConfigurationHelper.GetValue(ConfigurationKeys.ConnectAddress, string.Empty); var port = address.StartsWith("127") ? address.Substring(10) : "5555"; _logger.Information($"address: {address}, port: {port}"); string portCmd = "netstat -ano|findstr \"" + port + "\""; Process checkCmd = new Process { StartInfo = { FileName = "cmd.exe", UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }, }; try { checkCmd.Start(); } catch { _logger.Error("Failed to start cmd.exe"); checkCmd.Close(); return false; } checkCmd.StandardInput.WriteLine(portCmd); checkCmd.StandardInput.WriteLine("exit"); Regex reg = new Regex("\\s+", RegexOptions.Compiled); while (true) { var line = checkCmd.StandardOutput.ReadLine(); line = line?.Trim(); if (line == null) { break; } if (!line.StartsWith("TCP", StringComparison.OrdinalIgnoreCase)) { continue; } line = reg.Replace(line, ","); try { string[] arr = line.Split(','); if (arr.Length >= 2 && Convert.ToBoolean(string.Compare(arr[1], address, StringComparison.Ordinal))) { continue; } pid = int.Parse(arr[4]); break; } catch (Exception e) { _logger.Error("Failed to parse cmd.exe output: " + e.Message); } } if (pid == 0) { _logger.Error("Failed to get emulator PID"); return false; } try { Process emulator = Process.GetProcessById(pid); emulator.Kill(); } catch { return false; } finally { checkCmd.Close(); } return true; } /// /// Gets a value indicating whether it is initialized. /// public bool Inited { get; private set; } /// /// Sets it initialized. /// public void SetInited() { Inited = true; NotifyOfPropertyChange(nameof(Inited)); } private bool _idle; /// /// Gets or sets a value indicating whether it is idle. /// public bool Idle { get => _idle; set { SetAndNotify(ref _idle, value); if (!value) { return; } FightTaskRunning = false; InfrastTaskRunning = false; } } private bool _stopping; /// /// Gets a value indicating whether `stop` is awaiting. /// public bool Stopping { get => _stopping; private set => SetAndNotify(ref _stopping, value); } private bool _waiting; /// /// Gets a value indicating whether waiting for roguelike combat complete. /// public bool Waiting { // UI 会根据这个值来改变 Visibility // ReSharper disable once UnusedMember.Global get => _waiting; private set => SetAndNotify(ref _waiting, value); } private bool _fightTaskRunning; /// /// Gets or sets a value indicating whether the battle task is running. /// public bool FightTaskRunning { get => _fightTaskRunning; set => SetAndNotify(ref _fightTaskRunning, value); } private bool _infrastTaskRunning; public bool InfrastTaskRunning { get => _infrastTaskRunning; set => SetAndNotify(ref _infrastTaskRunning, value); } /* private bool _shutdown = false; public bool Shutdown { get => return _shutdown; set { SetAndNotify(ref _shutdown, value); if (value) { Hibernate = false; Suspend = false; } } } private bool _hibernate = false; // 休眠 public bool Hibernate { get => return _hibernate; set { SetAndNotify(ref _hibernate, value); if (value) { Shutdown = false; Suspend = false; } } } private bool _suspend = false; // 待机 public bool Suspend { get => return _suspend; set { SetAndNotify(ref _suspend, value); if (value) { Shutdown = false; Hibernate = false; } } } */ /// /// Gets or private sets the list of series. /// public List SeriesList { get; private set; } = ["1", "2", "3", "4", "5", "6"]; private ObservableCollection _stageList = new(); /// /// Gets or private sets the list of stages. /// public ObservableCollection StageList { get => _stageList; private set => SetAndNotify(ref _stageList, value); } public ObservableCollection RemainingSanityStageList { get; private set; } = new(); public ObservableCollection AlternateStageList { get; private set; } = new(); /// /// Gets the stage. /// public string Stage { get { Stage1 ??= _stage1Fallback; if (!Instances.SettingsViewModel.UseAlternateStage) { return Stage1; } if (IsStageOpen(Stage1)) { return Stage1; } if (IsStageOpen(Stage2)) { return Stage2; } return IsStageOpen(Stage3) ? Stage3 : Stage1; } } private readonly Dictionary _stageDictionary = new() { { "AN", "Annihilation" }, { "剿灭", "Annihilation" }, { "CE", "CE-6" }, { "龙门币", "CE-6" }, { "LS", "LS-6" }, { "经验", "LS-6" }, { "狗粮", "LS-6" }, { "CA", "CA-5" }, { "技能", "CA-5" }, { "AP", "AP-5" }, { "红票", "AP-5" }, { "SK", "SK-5" }, { "碳", "SK-5" }, { "炭", "SK-5" }, }; private string ToUpperAndCheckStage(string value) { if (string.IsNullOrEmpty(value)) { return value; } string upperValue = value.ToUpper(); if (_stageDictionary.TryGetValue(upperValue, out var stage)) { return stage; } if (StageList == null) { return value; } foreach (var item in StageList) { if (upperValue == item.Value.ToUpper() || upperValue == item.Display.ToUpper()) { return item.Value; } } return value; } /// Try to fix: issues#5742. 关卡选择为 null 时的一个补丁,可能是 StageList 改变后,wpf binding 延迟更新的问题。 private string _stage1Fallback = ConfigurationHelper.GetValue(ConfigurationKeys.Stage1, string.Empty) ?? string.Empty; private string _stage1 = ConfigurationHelper.GetValue(ConfigurationKeys.Stage1, string.Empty) ?? string.Empty; /// /// Gets or sets the stage1. /// public string Stage1 { get => _stage1; set { if (_stage1 == value) { SetAndNotify(ref _stage1, value); return; } if (CustomStageCode) { // 从后往前删 if (_stage1?.Length != 3 && value != null) { value = ToUpperAndCheckStage(value); } } SetAndNotify(ref _stage1, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.Stage1, value); UpdateDatePrompt(); } } private string _stage2 = ConfigurationHelper.GetValue(ConfigurationKeys.Stage2, string.Empty) ?? string.Empty; /// /// Gets or sets the stage2. /// public string Stage2 { get => _stage2; set { if (_stage2 == value) { SetAndNotify(ref _stage2, value); return; } if (CustomStageCode) { if (_stage2?.Length != 3 && value != null) { value = ToUpperAndCheckStage(value); } } SetAndNotify(ref _stage2, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.Stage2, value); UpdateDatePrompt(); } } private string _stage3 = ConfigurationHelper.GetValue(ConfigurationKeys.Stage3, string.Empty) ?? string.Empty; /// /// Gets or sets the stage2. /// public string Stage3 { get => _stage3; set { if (_stage3 == value) { SetAndNotify(ref _stage3, value); return; } if (CustomStageCode) { if (_stage3?.Length != 3 && value != null) { value = ToUpperAndCheckStage(value); } } SetAndNotify(ref _stage3, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.Stage3, value); UpdateDatePrompt(); } } private bool _useRemainingSanityStage = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.UseRemainingSanityStage, bool.TrueString)); public bool UseRemainingSanityStage { get => _useRemainingSanityStage; set => SetAndNotify(ref _useRemainingSanityStage, value); } private bool _customStageCode = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.CustomStageCode, bool.FalseString)); /// /// Gets or sets a value indicating whether to use custom stage code. /// public bool CustomStageCode { get => _customStageCode; set => SetAndNotify(ref _customStageCode, value); } private string _remainingSanityStage = ConfigurationHelper.GetValue(ConfigurationKeys.RemainingSanityStage, string.Empty) ?? string.Empty; public string RemainingSanityStage { get => _remainingSanityStage; set { if (_remainingSanityStage == value) { SetAndNotify(ref _remainingSanityStage, value); return; } if (CustomStageCode) { if (_remainingSanityStage?.Length != 3 && value != null) { value = ToUpperAndCheckStage(value); } } SetAndNotify(ref _remainingSanityStage, value); SetFightRemainingSanityParams(); ConfigurationHelper.SetValue(ConfigurationKeys.RemainingSanityStage, value); } } private bool _customInfrastEnabled = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.CustomInfrastEnabled, bool.FalseString)); public bool CustomInfrastEnabled { get => _customInfrastEnabled; set { SetAndNotify(ref _customInfrastEnabled, value); RefreshCustomInfrastPlan(); } } public bool NeedAddCustomInfrastPlanInfo { get; set; } = true; private int _customInfrastPlanIndex = Convert.ToInt32(ConfigurationHelper.GetValue(ConfigurationKeys.CustomInfrastPlanIndex, "0")); public int CustomInfrastPlanIndex { get { if (CustomInfrastPlanInfoList.Count == 0) { return 0; } if (_customInfrastPlanIndex >= CustomInfrastPlanInfoList.Count || _customInfrastPlanIndex < 0) { CustomInfrastPlanIndex = _customInfrastPlanIndex; } return _customInfrastPlanIndex; } set { if (CustomInfrastPlanInfoList.Count == 0) { return; } if (value >= CustomInfrastPlanInfoList.Count || value < 0) { var count = CustomInfrastPlanInfoList.Count; value = ((value % count) + count) % count; _logger.Warning($"CustomInfrastPlanIndex out of range, reset to Index % Count: {value}"); } if (value != _customInfrastPlanIndex && NeedAddCustomInfrastPlanInfo) { var plan = CustomInfrastPlanInfoList[value]; AddLog(plan.Name, UiLogColor.Message); foreach (var period in plan.PeriodList) { AddLog($"[ {period.BeginHour:D2}:{period.BeginMinute:D2} - {period.EndHour:D2}:{period.EndMinute:D2} ]"); } if (plan.Description != string.Empty) { AddLog(plan.Description); } } SetAndNotify(ref _customInfrastPlanIndex, value); SetInfrastParams(); ConfigurationHelper.SetValue(ConfigurationKeys.CustomInfrastPlanIndex, value.ToString()); } } public ObservableCollection> CustomInfrastPlanList { get; } = new(); public struct CustomInfrastPlanInfo { // ReSharper disable InconsistentNaming public int Index; public string Name; public string Description; public string DescriptionPost; // 有效时间段 public struct Period { public int BeginHour; public int BeginMinute; public int EndHour; public int EndMinute; } public List PeriodList; // ReSharper restore InconsistentNaming } private List CustomInfrastPlanInfoList { get; } = new(); private bool _customInfrastPlanHasPeriod; private bool _customInfrastInfoOutput; public void RefreshCustomInfrastPlan() { CustomInfrastPlanInfoList.Clear(); CustomInfrastPlanList.Clear(); _customInfrastPlanHasPeriod = false; if (!CustomInfrastEnabled) { return; } if (!File.Exists(Instances.SettingsViewModel.CustomInfrastFile)) { return; } try { string jsonStr = File.ReadAllText(Instances.SettingsViewModel.CustomInfrastFile); var root = (JObject)JsonConvert.DeserializeObject(jsonStr); if (root != null && _customInfrastInfoOutput && root.TryGetValue("title", out var title)) { AddLog(LocalizationHelper.GetString("CustomInfrastTitle"), UiLogColor.Message); AddLog($"title: {title}", UiLogColor.Info); if (root.TryGetValue("description", out var value)) { AddLog($"description: {value}", UiLogColor.Info); } } var planList = (JArray)root?["plans"]; if (planList != null) { for (int i = 0; i < planList.Count; ++i) { var plan = (JObject)planList[i]; string display = plan.TryGetValue("name", out var name) ? name.ToString() : ("Plan " + ((char)('A' + i))); CustomInfrastPlanList.Add(new GenericCombinedData { Display = display, Value = i }); string desc = plan.TryGetValue("description", out var description) ? description.ToString() : string.Empty; string descPost = plan.TryGetValue("description_post", out var descriptionPost) ? descriptionPost.ToString() : string.Empty; if (_customInfrastInfoOutput) { AddLog(display, UiLogColor.Message); } var periodList = new List(); if (plan.TryGetValue("period", out var token)) { var periodArray = (JArray)token; foreach (var periodJson in periodArray) { var period = default(CustomInfrastPlanInfo.Period); string beginTime = periodJson[0]?.ToString(); if (beginTime != null) { var beginSplit = beginTime.Split(':'); period.BeginHour = int.Parse(beginSplit[0]); period.BeginMinute = int.Parse(beginSplit[1]); } string endTime = periodJson[1]?.ToString(); if (endTime != null) { var endSplit = endTime.Split(':'); period.EndHour = int.Parse(endSplit[0]); period.EndMinute = int.Parse(endSplit[1]); } periodList.Add(period); if (_customInfrastInfoOutput) { AddLog($"[ {period.BeginHour:D2}:{period.BeginMinute:D2} - {period.EndHour:D2}:{period.EndMinute:D2} ]"); } } if (periodList.Count != 0) { _customInfrastPlanHasPeriod = true; } } if (_customInfrastInfoOutput && desc != string.Empty) { AddLog(desc); } if (_customInfrastInfoOutput && descPost != string.Empty) { AddLog(descPost); } CustomInfrastPlanInfoList.Add(new CustomInfrastPlanInfo { Index = i, Name = display, Description = desc, DescriptionPost = descPost, PeriodList = periodList, }); } } _customInfrastInfoOutput = true; } catch (Exception) { _customInfrastInfoOutput = true; AddLog(LocalizationHelper.GetString("CustomInfrastFileParseFailed"), UiLogColor.Error); return; } RefreshCustomInfrastPlanIndexByPeriod(); } public void RefreshCustomInfrastPlanIndexByPeriod() { if (!CustomInfrastEnabled || !_customInfrastPlanHasPeriod || InfrastTaskRunning) { return; } var now = DateTime.Now; foreach (var plan in CustomInfrastPlanInfoList.Where( plan => plan.PeriodList.Any( period => TimeLess(period.BeginHour, period.BeginMinute, now.Hour, now.Minute) && TimeLess(now.Hour, now.Minute, period.EndHour, period.EndMinute)))) { CustomInfrastPlanIndex = plan.Index; return; } if (CustomInfrastPlanIndex >= CustomInfrastPlanList.Count || CustomInfrastPlanList.Count < 0) { CustomInfrastPlanIndex = 0; } return; static bool TimeLess(int lHour, int lMin, int rHour, int rMin) => (lHour != rHour) ? (lHour < rHour) : (lMin <= rMin); } public void IncreaseCustomInfrastPlanIndex() { if (!CustomInfrastEnabled || _customInfrastPlanHasPeriod || CustomInfrastPlanInfoList.Count == 0) { return; } AddLog(LocalizationHelper.GetString("CustomInfrastPlanIndexAutoSwitch"), UiLogColor.Message); var prePlanPostDesc = CustomInfrastPlanInfoList[CustomInfrastPlanIndex].DescriptionPost; if (prePlanPostDesc != string.Empty) { AddLog(prePlanPostDesc); } ++CustomInfrastPlanIndex; } /// /// Reset unsaved battle parameters. /// public void ResetFightVariables() { if (UseMedicineWithNull == null) { UseStone = false; } if (UseMedicineWithNull == null) { UseMedicine = false; } if (HasTimesLimitedWithNull == null) { HasTimesLimited = false; } if (IsSpecifiedDropsWithNull == null) { IsSpecifiedDrops = false; } } private bool? _useMedicineWithNull = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.UseMedicine, bool.FalseString)); /// /// Gets or sets a value indicating whether to use medicine with null. /// public bool? UseMedicineWithNull { get => _useMedicineWithNull; set { SetAndNotify(ref _useMedicineWithNull, value); if (value == false) { UseStone = false; } SetFightParams(); value ??= false; ConfigurationHelper.SetValue(ConfigurationKeys.UseMedicine, value.ToString()); } } /// /// Gets or sets a value indicating whether to use medicine. /// private bool UseMedicine { get => UseMedicineWithNull != false; set => UseMedicineWithNull = value; } private string _medicineNumber = ConfigurationHelper.GetValue(ConfigurationKeys.UseMedicineQuantity, "999"); /// /// Gets or sets the amount of medicine used. /// public string MedicineNumber { get => _medicineNumber; set { if (_medicineNumber == value) { return; } SetAndNotify(ref _medicineNumber, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.UseMedicineQuantity, MedicineNumber); } } public static string UseStoneString => LocalizationHelper.GetString("UseOriginitePrime"); private bool? _useStoneWithNull = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.UseMedicine, bool.FalseString)) && Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.UseStone, bool.FalseString)); /// /// Gets or sets a value indicating whether to use originiums with null. /// public bool? UseStoneWithNull { get => _useStoneWithNull; set { if (!TaskSettingDataContext.AllowUseStoneSave && value == true) { value = null; } SetAndNotify(ref _useStoneWithNull, value); if (value != false) { MedicineNumber = "999"; if (!UseMedicine) { UseMedicineWithNull = value; } } // IsEnabled="{c:Binding UseStone}" NotifyOfPropertyChange(nameof(UseStone)); SetFightParams(); if (TaskSettingDataContext.AllowUseStoneSave) { ConfigurationHelper.SetValue(ConfigurationKeys.UseStone, (value ?? false).ToString()); } } } /// /// Gets or sets a value indicating whether to use originiums. /// // ReSharper disable once MemberCanBePrivate.Global public bool UseStone { get => UseStoneWithNull != false; set => UseStoneWithNull = value; } private string _stoneNumber = ConfigurationHelper.GetValue(ConfigurationKeys.UseStoneQuantity, "0"); /// /// Gets or sets the amount of originiums used. /// public string StoneNumber { get => _stoneNumber; set { if (_stoneNumber == value) { return; } SetAndNotify(ref _stoneNumber, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.UseStoneQuantity, StoneNumber); } } private bool? _hasTimesLimitedWithNull = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.TimesLimited, bool.FalseString)); /// /// Gets or sets a value indicating whether the number of times is limited with null. /// public bool? HasTimesLimitedWithNull { get => _hasTimesLimitedWithNull; set { SetAndNotify(ref _hasTimesLimitedWithNull, value); SetFightParams(); value ??= false; ConfigurationHelper.SetValue(ConfigurationKeys.TimesLimited, value.ToString()); } } /// /// Gets or sets a value indicating whether the number of times is limited. /// private bool HasTimesLimited { get => HasTimesLimitedWithNull != false; set => HasTimesLimitedWithNull = value; } private string _maxTimes = ConfigurationHelper.GetValue(ConfigurationKeys.TimesLimitedQuantity, "5"); /// /// Gets or sets the max number of times. /// public string MaxTimes { get => _maxTimes; set { if (MaxTimes == value) { return; } SetAndNotify(ref _maxTimes, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.TimesLimitedQuantity, MaxTimes); } } private string _series = ConfigurationHelper.GetValue(ConfigurationKeys.SeriesQuantity, "1"); /// /// Gets or sets the max number of times. /// // 所以为啥这玩意是 string 呢?改配置的时候把上面那些也都改成 int 吧 public string Series { get => _series; set { if (_series == value) { return; } SetAndNotify(ref _series, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.SeriesQuantity, value); } } #region Drops private bool? _isSpecifiedDropsWithNull = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.DropsEnable, bool.FalseString)); /// /// Gets or sets a value indicating whether the drops are specified. /// public bool? IsSpecifiedDropsWithNull { get => _isSpecifiedDropsWithNull; set { SetAndNotify(ref _isSpecifiedDropsWithNull, value); SetFightParams(); value ??= false; ConfigurationHelper.SetValue(ConfigurationKeys.DropsEnable, value.ToString()); } } /// /// Gets or sets a value indicating whether the drops are specified. /// private bool IsSpecifiedDrops { get => IsSpecifiedDropsWithNull != false; set => IsSpecifiedDropsWithNull = value; } /// /// Gets the list of all drops. /// private List AllDrops { get; } = new(); /// /// 关卡不可掉落的材料 /// private static readonly HashSet _excludedValues = [ "3213", "3223", "3233", "3243", // 双芯片 "3253", "3263", "3273", "3283", // 双芯片 "7001", "7002", "7003", "7004", // 许可 "4004", "4005", // 凭证 "3105", "3131", "3132", "3233", // 龙骨/加固建材 "6001", // 演习券 "3141", "4002", // 源石 "32001", // 芯片助剂 "30115", // 聚合剂 "30125", // 双极纳米片 "30135", // D32钢 "30145", // 晶体电子单元 "30155", // 烧结核凝晶 ]; private void InitDrops() { foreach (var (val, value) in ItemListHelper.ArkItems) { // 不是数字的东西都是正常关卡不会掉的(大概吧) if (!int.TryParse(val, out _)) { continue; } var dis = value.Name; if (_excludedValues.Contains(val)) { continue; } AllDrops.Add(new CombinedData { Display = dis, Value = val }); } AllDrops.Sort((a, b) => string.Compare(a.Value, b.Value, StringComparison.Ordinal)); DropsList = new ObservableCollection(AllDrops); } /// /// Gets or private sets the list of drops. /// public ObservableCollection DropsList { get; private set; } private string _dropsItemId = ConfigurationHelper.GetValue(ConfigurationKeys.DropsItemId, string.Empty); /// /// Gets or sets the item ID of drops. /// public string DropsItemId { get => _dropsItemId; set { SetAndNotify(ref _dropsItemId, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.DropsItemId, DropsItemId); } } private string _dropsItemName = ConfigurationHelper.GetValue(ConfigurationKeys.DropsItemName, LocalizationHelper.GetString("NotSelected")); /// /// Gets or sets the item Name of drops. /// public string DropsItemName { get => _dropsItemName; set { SetAndNotify(ref _dropsItemName, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.DropsItemName, DropsItemName); } } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void DropsListDropDownClosed() { foreach (var item in DropsList) { if (DropsItemName != item.Display) { continue; } DropsItemId = item.Value; if (DropsItemName != item.Display || DropsItemId != item.Value) { DropsItemName = LocalizationHelper.GetString("NotSelected"); } return; } DropsItemName = LocalizationHelper.GetString("NotSelected"); } private string _dropsQuantity = ConfigurationHelper.GetValue(ConfigurationKeys.DropsQuantity, "5"); /// /// Gets or sets the quantity of drops. /// public string DropsQuantity { get => _dropsQuantity; set { SetAndNotify(ref _dropsQuantity, value); SetFightParams(); ConfigurationHelper.SetValue(ConfigurationKeys.DropsQuantity, DropsQuantity); } } /// /// Make comboBox searchable /// /// Event sender /// Event args // UI 绑定的方法 // EventArgs 不能省略,否则会报错 // ReSharper disable once UnusedMember.Global // ReSharper disable once UnusedParameter.Global public void MakeComboBoxSearchable(object sender, EventArgs e) { (sender as ComboBox)?.MakeComboBoxSearchable(); } #endregion Drops } }