feat(wpf): 配置存储支持条件优化 (#15850)

This commit is contained in:
Status102
2026-04-17 13:48:54 +08:00
committed by GitHub
parent 4e45915f37
commit 9fd6602f11
5 changed files with 245 additions and 2 deletions

View File

@@ -0,0 +1,182 @@
// <copyright file="JsonPredictSerializationModifier.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.Collections;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization.Metadata;
using MaaWpfGui.Utilities;
namespace MaaWpfGui.Configuration.Converter;
/// <summary>
/// 为 <see cref="DefaultJsonTypeInfoResolver"/> 提供 Modifier对带 <see cref="JsonPredictAttribute"/> 的属性按条件属性/路径与 <see cref="JsonPredictAttribute.CompareValue"/> 的比较结果决定是否序列化。
/// </summary>
public static class JsonPredictSerializationModifier
{
private const BindingFlags PropertyFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
/// <summary>
/// 修改类型元数据:对标记了 <see cref="JsonPredictAttribute"/> 的属性按条件设置是否参与序列化。
/// </summary>
/// <param name="typeInfo">当前正在解析的类型的 JSON 序列化元数据。</param>
public static void Modify(JsonTypeInfo typeInfo)
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
{
return;
}
for (var t = typeInfo.Type; t != null; t = t.BaseType)
{
foreach (var pi in t.GetProperties(PropertyFlags))
{
if (pi.GetCustomAttribute<JsonPredictAttribute>() is not { } attr)
{
continue;
}
var jsonProp = typeInfo.Properties.FirstOrDefault(p => p.Name == pi.Name);
if (jsonProp == null)
{
continue;
}
if (attr.ConditionPropertyName is not { } conditionName)
{
continue;
}
var compareValue = attr.CompareValue;
var whenEqual = attr.SerializeWhenEqual;
jsonProp.ShouldSerialize = (parent, _) => ShouldSerializeByCondition(parent, conditionName, compareValue, whenEqual);
}
}
}
/// <summary>
/// 根据 serializeWhenEqual为 true 时仅当条件值等于 comparedValue 才序列化;为 false 时仅当不等于才序列化。
/// conditionPropertyName 支持点号路径,如 "StagePlan.Count"。
/// 条件值为 null 时:若 comparedValue 也为 null 且 serializeWhenEqual 为 true 则序列化,否则按 comparedValue 与 serializeWhenEqual 决定是否序列化。
/// comparedValue 为 null 时,按 value 类型推断默认比较值bool 视为与 true 比较int 视为与 0 比较(此时语义为“不等于 0 才序列化”);
/// 若 value 为容器IEnumerable/ICollection则先将 value 替换为元素个数再按整数与 0 比较;其他类型不设默认比较值,会序列化。
/// </summary>
private static bool ShouldSerializeByCondition(object? parent, string conditionPropertyName, object? comparedValue, bool serializeWhenEqual = true)
{
if (parent == null)
{
return false;
}
var value = GetValueByPath(parent, conditionPropertyName);
if (value is null)
{
if (serializeWhenEqual)
{
return comparedValue is null;
}
else
{
return comparedValue is not null;
}
}
if (comparedValue is null)
{
// 容器:将 value 替换为元素个数,后续按整数处理
if (value is ICollection col)
{
value = col.Count;
}
else if (value is IEnumerable enumerable && value is not string)
{
value = enumerable.Cast<object>().Count();
}
object? effectiveCompareValue = null;
if (value is bool)
{
effectiveCompareValue = true;
}
else if (value is int or long or short or byte)
{
effectiveCompareValue = 0;
serializeWhenEqual = !serializeWhenEqual; // 与 0 比较时语义相反:为 true 时仅当不等于 0 才序列化;为 false 时仅当等于 0 才序列化
}
// 其他类型 effectiveCompareValue 保持 null
if (effectiveCompareValue != null)
{
bool isEqual = (effectiveCompareValue is int or long or short or byte) && (value is int or long or short or byte)
? Convert.ToInt64(value) == Convert.ToInt64(effectiveCompareValue)
: Equals(value, effectiveCompareValue);
return serializeWhenEqual ? isEqual : !isEqual;
}
return true;
}
var equal = Equals(value, comparedValue);
return serializeWhenEqual ? equal : !equal;
}
/// <summary>
/// 按点号路径从对象取值,如 "StagePlan.Count" 表示 obj.StagePlan.Count。
/// </summary>
private static object? GetValueByPath(object obj, string path)
{
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
var current = obj;
var segments = path.Split('.');
for (var i = 0; i < segments.Length; i++)
{
if (current == null)
{
return null;
}
var segment = segments[i].Trim();
if (segment.Length == 0)
{
return null;
}
var type = current.GetType();
PropertyInfo? prop = null;
for (var t = type; t != null; t = t.BaseType)
{
prop = t.GetProperty(segment, flags);
if (prop != null)
{
break;
}
}
if (prop == null)
{
return null;
}
current = prop.GetValue(current);
if (i == segments.Length - 1)
{
return current;
}
}
return current;
}
}

