Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 47 additions & 18 deletions Silksong.ModMenu/Models/TextModels.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Silksong.ModMenu.Internal;
using UnityEngine;

Expand All @@ -18,35 +19,63 @@ public static ParserTextModel<string> ForStrings() =>
new(DefaultUnparse<string>, DefaultUnparse<string>);

/// <summary>
/// An ITextModel which parses its input into an integer.
/// An ITextModel which parses its input into into <typeparamref name="T"/> values clamped between a min and max.
/// </summary>
public static ParserTextModel<int> ForIntegers() => new(int.TryParse, DefaultUnparse<int>);

/// <summary>
/// An ITextModel which parses its input into an integer clamped between a min and max.
/// </summary>
public static ParserTextModel<int> ForIntegers(int min, int max)
/// <remarks>
/// Only works with numeric value types such as <see langword="int"/>, <see langword="float"/>, etc.
/// </remarks>
public static ParserTextModel<T> ForNumbers<T>(T min, T max)
where T : struct, IComparable<T>
{
var model = ForIntegers();
var model = ForNumbers<T>();
model.ConstraintFn = RangeConstraint(min, max);
return model;
}

/// <summary>
/// An ITextModel which parses its input into a float.
/// </summary>
public static ParserTextModel<float> ForFloats() => new(float.TryParse, DefaultUnparse<float>);

/// <summary>
/// An ITextModel which parses its input into a float clamped between a min and max.
/// An ITextModel which parses its input into <typeparamref name="T"/> values.
/// </summary>
public static ParserTextModel<float> ForFloats(float min, float max)
/// <remarks>
/// Only works with numeric value types such as <see langword="int"/>, <see langword="float"/>, etc.
/// </remarks>
public static ParserTextModel<T> ForNumbers<T>()
where T : struct, IComparable<T>
{
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<T>.Parse)parser.Value, DefaultUnparse);
}

private static readonly Dictionary<Type, Lazy<Delegate>> 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<Delegate>(() =>
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>(T value, out string text)
{
text = $"{value}";
Expand Down
173 changes: 133 additions & 40 deletions Silksong.ModMenu/Plugin/ConfigEntryFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ public delegate bool MenuElementGenerator(
GenerateEnumChoiceElement,
GenerateAcceptableValuesChoiceElement,
GenerateBoolElement,
GenerateIntElement,
GenerateFloatElement,
GenerateNumberElement,
GenerateStringElement,
GenerateColorElement,
];
Expand Down Expand Up @@ -354,57 +353,151 @@ public static bool GenerateBoolElement(
}

/// <summary>
/// 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.
/// </summary>
public static bool GenerateIntElement(
public static bool GenerateNumberElement(
ConfigEntryBase entry,
[MaybeNullWhen(false)] out MenuElement menuElement
)
{
if (entry is not ConfigEntry<int> intEntry)
switch (entry)
{
menuElement = default;
return false;
}
case ConfigEntry<byte> byteEntry:
TextInput<byte> byteText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<byte> byteRange)
? TextModels.ForNumbers(byteRange.MinValue, byteRange.MaxValue)
: TextModels.ForNumbers<byte>(),
entry.DescriptionLine()
);
byteText.SynchronizeWith(byteEntry);
menuElement = byteText;
return true;

var acceptableValues = entry.Description.AcceptableValues;
var model =
(acceptableValues is AcceptableValueRange<int> range)
? TextModels.ForIntegers(range.MinValue, range.MaxValue)
: TextModels.ForIntegers();
case ConfigEntry<sbyte> sbyteEntry:
TextInput<sbyte> sbyteText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<sbyte> sbyteRange)
? TextModels.ForNumbers(sbyteRange.MinValue, sbyteRange.MaxValue)
: TextModels.ForNumbers<sbyte>(),
entry.DescriptionLine()
);
sbyteText.SynchronizeWith(sbyteEntry);
menuElement = sbyteText;
return true;

