diff --git a/.gitignore b/.gitignore index 80750dc..e16dcae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ obj/ bin/ *.user -*.lock.json \ No newline at end of file +*.lock.json +.qodo diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 46f0a16..233f174 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,19 +1,13 @@ -using System; -using BepInEx; +using BepInEx; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; -using Il2CppInterop.Runtime; using MiraAPI.PluginLoading; -using MiraAPI.Roles; -using MonoMod.Utils; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; using Reactor.Utilities; -using TMPro; using UnityEngine; -using UnityEngine.ProBuilder; -using Delegate = Il2CppSystem.Delegate; namespace MiraAPI; @@ -26,9 +20,13 @@ namespace MiraAPI; [ReactorModFlags(ModFlags.RequireOnAllClients)] public partial class MiraApiPlugin : BasePlugin { - public static Color MiraColor { get; } = new Color32(238, 154, 112, 255); - public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255); + internal static Color MiraColor { get; } = new Color32(238, 154, 112, 255); + internal static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255); + private static MiraPluginManager? PluginManager { get; set; } + + internal static ConfigEntry? DistanceSuffix { get; private set; } + internal Harmony Harmony { get; } = new(Id); /// @@ -38,6 +36,8 @@ public partial class MiraApiPlugin : BasePlugin ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow); + DistanceSuffix = Config.Bind("Display", "Distance", "unit(s)", "Dictates how a distance is shown in numeric option displays"); + PluginManager = new MiraPluginManager(); PluginManager.Initialize(); } diff --git a/MiraAPI/Modules/ConversionFailException.cs b/MiraAPI/Modules/ConversionFailException.cs new file mode 100644 index 0000000..8368eee --- /dev/null +++ b/MiraAPI/Modules/ConversionFailException.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiraAPI.Modules; + +/// +/// Denotes a failure in conversion of values. +/// +/// The message to log. +public class ConversionFailException(string message) : Exception(message); diff --git a/MiraAPI/Modules/MultiSelectValue.cs b/MiraAPI/Modules/MultiSelectValue.cs new file mode 100644 index 0000000..e95c906 --- /dev/null +++ b/MiraAPI/Modules/MultiSelectValue.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Utilities; + +namespace MiraAPI.Modules; + +/// +/// Represents a collection of enum values that can be stored and manipulated as a comma-separated string. +/// +/// The enum type that this collection will store. +/// One or more enum values to initialize the collection with. +[Serializable] +public struct MultiSelectValue(params T[] values) : IEnumerable, IEquatable> where T : struct, Enum +{ + /// + /// Gets or sets the underlying comma-separated string representation of enum values. + /// + public string Values + { + readonly get => Helpers.Join(',', values); + set => values = [.. value.TrueSplit(',').Select(Enum.Parse)]; + } + private HashSet values = [.. values]; + + /// + /// Gets the number of values contained within. + /// + public readonly int Count => values.Count; + + /// + /// Adds a single enum value to the end of the collection. + /// + /// The enum value to add. + /// true if successfully added; otherwise, false. + public readonly bool Add(T item) => values.Add(item); + + /// + /// Removes the first occurrence of the specified enum value.
+ /// Returns true if the value was found and removed, false otherwise. + ///
+ /// The enum value to remove. + /// true if successfully removed; otherwise, false. + public readonly bool Remove(T item) => values.Remove(item); + + /// + /// Adds multiple enum values to the end of the collection. + /// + /// The collection of enum values to add. + public readonly void AddRange(IEnumerable items) + { + foreach (var item in items) + values.Add(item); + } + + /// + /// Removes multiple enum values from the collection. + /// + /// The enum values to remove. + /// The number of items successfully removed. + public readonly int RemoveRange(params T[] items) => RemoveRange((IEnumerable)items); + + /// + /// Removes multiple enum values from the collection. + /// + /// The collection of enum values to remove. + /// The number of items successfully removed. + public readonly int RemoveRange(IEnumerable items) => items.Count(Remove); + + /// + /// Determines whether the collection contains the specified enum value. + /// + /// The enum value to locate. + /// true if the item is found in the collection; otherwise, false. + public readonly bool Contains(T item) => values.Contains(item); + + /// + /// Removes all enum values that match the specified predicate condition.
+ /// Iterates through the collection and removes each matching value. + ///
+ /// A function that defines the condition for removing items. + /// The number of items removed from the collection. + public readonly int RemoveAll(Func predicate) + { + var count = 0; + + foreach (var item in this) + { + if (predicate(item) && Remove(item)) + count++; + } + + return count; + } + + /// + /// Removes all enum values from the collection. + /// + public readonly void Clear() => values.Clear(); + + /// + /// Gets the first value in the collection. + /// + /// Returns the first value. + public readonly T First() => values.First(); + + /// + /// Converts the collection to its string representation. + /// + /// A comma-separated string of enum values. + public override readonly string ToString() => Values; + + /// + public override readonly bool Equals(object? obj) => obj is MultiSelectValue other && Equals(other); + + /// + public readonly bool Equals(MultiSelectValue other) => values.SetEquals(other.values); + + /// + public override readonly int GetHashCode() => Values?.GetHashCode() ?? 0; + + /// + public readonly IEnumerator GetEnumerator() => ((IEnumerable)values).GetEnumerator(); + + /// + readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Converts the current instance to its string representation. + /// + /// The value to convert. + public static implicit operator string(MultiSelectValue value) => value.Values; + + /// + /// Converts the current instance to its array representation of values. + /// + /// The value to convert. + public static implicit operator T[](MultiSelectValue value) => [.. value.values]; + + /// + /// Converts the current instance to one singular value. + /// + /// The value to convert. + /// There were either no values, or more than one value. +#pragma warning disable S3877 // Exceptions should not be thrown from unexpected methods + public static implicit operator T(MultiSelectValue value) => + value.Count == 1 + ? value.values.First() + : throw new ConversionFailException($"Tried to convert multiple or no values ({value.Values}) to a singular one"); +#pragma warning restore S3877 // Exceptions should not be thrown from unexpected methods + + /// + /// Converts the value to an instance of MultiSelectValue. + /// + /// The value to convert. + public static implicit operator MultiSelectValue(T value) => new(value); + + /// + /// Converts the array of values to an instance of MultiSelectValue. + /// + /// The values to convert. + public static implicit operator MultiSelectValue(T[] values) => new(values); + + /// + /// Converts the value string to an instance of MultiSelectValue. + /// + /// The values to convert. + public static implicit operator MultiSelectValue(string values) => new([.. values.TrueSplit(',').Select(Enum.Parse)]); + + /// + /// Equality comparison. + /// + /// Left. + /// Right. + public static bool operator ==(MultiSelectValue left, MultiSelectValue right) => left.Equals(right); + + /// + /// Inequality comparison. + /// + /// Left. + /// Right. + public static bool operator !=(MultiSelectValue left, MultiSelectValue right) => !left.Equals(right); +} diff --git a/MiraAPI/Patches/Options/SettingPatches.cs b/MiraAPI/Patches/Options/SettingPatches.cs index 72a0efe..2d22bc3 100644 --- a/MiraAPI/Patches/Options/SettingPatches.cs +++ b/MiraAPI/Patches/Options/SettingPatches.cs @@ -39,6 +39,7 @@ public static class SettingPatches MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo), MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x", MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%", + MiraNumberSuffixes.Distance => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + MiraApiPlugin.DistanceSuffix.Value, _ => TranslationController.Instance.GetString( StringNames.GameSecondsAbbrev, (Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]), diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 513789e..541da92 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,7 +1,9 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using HarmonyLib; using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -37,7 +39,7 @@ public static class Helpers return true; default: { - var num = Random.RandomRangeInt(1, 101); + var num = UnityEngine.Random.RandomRangeInt(1, 101); return num <= probability; } } @@ -375,6 +377,7 @@ public static class Helpers MiraNumberSuffixes.Multiplier => "x", MiraNumberSuffixes.Seconds => "s", MiraNumberSuffixes.Percent => "%", + MiraNumberSuffixes.Distance => MiraApiPlugin.DistanceSuffix?.Value ?? "unit", _ => string.Empty, }; } @@ -397,7 +400,34 @@ public static class Helpers /// The random string. public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") { - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray()); + return new string([.. Enumerable.Repeat(chars, length).Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)])]); + } + + /// + /// Shorthand for that removes empty entries and trims leading and ending spaces. + /// + /// The string to split. + /// The separator. + /// An array of substrings. + public static string[] TrueSplit(this string value, char separator) => value.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + /// + /// Joins a collection of items that are separated by a character. + /// + /// The separator. + /// The collection of items to string together. + /// The type of the items. + /// A string formed by concatenating the ToString results of each item, each separated by a separator. + public static string Join(char separator, IEnumerable items) => Join(separator.ToString(), items); + + /// + public static string Join(string separator, IEnumerable items) + { + if (!items.Any()) + return string.Empty; + + var result = string.Empty; + items.Do(x => result += $"{separator}{x}"); + return result[separator.Length..]; } } diff --git a/MiraAPI/Utilities/MiraNumberSuffixes.cs b/MiraAPI/Utilities/MiraNumberSuffixes.cs index 20ed21c..dcd7c57 100644 --- a/MiraAPI/Utilities/MiraNumberSuffixes.cs +++ b/MiraAPI/Utilities/MiraNumberSuffixes.cs @@ -1,8 +1,32 @@ namespace MiraAPI.Utilities; + +/// +/// An enum to dictate how to represent a numeric value. +/// public enum MiraNumberSuffixes { + /// + /// There should be no suffix. + /// None, + + /// + /// Display as a multiplier. + /// Multiplier, + + /// + /// Display in seconds. + /// Seconds, + + /// + /// Display as a percentage. + /// Percent, + + /// + /// Display in units. + /// + Distance, }