rft: 过期关卡使用删除线表示 (#15657)

* rft: 使用`不修改关卡`替代 显式修改为`无效关卡`

* fix: 过滤过期关卡

* feat: 过期关卡增加删除线

* perf: 移除不必要的警告

* perf: 简化过滤

* fix: 无法隐藏关卡, 取消手动输入关卡名后验证关卡名有效性

---------

Co-authored-by: uye <99072975+ABA2396@users.noreply.github.com>
This commit is contained in:
Status102
2026-02-10 23:50:32 +08:00
committed by GitHub
parent 02cf529bf8
commit 8785c9d00a
14 changed files with 222 additions and 51 deletions

View File

@@ -57,13 +57,13 @@ internal class FightTaskStageResetModeConverter : JsonConverter<Root>
{
if (!taskElement.TryGetProperty("StageResetMode", out _))
{
if (fightTask.UseOptionalStage)
if (fightTask.HideUnavailableStage)
{
fightTask.StageResetMode = FightStageResetMode.Invalid;
fightTask.StageResetMode = FightStageResetMode.Current;
}
else
{
fightTask.StageResetMode = FightStageResetMode.Current;
fightTask.StageResetMode = FightStageResetMode.Ignore;
}
}
}

View File

@@ -0,0 +1,133 @@
// <copyright file="FightTaskStageResetModeInvalidToIgnoreConverter.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.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MaaWpfGui.Configuration.Converter;
internal class FightTaskStageResetModeInvalidToIgnoreConverter : JsonConverter<Root>
{
private const string TypeDiscriminatorProperty = "$type";
private const string FightTaskTypeName = "FightTask";
private const string StageResetModeProperty = "StageResetMode";
private const string InvalidValue = "Invalid";
private const string IgnoreValue = "Ignore";
public override Root? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject token");
}
using var jsonDoc = JsonDocument.ParseValue(ref reader);
JsonNode? rootNode = JsonNode.Parse(jsonDoc.RootElement.GetRawText());
if (rootNode is JsonObject rootObj)
{
ReplaceInvalidStageResetModeInTaskQueues(rootObj);
}
string modifiedJson = rootNode?.ToJsonString() ?? string.Empty;
return JsonSerializer.Deserialize<Root>(modifiedJson, GetOptionsWithoutThisConverter(options));
}
/// <summary>
/// 遍历 Configurations.*.TaskQueue将 $type 为 FightTask 且 StageResetMode 为 "Invalid" 的项改为 "Ignore"
/// </summary>
private static void ReplaceInvalidStageResetModeInTaskQueues(JsonObject root)
{
if (root["Configurations"] is not JsonObject configurations)
{
return;
}
foreach (var (_, configNode) in configurations)
{
if (configNode is not JsonObject config)
{
continue;
}
if (config["TaskQueue"] is not JsonArray taskQueue)
{
continue;
}
foreach (var taskNode in taskQueue)
{
if (taskNode is not JsonObject task)
{
continue;
}
if (!IsFightTask(task))
{
continue;
}
if (task[StageResetModeProperty] is JsonValue modeNode &&
modeNode.GetValue<string>() == InvalidValue)
{
task[StageResetModeProperty] = IgnoreValue;
}
if (task["StagePlan"] is JsonArray stagePlan)
{
for (int i = 0; i < stagePlan.Count; i++)
{
if (stagePlan[i] is JsonValue stageNode &&
stageNode.GetValue<string>() == "__INVALID__")
{
stagePlan[i] = "OR-8";
}
}
}
}
}
}
/// <summary>
/// 判断任务节点是否为 FightTask依据 $type 字段)
/// </summary>
private static bool IsFightTask(JsonObject task)
{
if (task[TypeDiscriminatorProperty] is not JsonValue typeNode)
{
return false;
}
return typeNode.GetValue<string>() == FightTaskTypeName;
}
public override void Write(Utf8JsonWriter writer, Root value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, GetOptionsWithoutThisConverter(options));
}
private static JsonSerializerOptions GetOptionsWithoutThisConverter(JsonSerializerOptions options)
{
var newOptions = new JsonSerializerOptions(options);
for (int i = newOptions.Converters.Count - 1; i >= 0; i--)
{
if (newOptions.Converters[i] is FightTaskStageResetModeInvalidToIgnoreConverter)
{
newOptions.Converters.RemoveAt(i);
}
}
return newOptions;
}
}

View File