TextInput<int> text = new(entry.LabelName(), model, entry.DescriptionLine());
text.SynchronizeWith(intEntry);
case ConfigEntry<short> shortEntry:
TextInput<short> shortText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<short> shortRange)
? TextModels.ForNumbers(shortRange.MinValue, shortRange.MaxValue)
: TextModels.ForNumbers<short>(),
entry.DescriptionLine()
);
shortText.SynchronizeWith(shortEntry);
menuElement = shortText;
return true;

menuElement = text;
return true;
}
case ConfigEntry<ushort> ushortEntry:
TextInput<ushort> ushortText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<ushort> ushortRange)
? TextModels.ForNumbers(ushortRange.MinValue, ushortRange.MaxValue)
: TextModels.ForNumbers<ushort>(),
entry.DescriptionLine()
);
ushortText.SynchronizeWith(ushortEntry);
menuElement = ushortText;
return true;

/// <summary>
/// Generates a menu element for a config setting with a free or ranged float value.
/// </summary>
public static bool GenerateFloatElement(
ConfigEntryBase entry,
[MaybeNullWhen(false)] out MenuElement menuElement
)
{
if (entry is not ConfigEntry<float> floatEntry)
{
menuElement = default;
return false;
}
case ConfigEntry<int> intEntry:
TextInput<int> intText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<int> intRange)
? TextModels.ForNumbers(intRange.MinValue, intRange.MaxValue)
: TextModels.ForNumbers<int>(),
entry.DescriptionLine()
);
intText.SynchronizeWith(intEntry);
menuElement = intText;
return true;

var acceptableValues = entry.Description.AcceptableValues;
var model =
(acceptableValues is AcceptableValueRange<float> range)
? TextModels.ForFloats(range.MinValue, range.MaxValue)
: TextModels.ForFloats();
case ConfigEntry<uint> uintEntry:
TextInput<uint> uintText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<uint> uintRange)
? TextModels.ForNumbers(uintRange.MinValue, uintRange.MaxValue)
: TextModels.ForNumbers<uint>(),
entry.DescriptionLine()
);
uintText.SynchronizeWith(uintEntry);
menuElement = uintText;
return true;

TextInput<float> text = new(entry.LabelName(), model, entry.DescriptionLine());
text.SynchronizeWith(floatEntry);
case ConfigEntry<long> longEntry:
TextInput<long> longText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<long> longRange)
? TextModels.ForNumbers(longRange.MinValue, longRange.MaxValue)
: TextModels.ForNumbers<long>(),
entry.DescriptionLine()
);
longText.SynchronizeWith(longEntry);
menuElement = longText;
return true;

menuElement = text;
return true;
case ConfigEntry<ulong> ulongEntry:
TextInput<ulong> ulongText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<ulong> ulongRange)
? TextModels.ForNumbers(ulongRange.MinValue, ulongRange.MaxValue)
: TextModels.ForNumbers<ulong>(),
entry.DescriptionLine()
);
ulongText.SynchronizeWith(ulongEntry);
menuElement = ulongText;
return true;

case ConfigEntry<float> floatEntry:
TextInput<float> floatText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<float> floatRange)
? TextModels.ForNumbers(floatRange.MinValue, floatRange.MaxValue)
: TextModels.ForNumbers<float>(),
entry.DescriptionLine()
);
floatText.SynchronizeWith(floatEntry);
menuElement = floatText;
return true;

case ConfigEntry<double> doubleEntry:
TextInput<double> doubleText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<double> doubleRange)
? TextModels.ForNumbers(doubleRange.MinValue, doubleRange.MaxValue)
: TextModels.ForNumbers<double>(),
entry.DescriptionLine()
);
doubleText.SynchronizeWith(doubleEntry);
menuElement = doubleText;
return true;

case ConfigEntry<decimal> decimalEntry:
TextInput<decimal> decimalText = new(
entry.LabelName(),
(entry.Description.AcceptableValues is AcceptableValueRange<decimal> decRange)
? TextModels.ForNumbers(decRange.MinValue, decRange.MaxValue)
: TextModels.ForNumbers<decimal>(),
entry.DescriptionLine()
);
decimalText.SynchronizeWith(decimalEntry);
menuElement = decimalText;
return true;

default:
menuElement = default;
return false;
}
}

/// <summary>
Expand Down