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 @@