@@ -61,7 +61,7 @@ public static class ConfigFactory
// ReSharper disable once EventNeverSubscribedTo.Global
public static event ConfigurationUpdateEventHandler? ConfigurationUpdateEvent;
private static readonly JsonSerializerOptions _options = new() { WriteIndented = true, Converters = { new JsonStringEnumConverter(), new FightTaskStageResetModeConverter() }, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
private static readonly JsonSerializerOptions _options = new() { WriteIndented = true, Converters = { new JsonStringEnumConverter(), new FightTaskStageResetModeInvalidToIgnoreConverter (), new FightTaskStageResetModeConverter() }, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
// TODO: 参考 ConfigurationHelper ,拆几个函数出来
private static readonly Lazy<Root> _rootConfig = new(() => {

View File

@@ -21,7 +21,7 @@ public enum FightStageResetMode
Current,
/// <summary>
/// 无效关卡
/// 忽略 / 不变
/// </summary>
Invalid,
Ignore,
}

View File

@@ -212,7 +212,7 @@ public class ConfigConverter
ConfigurationHelper.DeleteValue(ConfigurationKeys.HideUnavailableStage);
ConfigurationHelper.DeleteValue(ConfigurationKeys.CustomStageCode);
ConfigurationHelper.DeleteValue(ConfigurationKeys.UseAlternateStage);
fightTask.StageResetMode = fightTask.UseOptionalStage ? FightStageResetMode.Invalid : FightStageResetMode.Current;
fightTask.StageResetMode = fightTask.HideUnavailableStage ? FightStageResetMode.Current : FightStageResetMode.Ignore;
if (fightTask.Series > 6)
{

View File

@@ -1426,7 +1426,7 @@ public class AsstProxy
&& (Instances.TaskQueueViewModel.TaskItemViewModels.FirstOrDefault(t => t.TaskId == taskId)?.Index ?? -1) is int index and > -1
&& index <= ConfigFactory.CurrentConfig.TaskQueue.Count
&& ConfigFactory.CurrentConfig.TaskQueue[index] is Configuration.Single.MaaTask.FightTask fight
&& FightTask.GetFightStage(fight.StagePlan) is "Annihilation")
&& FightTask.GetFightStage(fight) is "Annihilation")
{
Instances.TaskQueueViewModel.AddLog("AnnihilationStage, " + LocalizationHelper.GetString("GiveUpUploadingPenguins"));
break;

View File

@@ -748,7 +748,7 @@ Do not adjust the proxy multiplier setting in the game</system:String>
<!--<system:String x:Key="CurrentStage">Current Stage</system:String>-->
<!--<system:String x:Key="LastBattle">Last Battle</system:String>-->
<system:String x:Key="DefaultStage">Cur/Last</system:String>
<system:String x:Key="InvalidStage">Invalid</system:String>
<!--<system:String x:Key="InvalidStage">Invalid</system:String>-->
<system:String x:Key="ClosedStage">The event is not open</system:String>
<system:String x:Key="UnsupportedStages">Unsupported stages</system:String>
<system:String x:Key="LowVersion">Old Version, Update required</system:String>

View File

@@ -749,7 +749,7 @@ C:\\leidian\\LDPlayer9
<!--<system:String x:Key="CurrentStage">当前关卡</system:String> -->
<!--<system:String x:Key="LastBattle">上次作战</system:String>-->
<system:String x:Key="DefaultStage">現在/前回</system:String>
<system:String x:Key="InvalidStage">無効なレベル</system:String>
<!--<system:String x:Key="InvalidStage">無効なレベル</system:String>-->
<system:String x:Key="ClosedStage">ステージが未公開か既に終了している</system:String>
<system:String x:Key="UnsupportedStages">未サポートステージ</system:String>
<system:String x:Key="LowVersion">バージョンの更新が必要</system:String>

View File

@@ -750,7 +750,7 @@ C:\\leidian\\LDPlayer9
<!--<system:String x:Key="CurrentStage">현재 스테이지</system:String>
<system:String x:Key="LastBattle">최근 작전</system:String>-->
<system:String x:Key="DefaultStage">현재/최근</system:String>
<system:String x:Key="InvalidStage">잘못된 스테이지</system:String>
<!--<system:String x:Key="InvalidStage">잘못된 스테이지</system:String>-->
<system:String x:Key="ClosedStage">미개방 스테이지</system:String>
<system:String x:Key="UnsupportedStages">미지원 스테이지</system:String>
<system:String x:Key="LowVersion">낮은 버전</system:String>

View File

@@ -749,7 +749,7 @@ C:\\leidian\\LDPlayer9。\n
<!--<system:String x:Key="CurrentStage">当前关卡</system:String> -->
<!--<system:String x:Key="LastBattle">上次作战</system:String>-->
<system:String x:Key="DefaultStage">当前/上次</system:String>
<system:String x:Key="InvalidStage">无效关卡</system:String>
<!--<system:String x:Key="InvalidStage">无效关卡</system:String>-->
<system:String x:Key="ClosedStage">活动未开放</system:String>
<system:String x:Key="UnsupportedStages">不支持的关卡</system:String>
<system:String x:Key="LowVersion">版本过低</system:String>

View File

@@ -749,7 +749,7 @@ C:\\leidian\\LDPlayer9\n
<!--<system:String x:Key="CurrentStage">目前關卡</system:String>-->
<!--<system:String x:Key="LastBattle">上次作戰</system:String>-->
<system:String x:Key="DefaultStage">目前/上次</system:String>
<system:String x:Key="InvalidStage">無效關卡</system:String>
<!--<system:String x:Key="InvalidStage">無效關卡</system:String>-->
<system:String x:Key="ClosedStage">活動未開放</system:String>
<system:String x:Key="UnsupportedStages">不支援的關卡</system:String>
<system:String x:Key="LowVersion">版本過低</system:String>

View File

@@ -92,7 +92,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
{ "炭", "SK-5" },
};
private readonly StageSourceItem InvalidStage = new() { Display = LocalizationHelper.GetString("InvalidStage"), Value = "__INVALID__", IsOpen = false, IsVisible = false };
/* private readonly StageSourceItem InvalidStage = new() { Display = LocalizationHelper.GetString("InvalidStage"), Value = "__INVALID__", IsOpen = false, IsVisible = false };*/
public ObservableCollection<StagePlanItem> StagePlan { get => field; set => SetAndNotify(ref field, value); } = [];
@@ -151,7 +151,23 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
public bool CustomStageCode
{
get => GetTaskConfig<FightTask>().IsStageManually;
set => SetTaskConfig<FightTask>(t => t.IsStageManually == value, t => t.IsStageManually = value);
set {
bool ret = SetTaskConfig<FightTask>(t => t.IsStageManually == value, t => t.IsStageManually = value);
if (ret && !value)
{
var stagePlan = GetTaskConfig<FightTask>().StagePlan;
for (int i = 0; i < stagePlan.Count; i++)
{
var stage = stagePlan[i];
if (!Instances.StageManager.GetStageList().Any(p => p.Value == stage))
{
stagePlan[i] = string.Empty;
}
}
SetTaskConfig<FightTask>(t => t.StagePlan.SequenceEqual(stagePlan), t => t.StagePlan = stagePlan);
RefreshCurrentStagePlan();
}
}
}
/// <summary>
@@ -490,7 +506,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
if (value)
{
HideUnavailableStage = false;
StageResetMode = FightStageResetMode.Invalid;
StageResetMode = FightStageResetMode.Ignore;
}
else
{
@@ -569,7 +585,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
public List<GenericCombinedData<FightStageResetMode>> StageResetModeList { get; } =
[
new() { Display = LocalizationHelper.GetString("DefaultStage"), Value = FightStageResetMode.Current },
new() { Display = LocalizationHelper.GetString("InvalidStage"), Value = FightStageResetMode.Invalid },
new() { Display = LocalizationHelper.GetString("NotSwitch"), Value = FightStageResetMode.Ignore },
];
public FightStageResetMode StageResetMode
@@ -636,11 +652,16 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
return value;
}
public static string? GetFightStage(IEnumerable<string> stageNames)
public static string? GetFightStage(FightTask fightTask)
{
var list = stageNames.Where(i => i != Instance.InvalidStage.Value);
var stage = list.FirstOrDefault(Instances.TaskQueueViewModel.IsStageOpen);
stage ??= list.FirstOrDefault();
if (fightTask == null)
{
return null;
}
var list = fightTask.StagePlan;
var stage = list?.FirstOrDefault(Instances.TaskQueueViewModel.IsStageOpen);
stage ??= list?.FirstOrDefault();
return stage;
}
@@ -711,7 +732,6 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
{
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;
}
@@ -728,7 +748,7 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
}
using var scope = _lock.EnterScope();
var stage = GetFightStage(fight.StagePlan);
var stage = GetFightStage(fight);
if (stage is null)
{
return null;
@@ -792,19 +812,24 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
RefreshStageList();
foreach (var task in ConfigFactory.CurrentConfig.TaskQueue.OfType<FightTask>().Where(i => !i.IsStageManually))
{
var originalPlan = task.StagePlan.ToList();
bool reset = false;
for (int i = 0; i < task.StagePlan.Count; i++)
{
var stage = task.StagePlan[i];
if (stage != InvalidStage.Value && !stageList.Any(p => p.Value == stage))
if (!stageList.Any(p => p.Value == stage))
{
task.StagePlan[i] = task.StageResetMode switch {
FightStageResetMode.Current => string.Empty,
FightStageResetMode.Invalid => InvalidStage.Value,
_ => string.Empty,
};
_logger.Information("Reset non-existing stage from plan: {Stage} to {}", stage, task.StagePlan[i]);
reset = true;
if (task.StageResetMode == FightStageResetMode.Current)
{
task.StagePlan[i] = string.Empty;
}
}
}
if (reset)
{
_logger.Information("Reset non-existing stage: {} to {}", string.Join(", ", originalPlan), string.Join(", ", task.StagePlan));
}
}
RefreshCurrentStagePlan();
});
@@ -822,11 +847,10 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
var listSource = stageList.Select(i => new StageSourceItem() { Display = i.Display, Value = i.Value, IsVisible = !HideUnavailableStage || i.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek), IsOpen = Instances.StageManager.GetStageList().FirstOrDefault(p => p.Value == i.Value)?.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek) ?? true }).ToList();
// 补过期关卡进来
foreach (var item in listCurrent.Where(i => i != InvalidStage.Value && !listSource.Any(p => p.Value == i)))
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 });
listSource.Add(new StageSourceItem() { Display = item, Value = item, IsOpen = false, IsVisible = false, IsOutdated = true });
}
listSource.Add(InvalidStage); // 无效关卡
listSource.FirstOrDefault(i => i.Value == "Annihilation")?.Display = current.UseCustomAnnihilation ? (AnnihilationModeList.FirstOrDefault(i => i.Value == current.AnnihilationStage).Key ?? LocalizationHelper.GetString("Annihilation.Current")) : LocalizationHelper.GetString("Annihilation.Current");
StageListSource = new ObservableCollection<StageSourceItem>(listSource);
current.StagePlan = listCurrent; // StageListSource更新后, 恢复StagePlan
@@ -935,6 +959,11 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
public bool IsOpen { get => field; set => SetAndNotify(ref field, value); } = true;
public bool IsVisible { get => field; set => SetAndNotify(ref field, value); } = true;
/// <summary>
/// Gets or sets a value indicating whether 过期活动关卡, 加删除线
/// </summary>
public bool IsOutdated { get; set; } = false;
}
public class StagePlanItem(string stage = "") : PropertyChangedBase
@@ -958,18 +987,12 @@ public class FightSettingsUserControlModel : TaskSettingsViewModel
return;
}
if (value == Instance.InvalidStage.Value)
{
IsOpen = false;
}
else
{
IsOpen = Instances.StageManager.GetStageList().FirstOrDefault(p => p.Value == value)?.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek) ?? true;
}
IsOpen = Instances.StageManager.GetStageList().FirstOrDefault(p => p.Value == value)?.IsStageOpen(Instances.TaskQueueViewModel.CurDayOfWeek) ?? true;
Instance.SetFightParams();
}
} = stage;
public bool IsOpen { get => field; set => SetAndNotify(ref field, value); } = true;
// 仅供 ComboBox本身 和 手写Stage的TextBlock 绑定使用
public bool IsOpen { get => field; set => SetAndNotify(ref field, value); } = Instances.TaskQueueViewModel.IsStageOpen(stage);
}
}

