From 3c6f20ed53e802f9897e8824504662a49e630eab4dc642674f4ee56d64372ce6 Mon Sep 17 00:00:00 2001 From: copecone Date: Fri, 12 Jun 2026 14:22:14 +0900 Subject: [PATCH] Add UI Value Validators --- Data/DoubleInputValidator.cs | 98 +++++++++++++++++++++++++++++++++++ Data/DoubleLimit.cs | 78 ++++++++++++++++++++++++++++ Data/IntegerInputValidator.cs | 95 ++++++++++++++++++++++++++++++++- Data/IntegerLimit.cs | 78 ++++++++++++++++++++++++++++ UI/RenderConfigUIManager.cs | 12 +++++ 5 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 Data/DoubleInputValidator.cs create mode 100644 Data/DoubleLimit.cs create mode 100644 Data/IntegerLimit.cs create mode 100644 UI/RenderConfigUIManager.cs diff --git a/Data/DoubleInputValidator.cs b/Data/DoubleInputValidator.cs new file mode 100644 index 0000000..566582d --- /dev/null +++ b/Data/DoubleInputValidator.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using MelonLoader; +using UnityEngine.UIElements; + +namespace ADOFAIRenderer.Data +{ + public class DoubleInputValidator + { + private struct FieldEntry + { + public List Limits; + public bool Optional; + } + + private readonly VisualElement _rootVisualElement; + private readonly Dictionary _fields = new(); + + public DoubleInputValidator(VisualElement rootVisualElement) + { + _rootVisualElement = rootVisualElement; + } + + /// + /// Registers one or more s for the field with the given name. + /// Multiple limits can be added to the same field — they are all checked in order. + /// + /// The name of the DoubleField in the UXML. + /// + /// If true, the field is allowed to be empty. Limits are only enforced when a value is present. + /// + /// The limits to apply. + public DoubleInputValidator AddLimits(string fieldName, bool optional, params DoubleLimit[] limits) + { + if (!_fields.TryGetValue(fieldName, out var entry)) + { + entry = new FieldEntry + { + Limits = new List(), + Optional = optional + }; + } + + entry.Optional = optional; + entry.Limits.AddRange(limits); + _fields[fieldName] = entry; + return this; + } + + /// + /// Shorthand for required (non-optional) fields. + public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits) + { + return AddLimits(fieldName, false, limits); + } + + /// + /// Queries all registered fields from the root VisualElement and + /// registers value-changed callbacks that rollback invalid values. + /// + public void SetupValidation() + { + foreach (var kvp in _fields) + { + var fieldName = kvp.Key; + var entry = kvp.Value; + + var field = _rootVisualElement.Q(fieldName); + if (field == null) + { + Melon.Logger.Warning( + $"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); + continue; + } + + var limits = entry.Limits; + var optional = entry.Optional; + + field.RegisterValueChangedCallback(evt => + { + // Optional fields allow empty input — skip validation when cleared + if (optional && string.IsNullOrEmpty(field.text)) + { + return; + } + + foreach (var limit in limits) + { + if (!limit.Validate(evt.newValue, out _)) + { + field.SetValueWithoutNotify(evt.previousValue); + return; + } + } + }); + } + } + } +} diff --git a/Data/DoubleLimit.cs b/Data/DoubleLimit.cs new file mode 100644 index 0000000..ff072d3 --- /dev/null +++ b/Data/DoubleLimit.cs @@ -0,0 +1,78 @@ +namespace ADOFAIRenderer.Data +{ + /// + /// Base class for double input validation limits. + /// Multiple limits can be composed on a single input field — all are checked in order. + /// + public abstract class DoubleLimit + { + /// + /// Validates the given double value against this limit. + /// + /// The parsed double value to validate. + /// + /// When validation fails, set to a human-readable error message. + /// When validation succeeds, set to null. + /// + /// true if the value satisfies this limit; false otherwise. + public abstract bool Validate(double value, out string errorMessage); + } + + /// + /// Requires the double value to be strictly positive (> 0). + /// + public class PositiveOnlyDoubleLimit : DoubleLimit + { + public override bool Validate(double value, out string errorMessage) + { + if (value > 0) + { + errorMessage = null!; + return true; + } + + errorMessage = "값은 0보다 큰 양수여야 합니다."; + return false; + } + } + + /// + /// Requires the double value to be within an inclusive [Min, Max] range. + /// Either bound can be null to represent an unbounded side. + /// + public class RangeDoubleLimit : DoubleLimit + { + public double? Min { get; } + public double? Max { get; } + + /// Inclusive lower bound, or null for no lower bound. + /// Inclusive upper bound, or null for no upper bound. + public RangeDoubleLimit(double? min, double? max) + { + Min = min; + Max = max; + } + + public override bool Validate(double value, out string errorMessage) + { + if (Min.HasValue && value < Min.Value) + { + errorMessage = Max.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." + : $"값은 {Min.Value} 이상의 숫자여야 합니다."; + return false; + } + + if (Max.HasValue && value > Max.Value) + { + errorMessage = Min.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다." + : $"값은 {Max.Value} 이하의 숫자여야 합니다."; + return false; + } + + errorMessage = null!; + return true; + } + } +} diff --git a/Data/IntegerInputValidator.cs b/Data/IntegerInputValidator.cs index d37f62e..f9af349 100644 --- a/Data/IntegerInputValidator.cs +++ b/Data/IntegerInputValidator.cs @@ -1,7 +1,98 @@ +using System.Collections.Generic; +using MelonLoader; +using UnityEngine.UIElements; + namespace ADOFAIRenderer.Data { - public class PositiveIntegerValidator + public class IntegerInputValidator { - + private struct FieldEntry + { + public List Limits; + public bool Optional; + } + + private readonly VisualElement _rootVisualElement; + private readonly Dictionary _fields = new(); + + public IntegerInputValidator(VisualElement rootVisualElement) + { + _rootVisualElement = rootVisualElement; + } + + /// + /// Registers one or more s for the field with the given name. + /// Multiple limits can be added to the same field — they are all checked in order. + /// + /// The name of the IntegerField in the UXML. + /// + /// If true, the field is allowed to be empty. Limits are only enforced when a value is present. + /// + /// The limits to apply. + public IntegerInputValidator AddLimits(string fieldName, bool optional, params IntegerLimit[] limits) + { + if (!_fields.TryGetValue(fieldName, out var entry)) + { + entry = new FieldEntry + { + Limits = new List(), + Optional = optional + }; + } + + entry.Optional = optional; + entry.Limits.AddRange(limits); + _fields[fieldName] = entry; + return this; + } + + /// + /// Shorthand for required (non-optional) fields. + public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits) + { + return AddLimits(fieldName, false, limits); + } + + /// + /// Queries all registered fields from the root VisualElement and + /// registers value-changed callbacks that rollback invalid values. + /// + public void SetupValidation() + { + foreach (var kvp in _fields) + { + var fieldName = kvp.Key; + var entry = kvp.Value; + + var field = _rootVisualElement.Q(fieldName); + if (field == null) + { + Melon.Logger.Warning( + $"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다."); + continue; + } + + var limits = entry.Limits; + var optional = entry.Optional; + + field.RegisterValueChangedCallback(evt => + { + // Optional fields allow empty input — skip validation when cleared + if (optional && string.IsNullOrEmpty(field.text)) + { + return; + } + + foreach (var limit in limits) + { + if (!limit.Validate(evt.newValue, out _)) + { + field.SetValueWithoutNotify(evt.previousValue); + return; + } + } + }); + } + } } } \ No newline at end of file diff --git a/Data/IntegerLimit.cs b/Data/IntegerLimit.cs new file mode 100644 index 0000000..3e05d0f --- /dev/null +++ b/Data/IntegerLimit.cs @@ -0,0 +1,78 @@ +namespace ADOFAIRenderer.Data +{ + /// + /// Base class for integer input validation limits. + /// Multiple limits can be composed on a single input field — all are checked in order. + /// + public abstract class IntegerLimit + { + /// + /// Validates the given integer value against this limit. + /// + /// The parsed integer value to validate. + /// + /// When validation fails, set to a human-readable error message. + /// When validation succeeds, set to null. + /// + /// true if the value satisfies this limit; false otherwise. + public abstract bool Validate(int value, out string errorMessage); + } + + /// + /// Requires the integer value to be strictly positive (> 0). + /// + public class PositiveOnlyIntegerLimit : IntegerLimit + { + public override bool Validate(int value, out string errorMessage) + { + if (value > 0) + { + errorMessage = null!; + return true; + } + + errorMessage = "값은 0보다 큰 양의 정수여야 합니다."; + return false; + } + } + + /// + /// Requires the integer value to be within an inclusive [Min, Max] range. + /// Either bound can be null to represent an unbounded side. + /// + public class RangeIntegerLimit : IntegerLimit + { + public int? Min { get; } + public int? Max { get; } + + /// Inclusive lower bound, or null for no lower bound. + /// Inclusive upper bound, or null for no upper bound. + public RangeIntegerLimit(int? min, int? max) + { + Min = min; + Max = max; + } + + public override bool Validate(int value, out string errorMessage) + { + if (Min.HasValue && value < Min.Value) + { + errorMessage = Max.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." + : $"값은 {Min.Value} 이상의 정수여야 합니다."; + return false; + } + + if (Max.HasValue && value > Max.Value) + { + errorMessage = Min.HasValue + ? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다." + : $"값은 {Max.Value} 이하의 정수여야 합니다."; + return false; + } + + errorMessage = null!; + return true; + } + } +} diff --git a/UI/RenderConfigUIManager.cs b/UI/RenderConfigUIManager.cs new file mode 100644 index 0000000..7989c78 --- /dev/null +++ b/UI/RenderConfigUIManager.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace ADOFAIRenderer.Core +{ + public class RenderConfigUIManager : MonoBehaviour + { + void Awake() + { + + } + } +} \ No newline at end of file