From 37072113db766c89e304f291a0f05285c59b1abb Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Tue, 5 May 2026 18:06:01 -0400 Subject: [PATCH 01/10] add TextModels, validation, auto-generation for Vectors & Quaternion --- Silksong.ModMenu/Elements/TextInput.cs | 42 +++++++ Silksong.ModMenu/Models/TextModels.cs | 114 ++++++++++++++++++ Silksong.ModMenu/Plugin/ConfigEntryFactory.cs | 104 ++++++++++++++++ .../Tests/ModMenuAutoTestingPlugin.cs | 26 ++-- 4 files changed, 273 insertions(+), 13 deletions(-) diff --git a/Silksong.ModMenu/Elements/TextInput.cs b/Silksong.ModMenu/Elements/TextInput.cs index 91dede6..7d42909 100644 --- a/Silksong.ModMenu/Elements/TextInput.cs +++ b/Silksong.ModMenu/Elements/TextInput.cs @@ -24,6 +24,13 @@ public class TextInput : SelectableValueElement typeof(ulong), ]; private static readonly HashSet floatTypes = [typeof(float), typeof(double)]; + private static readonly HashSet floatListTypes = + [ + typeof(Vector2), + typeof(Vector3), + typeof(Vector4), + typeof(Quaternion), + ]; /// /// Construct a basic text input. @@ -48,6 +55,14 @@ public TextInput(LocalizedText label, ITextModel model, LocalizedText descrip InputField.contentType = InputField.ContentType.IntegerNumber; else if (floatTypes.Contains(typeof(T))) InputField.contentType = InputField.ContentType.DecimalNumber; + else if (floatListTypes.Contains(typeof(T))) + { + InputField.contentType = InputField.ContentType.Custom; + InputField.onValidateInput = FloatListValidation; + ApplyDefaultColors = true; + OnTextValueChanged += _ => + State = TextModel.IsTextValid ? ElementState.DEFAULT : ElementState.INVALID; + } } /// @@ -100,4 +115,31 @@ public override void SetFontSizes(FontSizes fontSizes) DescriptionText.fontSize = fontSizes.DescriptionSize(); InputField.textComponent.fontSize = fontSizes.ChoiceSize(); } + + /// + /// validation for comma-delimited lists + /// of float values, with or without enclosing brackets. + /// + static char FloatListValidation(string input, int pos, char ch) + { + int leftPos = input.IndexOf('('), + rightPos = input.IndexOf(')'); + + // Everything must go within the brackets, if they're present + if (leftPos >= pos || (rightPos < pos && rightPos >= 0)) + return '\0'; + + if ( + // Brackets must be unique and positioned on the appropriate end of the string + (ch == '(' && leftPos == -1 && pos == 0) + || (ch == ')' && rightPos == -1 && pos == input.Length) + // Other valid characters + || char.IsDigit(ch) + || char.IsWhiteSpace(ch) + || ",.-".Contains(ch) + ) + return ch; + + return '\0'; + } } diff --git a/Silksong.ModMenu/Models/TextModels.cs b/Silksong.ModMenu/Models/TextModels.cs index 12733e1..f70ba59 100644 --- a/Silksong.ModMenu/Models/TextModels.cs +++ b/Silksong.ModMenu/Models/TextModels.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Silksong.ModMenu.Internal; using UnityEngine; @@ -17,6 +18,8 @@ public static class TextModels public static ParserTextModel ForStrings() => new(DefaultUnparse, DefaultUnparse); + #region Numbers + /// /// An ITextModel which parses its input into a signed byte. /// @@ -172,6 +175,8 @@ public static ParserTextModel ForDoubles(double min, double max) return model; } + #endregion + private static bool DefaultUnparse(T value, out string text) { text = $"{value}"; @@ -193,6 +198,8 @@ bool Check(T value) return Check; } + #region Colors + /// /// An ITextModel which parses 3, 6, or 8 character hex strings to and from s. /// @@ -243,4 +250,111 @@ private static bool HexUnparser(Color c, out string x) x = "###"; return false; } + + #endregion + #region Vectors & Quaternions + + /// + /// An ITextModel which parses comma-delimited pairs of numbers to and from s. + /// + public static ParserTextModel ForVector2() => + new(Vector2Parser, Vector2Unparser, INVALID_VECTOR); + + /// + /// An ITextModel which parses comma-delimited trios of numbers to and from s. + /// + public static ParserTextModel ForVector3() => + new(Vector3Parser, Vector3Unparser, INVALID_VECTOR); + + /// + /// An ITextModel which parses comma-delimited quartets of numbers to and from s. + /// + public static ParserTextModel ForVector4() => + new(Vector4Parser, Vector4Unparser, INVALID_VECTOR); + + /// + /// An ITextModel which parses comma-delimited quartets of numbers to and from s. + /// + public static ParserTextModel ForQuaternion() => + new(QuaternionParser, QuaternionUnparser, INVALID_QUATERNION); + + private static readonly Vector4 INVALID_VECTOR = Vector4.positiveInfinity; + private static readonly Quaternion INVALID_QUATERNION = new( + float.PositiveInfinity, + float.PositiveInfinity, + float.PositiveInfinity, + float.PositiveInfinity + ); + + private static bool Vector2Parser(string x, out Vector2 v) + { + bool success = FloatListParser(x, out float[] c) && c.Length == 2; + v = success ? new(c[0], c[1]) : INVALID_VECTOR; + return success; + } + + private static bool Vector2Unparser(Vector2 v, out string x) + { + x = v.ToString("F2", CultureInfo.InvariantCulture.NumberFormat); + return true; + } + + private static bool Vector3Parser(string x, out Vector3 v) + { + bool success = FloatListParser(x, out float[] c) && c.Length == 3; + v = success ? new(c[0], c[1], c[2]) : INVALID_VECTOR; + return success; + } + + private static bool Vector3Unparser(Vector3 v, out string x) + { + x = v.ToString("F2", CultureInfo.InvariantCulture.NumberFormat); + return true; + } + + private static bool Vector4Parser(string x, out Vector4 v) + { + bool success = FloatListParser(x, out float[] c) && c.Length == 4; + v = success ? new(c[0], c[1], c[2], c[3]) : INVALID_VECTOR; + return success; + } + + private static bool Vector4Unparser(Vector4 v, out string x) + { + x = v.ToString("F2", CultureInfo.InvariantCulture.NumberFormat); + return true; + } + + private static bool QuaternionParser(string x, out Quaternion q) + { + bool success = FloatListParser(x, out float[] c) && c.Length == 4; + q = success ? new(c[0], c[1], c[2], c[3]) : INVALID_QUATERNION; + return success; + } + + private static bool QuaternionUnparser(Quaternion q, out string x) + { + x = q.ToString("F3", CultureInfo.InvariantCulture.NumberFormat); + return true; + } + + /// + /// Parses strings of the format "(-0.0, -0.0, ... , -0.0)" with or without brackets + /// into an array of floats. + /// + private static bool FloatListParser(string x, out float[] results) + { + string[] rawVals = [.. x.Trim().Trim('(', ')').Split(',').Select(s => s.Trim())]; + results = new float[rawVals.Length]; + + NumberFormatInfo format = CultureInfo.InvariantCulture.NumberFormat; + + for (int i = 0; i < rawVals.Length; i++) + if (!float.TryParse(rawVals[i], NumberStyles.Float, format, out results[i])) + return false; + + return true; + } + + #endregion } diff --git a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs index 89ed00f..425b2a5 100644 --- a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs +++ b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs @@ -45,6 +45,10 @@ public delegate bool MenuElementGenerator( GenerateDoubleElement, GenerateStringElement, GenerateColorElement, + GenerateVector2Element, + GenerateVector3Element, + GenerateVector4Element, + GenerateQuaternionElement, ]; /// @@ -704,6 +708,106 @@ public static bool GenerateColorElement( return true; } + /// + /// Generate a text element for a Vector2. + /// + public static bool GenerateVector2Element( + ConfigEntryBase entry, + [MaybeNullWhen(false)] out MenuElement menuElement + ) + { + if (entry is not ConfigEntry vectorEntry) + { + menuElement = default; + return false; + } + + TextInput vector = new( + entry.LabelName(), + TextModels.ForVector2(), + entry.DescriptionLine() + ); + vector.SynchronizeWith(vectorEntry); + + menuElement = vector; + return true; + } + + /// + /// Generate a text element for a Vector3. + /// + public static bool GenerateVector3Element( + ConfigEntryBase entry, + [MaybeNullWhen(false)] out MenuElement menuElement + ) + { + if (entry is not ConfigEntry vectorEntry) + { + menuElement = default; + return false; + } + + TextInput vector = new( + entry.LabelName(), + TextModels.ForVector3(), + entry.DescriptionLine() + ); + vector.SynchronizeWith(vectorEntry); + + menuElement = vector; + return true; + } + + /// + /// Generate a text element for a Vector4. + /// + public static bool GenerateVector4Element( + ConfigEntryBase entry, + [MaybeNullWhen(false)] out MenuElement menuElement + ) + { + if (entry is not ConfigEntry vectorEntry) + { + menuElement = default; + return false; + } + + TextInput vector = new( + entry.LabelName(), + TextModels.ForVector4(), + entry.DescriptionLine() + ); + vector.SynchronizeWith(vectorEntry); + + menuElement = vector; + return true; + } + + /// + /// Generate a text element for a Quaternion. + /// + public static bool GenerateQuaternionElement( + ConfigEntryBase entry, + [MaybeNullWhen(false)] out MenuElement menuElement + ) + { + if (entry is not ConfigEntry quaternionEntry) + { + menuElement = default; + return false; + } + + TextInput quaternion = new( + entry.LabelName(), + TextModels.ForQuaternion(), + entry.DescriptionLine() + ); + quaternion.SynchronizeWith(quaternionEntry); + + menuElement = quaternion; + return true; + } + private record ElementTreeNode { public readonly List<(string path, MenuElement element)> Elements = []; diff --git a/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs b/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs index 57112a3..8ed13d6 100644 --- a/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs +++ b/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs @@ -19,24 +19,24 @@ protected override void Setup(ConfigFile config) { config.Bind("Unity Types", "KeyCode Option", KeyCode.A); config.Bind("Unity Types", "Color Option", Color.green); - config.Bind("Unity Types", "Vector2 Option", Vector2.one); // not done - config.Bind("Unity Types", "Vector3 Option", Vector3.one); // not done - config.Bind("Unity Types", "Vector4 Option", Vector4.one); // not done - config.Bind("Unity Types", "Quaternion Option", Quaternion.identity); // not done + config.Bind("Unity Types", "Vector2 Option", Vector2.one); + config.Bind("Unity Types", "Vector3 Option", Vector3.one); + config.Bind("Unity Types", "Vector4 Option", Vector4.one); + config.Bind("Unity Types", "Quaternion Option", Quaternion.identity); config.Bind("Value Types", "String Option", "value"); config.Bind("Value Types", "Enum Option", TestEnum.EnumOne); config.Bind("Value Types", "Bool Option", true); - config.Bind("Value Types", "Byte Option", (byte)0); // not done - config.Bind("Value Types", "SByte Option", (sbyte)0); // not done - config.Bind("Value Types", "Short Option", (short)0); // not done - config.Bind("Value Types", "UShort Option", (ushort)0); // not done + config.Bind("Value Types", "Byte Option", (byte)0); + config.Bind("Value Types", "SByte Option", (sbyte)0); + config.Bind("Value Types", "Short Option", (short)0); + config.Bind("Value Types", "UShort Option", (ushort)0); config.Bind("Value Types", "Int Option", 0); - config.Bind("Value Types", "UInt Option", 0u); // not done - config.Bind("Value Types", "Long Option", 0L); // not done - config.Bind("Value Types", "ULong Option", 0UL); // not done + config.Bind("Value Types", "UInt Option", 0u); + config.Bind("Value Types", "Long Option", 0L); + config.Bind("Value Types", "ULong Option", 0UL); config.Bind("Value Types", "Float Option", 0.0f); - config.Bind("Value Types", "Double Option", 0.0d); // not done - config.Bind("Value Types", "Decimal Option", 0.0m); // not done + config.Bind("Value Types", "Double Option", 0.0d); + config.Bind("Value Types", "Decimal Option", 0.0m); } } From a760edd7ad6bbd491ee459e6b2dd336994cf66a0 Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Tue, 5 May 2026 18:07:56 -0400 Subject: [PATCH 02/10] add an underline to TextInputs To clarify that there's a spot to type when the field is empty. --- Silksong.ModMenu/Internal/MenuPrefabs.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Silksong.ModMenu/Internal/MenuPrefabs.cs b/Silksong.ModMenu/Internal/MenuPrefabs.cs index 23dd401..cfc6efb 100644 --- a/Silksong.ModMenu/Internal/MenuPrefabs.cs +++ b/Silksong.ModMenu/Internal/MenuPrefabs.cs @@ -151,6 +151,12 @@ private MenuPrefabs(UIManager uiManager) textInputChild.FindChild("CursorRight")!.GetComponent(), ]; + GameObject underlineObj = new("Underline") { layer = (int)PhysLayers.UI }; + underlineObj.transform.SetParentReset(textInputField.textComponent.transform); + underlineObj.AddComponent(); + underlineObj.RectTransform.FitToParentHorizontal(anchorY: 0); + underlineObj.RectTransform.sizeDelta = new Vector2(0, 3); + sliderTemplate = Object.Instantiate( canvas.FindChild("AudioMenuScreen/Content/MasterVolume")! ); From 4d037e84924b23d25728bbc50036b61201fb4fd7 Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Tue, 5 May 2026 18:22:18 -0400 Subject: [PATCH 03/10] bump version to v0.7.3 --- Silksong.ModMenu/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Silksong.ModMenu/Directory.Build.props b/Silksong.ModMenu/Directory.Build.props index aeb5331..0d58775 100644 --- a/Silksong.ModMenu/Directory.Build.props +++ b/Silksong.ModMenu/Directory.Build.props @@ -11,6 +11,6 @@ It should follow the format major.minor.patch (semantic versioning). If you publish your mod as a library to NuGet, this version will also be used as the package version. --> - 0.7.2 + 0.7.3 From 92e6ff3f3528d19e4ab2efa0f2f3ad974e80158f Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Tue, 5 May 2026 21:53:26 -0400 Subject: [PATCH 04/10] add Rect model, make vector/quaternion/rect parser stricter about brackets --- Silksong.ModMenu/Models/TextModels.cs | 42 ++++++++++++++++++- Silksong.ModMenu/Plugin/ConfigEntryFactory.cs | 25 +++++++++++ .../Tests/ModMenuAutoTestingPlugin.cs | 1 + 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/Silksong.ModMenu/Models/TextModels.cs b/Silksong.ModMenu/Models/TextModels.cs index f70ba59..40f5bdc 100644 --- a/Silksong.ModMenu/Models/TextModels.cs +++ b/Silksong.ModMenu/Models/TextModels.cs @@ -252,7 +252,7 @@ private static bool HexUnparser(Color c, out string x) } #endregion - #region Vectors & Quaternions + #region Vectors & Quaternions & Rects /// /// An ITextModel which parses comma-delimited pairs of numbers to and from s. @@ -278,6 +278,11 @@ public static ParserTextModel ForVector4() => public static ParserTextModel ForQuaternion() => new(QuaternionParser, QuaternionUnparser, INVALID_QUATERNION); + /// + /// An ITextModel which parses comma-delimited quartets of numbers to and from s. + /// + public static ParserTextModel ForRect() => new(RectParser, RectUnparser, INVALID_RECT); + private static readonly Vector4 INVALID_VECTOR = Vector4.positiveInfinity; private static readonly Quaternion INVALID_QUATERNION = new( float.PositiveInfinity, @@ -285,6 +290,12 @@ public static ParserTextModel ForQuaternion() => float.PositiveInfinity, float.PositiveInfinity ); + private static readonly Rect INVALID_RECT = new( + float.PositiveInfinity, + float.PositiveInfinity, + float.PositiveInfinity, + float.PositiveInfinity + ); private static bool Vector2Parser(string x, out Vector2 v) { @@ -338,13 +349,40 @@ private static bool QuaternionUnparser(Quaternion q, out string x) return true; } + private static bool RectParser(string x, out Rect r) + { + bool success = FloatListParser(x, out float[] c) && c.Length == 4; + r = success ? new(c[0], c[1], c[2], c[3]) : INVALID_RECT; + return success; + } + + private static bool RectUnparser(Rect r, out string x) + { + const string format = "F2"; + var provider = CultureInfo.InvariantCulture.NumberFormat; + x = UnityString.Format( + "({0}, {1}, {2}, {3})", + r.x.ToString(format, provider), + r.y.ToString(format, provider), + r.width.ToString(format, provider), + r.height.ToString(format, provider) + ); + return true; + } + /// /// Parses strings of the format "(-0.0, -0.0, ... , -0.0)" with or without brackets /// into an array of floats. /// private static bool FloatListParser(string x, out float[] results) { - string[] rawVals = [.. x.Trim().Trim('(', ')').Split(',').Select(s => s.Trim())]; + x = x.Trim(); + if (x[0] == '(') + x = x[1..]; + if (x[^1] == ')') + x = x[..^1]; + + string[] rawVals = x.Split(','); results = new float[rawVals.Length]; NumberFormatInfo format = CultureInfo.InvariantCulture.NumberFormat; diff --git a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs index 41b4d80..e23e928 100644 --- a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs +++ b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs @@ -808,6 +808,31 @@ public static bool GenerateQuaternionElement( return true; } + /// + /// Generate a text element for a Rect. + /// + public static bool GenerateRectElement( + ConfigEntryBase entry, + [MaybeNullWhen(false)] out MenuElement menuElement + ) + { + if (entry is not ConfigEntry rectEntry) + { + menuElement = default; + return false; + } + + TextInput rect = new( + entry.LabelName(), + TextModels.ForRect(), + entry.DescriptionLine() + ); + rect.SynchronizeWith(rectEntry); + + menuElement = rect; + return true; + } + private record ElementTreeNode { public readonly List<(string path, MenuElement element)> Elements = []; diff --git a/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs b/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs index 8ed13d6..1cf0b74 100644 --- a/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs +++ b/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs @@ -23,6 +23,7 @@ protected override void Setup(ConfigFile config) config.Bind("Unity Types", "Vector3 Option", Vector3.one); config.Bind("Unity Types", "Vector4 Option", Vector4.one); config.Bind("Unity Types", "Quaternion Option", Quaternion.identity); + config.Bind("Unity Types", "Rect Option", Rect.zero); config.Bind("Value Types", "String Option", "value"); config.Bind("Value Types", "Enum Option", TestEnum.EnumOne); From 2ec8480114e7e281c83197d29dc20a77cca20055 Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Wed, 6 May 2026 00:59:15 -0400 Subject: [PATCH 05/10] add GenerateTextInput method to ConfigEntryFactory --- Silksong.ModMenu/Elements/TextInput.cs | 1 + Silksong.ModMenu/Plugin/ConfigEntryFactory.cs | 120 +++--------------- 2 files changed, 21 insertions(+), 100 deletions(-) diff --git a/Silksong.ModMenu/Elements/TextInput.cs b/Silksong.ModMenu/Elements/TextInput.cs index 7d42909..d0910ec 100644 --- a/Silksong.ModMenu/Elements/TextInput.cs +++ b/Silksong.ModMenu/Elements/TextInput.cs @@ -30,6 +30,7 @@ public class TextInput : SelectableValueElement typeof(Vector3), typeof(Vector4), typeof(Quaternion), + typeof(Rect), ]; /// diff --git a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs index e23e928..a09c415 100644 --- a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs +++ b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs @@ -49,6 +49,7 @@ public delegate bool MenuElementGenerator( GenerateVector3Element, GenerateVector4Element, GenerateQuaternionElement, + GenerateRectElement, ]; /// @@ -662,24 +663,7 @@ public static bool GenerateDoubleElement( public static bool GenerateStringElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry stringEntry) - { - menuElement = default; - return false; - } - - TextInput text = new( - entry.LabelName(), - TextModels.ForStrings(), - entry.DescriptionLine() - ); - text.SynchronizeWith(stringEntry); - - menuElement = text; - return true; - } + ) => GenerateTextInput(TextModels.ForStrings, entry, out menuElement); /// /// Generate a text element for a color. @@ -714,24 +698,7 @@ public static bool GenerateColorElement( public static bool GenerateVector2Element( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry vectorEntry) - { - menuElement = default; - return false; - } - - TextInput vector = new( - entry.LabelName(), - TextModels.ForVector2(), - entry.DescriptionLine() - ); - vector.SynchronizeWith(vectorEntry); - - menuElement = vector; - return true; - } + ) => GenerateTextInput(TextModels.ForVector2, entry, out menuElement); /// /// Generate a text element for a Vector3. @@ -739,24 +706,7 @@ public static bool GenerateVector2Element( public static bool GenerateVector3Element( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry vectorEntry) - { - menuElement = default; - return false; - } - - TextInput vector = new( - entry.LabelName(), - TextModels.ForVector3(), - entry.DescriptionLine() - ); - vector.SynchronizeWith(vectorEntry); - - menuElement = vector; - return true; - } + ) => GenerateTextInput(TextModels.ForVector3, entry, out menuElement); /// /// Generate a text element for a Vector4. @@ -764,24 +714,7 @@ public static bool GenerateVector3Element( public static bool GenerateVector4Element( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry vectorEntry) - { - menuElement = default; - return false; - } - - TextInput vector = new( - entry.LabelName(), - TextModels.ForVector4(), - entry.DescriptionLine() - ); - vector.SynchronizeWith(vectorEntry); - - menuElement = vector; - return true; - } + ) => GenerateTextInput(TextModels.ForVector4, entry, out menuElement); /// /// Generate a text element for a Quaternion. @@ -789,24 +722,7 @@ public static bool GenerateVector4Element( public static bool GenerateQuaternionElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry quaternionEntry) - { - menuElement = default; - return false; - } - - TextInput quaternion = new( - entry.LabelName(), - TextModels.ForQuaternion(), - entry.DescriptionLine() - ); - quaternion.SynchronizeWith(quaternionEntry); - - menuElement = quaternion; - return true; - } + ) => GenerateTextInput(TextModels.ForQuaternion, entry, out menuElement); /// /// Generate a text element for a Rect. @@ -814,22 +730,26 @@ public static bool GenerateQuaternionElement( public static bool GenerateRectElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement + ) => GenerateTextInput(TextModels.ForRect, entry, out menuElement); + + /// + /// Generate a text element for a config setting with a value + /// and a model created by the given function. + /// + public static bool GenerateTextInput( + Func> model, + ConfigEntryBase entry, + [MaybeNullWhen(false)] out MenuElement menuElement ) { - if (entry is not ConfigEntry rectEntry) + if (entry is not ConfigEntry typedEntry) { menuElement = default; return false; } - - TextInput rect = new( - entry.LabelName(), - TextModels.ForRect(), - entry.DescriptionLine() - ); - rect.SynchronizeWith(rectEntry); - - menuElement = rect; + TextInput input = new(entry.LabelName(), model(), entry.DescriptionLine()); + input.SynchronizeWith(typedEntry); + menuElement = input; return true; } From 18f38f6d31d2f2249c41ddac8638d2d72533546c Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Wed, 6 May 2026 01:05:26 -0400 Subject: [PATCH 06/10] add supported unity structs to menu generator --- Silksong.ModMenu/Generator/Attributes.cs | 6 ++ Silksong.ModMenuAnalyzers/MenuProperty.cs | 64 ++++++++++++++++++- .../Tests/GeneratorTest.cs | 13 +++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/Silksong.ModMenu/Generator/Attributes.cs b/Silksong.ModMenu/Generator/Attributes.cs index da6dc7b..bad1476 100644 --- a/Silksong.ModMenu/Generator/Attributes.cs +++ b/Silksong.ModMenu/Generator/Attributes.cs @@ -52,6 +52,12 @@ public class ModMenuRangeAttribute(object Min, object Max) : Attribute public readonly object Max = Max; } +/// +/// Attribute to apply to a property, to specify it should only support RGB values with 100% opacity. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] +public class ModMenuRGBAttribute() : Attribute { } + /// /// Attribute to mark a field or property of a data class as requiring its own custom sub-menu, of the parameterized type. /// diff --git a/Silksong.ModMenuAnalyzers/MenuProperty.cs b/Silksong.ModMenuAnalyzers/MenuProperty.cs index ea71ee3..87feba7 100644 --- a/Silksong.ModMenuAnalyzers/MenuProperty.cs +++ b/Silksong.ModMenuAnalyzers/MenuProperty.cs @@ -193,12 +193,24 @@ out var menuRangeAttr ) return false; + if ( + !GetUniqueAttr( + diagnostics, + a => + a.AttributeClass?.ToDisplayString() + == "Silksong.ModMenu.Generator.ModMenuRGBAttribute", + out var menuRgbAttr + ) + ) + return false; + List uniqueAttrs = [ subMenuAttr, elementFactoryAttr, menuOptionsAttr, menuRangeAttr, + menuRgbAttr, ]; if (uniqueAttrs.Count(a => a != null) > 1) { @@ -221,8 +233,14 @@ out var menuRangeAttr && !InitBoolType() && !InitKeyCodeType() && !InitEnumType() - && !InitTextType() - ) + && !InitStringType() + && !InitColorType(menuRgbAttr) + && !InitVector2Type() + && !InitVector3Type() + && !InitVector4Type() + && !InitQuaternionType() + && !InitRectType() + ) { Diagnostics .UnsupportedType(DataType.ToDisplayString()) @@ -355,7 +373,7 @@ private bool InitEnumType() return true; } - private bool InitTextType() + private bool InitStringType() { if (DataType.SpecialType != SpecialType.System_String) return false; @@ -366,6 +384,46 @@ private bool InitTextType() return true; } + private bool InitColorType(AttributeData? rgbAttr) + { + if (DataType.ToDisplayString() != "UnityEngine.Color") + return false; + + var format = rgbAttr != null + ? @" { Format = Silksong.ModMenu.Elements.ColorInput.InputFormat.RGB }" + : ""; + DefaultInitializer.Add( + $@"{Name} = new Silksong.ModMenu.Elements.ColorInput({DisplayName.MakeLiteral()}, {Description.MakeLiteral()}){format};" + ); + return true; + } + + private bool InitVector2Type() => + InitTextTypeHelper("UnityEngine.Vector2", "Silksong.ModMenu.Models.TextModels.ForVector2"); + + private bool InitVector3Type() => + InitTextTypeHelper("UnityEngine.Vector3", "Silksong.ModMenu.Models.TextModels.ForVector3"); + + private bool InitVector4Type() => + InitTextTypeHelper("UnityEngine.Vector4", "Silksong.ModMenu.Models.TextModels.ForVector4"); + + private bool InitQuaternionType() => + InitTextTypeHelper("UnityEngine.Quaternion", "Silksong.ModMenu.Models.TextModels.ForQuaternion"); + + private bool InitRectType() => + InitTextTypeHelper("UnityEngine.Rect", "Silksong.ModMenu.Models.TextModels.ForRect"); + + private bool InitTextTypeHelper(string typeName, string modelFn) + { + if (DataType.ToDisplayString() != typeName) + return false; + + DefaultInitializer.Add( + $@"{Name} = new Silksong.ModMenu.Elements.TextInput<{typeName}>({DisplayName.MakeLiteral()}, {modelFn}(), {Description.MakeLiteral()});" + ); + return true; + } + internal string DefineProperty() { string type; diff --git a/Silksong.ModMenuTesting/Tests/GeneratorTest.cs b/Silksong.ModMenuTesting/Tests/GeneratorTest.cs index a23402f..9984979 100644 --- a/Silksong.ModMenuTesting/Tests/GeneratorTest.cs +++ b/Silksong.ModMenuTesting/Tests/GeneratorTest.cs @@ -3,6 +3,7 @@ using Silksong.ModMenu.Generator; using Silksong.ModMenu.Models; using Silksong.ModMenu.Screens; +using UnityEngine; namespace Silksong.ModMenuTesting.Tests; @@ -65,7 +66,17 @@ public int IntProperty } [ModMenuOptions(2, 3, 5, 7)] - public int PrimeInt; + public int PrimeInt = 2; + + [ModMenuRGB] + public Color RgbColor = Color.cyan; + public Color RgbaColor = Color.green with { a = 0.5f }; + + public Vector2 Vector2Value = Vector2.zero; + public Vector3 Vector3Value = Vector3.zero; + public Vector4 Vector4Value = Vector4.zero; + public Quaternion QuaternionValue = Quaternion.identity; + public Rect RectValue = Rect.zero; [SubMenu] public SubMenuData SubMenu1 = new(); From 2545120b2b85fc1e2dcb016633e2e4074f805c50 Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Fri, 8 May 2026 15:29:00 -0400 Subject: [PATCH 07/10] condense unity type source gen, add RGBElementFactory --- Silksong.ModMenu/Generator/Attributes.cs | 6 -- .../Generator/RGBElementFactory.cs | 14 ++++ Silksong.ModMenuAnalyzers/MenuProperty.cs | 76 +++++-------------- .../Tests/GeneratorTest.cs | 7 +- 4 files changed, 38 insertions(+), 65 deletions(-) create mode 100644 Silksong.ModMenu/Generator/RGBElementFactory.cs diff --git a/Silksong.ModMenu/Generator/Attributes.cs b/Silksong.ModMenu/Generator/Attributes.cs index bad1476..da6dc7b 100644 --- a/Silksong.ModMenu/Generator/Attributes.cs +++ b/Silksong.ModMenu/Generator/Attributes.cs @@ -52,12 +52,6 @@ public class ModMenuRangeAttribute(object Min, object Max) : Attribute public readonly object Max = Max; } -/// -/// Attribute to apply to a property, to specify it should only support RGB values with 100% opacity. -/// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] -public class ModMenuRGBAttribute() : Attribute { } - /// /// Attribute to mark a field or property of a data class as requiring its own custom sub-menu, of the parameterized type. /// diff --git a/Silksong.ModMenu/Generator/RGBElementFactory.cs b/Silksong.ModMenu/Generator/RGBElementFactory.cs new file mode 100644 index 0000000..9967608 --- /dev/null +++ b/Silksong.ModMenu/Generator/RGBElementFactory.cs @@ -0,0 +1,14 @@ +using Silksong.ModMenu.Elements; +using UnityEngine; + +namespace Silksong.ModMenu.Generator; + +/// +/// Generator for a element which only accepts RGB values with full alpha. +/// +public class RGBElementFactory : IElementFactory +{ + /// + public ColorInput CreateElement(LocalizedText name, LocalizedText description) => + new(name, description) { Format = ColorInput.InputFormat.RGB }; +} diff --git a/Silksong.ModMenuAnalyzers/MenuProperty.cs b/Silksong.ModMenuAnalyzers/MenuProperty.cs index 744acd2..2a43fcb 100644 --- a/Silksong.ModMenuAnalyzers/MenuProperty.cs +++ b/Silksong.ModMenuAnalyzers/MenuProperty.cs @@ -44,7 +44,16 @@ private ITypeSymbol DataType "Silksong.ModMenu.Models.ModMenuNameAttribute", }; - private static bool IsRelevant(KnownTypes knownTypes, AttributeData data) + private static readonly IReadOnlyDictionary UnityTypeFactories = new Dictionary() + { + { "UnityEngine.Vector2", "Silksong.ModMenu.Models.TextModels.ForVector2" }, + { "UnityEngine.Vector3", "Silksong.ModMenu.Models.TextModels.ForVector3" }, + { "UnityEngine.Vector4", "Silksong.ModMenu.Models.TextModels.ForVector4" }, + { "UnityEngine.Quaternion", "Silksong.ModMenu.Models.TextModels.ForQuaternion" }, + { "UnityEngine.Rect", "Silksong.ModMenu.Models.TextModels.ForRect" }, + }; + + private static bool IsRelevant(KnownTypes knownTypes, AttributeData data) { if (data.AttributeClass == null) return false; @@ -193,24 +202,12 @@ out var menuRangeAttr ) return false; - if ( - !GetUniqueAttr( - diagnostics, - a => - a.AttributeClass?.ToDisplayString() - == "Silksong.ModMenu.Generator.ModMenuRGBAttribute", - out var menuRgbAttr - ) - ) - return false; - List uniqueAttrs = [ subMenuAttr, elementFactoryAttr, menuOptionsAttr, menuRangeAttr, - menuRgbAttr, ]; if (uniqueAttrs.Count(a => a != null) > 1) { @@ -235,11 +232,7 @@ out var menuRgbAttr && !InitEnumType() && !InitColorType() && !InitTextType() - && !InitVector2Type() - && !InitVector3Type() - && !InitVector4Type() - && !InitQuaternionType() - && !InitRectType() + && !InitUnityType() ) { Diagnostics @@ -395,44 +388,17 @@ private bool InitTextType() return true; } - private bool InitColorType(AttributeData? rgbAttr) + private bool InitUnityType() { - if (DataType.ToDisplayString() != "UnityEngine.Color") - return false; - - var format = rgbAttr != null - ? @" { Format = Silksong.ModMenu.Elements.ColorInput.InputFormat.RGB }" - : ""; - DefaultInitializer.Add( - $@"{Name} = new Silksong.ModMenu.Elements.ColorInput({DisplayName.MakeLiteral()}, {Description.MakeLiteral()}){format};" - ); - return true; - } - - private bool InitVector2Type() => - InitTextTypeHelper("UnityEngine.Vector2", "Silksong.ModMenu.Models.TextModels.ForVector2"); - - private bool InitVector3Type() => - InitTextTypeHelper("UnityEngine.Vector3", "Silksong.ModMenu.Models.TextModels.ForVector3"); - - private bool InitVector4Type() => - InitTextTypeHelper("UnityEngine.Vector4", "Silksong.ModMenu.Models.TextModels.ForVector4"); - - private bool InitQuaternionType() => - InitTextTypeHelper("UnityEngine.Quaternion", "Silksong.ModMenu.Models.TextModels.ForQuaternion"); - - private bool InitRectType() => - InitTextTypeHelper("UnityEngine.Rect", "Silksong.ModMenu.Models.TextModels.ForRect"); - - private bool InitTextTypeHelper(string typeName, string modelFn) - { - if (DataType.ToDisplayString() != typeName) - return false; - - DefaultInitializer.Add( - $@"{Name} = new Silksong.ModMenu.Elements.TextInput<{typeName}>({DisplayName.MakeLiteral()}, {modelFn}(), {Description.MakeLiteral()});" - ); - return true; + string typeName = DataType.ToDisplayString(); + if (UnityTypeFactories.TryGetValue(typeName, out string factory)) + { + DefaultInitializer.Add( + $@"{Name} = new Silksong.ModMenu.Elements.TextInput<{typeName}>({DisplayName.MakeLiteral()}, {factory}(), {Description.MakeLiteral()});" + ); + return true; + } + return false; } internal string DefineProperty() diff --git a/Silksong.ModMenuTesting/Tests/GeneratorTest.cs b/Silksong.ModMenuTesting/Tests/GeneratorTest.cs index 7496eae..e305a41 100644 --- a/Silksong.ModMenuTesting/Tests/GeneratorTest.cs +++ b/Silksong.ModMenuTesting/Tests/GeneratorTest.cs @@ -52,6 +52,9 @@ public class GeneratedData public Color ColorValue = Color.red; + [ElementFactory] + public Color ColorRgbValue = Color.cyan; + [ModMenuName("Custom Bool Name")] public bool BoolValue; @@ -70,10 +73,6 @@ public int IntProperty [ModMenuOptions(2, 3, 5, 7)] public int PrimeInt = 2; - [ModMenuRGB] - public Color RgbColor = Color.cyan; - public Color RgbaColor = Color.green with { a = 0.5f }; - public Vector2 Vector2Value = Vector2.zero; public Vector3 Vector3Value = Vector3.zero; public Vector4 Vector4Value = Vector4.zero; From 1fa425b6c72e029bc091a00e8203bcbb479ab96f Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Fri, 8 May 2026 15:49:42 -0400 Subject: [PATCH 08/10] condense numeric config entry factories --- Silksong.ModMenu/Plugin/ConfigEntryFactory.cs | 280 +++++------------- 1 file changed, 80 insertions(+), 200 deletions(-) diff --git a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs index a09c415..b0c58fd 100644 --- a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs +++ b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs @@ -393,26 +393,14 @@ public static bool GenerateBoolElement( public static bool GenerateSByteElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry sByteEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForSignedBytes(range.MinValue, range.MaxValue) - : TextModels.ForSignedBytes(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(sByteEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForSignedBytes(range.MinValue, range.MaxValue) + : TextModels.ForSignedBytes, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged byte value. @@ -420,26 +408,14 @@ public static bool GenerateSByteElement( public static bool GenerateByteElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry byteEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForBytes(range.MinValue, range.MaxValue) - : TextModels.ForBytes(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(byteEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForBytes(range.MinValue, range.MaxValue) + : TextModels.ForBytes, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged short value. @@ -447,26 +423,14 @@ public static bool GenerateByteElement( public static bool GenerateShortElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry shortEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForShorts(range.MinValue, range.MaxValue) - : TextModels.ForShorts(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(shortEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForShorts(range.MinValue, range.MaxValue) + : TextModels.ForShorts, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged unsigned short value. @@ -474,26 +438,14 @@ public static bool GenerateShortElement( public static bool GenerateUShortElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry uShortEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForUnsignedShorts(range.MinValue, range.MaxValue) - : TextModels.ForUnsignedShorts(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(uShortEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForUnsignedShorts(range.MinValue, range.MaxValue) + : TextModels.ForUnsignedShorts, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged int value. @@ -501,26 +453,14 @@ public static bool GenerateUShortElement( public static bool GenerateIntElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry intEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForIntegers(range.MinValue, range.MaxValue) - : TextModels.ForIntegers(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(intEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForIntegers(range.MinValue, range.MaxValue) + : TextModels.ForIntegers, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged unsigned int value. @@ -528,26 +468,14 @@ public static bool GenerateIntElement( public static bool GenerateUIntElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry uIntEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForUnsignedIntegers(range.MinValue, range.MaxValue) - : TextModels.ForUnsignedIntegers(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(uIntEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForUnsignedIntegers(range.MinValue, range.MaxValue) + : TextModels.ForUnsignedIntegers, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged long value. @@ -555,26 +483,14 @@ public static bool GenerateUIntElement( public static bool GenerateLongElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry longEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForLongs(range.MinValue, range.MaxValue) - : TextModels.ForLongs(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(longEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForLongs(range.MinValue, range.MaxValue) + : TextModels.ForLongs, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged unsigned long value. @@ -582,26 +498,14 @@ public static bool GenerateLongElement( public static bool GenerateULongElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry uLongEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForUnsignedLongs(range.MinValue, range.MaxValue) - : TextModels.ForUnsignedLongs(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(uLongEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForUnsignedLongs(range.MinValue, range.MaxValue) + : TextModels.ForUnsignedLongs, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged float value. @@ -609,26 +513,14 @@ public static bool GenerateULongElement( public static bool GenerateFloatElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry floatEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForFloats(range.MinValue, range.MaxValue) - : TextModels.ForFloats(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(floatEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForFloats(range.MinValue, range.MaxValue) + : TextModels.ForFloats, + entry, + out menuElement + ); /// /// Generates a menu element for a config setting with a free or ranged double value. @@ -636,26 +528,14 @@ public static bool GenerateFloatElement( public static bool GenerateDoubleElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry doubleEntry) - { - menuElement = default; - return false; - } - - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForDoubles(range.MinValue, range.MaxValue) - : TextModels.ForDoubles(); - - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(doubleEntry); - - menuElement = text; - return true; - } + ) => + GenerateTextInput( + (entry.Description.AcceptableValues is AcceptableValueRange range) + ? () => TextModels.ForDoubles(range.MinValue, range.MaxValue) + : TextModels.ForDoubles, + entry, + out menuElement + ); /// /// Generate a text element for an arbitrary string. From 096dfdb8e9330d95cc0fd2f23524e67c689084cd Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Fri, 8 May 2026 15:50:58 -0400 Subject: [PATCH 09/10] bump version to v0.7.5 --- Silksong.ModMenu/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Silksong.ModMenu/Directory.Build.props b/Silksong.ModMenu/Directory.Build.props index 1340085..3f9cd3d 100644 --- a/Silksong.ModMenu/Directory.Build.props +++ b/Silksong.ModMenu/Directory.Build.props @@ -11,6 +11,6 @@ It should follow the format major.minor.patch (semantic versioning). If you publish your mod as a library to NuGet, this version will also be used as the package version. --> - 0.7.4 + 0.7.5 From 64a0f92ee71d4f2f2a21a74fbbde53a1e809d311 Mon Sep 17 00:00:00 2001 From: kaycodes13 Date: Tue, 19 May 2026 14:18:07 -0400 Subject: [PATCH 10/10] undo version bump --- Silksong.ModMenu/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Silksong.ModMenu/Directory.Build.props b/Silksong.ModMenu/Directory.Build.props index 3f9cd3d..1340085 100644 --- a/Silksong.ModMenu/Directory.Build.props +++ b/Silksong.ModMenu/Directory.Build.props @@ -11,6 +11,6 @@ It should follow the format major.minor.patch (semantic versioning). If you publish your mod as a library to NuGet, this version will also be used as the package version. --> - 0.7.5 + 0.7.4