View File

@@ -196,7 +196,7 @@ public class MallSettingsUserControlModel : TaskSettingsViewModel
var fightStageEmpty = ConfigFactory.CurrentConfig.TaskQueue
.OfType<FightTask>()
.Where(task => task.IsEnable is not false)
.Any(task => string.IsNullOrEmpty(FightSettingsUserControlModel.GetFightStage(task.StagePlan)));
.Any(task => string.IsNullOrEmpty(FightSettingsUserControlModel.GetFightStage(task)));
var task = new AsstMallTask() {
CreditFight = CreditFightTaskEnabled && !fightStageEmpty,
FormationIndex = CreditFightSelectFormation,

View File

@@ -266,28 +266,43 @@
Margin="-10,0,0,0">
<!-- 当 CustomStageCode 为 false 时显示 ComboBox -->
<ComboBox
DisplayMemberPath="Display"
ItemsSource="{Binding StageListSource, Source={x:Static ui_vms:TaskQueueViewModel.FightTask}}"
Opacity="{c:Binding 'IsOpen ? 1 : 0.5'}"
ScrollViewer.CanContentScroll="False"
SelectedValue="{Binding Stage}"
SelectedValuePath="Value"
Visibility="{c:Binding !CustomStageCode,
Source={x:Static ui_vms:TaskQueueViewModel.FightTask}}">
<ComboBox.ItemContainerStyle>
<Style BasedOn="{StaticResource ComboBoxItemBaseStyle}" TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding IsOpen}" Value="False">
<Setter Property="Opacity" Value="0.4" />
</DataTrigger>
<DataTrigger Binding="{Binding IsOpen}" Value="True">
<Setter Property="Opacity" Value="1" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Display}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsOutdated}" Value="True">
<Setter Property="TextDecorations" Value="Strikethrough" />
</DataTrigger>
<DataTrigger Binding="{Binding IsOpen}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
<DataTrigger Binding="{Binding IsOpen}" Value="True">
<Setter Property="Opacity" Value="1" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- 当 CustomStageCode 为 true 时显示 TextBox -->