From de4740af067d6efecb42ff0bbad6c153893c9bbb Mon Sep 17 00:00:00 2001 From: Status102 <102887808+status102@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:01:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(wpf):=20=E9=9D=9E=E6=B3=95Enum=E5=80=BC?= =?UTF-8?q?=E5=B0=86=E4=BD=BF=E7=94=A8=E5=B1=9E=E6=80=A7=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E9=BB=98=E8=AE=A4=E5=80=BC=E4=BD=9C=E4=B8=BA=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=20(#16138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(wpf): 非法Enum值将使用属性默认值作为替代 --- .../FightTaskStageResetModeConverter.cs | 2 +- .../InvalidEnumValueRemoveConverter.cs | 222 ++++++++++++++++++ .../Configuration/Factory/ConfigFactory.cs | 2 +- 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/MaaWpfGui/Configuration/Converter/InvalidEnumValueRemoveConverter.cs diff --git a/src/MaaWpfGui/Configuration/Converter/FightTaskStageResetModeConverter.cs b/src/MaaWpfGui/Configuration/Converter/FightTaskStageResetModeConverter.cs index 30cd61f232..61772ccdc9 100644 --- a/src/MaaWpfGui/Configuration/Converter/FightTaskStageResetModeConverter.cs +++ b/src/MaaWpfGui/Configuration/Converter/FightTaskStageResetModeConverter.cs @@ -55,7 +55,7 @@ internal class FightTaskStageResetModeConverter : JsonConverter var task = rootObj.Configurations[configName].TaskQueue[taskIndex]; if (task is FightTask fightTask) { - if (!taskElement.TryGetProperty("StageResetMode", out _)) + if (!taskElement.TryGetProperty("StageResetMode", out var mode) || !Enum.TryParse(mode.GetString(), out var _)) { if (fightTask.HideUnavailableStage) { diff --git a/src/MaaWpfGui/Configuration/Converter/InvalidEnumValueRemoveConverter.cs b/src/MaaWpfGui/Configuration/Converter/InvalidEnumValueRemoveConverter.cs new file mode 100644 index 0000000000..1fe9ca27a8 --- /dev/null +++ b/src/MaaWpfGui/Configuration/Converter/InvalidEnumValueRemoveConverter.cs @@ -0,0 +1,222 @@ +// +// 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.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Serilog; + +namespace MaaWpfGui.Configuration.Converter; + +internal sealed class InvalidEnumValueRemoveConverter : JsonConverter +{ + private readonly ILogger _logger = Log.ForContext(); + + 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); + string json = jsonDoc.RootElement.GetRawText(); + try + { + return JsonSerializer.Deserialize(json, GetOptionsWithoutThisConverter(options)); + } + catch (JsonException ex) when (IsRecoverableInvalidEnumFailure(ex)) + { + var node = JsonNode.Parse(json); + if (node is not JsonObject rootObj) + { + throw new JsonException("Root configuration JSON must be a JSON object.", ex); + } + + if (!TryRemovePropertyByMetadataPath(rootObj, ex.Path)) + { + throw; + } + + _logger.Warning(ex, "Invalid enum in configuration JSON at path {Path}; removed property and retrying deserialization once", ex.Path); + + var obj = JsonSerializer.Deserialize(rootObj.ToJsonString(), GetOptionsWithoutThisConverter(options)); + _logger.Information("Deserialization succeeded after removing invalid enum property at path {Path}", ex.Path); + return obj; + } + } + + public override void Write(Utf8JsonWriter writer, Root value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, GetOptionsWithoutThisConverter(options)); // 使用默认序列化 + } + + private static bool IsRecoverableInvalidEnumFailure(JsonException ex) + { + if (string.IsNullOrWhiteSpace(ex.Path)) + { + return false; + } + + var text = ex.ToString(); + return text.Contains("could not be converted", StringComparison.OrdinalIgnoreCase) && text.Contains("Enum", StringComparison.OrdinalIgnoreCase); + } + + private static bool TryRemovePropertyByMetadataPath(JsonObject root, string? path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + + var segments = new List(); + if (!TryParseJsonExceptionPath(path, segments) || segments.Count == 0) + { + return false; + } + + JsonNode? parent = root; + for (int i = 0; i < segments.Count - 1; i++) + { + parent = NavigateJsonPathSegment(parent, segments[i]); + if (parent is null) + { + return false; + } + } + + var leaf = segments[^1]; + return parent is JsonObject leafObject && leafObject.Remove(leaf); + } + + private static JsonNode? NavigateJsonPathSegment(JsonNode? current, string segment) + { + return current switch { + JsonObject obj => obj[segment], + JsonArray arr when int.TryParse(segment, out var idx) && idx >= 0 && idx < arr.Count => arr[idx], + _ => null, + }; + } + + /// + /// 解析 风格路径(如 $.Configurations.default.TaskQueue[0].StageResetMode)。 + /// + private static bool TryParseJsonExceptionPath(string path, List segments) + { + segments.Clear(); + if (path.Length == 0 || path[0] != '$') + { + return false; + } + + var i = 1; + if (i < path.Length && path[i] == '.') + { + i++; + } + + while (i < path.Length) + { + if (path[i] == '.') + { + i++; + continue; + } + + if (path[i] == '[') + { + i++; + if (i >= path.Length) + { + return false; + } + + if (path[i] == '\'' || path[i] == '"') + { + var q = path[i++]; + var start = i; + while (i < path.Length && path[i] != q) + { + i++; + } + + if (i >= path.Length) + { + return false; + } + + segments.Add(path.Substring(start, i - start)); + i++; + if (i < path.Length && path[i] == ']') + { + i++; + } + } + else + { + var start = i; + while (i < path.Length && path[i] != ']') + { + i++; + } + + if (i >= path.Length) + { + return false; + } + + segments.Add(path.Substring(start, i - start).Trim()); + } + + if (i < path.Length && path[i] == ']') + { + i++; + } + } + else + { + var start = i; + while (i < path.Length && path[i] != '.' && path[i] != '[') + { + i++; + } + + var name = path.Substring(start, i - start); + if (name.Length > 0) + { + segments.Add(name); + } + } + } + + return segments.Count > 0; + } + + /// + /// 获取不包含当前 Converter 的 JsonSerializerOptions,避免无限递归 + /// + 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 InvalidEnumValueRemoveConverter) + { + newOptions.Converters.RemoveAt(i); + } + } + return newOptions; + } +} diff --git a/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs b/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs index 8fdc236cf6..0fcafa2d38 100644 --- a/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs +++ b/src/MaaWpfGui/Configuration/Factory/ConfigFactory.cs @@ -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 FightTaskStageResetModeInvalidToIgnoreConverter(), new FightTaskStageResetModeConverter() }, 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 }; // TODO: 参考 ConfigurationHelper ,拆几个函数出来 private static readonly Lazy _rootConfig = new(() => {