diff --git a/Silksong.ModMenu/Models/TextModels.cs b/Silksong.ModMenu/Models/TextModels.cs index 6e91719..d68e9bb 100644 --- a/Silksong.ModMenu/Models/TextModels.cs +++ b/Silksong.ModMenu/Models/TextModels.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Silksong.ModMenu.Internal; using UnityEngine; @@ -18,35 +19,63 @@ public static ParserTextModel ForStrings() => new(DefaultUnparse, DefaultUnparse); /// - /// An ITextModel which parses its input into an integer. + /// An ITextModel which parses its input into into values clamped between a min and max. /// - public static ParserTextModel ForIntegers() => new(int.TryParse, DefaultUnparse); - - /// - /// An ITextModel which parses its input into an integer clamped between a min and max. - /// - public static ParserTextModel ForIntegers(int min, int max) + /// + /// Only works with numeric value types such as , , etc. + /// + public static ParserTextModel ForNumbers(T min, T max) + where T : struct, IComparable { - var model = ForIntegers(); + var model = ForNumbers(); model.ConstraintFn = RangeConstraint(min, max); return model; } /// - /// An ITextModel which parses its input into a float. - /// - public static ParserTextModel ForFloats() => new(float.TryParse, DefaultUnparse); - - /// - /// An ITextModel which parses its input into a float clamped between a min and max. + /// An ITextModel which parses its input into values. /// - public static ParserTextModel ForFloats(float min, float max) + /// + /// Only works with numeric value types such as , , etc. + /// + public static ParserTextModel ForNumbers() + where T : struct, IComparable { - var model = ForFloats(); - model.ConstraintFn = RangeConstraint(min, max); - return model; + if (!numericParsers.TryGetValue(typeof(T), out var parser)) + throw new ArgumentException($"{typeof(T)} is not a numeric value type."); + + return new((ParserTextModel.Parse)parser.Value, DefaultUnparse); } + private static readonly Dictionary> numericParsers = new Type[] + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + }.ToDictionary( + k => k, + v => new Lazy(() => + v.GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(m => + { + var args = m.GetParameters(); + return m.Name == "TryParse" + && args.Length == 2 + && args[0].ParameterType == typeof(string) + && args[1].IsOut; + }) + .CreateDelegate(typeof(ParserTextModel<>.Parse).MakeGenericType(v)) + ) + ); + private static bool DefaultUnparse(T value, out string text) { text = $"{value}"; diff --git a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs index 71416a0..269394b 100644 --- a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs +++ b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs @@ -33,8 +33,7 @@ public delegate bool MenuElementGenerator( GenerateEnumChoiceElement, GenerateAcceptableValuesChoiceElement, GenerateBoolElement, - GenerateIntElement, - GenerateFloatElement, + GenerateNumberElement, GenerateStringElement, GenerateColorElement, ]; @@ -354,57 +353,151 @@ public static bool GenerateBoolElement( } /// - /// Generates a menu element for a config setting with a free or ranged int value. + /// Generates a menu element for a config setting with a free or ranged numeric value. /// - public static bool GenerateIntElement( + public static bool GenerateNumberElement( ConfigEntryBase entry, [MaybeNullWhen(false)] out MenuElement menuElement ) { - if (entry is not ConfigEntry intEntry) + switch (entry) { - menuElement = default; - return false; - } + case ConfigEntry byteEntry: + TextInput byteText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange byteRange) + ? TextModels.ForNumbers(byteRange.MinValue, byteRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + byteText.SynchronizeWith(byteEntry); + menuElement = byteText; + return true; - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForIntegers(range.MinValue, range.MaxValue) - : TextModels.ForIntegers(); + case ConfigEntry sbyteEntry: + TextInput sbyteText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange sbyteRange) + ? TextModels.ForNumbers(sbyteRange.MinValue, sbyteRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + sbyteText.SynchronizeWith(sbyteEntry); + menuElement = sbyteText; + return true; - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(intEntry); + case ConfigEntry shortEntry: + TextInput shortText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange shortRange) + ? TextModels.ForNumbers(shortRange.MinValue, shortRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + shortText.SynchronizeWith(shortEntry); + menuElement = shortText; + return true; - menuElement = text; - return true; - } + case ConfigEntry ushortEntry: + TextInput ushortText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange ushortRange) + ? TextModels.ForNumbers(ushortRange.MinValue, ushortRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + ushortText.SynchronizeWith(ushortEntry); + menuElement = ushortText; + return true; - /// - /// Generates a menu element for a config setting with a free or ranged float value. - /// - public static bool GenerateFloatElement( - ConfigEntryBase entry, - [MaybeNullWhen(false)] out MenuElement menuElement - ) - { - if (entry is not ConfigEntry floatEntry) - { - menuElement = default; - return false; - } + case ConfigEntry intEntry: + TextInput intText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange intRange) + ? TextModels.ForNumbers(intRange.MinValue, intRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + intText.SynchronizeWith(intEntry); + menuElement = intText; + return true; - var acceptableValues = entry.Description.AcceptableValues; - var model = - (acceptableValues is AcceptableValueRange range) - ? TextModels.ForFloats(range.MinValue, range.MaxValue) - : TextModels.ForFloats(); + case ConfigEntry uintEntry: + TextInput uintText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange uintRange) + ? TextModels.ForNumbers(uintRange.MinValue, uintRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + uintText.SynchronizeWith(uintEntry); + menuElement = uintText; + return true; - TextInput text = new(entry.LabelName(), model, entry.DescriptionLine()); - text.SynchronizeWith(floatEntry); + case ConfigEntry longEntry: + TextInput longText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange longRange) + ? TextModels.ForNumbers(longRange.MinValue, longRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + longText.SynchronizeWith(longEntry); + menuElement = longText; + return true; - menuElement = text; - return true; + case ConfigEntry ulongEntry: + TextInput ulongText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange ulongRange) + ? TextModels.ForNumbers(ulongRange.MinValue, ulongRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + ulongText.SynchronizeWith(ulongEntry); + menuElement = ulongText; + return true; + + case ConfigEntry floatEntry: + TextInput floatText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange floatRange) + ? TextModels.ForNumbers(floatRange.MinValue, floatRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + floatText.SynchronizeWith(floatEntry); + menuElement = floatText; + return true; + + case ConfigEntry doubleEntry: + TextInput doubleText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange doubleRange) + ? TextModels.ForNumbers(doubleRange.MinValue, doubleRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + doubleText.SynchronizeWith(doubleEntry); + menuElement = doubleText; + return true; + + case ConfigEntry decimalEntry: + TextInput decimalText = new( + entry.LabelName(), + (entry.Description.AcceptableValues is AcceptableValueRange decRange) + ? TextModels.ForNumbers(decRange.MinValue, decRange.MaxValue) + : TextModels.ForNumbers(), + entry.DescriptionLine() + ); + decimalText.SynchronizeWith(decimalEntry); + menuElement = decimalText; + return true; + + default: + menuElement = default; + return false; + } } ///