// // 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.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Windows; using JetBrains.Annotations; using MaaWpfGui.Configuration.Factory; using MaaWpfGui.Configuration.Single.MaaTask; using MaaWpfGui.Constants; using MaaWpfGui.Helper; using MaaWpfGui.Models; using MaaWpfGui.Models.AsstTasks; using MaaWpfGui.Services; using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UI; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using Stylet; using static MaaWpfGui.Main.AsstProxy; namespace MaaWpfGui.ViewModels.UserControl.TaskQueue; /// /// 理智作战 /// public class FightSettingsUserControlModel : TaskSettingsViewModel { private static readonly ILogger _logger = Log.ForContext(); private static readonly Lock _lock = new(); public static FightTimes? FightReport { get; set; } public static SanityInfo? SanityReport { get; set; } static FightSettingsUserControlModel() { Instance = new(); } public FightSettingsUserControlModel() { foreach (var i in WeeklyScheduleSource) { i.PropertyChanged += (_, __) => SaveWeeklySchedule(); } var item = new StagePlanItem(); item.PropertyChanged += (_, __) => SaveStagePlan(); StagePlan.Add(item); } public static FightSettingsUserControlModel Instance { get; } /// /// Gets or private sets the list of stages. /// public ObservableCollection StageListSource { get => field; private set => SetAndNotify(ref field, value); } = []; private static readonly Dictionary _stageDictionary = new() { { "AN", "Annihilation" }, { "剿灭", "Annihilation" }, { "CE", "CE-6" }, { "龙门币", "CE-6" }, { "LS", "LS-6" }, { "经验", "LS-6" }, { "狗粮", "LS-6" }, { "CA", "CA-5" }, { "技能", "CA-5" }, { "AP", "AP-5" }, { "红票", "AP-5" }, { "SK", "SK-5" }, { "碳", "SK-5" }, { "炭", "SK-5" }, }; public ObservableCollection StagePlan { get => field; set => SetAndNotify(ref field, value); } = [new()]; // UI 绑定的方法 [UsedImplicitly] public void AddStageToPlan() { var item = new StagePlanItem(); item.PropertyChanged += (_, __) => SaveStagePlan(); StagePlan.Add(item); } // UI 绑定的方法 [UsedImplicitly] public void RemoveStageFromPlan(StagePlanItem plan) { if (StagePlan.Count == 1) { _logger.Warning("Attempted to remove the last stage from the plan. Operation aborted."); return; } StagePlan.Remove(plan); } /// /// Gets or sets the stage1. /// public string? Stage { get => field; set { if (field == value) { return; } if (CustomStageCode) { // 从后往前删 if (field?.Length != 3 && value != null) { value = ToUpperAndCheckStage(value); } } SetAndNotify(ref field, value); SetFightParams(); Instances.TaskQueueViewModel.UpdateDatePrompt(); } } /// /// Gets or sets a value indicating whether to use custom stage code. /// public bool CustomStageCode { get => GetTaskConfig().IsStageManually; set => SetTaskConfig(t => t.IsStageManually == value, t => t.IsStageManually = value); } /// /// Reset unsaved battle parameters. /// /// The fight task. public static void ResetFightVariables(FightTask? fight) { fight?.UseStone ??= false; fight?.UseMedicine ??= false; fight?.EnableTimesLimit ??= false; fight?.EnableTargetDrop ??= false; } /// /// Gets or sets a value indicating whether to use medicine with null. /// public bool? UseMedicine { get => GetTaskConfig().UseMedicine; set { if (!SetTaskConfig(t => t.UseMedicine == value, t => t.UseMedicine = value)) { return; } if (value == false) { UseStoneDisplay = false; } SetFightParams(); } } /// /// Gets or sets the amount of medicine used. /// public int MedicineNumber { get => GetTaskConfig().MedicineCount; set { if (!SetTaskConfig(t => t.MedicineCount == value, t => t.MedicineCount = value)) { return; } SetFightParams(); } } public static string UseStoneString => LocalizationHelper.GetString("UseOriginitePrime"); /// /// Gets or sets a value indicating whether to use originiums with null. /// public bool? UseStone { get => GetTaskConfig().UseStone; set { if (!AllowUseStoneSave && value == true) { value = null; } if (value != false) { MedicineNumber = 999; if (UseMedicine == false) { UseMedicine = value; } } SetFightParams(); SetTaskConfig(t => t.UseStone == value, t => t.UseStone = value); } } /// /// Gets or sets a value indicating whether to use originiums. /// // ReSharper disable once MemberCanBePrivate.Global [PropertyDependsOn(nameof(UseStone))] public bool UseStoneDisplay { get => UseStone != false; set => UseStone = value; } /// /// Gets or sets the amount of originiums used. /// public int StoneNumber { get => GetTaskConfig().StoneCount; set { if (!SetTaskConfig(t => t.StoneCount == value, t => t.StoneCount = value)) { return; } SetFightParams(); } } /// /// Gets or sets a value indicating whether the number of times is limited with null. /// public bool? HasTimesLimited { get => GetTaskConfig().EnableTimesLimit; set { if (!SetTaskConfig(t => t.EnableTimesLimit == value, t => t.EnableTimesLimit = value)) { return; } SetFightParams(); } } /// /// Gets or sets the max number of times. /// public int MaxTimes { get => GetTaskConfig().TimesLimit; set { if (!SetTaskConfig(t => t.TimesLimit == value, t => t.TimesLimit = value)) { return; } SetFightParams(); } } public static Dictionary SeriesList { get; set; } = new() { { "AUTO", 0 }, { "6", 6 }, { "5", 5 }, { "4", 4 }, { "3", 3 }, { "2", 2 }, { "1", 1 }, { LocalizationHelper.GetString("NotSwitch"), -1 }, }; /// /// Gets or sets the max number of times. /// public int Series { get => GetTaskConfig().Series; set { if (!SetTaskConfig(t => t.Series == value, t => t.Series = value)) { return; } SetFightParams(); } } #region Drops /// /// Gets or sets a value indicating whether the drops are specified. /// public bool? IsSpecifiedDrops { get => GetTaskConfig().EnableTargetDrop; set { if (!SetTaskConfig(t => t.EnableTargetDrop == value, t => t.EnableTargetDrop = value)) { return; } SetFightParams(); } } /// /// Gets the list of all drops. /// private List AllDrops { get; } = []; /// /// 关卡不可掉落的材料 /// private static readonly HashSet _excludedValues = [ "3213", "3223", "3233", "3243", // 双芯片 "3253", "3263", "3273", "3283", // 双芯片 "7001", "7002", "7003", "7004", // 许可 "4004", "4005", // 凭证 "3105", "3131", "3132", "3133", // 龙骨/加固建材 "6001", // 演习券 "3141", "4002", // 源石 "32001", // 芯片助剂 "30115", // 聚合剂 "30125", // 双极纳米片 "30135", // D32钢 "30145", // 晶体电子单元 "30155", // 烧结核凝晶 "30165", // 重相位对映体 ]; private void InitDrops() { AllDrops.Add(new() { Display = LocalizationHelper.GetString("NotSelected"), Value = string.Empty }); foreach (var (val, value) in ItemListHelper.ArkItems) { // 不是数字的东西都是正常关卡不会掉的(大概吧) if (!int.TryParse(val, out _)) { continue; } var dis = value.Name; if (_excludedValues.Contains(val)) { continue; } AllDrops.Add(new() { Display = dis, Value = val }); } AllDrops.Sort((a, b) => string.Compare(a.Value, b.Value, StringComparison.Ordinal)); DropsList = [.. AllDrops]; if (AllDrops.FirstOrDefault(i => i.Value == DropsItemId) is { } item) { DropsItemName = item.Display; NotifyOfPropertyChange(nameof(DropsItemName)); } else { DropsItemId = string.Empty; DropsItemName = string.Empty; NotifyOfPropertyChange(nameof(DropsItemName)); } } /// /// Gets or private sets the list of drops. /// public ObservableCollection DropsList { get; private set; } = []; /// /// Gets or sets the item ID of drops. /// public string DropsItemId { get => GetTaskConfig().DropId; set { SetTaskConfig(t => t.DropId == value, t => t.DropId = value); SetFightParams(); } } /// /// Gets or sets the item Name of drops. /// public string DropsItemName { get; set; } = string.Empty; // UI 绑定的方法 [UsedImplicitly] public void DropsListDropDownClosed() { if (DropsList.FirstOrDefault(i => i.Display == DropsItemName) is { } item) { DropsItemId = item.Value; } else { DropsItemId = string.Empty; DropsItemName = LocalizationHelper.GetString("NotSelected"); NotifyOfPropertyChange(nameof(DropsItemName)); } } /// /// Gets or sets the quantity of drops. /// public int DropsQuantity { get => GetTaskConfig().DropCount; set { SetTaskConfig(t => t.DropCount == value, t => t.DropCount = value); SetFightParams(); } } #endregion Drops public static Dictionary AnnihilationModeList { get; } = new() { { LocalizationHelper.GetString("Annihilation"), "Annihilation" }, { LocalizationHelper.GetString("Chernobog"), "Chernobog@Annihilation" }, { LocalizationHelper.GetString("LungmenOutskirts"), "LungmenOutskirts@Annihilation" }, { LocalizationHelper.GetString("LungmenDowntown"), "LungmenDowntown@Annihilation" }, }; public bool UseCustomAnnihilation { get => GetTaskConfig().UseCustomAnnihilation; set => SetTaskConfig(t => t.UseCustomAnnihilation == value, t => t.UseCustomAnnihilation = value); } public string AnnihilationStage { get => GetTaskConfig().AnnihilationStage; set => SetTaskConfig(t => t.AnnihilationStage == value, t => t.AnnihilationStage = value); } /// /// Gets or sets a value indicating whether to use DrGrandet mode. /// public bool IsDrGrandet { get => GetTaskConfig().IsDrGrandet; set => SetTaskConfig(t => t.IsDrGrandet == value, t => t.IsDrGrandet = value); } /// /// Gets or sets a value indicating whether to use alternate stage. /// public bool UseAlternateStage { get => GetTaskConfig().UseOptionalStage; set { SetTaskConfig(t => t.UseOptionalStage == value, t => t.UseOptionalStage = value); if (value) { HideUnavailableStage = false; } else { var list = StagePlan; if (list.Count == 0) { var item = new StagePlanItem(); item.PropertyChanged += (_, __) => SaveStagePlan(); StagePlan.Add(item); } else { var stage = list[0]; StagePlan.Clear(); StagePlan.Add(stage); } } } } public bool AllowUseStoneSave { get => GetTaskConfig().UseStoneAllowSave; set { if (value) { var result = MessageBoxHelper.Show( LocalizationHelper.GetString("AllowUseStoneSaveWarning"), LocalizationHelper.GetString("Warning"), MessageBoxButton.YesNo, MessageBoxImage.Warning, no: LocalizationHelper.GetString("Confirm"), yes: LocalizationHelper.GetString("Cancel"), iconBrushKey: "DangerBrush"); if (result != MessageBoxResult.No) { return; } } SetTaskConfig(t => t.UseStoneAllowSave == value, t => t.UseStoneAllowSave = value); } } public bool UseExpiringMedicine { get => GetTaskConfig().UseExpiringMedicine; set { SetTaskConfig(t => t.UseExpiringMedicine == value, t => t.UseExpiringMedicine = value); SetFightParams(); } } /// /// Gets or sets a value indicating whether to hide unavailable stages. /// public bool HideUnavailableStage { get => GetTaskConfig().HideUnavailableStage; set { var update = SetTaskConfig(t => t.HideUnavailableStage == value, t => t.HideUnavailableStage = value); if (value) { UseAlternateStage = false; } if (update) { UpdateStageList(); } } } /// /// Gets or sets a value indicating whether to hide series. /// public bool HideSeries { get => GetTaskConfig().HideSeries; set => SetTaskConfig(t => t.HideSeries == value, t => t.HideSeries = value); } /// /// Gets or sets a value indicating whether to use weekly schedule. /// public bool UseWeeklySchedule { get => GetTaskConfig().UseWeeklySchedule; set => SetTaskConfig(t => t.UseWeeklySchedule == value, t => t.UseWeeklySchedule = value); } public ObservableCollection WeeklyScheduleSource { get; set; } = new(Enum.GetValues().Select(i => new WeeklyScheduleItem(i))); private bool _autoRestartOnDrop = ConfigurationHelper.GetValue(ConfigurationKeys.AutoRestartOnDrop, true); public bool AutoRestartOnDrop { get => _autoRestartOnDrop; set { SetAndNotify(ref _autoRestartOnDrop, value); ConfigurationHelper.SetValue(ConfigurationKeys.AutoRestartOnDrop, value.ToString()); } } private static string ToUpperAndCheckStage(string value) { if (string.IsNullOrEmpty(value)) { return value; } string upperValue = value.ToUpper(); if (_stageDictionary.TryGetValue(upperValue, out var stage)) { return stage; } if (Instance.StageListSource == null) { return value; } foreach (var item in Instance.StageListSource) { if (upperValue.Equals(item.Value, StringComparison.CurrentCultureIgnoreCase) || upperValue.Equals(item.Display, StringComparison.CurrentCultureIgnoreCase)) { return item.Value; } } return value; } public static string? GetFightStage(IEnumerable stageNames) { var stage = stageNames.FirstOrDefault(Instances.TaskQueueViewModel.IsStageOpen); stage ??= stageNames.FirstOrDefault(); return stage; } public override void RefreshUI(BaseTask baseTask) { if (baseTask is not FightTask fight) { return; } IsRefreshingUI = true; if (!UseAlternateStage && fight.StagePlan.Count == 0) { fight.StagePlan.Add(string.Empty); } InitDrops(); UpdateStageList(); // 临时修复, 应为同步 RefreshCurrentStagePlan(); RefreshWeeklySchedule(); Refresh(); IsRefreshingUI = false; } [Obsolete("使用SerializeTask作为代替")] public override (AsstTaskType Type, JObject Params) Serialize() { var task = new AsstFightTask() { // Stage = Stage, Medicine = UseMedicine != false ? MedicineNumber : 0, Stone = UseStoneDisplay ? StoneNumber : 0, Series = Series, MaxTimes = HasTimesLimited != false ? MaxTimes : int.MaxValue, ExpiringMedicine = UseExpiringMedicine ? 9999 : 0, IsDrGrandet = 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, }; if (task.Stage == "Annihilation" && UseCustomAnnihilation) { task.Stage = AnnihilationStage; } if (IsSpecifiedDrops != false && !string.IsNullOrEmpty(DropsItemId)) { task.Drops.Add(DropsItemId, DropsQuantity); } if (HasTimesLimited is not false && Series > 0 && MaxTimes % Series != 0) { Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetStringFormat("FightTimesMayNotExhausted", MaxTimes, Series), UiLogColor.Warning); } return task.Serialize(); } private bool? SetFightParams() { if (TaskSettingVisibilityInfo.CurrentTask is not FightTask fight) { return null; } if (ConfigFactory.CurrentConfig.TaskQueue.IndexOf(fight) is int index && index > -1) { return SerializeTask(fight, Instances.TaskQueueViewModel.TaskItemViewModels[index].TaskId); } _logger.Error("Failed to set fight params: current task is not in the task queue."); return null; } public override bool? SerializeTask(BaseTask? baseTask, int? taskId = null) { if (baseTask is not FightTask fight) { return null; } if (fight.UseWeeklySchedule && fight.WeeklySchedule.TryGetValue(Instances.TaskQueueViewModel.CurDayOfWeek, out var isEnabled) && !isEnabled) { return null; } using var scope = _lock.EnterScope(); var stage = GetFightStage(fight.StagePlan); if (stage is null) { return null; } var task = new AsstFightTask() { Stage = stage, Medicine = fight.UseMedicine != false ? fight.MedicineCount : 0, Stone = fight.UseStone != false ? fight.StoneCount : 0, Series = fight.Series, MaxTimes = fight.EnableTimesLimit != false ? fight.TimesLimit : int.MaxValue, ExpiringMedicine = fight.UseExpiringMedicine ? 9999 : 0, IsDrGrandet = fight.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, }; if (task.Stage == "Annihilation" && fight.UseCustomAnnihilation) { task.Stage = AnnihilationStage; } if (fight.EnableTargetDrop != false && !string.IsNullOrEmpty(fight.DropId)) { task.Drops.Add(fight.DropId, fight.DropCount); } if (fight.EnableTimesLimit is not false && fight.Series > 0 && fight.TimesLimit % fight.Series != 0) { Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetStringFormat("FightTimesMayNotExhausted", fight.TimesLimit, fight.Series), UiLogColor.Warning); } return taskId switch { int id when id > 0 => Instances.AsstProxy.AsstSetTaskParamsEncoded(id, task), null => Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Fight, task), _ => null, }; } #region 关卡列表更新 /// /// Updates stage list. /// 使用手动输入时,只更新关卡列表,不更新关卡选择 /// 使用隐藏当日不开放时,更新关卡列表,关卡选择为未开放的关卡时清空 /// 使用备选关卡时,更新关卡列表,关卡选择为未开放的关卡时在关卡列表中添加对应未开放关卡,避免清空导致进入上次关卡 /// 啥都不选时,更新关卡列表,关卡选择为未开放的关卡时在关卡列表中添加对应未开放关卡,避免清空导致进入上次关卡 /// 除手动输入外所有情况下,如果剩余理智为未开放的关卡,会被清空 /// // FIXME: 被注入对象只能在private函数内使用,只有Model显示之后才会被注入。如果Model还没有触发OnInitialActivate时调用函数会NullPointerException // 这个函数被列为public可见,意味着他注入对象前被调用 public void UpdateStageList() { Execute.PostToUIThreadAsync(() => { _logger.Information("Updating stage list..."); using var scope = _lock.EnterScope(); bool isFightTaskShow = TaskSettingVisibilityInfo.CurrentTask is FightTask; var hideUnavailble = isFightTaskShow && HideUnavailableStage; var stageList = Instances.StageManager.GetStageList().ToList(); var listCurrent = StagePlan.Select(i => i.Value).ToList(); var listSource = stageList.Select(i => new StageSourceItem() { Display = i.Display, Value = i.Value, IsVisible = !hideUnavailble || i.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek), IsOpen = Instances.StageManager.GetStageList().FirstOrDefault(p => p.Value == i.Value)?.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek) ?? true }).ToList(); if (isFightTaskShow) { foreach (var item in listCurrent.Where(i => !listSource.Any(p => p.Value == i))) // 补未开放进来 { listSource.Add(new StageSourceItem() { Display = item, Value = item, IsOpen = false, IsVisible = false }); } } StageListSource = new ObservableCollection(listSource); if (TaskSettingVisibilityInfo.CurrentTask is FightTask current) { current.StagePlan = listCurrent; } foreach (var task in ConfigFactory.CurrentConfig.TaskQueue.Where(i => i is FightTask).OfType()) { var removeList = task.StagePlan.Where(i => !stageList.Any(p => p.Value == i)).ToList(); if (removeList.Count != 0) { _logger.Information("Removed non-existing stage from plan: {Stage}", removeList); task.StagePlan = [.. task.StagePlan.Where(i => stageList.Any(p => p.Value == i))]; } } RefreshCurrentStagePlan(); }); } private void RefreshCurrentStagePlan() { if (TaskSettingVisibilityInfo.CurrentTask is not FightTask) { return; } var plan = GetTaskConfig().StagePlan.ToList(); var list = plan.Select((i, index) => new StagePlanItem() { Value = i }).ToList(); foreach (var item in list) { item.PropertyChanged += (_, __) => SaveStagePlan(); } StagePlan = new ObservableCollection(list); StagePlan.CollectionChanged += (_, __) => SaveStagePlan(); } private void SaveStagePlan() { var list = StagePlan.Select(i => i.Value).ToList(); SetTaskConfig(t => t.StagePlan.SequenceEqual(list), t => t.StagePlan = list); } private void RefreshWeeklySchedule() { var plan = GetTaskConfig().WeeklySchedule; foreach (var item in WeeklyScheduleSource) { item.Value = !plan.TryGetValue(item.DayOfWeek, out var value) || value; } } private void SaveWeeklySchedule() { if (IsRefreshingUI) { return; } var dict = WeeklyScheduleSource.ToDictionary(i => i.DayOfWeek, i => i.Value); SetTaskConfig(t => t.WeeklySchedule.SequenceEqual(dict), t => t.WeeklySchedule = dict); } #endregion 关卡列表更新 public class SanityInfo { [JsonProperty("current_sanity")] public int SanityCurrent { get; set; } [JsonProperty("max_sanity")] public int SanityMax { get; set; } [JsonProperty("report_time")] public DateTimeOffset ReportTime { get; set; } } public class FightTimes { [JsonProperty("sanity_cost")] public int SanityCost { get; set; } [JsonProperty("series")] public int Series { get; set; } [JsonProperty("times_finished")] public int TimesFinished { get; set; } [JsonProperty("finished")] public bool IsFinished { get; set; } } public class WeeklyScheduleItem(DayOfWeek dayOfWeek) : PropertyChangedBase { public string Display => LocalizationHelper.CustomCultureInfo.DateTimeFormat.GetDayName(DayOfWeek); public DayOfWeek DayOfWeek { get; } = dayOfWeek; public bool Value { get => field; set => SetAndNotify(ref field, value); } = true; } public class StageSourceItem : PropertyChangedBase { public string Display { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; public bool IsOpen { get => field; set => SetAndNotify(ref field, value); } = true; public bool IsVisible { get => field; set => SetAndNotify(ref field, value); } = true; } public class StagePlanItem : PropertyChangedBase { public string Value { get => field; set { value ??= string.Empty; if (TaskSettingVisibilityInfo.CurrentTask is FightTask task && task.UseOptionalStage) { // 从后往前删 if (value.Length != 3) { value = ToUpperAndCheckStage(value); } } if (!SetAndNotify(ref field, value)) { return; } IsOpen = Instances.StageManager.GetStageList().FirstOrDefault(p => p.Value == value)?.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek) ?? true; } } = string.Empty; public bool IsOpen { get => field; set => SetAndNotify(ref field, value); } = true; } }