// // 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.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Imaging; using JetBrains.Annotations; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Models.AsstTasks; using MaaWpfGui.Services.Notification; using MaaWpfGui.States; using MaaWpfGui.Utilities; using MaaWpfGui.ViewModels.UserControl.Settings; using MaaWpfGui.ViewModels.UserControl.TaskQueue; using MaaWpfGui.Views.UI; using Newtonsoft.Json.Linq; using Serilog; using Stylet; using Application = System.Windows.Application; using Screen = Stylet.Screen; using Task = System.Threading.Tasks.Task; namespace MaaWpfGui.ViewModels.UI; /// /// The view model of task queue. /// // 通过 container.Get(); 实例化或获取实例 // ReSharper disable once ClassNeverInstantiated.Global public class TaskQueueViewModel : Screen { 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.Instance; public static SettingsViewModel TaskSettingDataContext => Instances.SettingsViewModel; /// /// Gets the after action setting. /// public PostActionSetting PostActionSetting { get; } = PostActionSetting.Instance; #region 长草任务Model /// /// Gets 连接任务Model /// public static StartUpSettingsUserControlModel StartUpTask => StartUpSettingsUserControlModel.Instance; /// /// Gets 战斗任务Model /// public static FightSettingsUserControlModel FightTask => FightSettingsUserControlModel.Instance; /// /// Gets 招募任务Model /// public static RecruitSettingsUserControlModel RecruitTask => RecruitSettingsUserControlModel.Instance; /// /// Gets 信用及购物任务Model /// public static MallSettingsUserControlModel MallTask => MallSettingsUserControlModel.Instance; /// /// Gets 基建任务Model /// public static InfrastSettingsUserControlModel InfrastTask => InfrastSettingsUserControlModel.Instance; /// /// Gets 领取奖励任务 /// public static AwardSettingsUserControlModel AwardTask => AwardSettingsUserControlModel.Instance; /// /// Gets 肉鸽任务Model /// public static RoguelikeSettingsUserControlModel RoguelikeTask => RoguelikeSettingsUserControlModel.Instance; /// /// Gets 生稀盐酸任务Model /// public static ReclamationSettingsUserControlModel ReclamationTask => ReclamationSettingsUserControlModel.Instance; /// /// Gets 生稀盐酸任务Model /// public static CustomSettingsUserControlModel CustomTask => CustomSettingsUserControlModel.Instance; #endregion 长草任务Model private static readonly IEnumerable _taskViewModelTypes = InitTaskViewModelList(); /// /// 实时更新任务顺序 /// /// ignored object /// ignored NotifyCollectionChangedEventArgs public void TaskItemSelectionChanged(object? sender = null, NotifyCollectionChangedEventArgs? e = null) { _ = (sender, e); Execute.OnUIThread(() => { int index = 0; foreach (var item in TaskItemViewModels) { ConfigurationHelper.SetTaskOrder(item.OriginalName, index.ToString()); ++index; } }); } // a:Action 绑定,不能为 static public void ChooseOverlayTarget() { try { var owner = Application.Current.MainWindow; var picker = new ProcessPickerWindow { Owner = owner }; var ok = picker.ShowDialog(); if (ok == true && picker.SelectedHwnd != IntPtr.Zero) { var overlayVm = Instances.OverlayViewModel; if (overlayVm != null) { overlayVm.SetTargetHwnd(picker.SelectedHwnd); if (overlayVm.IsCreated) { overlayVm.Close(); overlayVm.EnsureCreated(); } } } } catch { // ignored } } private bool _isOverlayEnabled; public bool IsOverlayEnabled { get => _isOverlayEnabled; set => SetAndNotify(ref _isOverlayEnabled, value); } public void EnableOverlay() { if (IsOverlayEnabled) { return; } IsOverlayEnabled = true; Instances.OverlayViewModel?.EnsureCreated(); } public void DisableOverlay() { if (!IsOverlayEnabled) { return; } IsOverlayEnabled = false; Instances.OverlayViewModel?.Close(); } public void ToggleOverlay() { if (IsOverlayEnabled) { DisableOverlay(); } else { EnableOverlay(); } } /// /// Gets or private sets the view models of log items. /// public ObservableCollection LogItemViewModels { get; private set; } = []; /// /// Gets the grouped log cards. Each card contains multiple . /// public ObservableCollection LogCardViewModels { get; private set; } = []; private bool TryMergeIntoLastCard(string content, string color, string weight, ToolTip? toolTip) { // Merge into last existing card when it exists and is not sealed. if (LogCardViewModels.Count == 0) { return false; } var lastCard = LogCardViewModels[^1]; var log = new LogItemViewModel(content, color, weight, toolTip: toolTip); LogItemViewModels.Add(log); lastCard.Items.Add(log); return true; } /// /// Gets or private sets the single download-related log item. /// Use a single LogItemViewModel instead of a collection because only one entry is shown. /// private LogItemViewModel _downloadLogItemViewModel = new(string.Empty); public LogItemViewModel DownloadLogItemViewModel { get => _downloadLogItemViewModel; private set => SetAndNotify(ref _downloadLogItemViewModel, value); } #region LogThumbnails private readonly SemaphoreSlim _logThumbnailSemaphore = new(1, 1); private const int LogThumbnailWidth = 640; private const int LogThumbnailHeight = 360; private static int MaxLogItemsWithThumbnails => SettingsViewModel.GuiSettings.MaxNumberOfLogThumbnails; private async Task AttachThumbnailToCardAsync(LogCardViewModel card, bool forceScreencap) { if (card is null) { _logger.Warning("Cannot attach thumbnail to null log card."); return; } try { var thumbnail = await GetOrCaptureLogThumbnailAsync(forceScreencap).ConfigureAwait(false); if (thumbnail is null) { return; } await Execute.OnUIThreadAsync(() => { card.Thumbnail = thumbnail; TrimOldThumbnails(); }).ConfigureAwait(false); } catch { _logger.Warning("Failed to attach thumbnail to log card."); } } private async Task GetOrCaptureLogThumbnailAsync(bool forceScreencap = false) { if (!await _logThumbnailSemaphore.WaitAsync(0).ConfigureAwait(false)) { return null; } try { var frameData = await Instances.AsstProxy.AsstGetImageBgrDataAsync(forceScreencap: forceScreencap).ConfigureAwait(false); if (frameData is null || frameData.Length == 0) { return null; } try { // 只保留小图,避免日志列表长期运行时占用过多内存。 var thumbnail = AsstProxy.CreateBgrBitmapSourceScaled(frameData, LogThumbnailWidth, LogThumbnailHeight); return thumbnail; } finally { ArrayPool.Shared.Return(frameData); } } finally { _logThumbnailSemaphore.Release(); } } private void TrimOldThumbnails() { var thumbnailIndices = LogCardViewModels .Select((vm, index) => new { vm, index }) .Where(x => x.vm.Thumbnail != null) .Select(x => x.index) .ToList(); if (thumbnailIndices.Count > MaxLogItemsWithThumbnails) { for (int i = 0; i < thumbnailIndices.Count - MaxLogItemsWithThumbnails; i++) { LogCardViewModels[thumbnailIndices[i]].Thumbnail = null; } } } #endregion #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.Instance.Set("AfterAction", value); } } /// /// Checks after completion. /// /// Task public async Task CheckAfterCompleted() { await Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript")); var actions = PostActionSetting; _logger.Information("Post actions: " + actions.ActionDescription); if (actions.BackToAndroidHome) { Instances.AsstProxy.AsstBackToHome(); await Task.Delay(1000); } if (actions.ExitArknights) { var clientType = SettingsViewModel.GameSettings.ClientType; if (!Instances.AsstProxy.AsstStartCloseDown(clientType)) { 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}", processesCount); return processesCount > 1; } void DoKillEmulator() { if (!EmulatorHelper.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() { PowerManagement.Shutdown(); await Execute.OnUIThreadAsync(() => Instances.MainWindowManager?.Show()); if (await TimerCanceledAsync( LocalizationHelper.GetString("Shutdown"), LocalizationHelper.GetString("AboutToShutdown"), LocalizationHelper.GetString("Cancel"), 60)) { PowerManagement.AbortShutdown(); return; } _logger.Information("Shutdown not canceled, proceeding to exit application."); 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. /// public TaskQueueViewModel() { _runningState = RunningState.Instance; _runningState.StateChanged += (_, e) => { Idle = e.Idle; Inited = e.Inited; Stopping = e.Stopping; Instances.SettingsViewModel.Idle = e.Idle; if (!e.Idle) { Instances.Data.ClearCache(); } }; _runningState.TimeoutOccurred += RunningState_TimeOut; } private void RunningState_TimeOut(object? sender, string message) { Execute.OnUIThread(() => { AddLog(message, UiLogColor.Warning); ToastNotification.ShowDirect(message); if (!SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenTimeout) { return; } var lastLogs = LogItemViewModels .TakeLast(5) .Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n"); ExternalNotificationService.Send(message, lastLogs); }); } protected override void OnInitialActivate() { base.OnInitialActivate(); DisplayName = LocalizationHelper.GetString("Farming"); LogItemViewModels = []; InitializeItems(); InitTimer(); _ = UpdateDatePromptAndStagesWeb(); } /// protected override void OnClose() { base.OnClose(); } /* 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 System.Timers.Timer _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 (SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates) { var result = MessageBoxHelper.Show( LocalizationHelper.GetString("UpdateConfirmExitText"), LocalizationHelper.GetString("UpdateConfirmExitTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question); Closing = false; return result == MessageBoxResult.Yes; } if (!Running) { // no need to confirm if no running task return true; } var confirmResult = MessageBoxHelper.Show( LocalizationHelper.GetString("ConfirmExitText"), LocalizationHelper.GetString("ConfirmExitTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question); Closing = false; return confirmResult == MessageBoxResult.Yes; } public override Task CanCloseAsync() { return Task.FromResult(this.ConfirmExit()); } private void InitTimer() { _timer.Interval = 30 * 1000; _timer.Elapsed += Timer1_Elapsed; _timer.Start(); } private DateTime _lastTimerElapsed = DateTime.MinValue; private async void Timer1_Elapsed(object? sender, EventArgs e) { try { // 提前记录时间,避免等待超过定时时间 DateTime currentTime = DateTime.Now; currentTime = new(currentTime.Year, currentTime.Month, currentTime.Day, currentTime.Hour, currentTime.Minute, 0); if (currentTime <= _lastTimerElapsed) { return; } _lastTimerElapsed = currentTime; VersionUpdateSettingsUserControlModel.Instance.RefreshMirrorChyanCdkRemaining(); HandleDatePromptUpdate(); HandleCheckForUpdates(); InfrastTask.RefreshCustomInfrastPlanDisplay(); await HandleTimerLogic(currentTime); } catch { // ignored } } 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; UpdateDatePromptAndStagesLocally(); var delayTime = CalculateRandomDelay(); _ = Task.Run(async () => { await Task.Delay(delayTime); await _runningState.UntilIdleAsync(60000); await UpdateDatePromptAndStagesWeb(); _isUpdatingDatePrompt = false; }); } private bool _isCheckingForUpdates; private void HandleCheckForUpdates() { if (!SettingsViewModel.VersionUpdateSettings.UpdateAutoCheck) { return; } if (!NeedToCheckForUpdates() || _isCheckingForUpdates) { return; } _isCheckingForUpdates = true; var delayTime = CalculateRandomDelay(); _ = Task.Run(async () => { _logger.Information("waiting for update check: {DelayTime}", delayTime); await Task.Delay(delayTime); await Instances.VersionUpdateViewModel.VersionUpdateAndAskToRestartAsync(); await ResourceUpdater.ResourceUpdateAndReloadAsync(); _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 (SettingsViewModel.TimerSettings.TimerModels.Timers[i].IsOn == false) { continue; } DateTime startTime = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day, SettingsViewModel.TimerSettings.TimerModels.Timers[i].Hour, SettingsViewModel.TimerSettings.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 != SettingsViewModel.TimerSettings.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() && !SettingsViewModel.TimerSettings.ForceScheduledStart) { return; } var (timeToStart, timeToChangeConfig, configIndex) = CheckTimers(currentTime); if (timeToChangeConfig) { _logger.Information("Scheduled configuration change: Timer Index: {ConfigIndex}", configIndex); HandleConfigChange(configIndex); return; } if (timeToStart) { _logger.Information("Scheduled start: Timer Index: {ConfigIndex}", configIndex); await HandleScheduledStart(configIndex); SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].IsOn ??= false; } } private void HandleConfigChange(int configIndex) { if (SettingsViewModel.TimerSettings.CustomConfig && (_runningState.GetIdle() || SettingsViewModel.TimerSettings.ForceScheduledStart)) { Instances.SettingsViewModel.CurrentConfiguration = SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig; } } private async Task HandleScheduledStart(int configIndex) { if (SettingsViewModel.TimerSettings.ForceScheduledStart) { if (SettingsViewModel.TimerSettings.CustomConfig && Instances.SettingsViewModel.CurrentConfiguration != SettingsViewModel.TimerSettings.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: {CurrentConfiguration}, Scheduled Configuration: {TimerConfig}", configIndex, Instances.SettingsViewModel.CurrentConfiguration, SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig); return; } if (SettingsViewModel.TimerSettings.ShowWindowBeforeForceScheduledStart) { await Execute.OnUIThreadAsync(() => Instances.MainWindowManager?.Show()); } if (await TimerCanceledAsync( LocalizationHelper.GetString("ForceScheduledStart"), LocalizationHelper.GetString("ForceScheduledStartTip"), LocalizationHelper.GetString("Cancel"))) { return; } if (!_runningState.GetIdle()) { _logger.Information("Not idle, Stop and CloseDown"); await Stop(); SetStopped(); } var mode = SettingsViewModel.GameSettings.ClientType; if (!Instances.AsstProxy.AsstAppendCloseDown(mode)) { AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); } ResetAllTemporaryVariable(); } await LinkStart(); AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ScheduleMasterGroup); } private static async Task TimerCanceledAsync(string content = "", string tipContent = "", string buttonContent = "", int seconds = 10) { if (Application.Current.Dispatcher.CheckAccess()) { return await ShowDialogAsync(); } return await await Application.Current.Dispatcher.InvokeAsync(ShowDialogAsync); async Task ShowDialogAsync() { var canceled = false; 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 tcs = new TaskCompletionSource(); dialogUserControl.Click += (_, _) => { canceled = true; dialog.Close(); tcs.TrySetResult(true); }; _logger.Information("Timer wait time: {Seconds}", seconds); await Task.WhenAny(Task.Delay(delay), tcs.Task); dialog.Close(); _logger.Information("Timer canceled: {Canceled}", canceled); return canceled; } } /// /// Initializes items. /// private void InitializeItems() { List taskList = [ "WakeUp", "Recruiting", "Base", "Combat", "Mall", "Mission", "AutoRoguelike", "Reclamation" ]; if (Instances.VersionUpdateViewModel.IsDebugVersion() || File.Exists("DEBUG") || File.Exists("DEBUG.txt")) { taskList.Add("Custom"); } 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" or "Custom")); if (task == TaskSettingVisibilityInfo.DefaultVisibleTaskSetting) { vm.EnableSetting = true; } if (!parsed || order < 0 || order >= tempOrderList.Count || tempOrderList[order] != null) { nonOrderList.Add(vm); } else { tempOrderList[order] = vm; } } foreach (var newVm in nonOrderList) { if (newVm == null) { continue; } int i = 0; while (tempOrderList[i] != null) { ++i; } tempOrderList[i] = newVm; ConfigurationHelper.SetTaskOrder(newVm.OriginalName, i.ToString()); } TaskItemViewModels = [.. tempOrderList.OfType()]; TaskItemViewModels.CollectionChanged += TaskItemSelectionChanged; FightTask.InitDrops(); NeedToUpdateDatePrompt(); UpdateDatePromptAndStagesLocally(); InfrastTask.ParseCustomInfrastPlan(); if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay()) { AddLog(LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), UiLogColor.Info); } } public DayOfWeek CurDayOfWeek { get; private set; } /// /// Determine whether the specified stage is open /// /// stage name /// Whether the specified stage is open public bool IsStageOpen(string name) => Instances.StageManager.IsStageOpen(name, CurDayOfWeek); /// /// Returns the valid stage if it is open, otherwise returns an empty string. /// /// The stage to check. /// The valid stage or an empty string. public string GetValidStage(string stage) => IsStageOpen(stage) ? stage : string.Empty; /// /// 更新日期提示和关卡列表 /// public void UpdateDatePromptAndStagesLocally() { UpdateDatePrompt(); FightTask.UpdateStageList(); ToolboxViewModel.UpdateMiniGameTaskList(); } /// /// 访问 api 获取更新后更新日期提示和关卡列表 /// /// 可等待 public async Task UpdateDatePromptAndStagesWeb() { await Instances.StageManager.UpdateStageWeb(); UpdateDatePromptAndStagesLocally(); } private DateOnly _lastPromptDate; private bool NeedToUpdateDatePrompt() { var now = DateTime.UtcNow.ToYjDateTime(); CurDayOfWeek = now.DayOfWeek; // yj历的 4/16 点 var today = DateOnly.FromDateTime(now); bool isCriticalTime = now is { Minute: 0, Hour: 0 or 12 }; bool isNewDate = today != _lastPromptDate; if (!isCriticalTime && !isNewDate) { return false; } _lastPromptDate = today; return true; } private static bool NeedToCheckForUpdates() { var now = DateTime.UtcNow.ToYjDateTime(); // yj历的 4/22 点 return now is { Minute: 0, Hour: 0 or 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 foreach (var stage in FightTask.Stages) { if (stage == null || Instances.StageManager.GetStageInfo(stage).IsActivityClosed() != true) { continue; } builder.Append(stage).Append(": ").AppendLine(LocalizationHelper.GetString("ClosedStage")); } // Open stages today var openStages = Instances.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); } public enum LogCardSplitMode { /// /// 不拆分日志卡片 /// None = 0, /// /// 插入日志前拆分卡片 /// Before = 1, /// /// 插入日志后拆分卡片 /// After = 2, /// /// 插入日志前后都拆分卡片 /// Both = 3, } /// /// Adds log. /// /// The content. /// The font color. /// The font weight. /// The toolTip /// Whether to update the containing card's image/thumbnail. /// Whether to force fetching a fresh screenshot instead of using cache. /// Whether to split cards before/after this log. public void AddLog(string? content, string color = UiLogColor.Trace, string weight = "Regular", ToolTip? toolTip = null, bool updateCardImage = false, bool fetchLatestImage = false, LogCardSplitMode splitMode = LogCardSplitMode.None) { if (string.IsNullOrEmpty(content)) { if (splitMode != LogCardSplitMode.None) { createNewCard(); } return; } Execute.OnUIThread(() => { if (LogCardViewModels.Count <= 0) { createNewCard(); } switch (splitMode) { case LogCardSplitMode.None: addToCard(); break; case LogCardSplitMode.Before: createNewCard(); addToCard(); break; case LogCardSplitMode.After: addToCard(); createNewCard(); break; case LogCardSplitMode.Both: createNewCard(); addToCard(); createNewCard(); break; } switch (color) { case UiLogColor.Error: _logger.Error("{Content}", content); break; case UiLogColor.Warning: _logger.Warning("{Content}", content); break; default: _logger.Information("{Content}", content); break; } }); return; void createNewCard() { if (LogCardViewModels.Count > 0 && LogCardViewModels[^1].Items.Count <= 0) { return; } var card = new LogCardViewModel(); LogCardViewModels.Add(card); } void addToCard() { if (TryMergeIntoLastCard(content, color, weight, toolTip)) { if (updateCardImage) { _ = AttachThumbnailToCardAsync(LogCardViewModels[^1], fetchLatestImage); } } } } /// /// Clears log. /// private void ClearLog() { Execute.OnUIThread(() => { LogItemViewModels.Clear(); LogCardViewModels.Clear(); DownloadLogItemViewModel = new(string.Empty); _logger.Information("Main windows log clear."); _logger.Information("{Empty}", string.Empty); }); } /// /// Update the dedicated download log area. Thread-safe and will run on UI thread. /// Mirrors previous logic which updated the first download log entry. /// /// The full text to show in download area. /// Optional tooltip. public void UpdateDownloadLog(string fullText, string? toolTip = null) { Execute.OnUIThread(() => { // Keep download area limited to a single entry. if (string.IsNullOrEmpty(fullText)) { DownloadLogItemViewModel = new(string.Empty); return; } var log = new LogItemViewModel(fullText, UiLogColor.Download, toolTip: toolTip?.CreateTooltip()); DownloadLogItemViewModel = log; }); } /// /// Selects all. /// UI 绑定的方法 /// [UsedImplicitly] public void SelectedAll() { foreach (var item in TaskItemViewModels) { switch (item.OriginalName) { case "AutoRoguelike": case "Reclamation": case "Custom": 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.GetGlobalValue(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.GetGlobalValue(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 绑定的方法 /// [UsedImplicitly] public void ChangeInverseMode() { InverseMode = !InverseMode; } /// /// Selects inversely. /// UI 绑定的方法 /// [UsedImplicitly] public void InverseSelected() { if (InverseMode) { foreach (var item in TaskItemViewModels) { switch (item.OriginalName) { case "AutoRoguelike": case "Reclamation": case "Custom": 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 = GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction; } } } /// /// 还原所有临时变量(右键半选) /// public void ResetAllTemporaryVariable() { FightTask.ResetFightVariables(); RecruitTask.ResetRecruitVariables(); ResetTaskSelection(); } private async Task ConnectToEmulator() { string errMsg = string.Empty; bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); // 尝试启动模拟器 if (!connected && SettingsViewModel.ConnectSettings.RetryOnDisconnected) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToStartEmulator")); await Task.Run(() => SettingsViewModel.StartSettings.TryToStartEmulator()); if (_runningState.GetStopping()) { 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(() => SettingsViewModel.StartSettings.ReconnectByAdb()); if (_runningState.GetStopping()) { SetStopped(); return false; } Instances.AsstProxy.Connected = false; connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); } // 尝试重启 ADB if (!connected && SettingsViewModel.ConnectSettings.AllowAdbRestart) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("RestartAdb")); await Task.Run(() => SettingsViewModel.StartSettings.RestartAdb()); if (_runningState.GetStopping()) { SetStopped(); return false; } connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); } // 尝试杀掉 ADB 进程 if (!connected && SettingsViewModel.ConnectSettings.AllowAdbHardRestart) { AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("HardRestartAdb")); await Task.Run(() => SettingsViewModel.StartSettings.HardRestartAdb()); if (_runningState.GetStopping()) { 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; } public int MainTasksCompletedCount { get; set; } public int MainTasksSelectedCount => TaskItemViewModels.Count(x => x.IsChecked); /// /// updates the main tasks progress. /// /// 已完成任务数,留空则代表 +1 public void UpdateMainTasksProgress(int? completedCount = null) { var rvm = (RootViewModel)this.Parent; if (MainTasksSelectedCount == 0) { rvm.TaskProgress = null; return; } MainTasksCompletedCount = completedCount ?? ++MainTasksCompletedCount; if (MainTasksCompletedCount >= MainTasksSelectedCount) { rvm.TaskProgress = null; } else { rvm.TaskProgress = (MainTasksCompletedCount, MainTasksSelectedCount); } } private DateTime? _taskStartTime; /// /// Starts. /// /// Task public async Task LinkStart() { if (!_runningState.GetIdle()) { _logger.Information("Not idle, return."); return; } _taskStartTime = DateTime.Now; ClearLog(); Instances.OverlayViewModel.LogItemsSource = LogItemViewModels; var buildDateTimeLong = VersionUpdateSettingsUserControlModel.BuildDateTimeCurrentCultureString; var resourceDateTimeLong = SettingsViewModel.VersionUpdateSettings.ResourceDateTimeCurrentCultureString; AddLog($"Build Time:\n{buildDateTimeLong}\nResource Time:\n{resourceDateTimeLong}"); var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); if (maxTimeInterval > 90) { AddLog( string.Format( LocalizationHelper.GetString("Achievement.Martian.ConditionsTip"), Math.Round(maxTimeInterval / 30, 1)), UiLogColor.Error); } var uiVersion = VersionUpdateSettingsUserControlModel.UiVersion; var coreVersion = VersionUpdateSettingsUserControlModel.CoreVersion; if (!Instances.VersionUpdateViewModel.IsDebugVersion() && uiVersion != coreVersion) { AddLog(string.Format(LocalizationHelper.GetString("VersionMismatch"), uiVersion, coreVersion), UiLogColor.Error); return; } MainTasksCompletedCount = 0; // 所有提前 return 都要放在 _runningState.SetIdle(false) 之前,否则会导致无法再次点击开始 _runningState.SetIdle(false); // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 TaskItemSelectionChanged(); InfrastTask.InfrastOrderSelectionChanged(); await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript")); AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); /* // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) { AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); } */ // 一般是点了“停止”按钮了 if (_runningState.GetStopping()) { SetStopped(); return; } if (!await ConnectToEmulator()) { return; } // 一般是点了“停止”按钮了 if (_runningState.GetStopping()) { SetStopped(); return; } bool taskRet = true; // 直接遍历TaskItemViewModels里面的内容,是排序后的 int count = 0; foreach (var item in TaskItemViewModels) { if (item.IsChecked == false || (GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction && item.IsCheckedWithNull == null)) { continue; } ++count; switch (item.OriginalName) { case "Base": taskRet &= AppendInfrast(); break; case "WakeUp": taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.StartUp, StartUpTask.Serialize()); break; case "Combat": taskRet &= AppendFight(); break; case "Recruiting": taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Recruit, RecruitTask.Serialize()); break; case "Mall": taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Mall, MallTask.Serialize()); break; case "Mission": taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Award, AwardTask.Serialize()); break; case "AutoRoguelike": taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Roguelike, RoguelikeTask.Serialize()); break; case "Reclamation": taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Reclamation, ReclamationTask.Serialize()); break; case "Custom": { var tasks = CustomTask.SerializeMultiTasks(); foreach (var (type, param) in tasks) { taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Custom, type, param); } break; } default: --count; _logger.Error("Unknown task: " + item.OriginalName); break; } if (taskRet) { continue; } AddLog($"{LocalizationHelper.GetString(item.OriginalName)} task append 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")); Instances.AsstProxy.StartTaskTime = DateTimeOffset.Now; } else { AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); await Stop(); SetStopped(); } AchievementTrackerHelper.Instance.MissionStartCountAdd(); AchievementTrackerHelper.Instance.UseDailyAdd(); } public void ManualStop() { if (Stopping || _runningState.GetIdle()) { _logger.Information("Already stopping or idle, return."); return; } _ = Stop(); AchievementTrackerHelper.Instance.Unlock(AchievementIds.TacticalRetreat); if (_taskStartTime is null) { return; } var duration = DateTime.Now - _taskStartTime.Value; if (duration.TotalSeconds < 5) { AchievementTrackerHelper.Instance.Unlock(AchievementIds.TaskStartCancel); } } /// /// 通常要和 一起使用,除非能保证回调消息能收到 `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) { _runningState.SetStopping(true); AddLog(LocalizationHelper.GetString("Stopping"), splitMode: LogCardSplitMode.Both); 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 绑定的方法 [UsedImplicitly] public async Task WaitAndStop() { Waiting = true; AddLog(LocalizationHelper.GetString("Waiting")); if (RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete) { await WaitUntilRoguelikeCombatComplete(); if (Instances.AsstProxy.AsstRunning() && !_runningState.GetStopping()) { await Stop(); } } } /// /// 等待肉鸽战斗结束,10分钟强制退出 /// private async Task WaitUntilRoguelikeCombatComplete() { int time = 0; while (RoguelikeTask.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 (SettingsViewModel.GameSettings.ManualStopWithScript) { Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript")); } if (!_runningState.GetIdle() || _runningState.GetStopping()) { AddLog(LocalizationHelper.GetString("Stopped"), splitMode: LogCardSplitMode.Both); } Waiting = false; _runningState.SetStopping(false); _runningState.SetIdle(true); // 只抑制“本轮任务期间”的自动开启;任务结束后应允许下一轮自动开启 LiveView。 } // 该函数将于未来被废弃,改用 LinkStart 代替 public async Task QuickSwitchAccount() { if (!_runningState.GetIdle()) { return; } _runningState.SetIdle(false); _taskStartTime = DateTime.Now; // 快速修复 // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 TaskItemSelectionChanged(); InfrastTask.InfrastOrderSelectionChanged(); ClearLog(); await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript")); AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); /* // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) { AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); } */ // 一般是点了“停止”按钮了 if (_runningState.GetStopping()) { SetStopped(); return; } if (!await ConnectToEmulator()) { return; } // 一般是点了“停止”按钮了 if (_runningState.GetStopping()) { SetStopped(); return; } bool taskRet = true; taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.StartUp, StartUpTask.Serialize()); taskRet &= Instances.AsstProxy.AsstStart(); if (taskRet) { AddLog(LocalizationHelper.GetString("Running")); } else { AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); await Stop(); SetStopped(); } } public bool AppendFight() { string curStage = FightTask.Stage; var (type, mainParam) = FightTask.Serialize(); bool mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Fight, type, mainParam); if (!mainFightRet) { AddLog(LocalizationHelper.GetString("UnsupportedStages") + ": " + curStage, UiLogColor.Error); return false; } if ((curStage == "Annihilation") && FightTask.UseAlternateStage) { foreach (var stage in FightTask.Stages) { if (stage is null || !IsStageOpen(stage) || (stage == curStage)) { continue; } AddLog(LocalizationHelper.GetString("AnnihilationTaskTip"), UiLogColor.Info); var task = mainParam.ToObject(); if (task != null) { task.Stage = stage; task.Stone = 0; task.MaxTimes = int.MaxValue; task.Drops = []; mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.FightAnnihilationAlternate, type, task.Serialize().Params); } break; } } if (mainFightRet && FightTask.UseRemainingSanityStage && !string.IsNullOrEmpty(FightTask.RemainingSanityStage)) { var task = new AsstFightTask() { Stage = FightTask.RemainingSanityStage, MaxTimes = int.MaxValue, Series = 0, IsDrGrandet = FightTask.IsDrGrandet, ReportToPenguin = SettingsViewModel.GameSettings.EnablePenguin, ReportToYituliu = SettingsViewModel.GameSettings.EnableYituliu, PenguinId = SettingsViewModel.GameSettings.PenguinId, YituliuId = SettingsViewModel.GameSettings.PenguinId, ServerType = Instances.SettingsViewModel.ServerType, ClientType = SettingsViewModel.GameSettings.ClientType, }; return Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.FightRemainingSanity, type, task.Serialize().Params); } return mainFightRet; } public bool EnableSetFightParams { get; set; } = true; /// /// Sets parameters. /// public void SetFightParams() { var type = AsstProxy.TaskType.Fight; var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; if (!EnableSetFightParams || id == 0) { return; } var taskParams = FightTask.Serialize().Params; Instances.AsstProxy.AsstSetTaskParamsEncoded(id, taskParams); } public static void SetFightRemainingSanityParams() { var type = AsstProxy.TaskType.FightRemainingSanity; var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; if (id == 0) { return; } var task = new AsstFightTask() { Stage = FightTask.RemainingSanityStage ?? string.Empty, MaxTimes = int.MaxValue, Series = 0, IsDrGrandet = FightTask.IsDrGrandet, ReportToPenguin = SettingsViewModel.GameSettings.EnablePenguin, ReportToYituliu = SettingsViewModel.GameSettings.EnableYituliu, PenguinId = SettingsViewModel.GameSettings.PenguinId, YituliuId = SettingsViewModel.GameSettings.PenguinId, ServerType = Instances.SettingsViewModel.ServerType, ClientType = SettingsViewModel.GameSettings.ClientType, }; var taskParams = task.Serialize().Params; Instances.AsstProxy.AsstSetTaskParamsEncoded(id, taskParams); } public bool AppendInfrast() { if (InfrastTask.InfrastMode == InfrastMode.Custom && (!File.Exists(InfrastTask.CustomInfrastFile) || InfrastTask.CustomInfrastPlanList.Count == 0)) { AddLog(LocalizationHelper.GetString("CustomizeInfrastSelectionEmpty"), UiLogColor.Error); return false; } try { return Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Infrast, InfrastTask.Serialize()); } catch (Exception ex) { _logger.Error(ex, "AppendInfrast error"); AddLog("Append Infrast Error: " + ex.Message, UiLogColor.Error); return false; } } private bool _inited = false; public bool Inited { get => _inited; set => SetAndNotify(ref _inited, value); } 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; } UpdateMainTasksProgress(0); } } 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 [UsedImplicitly] get => _waiting; private set => SetAndNotify(ref _waiting, 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; } } } */ private static IEnumerable InitTaskViewModelList() { var types = Assembly.GetExecutingAssembly() .GetTypes() .Where(t => t is { Namespace: "MaaWpfGui.ViewModels.UserControl.TaskQueue", IsClass: true, IsAbstract: false } && t.IsSubclassOf(typeof(TaskViewModel))); foreach (var type in types) { // 获取 Instance 字段 if (type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static) is not { } property) { continue; } // 获取实例 if (property.GetValue(null) is TaskViewModel instance) { yield return instance; } } } public static void InvokeProcSubTaskMsg(AsstMsg msg, JObject details) { foreach (var instance in _taskViewModelTypes) { // 调用 ProcSubTaskMsg 方法 instance.ProcSubTaskMsg(msg, details); } } }