diff --git a/src/MaaWpfGui/Configuration/Converter/JsonPredictSerializationModifier.cs b/src/MaaWpfGui/Configuration/Converter/JsonPredictSerializationModifier.cs new file mode 100644 index 0000000000..e93f272a72 --- /dev/null +++ b/src/MaaWpfGui/Configuration/Converter/JsonPredictSerializationModifier.cs @@ -0,0 +1,182 @@ +// +// 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; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization.Metadata; +using MaaWpfGui.Utilities; + +namespace MaaWpfGui.Configuration.Converter; + +/// +/// 为 提供 Modifier:对带 的属性按条件属性/路径与 的比较结果决定是否序列化。 +/// +public static class JsonPredictSerializationModifier +{ + private const BindingFlags PropertyFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + /// + /// 修改类型元数据:对标记了 的属性按条件设置是否参与序列化。 + /// + /// 当前正在解析的类型的 JSON 序列化元数据。 + 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() 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); + } + } + } + + /// + /// 根据 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 比较;其他类型不设默认比较值,会序列化。 + /// + 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().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; + } + + /// + /// 按点号路径从对象取值,如 "StagePlan.Count" 表示 obj.StagePlan.Count。 + /// + 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; + } +} diff --git a/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs b/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs index 0fcafa2d38..2dbd9787b8 100644 --- a/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs +++ b/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs @@ -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 _rootConfig = new(() => { diff --git a/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs b/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs index fbee1d57b6..1ef5fee94c 100644 --- a/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs +++ b/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs @@ -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 /// public bool UseWeeklySchedule { get; set; } + /// + /// Gets or sets 周计划。当 为 false 时不参与序列化。 + /// + [JsonPredict(nameof(UseWeeklySchedule))] public Dictionary WeeklySchedule { get; set; } = Enum.GetValues().ToDictionary(i => i, _ => true); public void OnDeserialized() diff --git a/src/MaaWpfGui/Configuration/Single/MaaTask/InfrastTask.cs b/src/MaaWpfGui/Configuration/Single/MaaTask/InfrastTask.cs index 5f5ae9ad7d..e9ae9beb05 100644 --- a/src/MaaWpfGui/Configuration/Single/MaaTask/InfrastTask.cs +++ b/src/MaaWpfGui/Configuration/Single/MaaTask/InfrastTask.cs @@ -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); } diff --git a/src/MaaWpfGui/Utilities/JsonPredictAttribute.cs b/src/MaaWpfGui/Utilities/JsonPredictAttribute.cs new file mode 100644 index 0000000000..ad66c5e838 --- /dev/null +++ b/src/MaaWpfGui/Utilities/JsonPredictAttribute.cs @@ -0,0 +1,54 @@ +// +// 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; + +namespace MaaWpfGui.Utilities; + +/// +/// 标记在 JSON 序列化时是否忽略该属性。按条件属性/路径与 的比较结果决定是否序列化(由 控制等于或不等于)。 +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public class JsonPredictAttribute : Attribute +{ + /// + /// Gets 条件属性名或点号路径。仅当该路径在父对象上的值满足与 的比较时才序列化(由 决定等于或不等于)。 + /// 支持多级路径,如 "StagePlan.Count"。 + /// + public string? ConditionPropertyName { get; } + + /// + /// Gets 仅当条件值等于此值时才序列化;不等于则跳过序列化。未指定时,对 bool 条件视为 true(为 true 时才序列化)。 + /// 使用示例:[JsonPredict("StagePlan.Count", 0)] 表示仅当 StagePlan.Count == 0 时序列化本属性。 + /// + public object? CompareValue { get; } + + /// + /// Gets a value indicating whether 为 true(默认)时:条件值等于 才序列化;为 false 时:条件值不等于该值才序列化。 + /// + public bool SerializeWhenEqual { get; } + + /// + /// Initializes a new instance of the class。按条件属性控制:仅当条件属性与 满足比较条件时序列化(由 决定等于还是不等于)。 + /// + /// 同一类型上的属性名(字符串常量,如 "UseWeeklySchedule"、"StoneCount") + /// 参与比较的值(常量,如 true、0) + /// 为 true(默认)时:等于该值才序列化;为 false 时:不等于该值才序列化 + public JsonPredictAttribute(string conditionPropertyName, object? value = null, bool serializeWhenEqual = true) + { + ConditionPropertyName = conditionPropertyName ?? throw new ArgumentNullException(nameof(conditionPropertyName)); + CompareValue = value; + SerializeWhenEqual = serializeWhenEqual; + } +}