Files
MaaAssistantArknights/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs
Status102 db1b3b0a8d rft: Wpf一键长草任务配置重构 (#15385)
* rft: 已重构部分

* rft: 差stageList

* rft: 补上界园常乐

* rft: 备选关卡

* i18n: 添加关卡 & 备选

* fix: 任务添加出错时中断

* fix: 当前上次关卡转换错误

* fix: 当前上次关卡转换错误

* rft: 刷理智任务序列化

* perf: 糊点

* fix: 漏了一个

* rft: 迁移标记

* fix: 加上剿灭

* fix: index漏了

* fix: 重构后的基建计划

* chore: customTask弃用多任务

* perf: 周几选择框动画

* fix: 移除备选CheckBox

* perf: 刷理智-剿灭不上传企鹅

* rft: rename

* feat: 刷理智 周几 设置

* chore: sync

* revert: debug

* revert: 使用备选关卡

* feat: 下拉框

# Conflicts:
#	src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml

* rft: rename

* fix: config

* feat: stageList刷新, 差移除过期关卡

* perf: StageListSource 当日未开放

* fix: bingding

* perf: UI 上下布局

* Revert "perf: UI 上下布局"

This reverts commit df39cb2f09.

* feat: 增加默认隐藏的删除和拖动区域

* chore: 优化触发事件绑定

* perf: 优化全反选按钮布局

* feat: 添加任务列表的删除和重命名

* perf: idle 或 inited 为 false 时禁用删除和重命名

* chore: 确保 StagePlan 有一个元素

* fix: ComboBox无法滚动

* Revert "perf: idle 或 inited 为 false 时禁用删除和重命名"

This reverts commit e0b71b53b6.

* perf: 非 idle 期间禁止重命名或删除任务

* feat: 添加启用周计划勾选框

* perf: 不启用周计划时不重置

* perf: 优化滚动

* fix: 一键长草任务添加或跳过时提示修复

* i18n: TaskQueue 添加任务失败 / 跳过

* feat: config 保存

* perf: 删除任务Dialog增加任务类型文本

* rft: 任务可见rename

* rft: 删除任务 & 任务切换

* perf: 合并备选 & 非备选的关卡选择

* fix: index

* rft: 任务设置界面刷新

* fix: 任务设置界面

* fix: 移除最后一个任务后不会自动选中上一个

* fix: stageList刷新

* perf: 当StageList仅存在一个关卡时隐藏删除按钮

* fix: 刷理智-手动输入关卡代码自动补全

* i18n: 添加关卡

* fix: CreditFightOnceADay set

* fix: ExpectedCollapsalParadigms set

* perf: 移除无用注释 & 变量

* feat: 迁移前加个备份

* fix: 刷理智迁移时重复添加关卡

* fix: 移除信用任务中设置基建任务的UI

* fix: TaskId

* fix: 任务后状态重置

* fix: 基建任务检查

* Revert "fix: 移除信用任务中设置基建任务的UI"

This reverts commit 1f51b5fe00.

* fix: 收取信用任务的基建设置说明

* fix: TaskQueue & 基建房间 拖动容器限制

* fix: CanShowAutoReload

* fix: CustomTask适配

* fix: 任务设置改名删除Context绑定

* perf: 移除 StagePlanItem 的 Index

* rft: TaskId 迁移

* feat: TaskStatus 显示任务颜色

* perf: UI状态颜色 for Light

* perf: config迁移

* fix: base

* perf: 调整界面布局

* perf: 调整运行时任务的颜色,调整圆角

* fix: 拖动标签后无法修改配置

* fix: 部分元素在运行中时未被正确禁用

* fix: 是 move 不是 swap

* docs: 移除文档中关于 `剩余理智` & `备选关卡`中剿灭任务相关的特殊逻辑

* i18n: update introduction\combat for zh-tw

* i18n: KR

* i18n: update zh-tw.xaml

---------

Co-authored-by: uye <99072975+ABA2396@users.noreply.github.com>
Co-authored-by: momomochi987 <as99us301@gmail.com>
Co-authored-by: HX3N <scarlet7518@gmail.com>
2026-01-18 21:47:11 +08:00

2063 lines
65 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// <copyright file="TaskQueueViewModel.cs" company="MaaAssistantArknights">
// 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
// </copyright>
#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.Configuration.Factory;
using MaaWpfGui.Configuration.Single.MaaTask;
using MaaWpfGui.Constants;
using MaaWpfGui.Extensions;
using MaaWpfGui.Helper;
using MaaWpfGui.Main;
using MaaWpfGui.Models;
using MaaWpfGui.Services.Notification;
using MaaWpfGui.States;
using MaaWpfGui.Utilities;
using MaaWpfGui.Utilities.ValueType;
using MaaWpfGui.ViewModels.Items;
using MaaWpfGui.ViewModels.UserControl.Settings;
using MaaWpfGui.ViewModels.UserControl.TaskQueue;
using MaaWpfGui.Views.Dialogs;
using Newtonsoft.Json.Linq;
using Serilog;
using Stylet;
using static MaaWpfGui.Main.AsstProxy;
using Application = System.Windows.Application;
using Screen = Stylet.Screen;
using Task = System.Threading.Tasks.Task;
namespace MaaWpfGui.ViewModels.UI;
/// <summary>
/// The view model of task queue.
/// </summary>
// 通过 container.Get<TaskQueueViewModel>(); 实例化或获取实例
// ReSharper disable once ClassNeverInstantiated.Global
public class TaskQueueViewModel : Screen
{
private readonly RunningState _runningState;
private static readonly ILogger _logger = Log.ForContext<TaskQueueViewModel>();
/// <summary>
/// Gets or private sets the view models of task items.
/// </summary>
public ObservableCollection<TaskItemViewModel> TaskItemViewModels { get; private set; } = [];
/// <summary>
/// Gets the visibility of task setting views.
/// </summary>
public static TaskSettingVisibilityInfo TaskSettingVisibilities => TaskSettingVisibilityInfo.Instance;
public static SettingsViewModel TaskSettingDataContext => Instances.SettingsViewModel;
/// <summary>
/// Gets the after action setting.
/// </summary>
public PostActionSetting PostActionSetting { get; } = PostActionSetting.Instance;
#region Model
/// <summary>
/// Gets 连接任务Model
/// </summary>
public static StartUpSettingsUserControlModel StartUpTask => StartUpSettingsUserControlModel.Instance;
/// <summary>
/// Gets 战斗任务Model
/// </summary>
public static FightSettingsUserControlModel FightTask => FightSettingsUserControlModel.Instance;
/// <summary>
/// Gets 招募任务Model
/// </summary>
public static RecruitSettingsUserControlModel RecruitTask => RecruitSettingsUserControlModel.Instance;
/// <summary>
/// Gets 信用及购物任务Model
/// </summary>
public static MallSettingsUserControlModel MallTask => MallSettingsUserControlModel.Instance;
/// <summary>
/// Gets 基建任务Model
/// </summary>
public static InfrastSettingsUserControlModel InfrastTask => InfrastSettingsUserControlModel.Instance;
/// <summary>
/// Gets 领取奖励任务
/// </summary>
public static AwardSettingsUserControlModel AwardTask => AwardSettingsUserControlModel.Instance;
/// <summary>
/// Gets 肉鸽任务Model
/// </summary>
public static RoguelikeSettingsUserControlModel RoguelikeTask => RoguelikeSettingsUserControlModel.Instance;
/// <summary>
/// Gets 生稀盐酸任务Model
/// </summary>
public static ReclamationSettingsUserControlModel ReclamationTask => ReclamationSettingsUserControlModel.Instance;
/// <summary>
/// Gets 生稀盐酸任务Model
/// </summary>
public static CustomSettingsUserControlModel CustomTask => CustomSettingsUserControlModel.Instance;
#endregion Model
private static readonly IEnumerable<TaskSettingsViewModel> _taskViewModelTypes = InitTaskViewModelList();
/// <summary>
/// 实时更新任务顺序
/// </summary>
/// <param name="sender">ignored object</param>
/// <param name="e">ignored NotifyCollectionChangedEventArgs</param>
public void TaskItemSelectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
Application.Current.Dispatcher.InvokeAsync(() => {
if (e.Action == NotifyCollectionChangedAction.Move)
{
int oldIndex = e.OldStartingIndex;
int newIndex = e.NewStartingIndex;
ConfigFactory.CurrentConfig.TaskQueue.Move(oldIndex, newIndex);
int start = Math.Min(oldIndex, newIndex);
int end = Math.Max(oldIndex, newIndex);
// Move 会导致区间内所有任务顺序变化,必须全部更新
for (int i = start; i <= end; i++)
{
TaskItemViewModels[i].Index = i;
}
}
else if (e.Action == NotifyCollectionChangedAction.Add)
{
TaskItemViewModels[e.NewStartingIndex].Index = e.NewStartingIndex;
TaskItemViewModels.FirstOrDefault(i => i.EnableSetting)?.EnableSetting = false;
TaskItemViewModels[e.NewStartingIndex].EnableSetting = true;
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldStartingIndex >= 0 && e.OldStartingIndex < ConfigFactory.CurrentConfig.TaskQueue.Count)
{
TaskSettingVisibilities.SetTaskSettingVisible(ConfigFactory.CurrentConfig.TaskQueue[e.OldStartingIndex], false);
ConfigFactory.CurrentConfig.TaskQueue.RemoveAt(e.OldStartingIndex);
}
for (int i = e.OldStartingIndex; i < TaskItemViewModels.Count; i++) // 更新后续任务的索引
{
TaskItemViewModels[i].Index = i;
}
if (e.OldStartingIndex == TaskSettingVisibilities.CurrentIndex)
{
var task = TaskItemViewModels.FirstOrDefault(i => i.Index == e.OldStartingIndex);
task ??= TaskItemViewModels.FirstOrDefault(i => i.Index == e.OldStartingIndex - 1);
task?.EnableSetting = true;
}
}
});
}
public static void ChooseOverlayTarget()
{
try
{
var owner = Application.Current.MainWindow;
var picker = new ProcessPickerDialogView { 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
}
}
public bool IsOverlayEnabled { get => field; set => SetAndNotify(ref field, value); }
public static void EnableOverlay()
{
if (Instances.TaskQueueViewModel.IsOverlayEnabled)
{
return;
}
Instances.TaskQueueViewModel.IsOverlayEnabled = true;
Instances.OverlayViewModel?.EnsureCreated();
}
public static void DisableOverlay()
{
if (!Instances.TaskQueueViewModel.IsOverlayEnabled)
{
return;
}
Instances.TaskQueueViewModel.IsOverlayEnabled = false;
Instances.OverlayViewModel?.Close();
}
public static void ToggleOverlay()
{
if (Instances.TaskQueueViewModel.IsOverlayEnabled)
{
DisableOverlay();
}
else
{
EnableOverlay();
}
}
/// <summary>
/// Gets or private sets the view models of log items.
/// </summary>
public ObservableCollection<LogItemViewModel> LogItemViewModels { get; private set; } = [];
/// <summary>
/// Gets the grouped log cards. Each card contains multiple <see cref="LogItemViewModel"/>.
/// </summary>
public ObservableCollection<LogCardItemViewModel> 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;
}
/// <summary>
/// Gets or private sets the single download-related log item.
/// Use a single LogItemViewModel instead of a collection because only one entry is shown.
/// </summary>
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(LogCardItemViewModel 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(() => {
// 检查卡片是否还在集合中,避免给已清空的卡片赋值
if (!LogCardViewModels.Contains(card))
{
return;
}
card.Thumbnail = thumbnail;
TrimOldThumbnails();
});
}
catch
{
_logger.Warning("Failed to attach thumbnail to log card.");
}
}
private async Task<BitmapSource?> GetOrCaptureLogThumbnailAsync(bool forceScreencap = false)
{
if (!await _logThumbnailSemaphore.WaitAsync(100))
{
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<byte>.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;
/// <summary>
/// Gets or sets a value indicating whether to show after task queue actions
/// </summary>
public bool EnableAfterActionSetting
{
get => _enableAfterActionSetting;
set {
SetAndNotify(ref _enableAfterActionSetting, value);
TaskSettingVisibilityInfo.Instance.SetPostAction(value);
}
}
/// <summary>
/// Checks after completion.
/// </summary>
/// <returns>Task</returns>
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
/// <summary>
/// Initializes a new instance of the <see cref="TaskQueueViewModel"/> class.
/// </summary>
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;
if (Instances.VersionUpdateDialogViewModel.IsDebugVersion() || File.Exists("DEBUG") || File.Exists("DEBUG.txt"))
{
CanShowAutoReload = true;
ShowDebugTask = true;
}
}
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();
}
/// <inheritdoc/>
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<bool> 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;
if ((currentTime.Hour == 3 || currentTime.Hour == 13 || currentTime.Hour == 23) && currentTime.Minute == 25 && !Idle)
{
AchievementTrackerHelper.Instance.Unlock(AchievementIds.Time325);
}
VersionUpdateSettingsUserControlModel.Instance.RefreshMirrorChyanCdkRemaining();
HandleDatePromptUpdate();
HandleCheckForUpdates();
InfrastTask.RefreshInfrastTimeRotationDisplay();
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.VersionUpdateDialogViewModel.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<bool> 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<bool> 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<bool>();
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;
}
}
/// <summary>
/// Initializes items.
/// </summary>
private void InitializeItems()
{
List<TaskItemViewModel> taskqueue = [];
for (int i = 0; i < ConfigFactory.CurrentConfig.TaskQueue.Count; i++)
{
var task = ConfigFactory.CurrentConfig.TaskQueue.ElementAt(i);
if (task is not null)
{
taskqueue.Add(new TaskItemViewModel(task.Name, task.IsEnable) { Index = i });
}
}
TaskItemViewModels = [.. taskqueue];
TaskItemViewModels.CollectionChanged += TaskItemSelectionChanged;
var taskItem = TaskItemViewModels.ElementAtOrDefault(ConfigFactory.CurrentConfig.TaskSelectedIndex);
taskItem ??= TaskItemViewModels.FirstOrDefault(i => ConfigFactory.CurrentConfig.TaskQueue[i.Index] is FightTask);
taskItem ??= TaskItemViewModels.FirstOrDefault();
if (taskItem is { })
{
taskItem.EnableSetting = true;
}
NeedToUpdateDatePrompt();
UpdateDatePromptAndStagesLocally();
if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay())
{
AddLog(LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), UiLogColor.Info);
}
}
public DayOfWeek CurDayOfWeek { get; private set; }
/// <summary>
/// Determine whether the specified stage is open
/// </summary>
/// <param name="name">stage name</param>
/// <returns>Whether the specified stage is open</returns>
public bool IsStageOpen(string name) => Instances.StageManager.IsStageOpen(name, CurDayOfWeek);
/// <summary>
/// Returns the valid stage if it is open, otherwise returns an empty string.
/// </summary>
/// <param name="stage">The stage to check.</param>
/// <returns>The valid stage or an empty string.</returns>
public string GetValidStage(string stage) => IsStageOpen(stage) ? stage : string.Empty;
/// <summary>
/// 更新日期提示和关卡列表
/// </summary>
public void UpdateDatePromptAndStagesLocally()
{
UpdateDatePrompt();
FightTask.UpdateStageList();
ToolboxViewModel.UpdateMiniGameTaskList();
}
/// <summary>
/// 访问 api 获取更新后更新日期提示和关卡列表
/// </summary>
/// <returns>可等待</returns>
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 };
}
/// <summary>
/// Updates date prompt.
/// </summary>
// 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;
/// <summary>
/// Gets or private sets the stages of today.
/// </summary>
public string StagesOfToday
{
get => _stagesOfToday;
private set => SetAndNotify(ref _stagesOfToday, value);
}
public enum LogCardSplitMode
{
/// <summary>
/// 不拆分日志卡片
/// </summary>
None = 0,
/// <summary>
/// 插入日志前拆分卡片
/// </summary>
Before = 1,
/// <summary>
/// 插入日志后拆分卡片
/// </summary>
After = 2,
/// <summary>
/// 插入日志前后都拆分卡片
/// </summary>
Both = 3,
}
/// <summary>
/// Adds log.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="color">The font color.</param>
/// <param name="weight">The font weight.</param>
/// <param name="toolTip">The toolTip</param>
/// <param name="updateCardImage">Whether to update the containing card's image/thumbnail.</param>
/// <param name="fetchLatestImage">Whether to force fetching a fresh screenshot instead of using cache.</param>
/// <param name="splitMode">Whether to split cards before/after this log.</param>
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)
{
bool isEmpty = string.IsNullOrEmpty(content);
bool needsBeforeSplit = splitMode == LogCardSplitMode.Before || splitMode == LogCardSplitMode.Both;
bool needsAfterSplit = splitMode == LogCardSplitMode.After || splitMode == LogCardSplitMode.Both;
Execute.OnUIThread(() => {
if (needsBeforeSplit)
{
createNewCard();
}
// 确保至少有一个卡片(如果没有内容且不需要切割,也需要确保有卡片才能更新图片)
if (LogCardViewModels.Count <= 0 && (!isEmpty || updateCardImage))
{
createNewCard();
}
if (LogCardViewModels.Count > 0)
{
// 如果有内容,添加到卡片
if (!isEmpty)
{
TryMergeIntoLastCard(content!, color, weight, toolTip);
}
if (updateCardImage)
{
_ = AttachThumbnailToCardAsync(LogCardViewModels[^1], fetchLatestImage);
}
}
if (needsAfterSplit)
{
createNewCard();
}
});
// 记录日志
if (!isEmpty)
{
switch (color)
{
case UiLogColor.Error:
_logger.Error("{Content}", content);
break;
case UiLogColor.Warning:
_logger.Warning("{Content}", content);
break;
default:
_logger.Information("{Content}", content);
break;
}
}
}
private void createNewCard()
{
if (LogCardViewModels.Count > 0 && LogCardViewModels[^1].Items.Count <= 0)
{
return;
}
var card = new LogCardItemViewModel();
LogCardViewModels.Add(card);
}
/// <summary>
/// Clears log.
/// </summary>
private void ClearLog()
{
Execute.OnUIThread(() => {
LogItemViewModels.Clear();
LogCardViewModels.Clear();
DownloadLogItemViewModel = new(string.Empty);
_logger.Information("Main windows log clear.");
_logger.Information("{Empty}", string.Empty);
});
}
/// <summary>
/// Update the dedicated download log area. Thread-safe and will run on UI thread.
/// Mirrors previous logic which updated the first download log entry.
/// </summary>
/// <param name="fullText">The full text to show in download area.</param>
/// <param name="toolTip">Optional tooltip.</param>
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;
});
}
/// <summary>
/// Selects all.
/// UI 绑定的方法
/// </summary>
[UsedImplicitly]
public void SelectedAll()
{
foreach (var item in TaskItemViewModels)
{
switch (ConfigFactory.CurrentConfig.TaskQueue[item.Index].TaskType)
{
case TaskType.Roguelike:
case TaskType.Reclamation:
case TaskType.Custom:
continue;
}
item.IsEnable = true;
}
}
public static ReadOnlyCollection<GenericCombinedData<Type>> TaskTypeList { get; } = Array.AsReadOnly(
[
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("StartUp"), Value = typeof(StartUpTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Fight"), Value = typeof(FightTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Infrast"), Value = typeof(InfrastTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Recruit"), Value = typeof(RecruitTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Mall"), Value = typeof(MallTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Award"), Value = typeof(AwardTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Roguelike"), Value = typeof(RoguelikeTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Reclamation"), Value = typeof(ReclamationTask) },
new GenericCombinedData<Type> { Display = LocalizationHelper.GetString("Custom"), Value = typeof(CustomTask) },
]);
public void AddTaskQueueTask(Type taskName)
{
if (Activator.CreateInstance(taskName) is BaseTask task)
{
task.Name = TaskTypeList.FirstOrDefault(t => t.Value == taskName)?.Display ?? taskName.Name;
ConfigFactory.CurrentConfig.TaskQueue.Add(task);
TaskItemViewModels.Add(new TaskItemViewModel(task.Name, task.IsEnable));
}
else
{
AddLog("could NOT create instance of " + taskName, UiLogColor.Error);
}
}
/// <summary>
/// 重命名任务
/// </summary>
/// <param name="taskItem">任务项</param>
[UsedImplicitly]
public void RenameTask(TaskItemViewModel taskItem)
{
if (taskItem == null || !Idle)
{
return;
}
var currentName = taskItem.Name;
var dialog = new Views.Dialogs.TextDialogUserControl(
LocalizationHelper.GetString("RenameTask"),
LocalizationHelper.GetString("RenameTaskPrompt"),
currentName) {
Owner = Application.Current.MainWindow,
};
var result = dialog.ShowDialog();
if (result == true && !string.IsNullOrWhiteSpace(dialog.InputText))
{
var newName = dialog.InputText.Trim();
taskItem.Name = newName;
if (taskItem.Index < ConfigFactory.CurrentConfig.TaskQueue.Count)
{
ConfigFactory.CurrentConfig.TaskQueue[taskItem.Index].Name = newName;
}
AddLog(string.Format(LocalizationHelper.GetString("TaskRenamed"), newName), UiLogColor.Info);
}
}
/// <summary>
/// 删除任务
/// </summary>
/// <param name="taskItem">任务项</param>
[UsedImplicitly]
public void RemoveTask(TaskItemViewModel taskItem)
{
if (taskItem == null || !Idle)
{
return;
}
var taskType = ConfigFactory.CurrentConfig.TaskQueue[taskItem.Index].TaskType;
var result = MessageBoxHelper.Show(
string.Format(LocalizationHelper.GetString("ConfirmDeleteTaskMessage"), LocalizationHelper.GetString(taskType.ToString()), taskItem.Name),
LocalizationHelper.GetString("ConfirmDeleteTask"),
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
var index = taskItem.Index;
if (index < ConfigFactory.CurrentConfig.TaskQueue.Count)
{
TaskItemViewModels.RemoveAt(index);
AddLog(string.Format(LocalizationHelper.GetString("TaskDeleted"), taskItem.Name), UiLogColor.Info);
}
}
}
private bool _inverseMode = ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, false);
/// <summary>
/// Gets or sets a value indicating whether to use inverse mode.
/// </summary>
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());
}
}
/// <summary>
/// The width of "Select All" when both.
/// </summary>
public const int SelectedAllWidthWhenBoth = 80;
private int _selectedAllWidth =
ConfigurationHelper.GetGlobalValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse" ? SelectedAllWidthWhenBoth : 85;
/// <summary>
/// Gets or sets the width of "Select All".
/// </summary>
public int SelectedAllWidth
{
get => _selectedAllWidth;
set => SetAndNotify(ref _selectedAllWidth, value);
}
private bool _showInverse = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse";
/// <summary>
/// Gets or sets a value indicating whether "Select inversely" is visible.
/// </summary>
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");
/// <summary>
/// Gets or private Sets the text to be displayed for "Select inversely".
/// </summary>
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");
/// <summary>
/// Gets or private sets the text of inversion menu.
/// </summary>
public string InverseMenuText
{
get => _inverseMenuText;
private set => SetAndNotify(ref _inverseMenuText, value);
}
/// <summary>
/// Changes inversion mode.
/// UI 绑定的方法
/// </summary>
[UsedImplicitly]
public void ChangeInverseMode()
{
InverseMode = !InverseMode;
}
/// <summary>
/// Selects inversely.
/// UI 绑定的方法
/// </summary>
[UsedImplicitly]
public void InverseSelected()
{
if (InverseMode)
{
foreach (var item in TaskItemViewModels)
{
switch (ConfigFactory.CurrentConfig.TaskQueue[item.Index].TaskType)
{
case TaskType.Roguelike:
case TaskType.Reclamation:
case TaskType.Custom:
continue;
}
item.IsEnable = !(item.IsEnable ?? true);
}
}
else
{
foreach (var item in TaskItemViewModels)
{
item.IsEnable = false;
}
}
}
/// <summary>
/// Reset unsaved task selection.
/// </summary>
public void ResetTaskSelection()
{
foreach (var item in TaskItemViewModels)
{
if (item.IsEnable == null)
{
item.IsEnable = GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction;
}
}
}
/// <summary>
/// 还原所有临时变量(右键半选)
/// </summary>
public void ResetAllTemporaryVariable()
{
foreach (var item in ConfigFactory.CurrentConfig.TaskQueue)
{
switch (item)
{
case FightTask fight:
FightSettingsUserControlModel.ResetFightVariables(fight);
if (TaskSettingVisibilityInfo.CurrentTask == fight)
{
RefreshTaskModel(fight);
}
break;
}
}
RecruitSettingsUserControlModel.ResetRecruitVariables();
ResetTaskSelection();
}
private async Task<bool> ConnectToEmulator()
{
string errMsg = string.Empty;
bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg));
if (!connected && SettingsViewModel.ConnectSettings.UseAttachWindow)
{
AddLog(errMsg, UiLogColor.Error);
_runningState.SetIdle(true);
SetStopped();
return false;
}
// 尝试启动模拟器
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.IsEnable ?? true));
/// <summary>
/// updates the main tasks progress.
/// </summary>
/// <param name="completedCount">已完成任务数,留空则代表 +1</param>
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);
}
}
public bool ShowDebugTask { get => field; set => SetAndNotify(ref field, value); }
public bool CanShowAutoReload { get => field; set => SetAndNotify(ref field, value); }
public bool EnableAutoReload { get => field; set => SetAndNotify(ref field, value); }
private DateTime? _taskStartTime;
/// <summary>
/// Starts.
/// </summary>
/// <returns>Task</returns>
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.VersionUpdateDialogViewModel.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 (index, item) in ConfigFactory.CurrentConfig.TaskQueue.Select((task, i) => (i, task)))
{
if (item.IsEnable == false || (GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction && item.IsEnable == null))
{
Instances.TaskQueueViewModel.TaskItemViewModels[index].Status = 4;
continue;
}
switch (SerializeTask(item))
{
case true:
++count;
Instances.TaskQueueViewModel.TaskItemViewModels[index].TaskId = Instances.AsstProxy.TasksStatus.Last().Key;
break;
case false:
taskRet = false;
AddLog(LocalizationHelper.GetStringFormat("TaskAppend.Error", LocalizationHelper.GetString(item.TaskType.ToString()), item.Name), UiLogColor.Error);
Instances.TaskQueueViewModel.TaskItemViewModels[index].Status = (int)Main.TaskStatus.Error;
break;
case null:
AddLog(LocalizationHelper.GetStringFormat("TaskAppend.Skip", LocalizationHelper.GetString(item.TaskType.ToString()), item.Name), UiLogColor.Info);
Instances.TaskQueueViewModel.TaskItemViewModels[index].Status = 4;
break;
}
}
if (count == 0)
{
AddLog(LocalizationHelper.GetString("UnselectedTask"));
_runningState.SetIdle(true);
Instances.AsstProxy.AsstStop();
SetStopped();
return;
}
AchievementTrackerHelper.Instance.SetProgress(AchievementIds.TaskChainKing, count);
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);
}
}
/// <summary>
/// <para>通常要和 <see cref="SetStopped()"/> 一起使用,除非能保证回调消息能收到 `AsstMsg.TaskChainStopped`</para>
/// <para>This is usually done with <see cref="SetStopped()"/> Unless you are guaranteed to receive the callback message `AsstMsg.TaskChainStopped`</para>
/// </summary>
/// <param name="timeout">Timeout millisecond</param>
/// <returns>A <see cref="Task"/>
/// <para>尝试等待 core 成功停止运行,默认超时时间一分钟</para>
/// <para>Try to wait for the core to stop running, the default timeout is one minute</para>
/// </returns>
public async Task<bool> 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();
}
}
}
/// <summary>
/// 等待肉鸽战斗结束10分钟强制退出
/// </summary>
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 &= StartUpTask.SerializeTask(TaskSettingVisibilityInfo.CurrentTask) is true;
taskRet &= Instances.AsstProxy.AsstStart();
if (taskRet)
{
AddLog(LocalizationHelper.GetString("Running"));
}
else
{
AddLog(LocalizationHelper.GetString("UnknownErrorOccurs"));
await Stop();
SetStopped();
}
}
public bool EnableSetFightParams { get; set; } = true;
private bool _inited = false;
public bool Inited
{
get => _inited;
set => SetAndNotify(ref _inited, value);
}
private bool _idle;
/// <summary>
/// Gets or sets a value indicating whether it is idle.
/// </summary>
public bool Idle
{
get => _idle;
set {
SetAndNotify(ref _idle, value);
if (!value)
{
return;
}
UpdateMainTasksProgress(0);
}
}
private bool _stopping;
/// <summary>
/// Gets a value indicating whether `stop` is awaiting.
/// </summary>
public bool Stopping
{
get => _stopping;
private set => SetAndNotify(ref _stopping, value);
}
private bool _waiting;
/// <summary>
/// Gets a value indicating whether waiting for roguelike combat complete.
/// </summary>
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<TaskSettingsViewModel> InitTaskViewModelList()
{
var types = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t is { Namespace: "MaaWpfGui.ViewModels.UserControl.TaskQueue", IsClass: true, IsAbstract: false } && t.IsSubclassOf(typeof(TaskSettingsViewModel)));
foreach (var type in types)
{
// 获取 Instance 字段
if (type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static) is not { } property)
{
continue;
}
// 获取实例
if (property.GetValue(null) is TaskSettingsViewModel instance)
{
yield return instance;
}
}
}
public static void InvokeProcSubTaskMsg(AsstMsg msg, JObject details)
{
foreach (var instance in _taskViewModelTypes)
{
// 调用 ProcSubTaskMsg 方法
instance.ProcSubTaskMsg(msg, details);
}
}
public void RefreshTaskModel(BaseTask task)
{
foreach (var instance in _taskViewModelTypes)
{
instance.RefreshUI(task);
}
}
/// <summary>序列化任务</summary>
/// <param name="task">存储的任务</param>
/// <param name="taskId">任务id, null时追加任务, 非null为设置任务参数</param>
/// <returns>null为未序列化, false失败, true成功</returns>
private static bool? SerializeTask(BaseTask task, int? taskId = null)
{
bool? ret = null;
foreach (var instance in _taskViewModelTypes)
{
ret ??= instance.SerializeTask(task, taskId);
}
return ret;
}
}