View File

@@ -22,6 +22,7 @@ using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
@@ -61,7 +62,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 FightTaskStageResetModeConverter(), new InvalidEnumValueRemoveConverter(), new JsonStringEnumConverter(), new FightTaskStageResetModeInvalidToIgnoreConverter() }, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
private static readonly JsonSerializerOptions _options = new() { WriteIndented = true, Converters = { new FightTaskStageResetModeConverter(), new InvalidEnumValueRemoveConverter(), new JsonStringEnumConverter(), new FightTaskStageResetModeInvalidToIgnoreConverter() }, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { JsonPredictSerializationModifier.Modify } } };
// TODO: 参考 ConfigurationHelper ,拆几个函数出来
private static readonly Lazy<Root> _rootConfig = new(() => {

View File

@@ -17,6 +17,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using MaaWpfGui.Constants.Enums;
using MaaWpfGui.Utilities;
using static MaaWpfGui.Main.AsstProxy;
namespace MaaWpfGui.Configuration.Single.MaaTask;
@@ -135,6 +136,10 @@ public class FightTask : BaseTask, IJsonOnDeserialized
/// </summary>
public bool UseWeeklySchedule { get; set; }
/// <summary>
/// Gets or sets 周计划。当 <see cref="UseWeeklySchedule"/> 为 false 时不参与序列化。
/// </summary>
[JsonPredict(nameof(UseWeeklySchedule))]
public Dictionary<DayOfWeek, bool> WeeklySchedule { get; set; } = Enum.GetValues<DayOfWeek>().ToDictionary(i => i, _ => true);
public void OnDeserialized()

View File

@@ -20,6 +20,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using MaaWpfGui.Constants.Enums;
using MaaWpfGui.Models;
using MaaWpfGui.Utilities;
using MaaWpfGui.ViewModels.UserControl.TaskQueue;
using Serilog;
using static MaaWpfGui.Main.AsstProxy;
@@ -139,5 +140,5 @@ public class InfrastTask : BaseTask, IJsonOnDeserialized
}
}
public record RoomInfo(InfrastRoomType Room, bool IsEnabled);
public record RoomInfo(InfrastRoomType Room, [property: JsonPredict("IsEnabled", false)] bool IsEnabled);
}

View File

@@ -0,0 +1,54 @@
// <copyright file="JsonPredictAttribute.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;
namespace MaaWpfGui.Utilities;
/// <summary>
/// 标记在 JSON 序列化时是否忽略该属性。按条件属性/路径与 <see cref="CompareValue"/> 的比较结果决定是否序列化(由 <see cref="SerializeWhenEqual"/> 控制等于或不等于)。
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class JsonPredictAttribute : Attribute
{
/// <summary>
/// Gets 条件属性名或点号路径。仅当该路径在父对象上的值满足与 <see cref="CompareValue"/> 的比较时才序列化(由 <see cref="SerializeWhenEqual"/> 决定等于或不等于)。
/// 支持多级路径,如 <c>"StagePlan.Count"</c>。
/// </summary>
public string? ConditionPropertyName { get; }
/// <summary>
/// Gets 仅当条件值等于此值时才序列化;不等于则跳过序列化。未指定时,对 bool 条件视为 true为 true 时才序列化)。
/// 使用示例:<c>[JsonPredict("StagePlan.Count", 0)]</c> 表示仅当 StagePlan.Count == 0 时序列化本属性。
/// </summary>
public object? CompareValue { get; }
/// <summary>
/// Gets a value indicating whether 为 true默认条件值等于 <see cref="CompareValue"/> 才序列化;为 false 时:条件值不等于该值才序列化。
/// </summary>
public bool SerializeWhenEqual { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonPredictAttribute"/> class。按条件属性控制仅当条件属性与 <paramref name="value"/> 满足比较条件时序列化(由 <paramref name="serializeWhenEqual"/> 决定等于还是不等于)。
/// </summary>
/// <param name="conditionPropertyName">同一类型上的属性名(字符串常量,如 "UseWeeklySchedule"、"StoneCount"</param>
/// <param name="value">参与比较的值(常量,如 true、0</param>
/// <param name="serializeWhenEqual">为 true默认等于该值才序列化为 false 时:不等于该值才序列化</param>
public JsonPredictAttribute(string conditionPropertyName, object? value = null, bool serializeWhenEqual = true)
{
ConditionPropertyName = conditionPropertyName ?? throw new ArgumentNullException(nameof(conditionPropertyName));
CompareValue = value;
SerializeWhenEqual = serializeWhenEqual;
}
}