diff --git a/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs b/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs index d5aa9384b0..b0440e1805 100644 --- a/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs +++ b/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs @@ -64,6 +64,11 @@ public class FightTask : BaseTask, IJsonOnDeserialized /// public int DropCount { get; set; } + /// + /// Gets or sets a value indicating whether 指定材料按库存目标计算 + /// + public bool IsInventoryTarget { get; set; } + /// /// Gets or sets 是否启用次数限制 /// diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index e01ba182b4..1af8719d01 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -2873,9 +2873,9 @@ public class AsstProxy public IReadOnlyDictionary TasksStatus => new Dictionary(_tasksStatus); - public delegate void TaskItemStatusDelegate(int taskId, TaskItemStatus status); + public delegate void TaskStatusDelegate(int taskId, TaskItemStatus status); - public event TaskItemStatusDelegate? OnTaskItemStatusChanged; + public event TaskStatusDelegate? OnTaskStatusChanged; private bool UpdateTaskStatus(AsstTaskId id, TaskStatus status) { @@ -2896,7 +2896,7 @@ public class AsstProxy } _tasksStatus[id] = (value.Type, status); - OnTaskItemStatusChanged?.Invoke(id, (TaskItemStatus)status); + OnTaskStatusChanged?.Invoke(id, (TaskItemStatus)status); if (status == TaskStatus.InProgress) { TaskSettingVisibilityInfo.Instance.NotifyOfTaskStatus(); diff --git a/src/MaaWpfGui/Res/Localizations/en-us.xaml b/src/MaaWpfGui/Res/Localizations/en-us.xaml index b4c63cf0ec..6b8ee59db3 100644 --- a/src/MaaWpfGui/Res/Localizations/en-us.xaml +++ b/src/MaaWpfGui/Res/Localizations/en-us.xaml @@ -322,7 +322,11 @@ To customize rotation schedules, please use 「{key=InfrastModeCustom}」.This option is effective once This option only takes effect once when right-clicked This option only skips once when right-clicked - This feature would not automatically calculate the optimal stage + The stage entered is still determined by the options below: 「{key=StageSelect}」 / 「{key=StageSelect2}」. +• {key=Quantity}: uses the selected material's drop count in this combat task as the target, and stops when the set amount is reached. +• {key=TargetInventory}: uses the saved inventory data from 「{key=Toolbox}-{key=DepotRecognition}」 and only farms until the target stock is reached. + • Inventory data is cached and may differ from your real stock after manual farming, crafting, or material use. Sync it with 「{key=UserDataUpdate}」 or 「{key=Toolbox}-{key=DepotRecognition}」. + • When {key=TargetInventory} is enabled, the material and amount cannot be changed during a run. Startup Update Check Scheduled Update Check In-game 00:00 / 18:00 (Server local time 04:00 / 22:00) @@ -775,7 +779,9 @@ Right-clicking other options will check/uncheck this option simultaneouslyUse Originium Perform Battles Material - Quantity + Farm + Keep Inventory + Goal Series • AUTO: Automatically identify the maximum proxy multiplier of the level, maintain the maximum proxy multiplier and do not overflow sanity after using sanity potion @@ -805,6 +811,7 @@ Do not adjust the proxy multiplier setting in the game Days left open:  Inventory Inventory data can be updated via 「{key=Toolbox}-{key=DepotRecognition}」 + Task "{0}" uses {key=TargetInventory}, but no inventory data is currently available, so it was skipped. Update it via 「{key=Toolbox}-{key=DepotRecognition}」. Less than 1 day 1-7 CE-6 diff --git a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml index 9f405139b5..67445e3573 100644 --- a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml +++ b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml @@ -322,7 +322,11 @@ このオプションは一度有効です このオプションは、右クリックで選択した時のみ一度だけ有効です このオプションは、右クリックで選択した時のみ一度だけスキップします - この機能では最適なステージを自動的に計算しません + 実際に入るステージは、引き続き下の 「{key=StageSelect}」 / 「{key=StageSelect2}」 で決まります。 +• {key=Quantity}: この理性作戦タスク中で、その素材のドロップ数が設定値に達するまで周回します。 +• {key=TargetInventory}: 「{key=Toolbox}-{key=DepotRecognition}」 に保存された在庫データを参照し、設定した在庫数になるまで補充します。 + • 在庫データはキャッシュのため、手動周回や素材の使用後は実際とずれる場合があります。「{key=UserDataUpdate}」 または 「{key=Toolbox}-{key=DepotRecognition}」 で同期してください。 + • {key=TargetInventory} を有効にすると、実行中は素材や数量を変更できません。 起動時の更新チェック 定期的な更新チェック ゲーム内 00:00 / 18:00(サーバー現地時間 04:00 / 22:00) @@ -777,6 +781,8 @@ C:\\leidian\\LDPlayer9 周回数指定 素材の指定 ドロップ数 + 在庫を維持 + 目標在庫 連戦回数 • 自動: レベルの最大代理乗数を自動で判別し、最大代理乗数を維持し、正気ポーション使用後に正気度オーバーフローが発生しないようにします。 @@ -806,6 +812,7 @@ C:\\leidian\\LDPlayer9 公開日まで: 在庫 在庫データは 「{key=Toolbox}-{key=DepotRecognition}」 で更新できます + タスク「{0}」では {key=TargetInventory} が有効ですが、利用可能な在庫データがないためスキップされました。「{key=Toolbox}-{key=DepotRecognition}」 で在庫を更新してください。 1日以内 1-7 龍門幣-6(CE-6) diff --git a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml index 8aa48a074e..22a4515fc2 100644 --- a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml +++ b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml @@ -322,7 +322,11 @@ 이 옵션은 한 번만 유효합니다 이 옵션은 우클릭 시 한 번만 적용됩니다 이 옵션은 우클릭 시 한 번만 건너뜁니다 - 이 기능은 최적의 스테이지를 자동으로 계산하지 않습니다 + 실제로 진입하는 스테이지는 아래의 「{key=StageSelect}」 / 「{key=StageSelect2}」 설정으로 계속 결정됩니다. +• {key=Quantity}: 현재 전투 작업에서 해당 재료의 드롭 수를 목표로 삼아, 설정한 수량에 도달하면 멈춥니다. +• {key=TargetInventory}: 「{key=Toolbox}-{key=DepotRecognition}」 에 저장된 재고 데이터를 참고하여 설정한 재고 수량까지만 보충합니다. + • 재고 데이터는 캐시이므로 수동 파밍, 제작, 재료 사용 후 실제 수량과 달라질 수 있습니다. 「{key=UserDataUpdate}」 또는 「{key=Toolbox}-{key=DepotRecognition}」 에서 동기화해 주세요. + • {key=TargetInventory} 사용 중에는 실행 도중 재료와 수량을 바꿀 수 없습니다. 시작 시 업데이트 확인 일정 시간마다 업데이트 확인 게임 내 00:00 / 18:00 (서버 현지 시간 04:00 / 22:00) @@ -778,6 +782,8 @@ C:\\leidian\\LDPlayer9 횟수 제한 드롭 제한 드롭 수 + 재고 유지 + 목표 재고 연속 전투 • AUTO: 스테이지의 가능한 최대 대리 지휘 횟수의 배수를 감지하고, 자동으로 최대 대리 지휘 횟수를 유지하며, 이성 회복제 사용 후 이성이 남지 않도록 합니다. @@ -807,6 +813,7 @@ C:\\leidian\\LDPlayer9 남은 오픈일 수: 재고 재고 데이터는 「{key=Toolbox}-{key=DepotRecognition}」 을 통해 업데이트 가능 + 작업 "{0}" 에서 {key=TargetInventory} 가 켜져 있지만 현재 사용할 수 있는 재고 데이터가 없어 건너뜁니다. 「{key=Toolbox}-{key=DepotRecognition}」 에서 재고를 업데이트해 주세요. 1일 미만 1-7 CE-6 (용문폐) diff --git a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml index 66c5fb3402..c164468fa8 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml @@ -322,7 +322,11 @@ 该选项仅生效一次 该选项右键选中时仅生效一次 该选项右键选中时仅跳过一次 - 该选项不会自动计算最优关卡 + 进入的关卡仍由下方 「{key=StageSelect}」 / 「{key=StageSelect2}」 决定。 +• {key=Quantity}:以当前理智作战任务中该材料的掉落数量为目标,达到设定数量后停止。 +• {key=TargetInventory}:会参考 「{key=Toolbox}-{key=DepotRecognition}」 中保存的库存数据,只补到设定库存为止。 + • 库存数据是缓存值,手动刷关、合成或消耗材料后可能与实际不一致;可通过 「{key=UserDataUpdate}」 或 「{key=Toolbox}-{key=DepotRecognition}」 同步。 + • 使用 {key=TargetInventory} 时,开始后不能临时修改材料和数量。 启动时检查更新 定时检查更新 游戏内 00:00 / 18:00(对应服务器当地时间 04:00 / 22:00) @@ -777,6 +781,8 @@ C:\\leidian\\LDPlayer9。\n 指定次数 指定材料 刷取数量 + 保持库存 + 目标库存 代理倍率 • AUTO: 自动识别关卡最大代理倍率, 保持最大代理倍率且使用理智药后理智不溢出 @@ -806,6 +812,7 @@ C:\\leidian\\LDPlayer9。\n 剩余天数: 库存 可通过 「{key=Toolbox}-{key=DepotRecognition}」 更新库存数据 + 任务「{0}」启用了 {key=TargetInventory},但当前没有可用的库存数据,已跳过。可通过 「{key=Toolbox}-{key=DepotRecognition}」 更新库存。 不到 1 天 1-7 龙门币-6/5 diff --git a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml index abaf0e2e67..4575ea4f43 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml @@ -322,7 +322,11 @@ 該選項僅生效一次 該選項點擊右鍵選取時僅生效一次 該選項點擊右鍵選取時僅跳過一次 - 此選項不會自動計算最優關卡 + 實際進入的關卡仍由下方 「{key=StageSelect}」 / 「{key=StageSelect2}」 決定。 +• {key=Quantity}:以目前理智作戰任務中該材料的掉落數量為目標,達到設定數量後停止。 +• {key=TargetInventory}:會參考 「{key=Toolbox}-{key=DepotRecognition}」 中保存的庫存資料,只補到設定庫存為止。 + • 庫存資料是快取值,手動刷關、合成或消耗材料後可能與實際不一致;可透過 「{key=UserDataUpdate}」 或 「{key=Toolbox}-{key=DepotRecognition}」 同步。 + • 使用 {key=TargetInventory} 時,開始後不能臨時修改材料與數量。 啟動時檢查更新 定時檢查更新 遊戲內 00:00 / 18:00(對應伺服器當地時間 04:00 / 22:00) @@ -777,6 +781,8 @@ C:\\leidian\\LDPlayer9\n 指定次數 指定材料 刷取數量 + 保持庫存 + 目標庫存 代理倍率 • AUTO: 自動辨識關卡最大代理倍率,保持最大代理倍率且使用理智藥後理智不溢出 @@ -806,6 +812,7 @@ C:\\leidian\\LDPlayer9\n 剩餘天數: 庫存 可透過 「{key=Toolbox}-{key=DepotRecognition}」 更新庫存數據 + 任務「{0}」啟用了 {key=TargetInventory},但目前沒有可用的庫存資料,已跳過。可透過 「{key=Toolbox}-{key=DepotRecognition}」 更新庫存。 不到 1 天 1-7 龍門幣-6/5 diff --git a/src/MaaWpfGui/ViewModels/TaskItemViewModel.cs b/src/MaaWpfGui/ViewModels/TaskItemViewModel.cs index 57a421f4f2..fc324a154c 100644 --- a/src/MaaWpfGui/ViewModels/TaskItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/TaskItemViewModel.cs @@ -28,7 +28,7 @@ public class TaskItemViewModel : PropertyChangedBase, IDisposable { _name = name; _isEnable = isCheckedWithNull; - Instances.AsstProxy.OnTaskItemStatusChanged += OnTaskStatusChanged; + Instances.AsstProxy.OnTaskStatusChanged += OnTaskStatusChanged; } private string _name; @@ -130,5 +130,5 @@ public class TaskItemViewModel : PropertyChangedBase, IDisposable } } - void IDisposable.Dispose() => Instances.AsstProxy.OnTaskItemStatusChanged -= OnTaskStatusChanged; + void IDisposable.Dispose() => Instances.AsstProxy.OnTaskStatusChanged -= OnTaskStatusChanged; } diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs index 0f6c43b724..f10ba42fc7 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs @@ -15,7 +15,9 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows; using JetBrains.Annotations; @@ -27,6 +29,7 @@ using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Models; using MaaWpfGui.Models.AsstTasks; +using MaaWpfGui.States; using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UI; @@ -44,6 +47,9 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting { public const string AnnihilationName = "Annihilation"; private static readonly ILogger _logger = Log.ForContext(); + private readonly RunningState _runningState; + private readonly Lock _inventoryTargetRuntimeStateLock = new(); + private Dictionary _inventoryTargetRuntimeStateByTaskId = []; public static FightTimes? FightReport { get; set; } @@ -56,6 +62,15 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting public FightSettingsUserControlModel() { + _runningState = RunningState.Instance; + _runningState.StateChanged += OnRunningStateChanged; + Instances.AsstProxy.OnTaskStatusChanged += OnTaskStatusChanged; + + if (Instances.ToolboxViewModel is { } toolboxViewModel) + { + toolboxViewModel.DepotResult.CollectionChanged += OnDepotResultCollectionChanged; + } + foreach (var i in WeeklyScheduleSource) { i.PropertyChanged += (_, __) => SaveWeeklySchedule(); @@ -66,15 +81,92 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting InitDrops(); } + /// + /// 当队列进入或离开空闲状态时,清理按任务保存的目标库存运行时状态。 + /// + private void OnRunningStateChanged(object? sender, RunningState.RunningStateChangedEventArgs e) + { + if (e.NewState.Idle == e.OldState.Idle) + { + return; + } + + ResetInventoryTargetRuntimeState(); + } + + /// + /// 重置各作战任务启动时捕获的目标库存运行时缓存。 + /// + private void ResetInventoryTargetRuntimeState() + { + lock (_inventoryTargetRuntimeStateLock) + { + _inventoryTargetRuntimeStateByTaskId = []; + } + + Execute.OnUIThread(NotifySpecifiedDropsStateChanged); + } + + /// + /// 当作战任务进入进行中状态时,仅捕获一次目标库存值,并在需要时刷新当前选中的面板。 + /// + private void OnTaskStatusChanged(int taskId, TaskItemStatus status) + { + if (status != TaskItemStatus.InProgress || taskId <= 0) + { + return; + } + + if (GetFightTaskByTaskId(taskId) is { } startedFight && + IsInventoryTargetDropEnabled(startedFight) && + GetInventoryTargetRuntimeState(taskId) == null) + { + SerializeTask(startedFight, taskId); + } + + Execute.OnUIThread(() => { + if (TaskSettingVisibilityInfo.CurrentTask is not FightTask currentFight || !IsInventoryTargetDropEnabled(currentFight)) + { + return; + } + + int currentIndex = TaskSettingVisibilityInfo.Instance.CurrentIndex; + if (currentIndex < 0 || currentIndex >= Instances.TaskQueueViewModel.TaskItemViewModels.Count) + { + return; + } + + if (!Instances.TaskQueueViewModel.TaskItemViewModels[currentIndex].TaskIds.Contains(taskId)) + { + return; + } + + NotifySpecifiedDropsStateChanged(); + }); + } + + /// + /// 当仓库识别数据变化时,刷新界面显示的目标库存信息。 + /// + private void OnDepotResultCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (!_runningState.Idle) + { + return; + } + + Execute.OnUIThread(NotifySpecifiedDropsStateChanged); + } + public static FightSettingsUserControlModel Instance { get; } /// - /// Gets or sets a value indicating whether a stage plan item is being dragged. + /// Gets or sets a value indicating whether 关卡规划项正在被拖拽。 /// - public bool IsDragging { get => field; set => SetAndNotify(ref field, value); } + public bool IsStageItemDragging { get => field; set => SetAndNotify(ref field, value); } /// - /// Gets or private sets the list of stages. + /// Gets or private sets a value indicating whether 关卡列表。 /// public ObservableCollection StageListSource { get => field; private set => SetAndNotify(ref field, value); } = []; @@ -123,7 +215,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to use custom stage code. + /// Gets or sets a value indicating whether 使用自定义关卡代码。 /// public bool CustomStageCode { @@ -148,9 +240,9 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Reset unsaved battle parameters. + /// 重置未保存的作战参数。 /// - /// The fight task. + /// 作战任务配置。 public static void ResetFightVariables(FightTask? fight) { fight?.UseStone ??= false; @@ -160,7 +252,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to use medicine with null. + /// Gets or sets a value indicating whether 使用理智药。 /// public bool? UseMedicine { @@ -181,7 +273,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets the amount of medicine used. + /// Gets or sets 使用理智药数量。 /// public int MedicineNumber { @@ -199,7 +291,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting public static string UseStoneString => LocalizationHelper.GetString("UseOriginitePrime"); /// - /// Gets or sets a value indicating whether to use originiums with null. + /// Gets or sets a value indicating whether 使用源石。 /// public bool? UseStone { @@ -225,7 +317,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to use originiums. + /// Gets or sets a value indicating whether 使用源石 with null /// // ReSharper disable once MemberCanBePrivate.Global [PropertyDependsOn(nameof(UseStone))] @@ -236,7 +328,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets the amount of originiums used. + /// Gets or sets 使用源石数量。 /// public int StoneNumber { @@ -252,7 +344,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether the number of times is limited with null. + /// Gets or sets a value indicating whether 限制次数 with null /// public bool? HasTimesLimited { @@ -268,7 +360,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets the max number of times. + /// Gets or sets 最大次数。 /// public int MaxTimes { @@ -296,7 +388,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting }; /// - /// Gets or sets the max number of times. + /// Gets or sets 连战次数。 /// public int Series { @@ -314,12 +406,17 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting #region Drops /// - /// Gets or sets a value indicating whether the drops are specified. + /// Gets or sets a value indicating whether 启用指定材料。 /// public bool? IsSpecifiedDrops { get => GetTaskConfig().EnableTargetDrop; set { + if (IsSpecifiedInventoryLocked) + { + return; + } + if (!SetTaskConfig(t => t.EnableTargetDrop == value, t => t.EnableTargetDrop = value)) { return; @@ -330,7 +427,245 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets the list of all drops. + /// Gets or sets a value indicating whether 指定材料按目标库存模式计算。 + /// + public bool UseInventoryTarget + { + get => GetTaskConfig().IsInventoryTarget; + set { + if (!_runningState.Idle) + { + return; + } + + if (!SetTaskConfig(t => t.IsInventoryTarget == value, t => t.IsInventoryTarget = value)) + { + return; + } + + NotifySpecifiedDropsStateChanged(); + SetFightParams(); + } + } + + public bool IsSpecifiedInventoryLocked => UseInventoryTarget && !_runningState.Idle; + + public bool UseDropQuantityMode + { + get => !UseInventoryTarget; + set { + if (value) + { + UseInventoryTarget = false; + } + } + } + + public bool UseTargetInventoryMode + { + get => UseInventoryTarget; + set { + if (value) + { + UseInventoryTarget = true; + } + } + } + + public string CurrentDropsInventoryText => FormatSpecifiedDropsCount(GetSpecifiedDropsInventoryCount(GetTaskConfig(), GetCurrentFightTaskId())); + + public string EffectiveDropsQuantityText => FormatSpecifiedDropsCount(GetSpecifiedDropsCoreQuantity(GetTaskConfig(), GetCurrentFightTaskId())); + + private static string FormatSpecifiedDropsCount(int? count) => count is int value ? value.FormatNumber(false) : "--"; + + private static bool IsInventoryTargetDropEnabled(FightTask fight) + { + return fight.EnableTargetDrop != false && !string.IsNullOrEmpty(fight.DropId) && fight.IsInventoryTarget; + } + + /// + /// 读取当前识别到的仓库数量,并转换为查询表。 + /// + private static Dictionary? GetCurrentInventoryCounts() + { + var depotResult = Instances.ToolboxViewModel?.DepotResult; + if (depotResult == null || depotResult.Count == 0) + { + return null; + } + + var snapshot = depotResult + .Where(item => item.Count >= 0) + .ToDictionary(item => item.Id, item => item.Count); + + return snapshot.Count > 0 ? snapshot : null; + } + + /// + /// 获取当前选中作战任务绑定的第一个 core task id。 + /// + private static int? GetCurrentFightTaskId() + { + if (TaskSettingVisibilityInfo.CurrentTask is not FightTask fight) + { + return null; + } + + int index = ConfigFactory.CurrentConfig.TaskQueue.IndexOf(fight); + if (index < 0 || index >= Instances.TaskQueueViewModel.TaskItemViewModels.Count) + { + return null; + } + + var taskIds = Instances.TaskQueueViewModel.TaskItemViewModels[index].TaskIds; + return taskIds.Count > 0 ? taskIds[0] : null; + } + + /// + /// 根据 core task id 反查对应的作战任务配置。 + /// + private static FightTask? GetFightTaskByTaskId(int taskId) + { + if (taskId <= 0) + { + return null; + } + + for (int index = 0; index < Instances.TaskQueueViewModel.TaskItemViewModels.Count; ++index) + { + if (!Instances.TaskQueueViewModel.TaskItemViewModels[index].TaskIds.Contains(taskId)) + { + continue; + } + + return index < ConfigFactory.CurrentConfig.TaskQueue.Count + ? ConfigFactory.CurrentConfig.TaskQueue[index] as FightTask + : null; + } + + return null; + } + + /// + /// 检查指定 core 作战任务是否已在运行。 + /// + private static bool IsTaskInProgress(int taskId) + { + return taskId > 0 && + Instances.AsstProxy.TasksStatus.TryGetValue(taskId, out var taskState) && + taskState.Status == MaaWpfGui.Main.TaskStatus.InProgress; + } + + /// + /// 获取指定 core 任务缓存的目标库存运行时状态。 + /// + private InventoryTargetRuntimeState? GetInventoryTargetRuntimeState(int? taskId) + { + if (taskId is not int id || id <= 0) + { + return null; + } + + lock (_inventoryTargetRuntimeStateLock) + { + return _inventoryTargetRuntimeStateByTaskId.GetValueOrDefault(id); + } + } + + /// + /// 获取指定掉落目标显示的库存数量。任务开始后,该显示值会被冻结。 + /// + private int? GetSpecifiedDropsInventoryCount(FightTask fight, int? taskId = null) + { + if (string.IsNullOrEmpty(fight.DropId)) + { + return null; + } + + if (GetInventoryTargetRuntimeState(taskId) is { } runtimeState && runtimeState.DropId == fight.DropId) + { + return runtimeState.StartInventory; + } + + var currentInventoryCounts = GetCurrentInventoryCounts(); + if (currentInventoryCounts == null) + { + return null; + } + + return currentInventoryCounts.TryGetValue(fight.DropId, out var count) ? count : 0; + } + + /// + /// 获取指定目标库存任务显示并下发给 core 的实际刷取数量。 + /// + private int? GetSpecifiedDropsCoreQuantity(FightTask fight, int? taskId = null) + { + if (!IsInventoryTargetDropEnabled(fight)) + { + return fight.DropCount; + } + + if (GetInventoryTargetRuntimeState(taskId) is { } runtimeState) + { + return runtimeState.EffectiveQuantity; + } + + int? inventoryCount = GetSpecifiedDropsInventoryCount(fight); + if (inventoryCount is not int currentInventory) + { + return null; + } + + return Math.Max(fight.DropCount - currentInventory, 0); + } + + /// + /// 捕获运行中作战任务需要保持固定的目标库存值。 + /// + private static InventoryTargetRuntimeState? CreateInventoryTargetRuntimeState(FightTask fight) + { + var currentInventoryCounts = GetCurrentInventoryCounts(); + if (currentInventoryCounts == null) + { + return null; + } + + int startInventory = currentInventoryCounts.GetValueOrDefault(fight.DropId); + int effectiveQuantity = Math.Max(fight.DropCount - startInventory, 0); + return new(fight.DropId, startInventory, effectiveQuantity); + } + + /// + /// 在捕获启动时的目标库存值后,保存该 core 作战任务的运行时状态。 + /// + private void RememberInventoryTargetRuntimeState(int taskId, InventoryTargetRuntimeState runtimeState) + { + if (taskId <= 0) + { + return; + } + + lock (_inventoryTargetRuntimeStateLock) + { + _inventoryTargetRuntimeStateByTaskId[taskId] = runtimeState; + } + } + + /// + /// 刷新目标库存模式及其显示数量相关的 UI 绑定。 + /// + private void NotifySpecifiedDropsStateChanged() + { + NotifyOfPropertyChange(nameof(IsSpecifiedInventoryLocked)); + NotifyOfPropertyChange(nameof(UseDropQuantityMode)); + NotifyOfPropertyChange(nameof(UseTargetInventoryMode)); + NotifyOfPropertyChange(nameof(CurrentDropsInventoryText)); + NotifyOfPropertyChange(nameof(EffectiveDropsQuantityText)); + } + + /// + /// Gets 全部掉落材料列表。 /// private List AllDrops { get; } = []; @@ -389,24 +724,34 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or private sets the list of drops. + /// Gets 获取或私有设置掉落材料列表。 /// public ObservableCollection DropsList { get; private set; } = []; /// - /// Gets or sets the item ID of drops. + /// Gets or sets 指定掉落材料 ID。 /// public string DropsItemId { get => GetTaskConfig().DropId; set { - SetTaskConfig(t => t.DropId == value, t => t.DropId = value); + if (IsSpecifiedInventoryLocked) + { + return; + } + + if (!SetTaskConfig(t => t.DropId == value, t => t.DropId = value)) + { + return; + } + + NotifySpecifiedDropsStateChanged(); SetFightParams(); } } /// - /// Gets or sets the item Name of drops. + /// Gets or sets 指定掉落材料名称。 /// public string DropsItemName { get => field; set => SetAndNotify(ref field, value); } = string.Empty; @@ -414,6 +759,13 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting [UsedImplicitly] public void DropsListDropDownClosed() { + if (IsSpecifiedInventoryLocked) + { + RefreshDropName(); + NotifySpecifiedDropsStateChanged(); + return; + } + if (DropsList.FirstOrDefault(i => i.Display == DropsItemName) is { } item) { DropsItemId = item.Value; @@ -427,13 +779,23 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets the quantity of drops. + /// Gets or sets 指定掉落数量。 /// public int DropsQuantity { get => GetTaskConfig().DropCount; set { - SetTaskConfig(t => t.DropCount == value, t => t.DropCount = value); + if (IsSpecifiedInventoryLocked) + { + return; + } + + if (!SetTaskConfig(t => t.DropCount == value, t => t.DropCount = value)) + { + return; + } + + NotifySpecifiedDropsStateChanged(); SetFightParams(); } } @@ -487,7 +849,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to use DrGrandet mode. + /// Gets or sets a value indicating whether 启用 DrGrandet 模式。 /// public bool IsDrGrandet { @@ -496,7 +858,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to use alternate stage. + /// Gets or sets a value indicating whether 使用备选关卡。 /// public bool UseAlternateStage { @@ -593,7 +955,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting public string ActivityInfo { get => field; private set => SetAndNotify(ref field, value); } = string.Empty; /// - /// Gets or sets a value indicating whether to hide unavailable stages. + /// Gets or sets a value indicating whether 隐藏未开放关卡。 /// public bool HideUnavailableStage { @@ -627,7 +989,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to hide series. + /// Gets or sets a value indicating whether 隐藏连战设置。 /// public bool HideSeries { @@ -636,7 +998,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting } /// - /// Gets or sets a value indicating whether to use weekly schedule. + /// Gets or sets a value indicating whether 使用周计划。 /// public bool UseWeeklySchedule { @@ -711,6 +1073,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting RefreshCurrentStagePlan(); RefreshWeeklySchedule(); RefreshDropName(); + NotifySpecifiedDropsStateChanged(); Refresh(); } @@ -733,7 +1096,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting #region 关卡列表更新 /// - /// Updates stage list. + /// 更新关卡列表。 /// 使用手动输入时,只更新关卡列表,不更新关卡选择 /// 使用隐藏当日不开放时,更新关卡列表,关卡选择为未开放的关卡时清空 /// 使用备选关卡时,更新关卡列表,关卡选择为未开放的关卡时在关卡列表中添加对应未开放关卡,避免清空导致进入上次关卡 @@ -741,8 +1104,8 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting /// 除手动输入外所有情况下,如果剩余理智为未开放的关卡,会被清空 /// /// 更新任务列表的Task - // FIXME: 被注入对象只能在private函数内使用,只有Model显示之后才会被注入。如果Model还没有触发OnInitialActivate时调用函数会NullPointerException - // 这个函数被列为public可见,意味着他注入对象前被调用 + // FIXME:被注入对象只能在 private 函数内使用,只有 Model 显示之后才会被注入。如果 Model 还没有触发 OnInitialActivate 时调用此函数,会导致空引用异常。 + // 这个函数被声明为 public,意味着它可能会在注入对象前被调用。 public Task UpdateStageList() { return Execute.OnUIThreadAsync(async () => { @@ -938,7 +1301,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting public bool IsVisible { get => field; set => SetAndNotify(ref field, value); } = true; /// - /// Gets or sets a value indicating whether 过期活动关卡, 加删除线 + /// Gets or sets a value indicating whether 关卡已过期并显示删除线。 /// public bool IsOutdated { get; set; } = false; } @@ -1028,12 +1391,52 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting var yjTime = DateTimeOffset.Now.ToYjDateTime().ToLocalTime(); var daysUntilEndOfWeek = ((7 - (int)yjTime.DayOfWeek + 7) % 7) + 1; // 距离本周结束的天数, 用鹰历计算 var activityExpireDays = activityExpireIn2Days && fight.UseExpireMedicineForActivity ? daysUntilEndOfWeek : 0; + InventoryTargetRuntimeState? inventoryTargetRuntimeState = null; + bool shouldRememberInventoryTargetRuntimeState = false; + int specifiedDropsQuantity = fight.DropCount; + + if (IsInventoryTargetDropEnabled(fight)) + { + if (taskId is int existingTaskId and > 0 && Instance.GetInventoryTargetRuntimeState(existingTaskId) is { } existingRuntimeState) + { + inventoryTargetRuntimeState = existingRuntimeState; + } + else + { + inventoryTargetRuntimeState = CreateInventoryTargetRuntimeState(fight); + if (taskId is int startedTaskId and > 0 && inventoryTargetRuntimeState != null && IsTaskInProgress(startedTaskId)) + { + shouldRememberInventoryTargetRuntimeState = true; + } + } + + if (inventoryTargetRuntimeState == null) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetStringFormat("SpecifiedDropsInventoryUnavailable", fight.NameDisplay), UiLogColor.Warning); + return (null, []); + } + + specifiedDropsQuantity = inventoryTargetRuntimeState.EffectiveQuantity; + if (specifiedDropsQuantity <= 0 && taskId is null) + { + return (null, []); + } + } + + var effectiveMaxTimes = fight.EnableTimesLimit != false ? fight.TimesLimit : int.MaxValue; + + // 任务运行时如果启用了目标掉落且指定的掉落数量小于等于 0,则将最大挑战次数设为 0,防止任务继续运行但无法获得目标掉落 + if (taskId is int and > 0 && IsInventoryTargetDropEnabled(fight) && specifiedDropsQuantity <= 0) + { + effectiveMaxTimes = 0; + } + 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, + MaxTimes = effectiveMaxTimes, MedicineExpireDays = Math.Max(expireDays, activityExpireDays), IsDrGrandet = fight.IsDrGrandet, ReportToPenguin = SettingsViewModel.GameSettings.EnablePenguin, @@ -1049,21 +1452,36 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel, FightSetting task.Stage = fight.AnnihilationStage; } - if (fight.EnableTargetDrop != false && !string.IsNullOrEmpty(fight.DropId)) + if (fight.EnableTargetDrop != false && !string.IsNullOrEmpty(fight.DropId) && specifiedDropsQuantity > 0) { - task.Drops.Add(fight.DropId, fight.DropCount); + task.Drops.Add(fight.DropId, specifiedDropsQuantity); } - if (fight.EnableTimesLimit is not false && fight.Series > 0 && fight.TimesLimit % fight.Series != 0) + if (effectiveMaxTimes > 0 && effectiveMaxTimes < int.MaxValue && fight.Series > 0 && effectiveMaxTimes % fight.Series != 0) { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetStringFormat("FightTimesMayNotExhausted", fight.TimesLimit, fight.Series), UiLogColor.Warning); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetStringFormat("FightTimesMayNotExhausted", effectiveMaxTimes, fight.Series), UiLogColor.Warning); } - return taskId switch { - int id when id > 0 => (Instances.AsstProxy.AsstSetTaskParamsEncoded(id, task), [id]), - null => FromSingle(Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Fight, task)), - _ => (null, []), - }; + if (taskId is int id and > 0) + { + bool updated = Instances.AsstProxy.AsstSetTaskParamsEncoded(id, task); + if (updated && shouldRememberInventoryTargetRuntimeState && inventoryTargetRuntimeState != null) + { + Instance.RememberInventoryTargetRuntimeState(id, inventoryTargetRuntimeState); + } + + return (updated, [id]); + } + + if (taskId is null) + { + var appendResult = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Fight, task); + return (appendResult.IsSuccess, appendResult.TaskId > 0 ? [appendResult.TaskId] : []); + } + + return (null, []); } } + + private sealed record InventoryTargetRuntimeState(string DropId, int StartInventory, int EffectiveQuantity); } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml b/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml index 675e568ab1..892a284f20 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml @@ -100,45 +100,67 @@ - - - - - - - - - - - + + + + + + Content="{DynamicResource AssignedMaterial}" + IsChecked="{Binding IsSpecifiedDrops}" + IsEnabled="{c:Binding !IsSpecifiedInventoryLocked}" + MouseRightButtonUp="{s:Action ToggleCheckBoxNullOnRightClick, + Target={x:Type helper:CheckBoxHelper}}" + ToolTip="{DynamicResource CheckBoxesNotSavedAsNull}" /> + + + + + + + + + + + + + + + + + + + + + @@ -310,7 +346,7 @@