diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index f40f3cf..2679f46 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -6,18 +6,76 @@ using UnityEngine; namespace MiraAPI.GameOptions; +/// +/// Represents a modded game option interface that can be used to create custom game settings. +/// public interface IModdedOption { + /// + /// Gets the unique identifier of the option. + /// uint Id { get; } + + /// + /// Gets the title of the option. + /// string Title { get; } + + /// + /// Gets the StringName object of the option. + /// StringNames StringName { get; } + + /// + /// Gets or sets the parent mod of the option. + /// IMiraPlugin? ParentMod { get; set; } - BaseGameSetting Data { get; } + + /// + /// Gets the BaseGameSetting data of the option. + /// + BaseGameSetting? Data { get; } + + /// + /// Gets the option behaviour of the option. + /// OptionBehaviour? OptionBehaviour { get; } + + /// + /// Gets or sets the visibility of the option. + /// Func Visible { get; set; } + + /// + /// Gets or sets the config definition of the option. + /// ConfigDefinition? ConfigDefinition { get; set; } + + /// + /// Creates the option behaviour. + /// + /// The ToggleOption prefab. + /// The NumberOption prefab. + /// The StringOption prefab. + /// The options container. + /// A new OptionBehaviour for this modded option. OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); + + /// + /// Gets the float data of the option. + /// + /// A float object representing the option's value. float GetFloatData(); + + /// + /// Gets the net data of the option. + /// + /// A NetData object representing this option's data. NetData GetNetData(); + + /// + /// Handles incoming net data. + /// + /// The NetData's byte array. void HandleNetData(byte[] data); } diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 7aaa154..d385686 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -44,9 +44,11 @@ public static class ModdedOptionsManager TypeToGroup.Add(type, group); pluginInfo.OptionGroups.Add(group); +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields typeof(OptionGroupSingleton<>).MakeGenericType(type) .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! .SetValue(null, group); +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields return true; } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 4a92267..8b4321c 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -116,7 +116,7 @@ public class ModdedEnumOption : ModdedOption where T : struct, Enum /// Initializes a new instance of the class. /// /// The title of the option. - /// The default value as an int. + /// The default value. /// An option list of string values to use in place of the enum name. public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue) { @@ -167,19 +167,19 @@ public class ModdedEnumOption : ModdedOption where T : struct, Enum /// public override void HandleNetData(byte[] data) { - SetValue((T)Enum.Parse(typeof(T), Encoding.Unicode.GetString(data))); + SetValue(Enum.Parse(Encoding.Unicode.GetString(data))); } /// public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { - return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo)); + return Enum.Parse(optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo)); } /// protected override void OnValueChanged(T newValue) { - HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)), false); + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data!.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)), false); if (!OptionBehaviour) { return; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedMultiSelectOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedMultiSelectOption.cs new file mode 100644 index 0000000..27771d3 --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedMultiSelectOption.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using System.Linq; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.Modules; +using MiraAPI.Networking; +using Reactor.Localization.Utilities; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// An option for selecting an enum value. +/// +/// The enum type. +public class ModdedMultiSelectOption : ModdedOption> where T : struct, Enum +{ + /// + /// Gets the string values of the enum. + /// + public string[]? Values { get; } + + /// + /// Gets a value indicating "all of the above". + /// + public T All { get; } + + /// + /// Gets a value indicating "none of the above". + /// + public T None { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The default value as an int. + /// The all value. + /// The none value. + /// An option list of string values to use in place of the enum name. + public ModdedMultiSelectOption(string title, T defaultValue, T all, T none, string[]? values = null) : base(title, defaultValue) + { + Values = values ?? Enum.GetNames(); + All = all; + None = none; + Data = ScriptableObject.CreateInstance(); + var data = (StringGameSetting)Data; + + data.Title = StringName; + data.Type = global::OptionTypes.String; + data.Values = (values is null + ? Enum.GetNames() + : values) + .Select(CustomStringName.CreateAndRegister).ToArray(); + + data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); + } + + /// + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + { + var stringOption = Object.Instantiate(stringOpt, container); + + stringOption.SetUpFromData(Data, 20); + stringOption.OnValueChanged = (Action)ValueChanged; + + // SetUpFromData method doesn't work correctly so we must set the values manually + stringOption.Title = StringName; + stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); + stringOption.Value = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); + + OptionBehaviour = stringOption; + + return stringOption; + } + + /// + public override float GetFloatData() + { + return Convert.ToSingle(Value, NumberFormatInfo.InvariantInfo); + } + + /// + public override NetData GetNetData() + { + return new NetData(Id, Value.ToBytes()); + } + + /// + public override void HandleNetData(byte[] data) + { + SetValue(MultiSelectValue.FromBytes(data)); + } + + /// + public override MultiSelectValue GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return []; // TODO: Add implementation + } + + /// + protected override void OnValueChanged(MultiSelectValue newValue) + { + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data!.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)), false); + if (!OptionBehaviour) + { + return; + } + + if (OptionBehaviour is StringOption opt) + { + opt.Value = Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo); + } + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 670fb3c..7e6f5cf 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -12,7 +12,7 @@ namespace MiraAPI.GameOptions.OptionTypes; /// Represents a modded option. /// /// The value type. -public abstract class ModdedOption : IModdedOption +public abstract class ModdedOption : IModdedOption where T : struct { private IMiraPlugin? _parentMod; @@ -34,7 +34,7 @@ public abstract class ModdedOption : IModdedOption /// /// Gets the BaseGameSetting data of the option. /// - public BaseGameSetting Data { get; protected init; } + public BaseGameSetting? Data { get; protected init; } /// /// Gets or sets the parent mod of the option. @@ -120,7 +120,7 @@ public abstract class ModdedOption : IModdedOption var oldVal = Value; Value = newValue; - if (Value?.Equals(oldVal) == false) + if (!Value.Equals(oldVal)) { ChangedEvent?.Invoke(Value); } diff --git a/MiraAPI/Modules/MultiSelectGameSetting.cs b/MiraAPI/Modules/MultiSelectGameSetting.cs new file mode 100644 index 0000000..be6ee2d --- /dev/null +++ b/MiraAPI/Modules/MultiSelectGameSetting.cs @@ -0,0 +1,22 @@ +#pragma warning disable // TODO: Remove later before completing the pr +using System; +using AmongUs.GameOptions; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Reactor.Utilities.Attributes; + +namespace MiraAPI.Modules; + +[RegisterInIl2Cpp, Serializable] +public class MultiSelectGameSetting : BaseGameSetting +{ + public MultiSelectGameSetting() => Type = OptionTypes.MultipleChoice; // How nice of the game to provide an enum value themselves + + public override string GetValueString(float value) + { + return TranslationController.Instance.GetString(default(StringNames), null, null); + } + + public Int32OptionNames OptionName; + + public Il2CppStructArray Values; +} diff --git a/MiraAPI/Modules/MultiSelectValue.cs b/MiraAPI/Modules/MultiSelectValue.cs index e95c906..4f5f579 100644 --- a/MiraAPI/Modules/MultiSelectValue.cs +++ b/MiraAPI/Modules/MultiSelectValue.cs @@ -1,8 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; +using HarmonyLib; using MiraAPI.Utilities; +using MonoMod.Utils; namespace MiraAPI.Modules; @@ -12,7 +15,7 @@ namespace MiraAPI.Modules; /// 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 +public struct MultiSelectValue(params T[] values) : IEnumerable, IEquatable>, IDisposable where T : struct, Enum { /// /// Gets or sets the underlying comma-separated string representation of enum values. @@ -105,6 +108,12 @@ public struct MultiSelectValue(params T[] values) : IEnumerable, IEquatabl /// Returns the first value. public readonly T First() => values.First(); + /// + /// Serializes the current instance to an array of bytes. + /// + /// An array of bytes representing the current instance. + public readonly byte[] ToBytes() => [.. values.Select(Helpers.EnumToBytes).SelectMany(x => x)]; + /// /// Converts the collection to its string representation. /// @@ -126,6 +135,16 @@ public struct MultiSelectValue(params T[] values) : IEnumerable, IEquatabl /// readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// + public readonly void Dispose() => values.Clear(); + + /// + /// Creates an instance of from the provided byte array. + /// + /// The bytes to deserialize from. + /// An instance of . + public static MultiSelectValue FromBytes(byte[] bytes) => new([.. Helpers.EnumsFromBytes(bytes)]); + /// /// Converts the current instance to its string representation. /// diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 541da92..03a9823 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using HarmonyLib; +using MonoMod.Utils; using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -430,4 +431,69 @@ public static class Helpers items.Do(x => result += $"{separator}{x}"); return result[separator.Length..]; } + + /// + /// Serializes an enum to an array of bytes. + /// + /// The enum type. + /// The value to serialize. + /// An array of bytes representing the enum value. + /// The enum type had an unknown underlying type. + public static byte[] EnumToBytes(T value) where T : struct, Enum + { + var enumType = typeof(T); + var underlyingType = enumType.GetEnumUnderlyingType(); + + if (underlyingType == typeof(byte)) + return [Convert.ToByte(value, NumberFormatInfo.InvariantInfo)]; + else if (underlyingType == typeof(sbyte)) + return [(byte)Convert.ToSByte(value, NumberFormatInfo.InvariantInfo)]; + else if (underlyingType == typeof(short)) + return BitConverter.GetBytes(Convert.ToInt16(value, NumberFormatInfo.InvariantInfo)); + else if (underlyingType == typeof(ushort)) + return BitConverter.GetBytes(Convert.ToUInt16(value, NumberFormatInfo.InvariantInfo)); + else if (underlyingType == typeof(int)) + return BitConverter.GetBytes(Convert.ToInt32(value, NumberFormatInfo.InvariantInfo)); + else if (underlyingType == typeof(uint)) + return BitConverter.GetBytes(Convert.ToUInt32(value, NumberFormatInfo.InvariantInfo)); + else if (underlyingType == typeof(long)) + return BitConverter.GetBytes(Convert.ToInt64(value, NumberFormatInfo.InvariantInfo)); + else if (underlyingType == typeof(ulong)) + return BitConverter.GetBytes(Convert.ToUInt64(value, NumberFormatInfo.InvariantInfo)); + else + throw new ArgumentException("Unknown underlying type for " + enumType.Name); + } + + public static T BytesToEnum(byte[] bytes, int offset) where T : struct, Enum => BytesToEnum(bytes, offset, typeof(T).GetEnumUnderlyingType()); + + public static T BytesToEnum(byte[] bytes, int offset, Type underlyingType) where T : struct, Enum // This version exists purely for performance and optimisation + { + var enumType = typeof(T); + + if (underlyingType == typeof(byte) || underlyingType == typeof(sbyte)) + return (T)(object)bytes[offset]; + else if (underlyingType == typeof(short)) + return (T)(object)BitConverter.ToInt16(bytes, offset); + else if (underlyingType == typeof(ushort)) + return (T)(object)BitConverter.ToUInt16(bytes, offset); + else if (underlyingType == typeof(int)) + return (T)(object)BitConverter.ToInt32(bytes, offset); + else if (underlyingType == typeof(uint)) + return (T)(object)BitConverter.ToUInt32(bytes, offset); + else if (underlyingType == typeof(long)) + return (T)(object)BitConverter.ToInt64(bytes, offset); + else if (underlyingType == typeof(ulong)) + return (T)(object)BitConverter.ToUInt64(bytes, offset); + else + throw new ArgumentException("Unknown underlying type for " + enumType.Name); + } + + public static IEnumerable EnumsFromBytes(byte[] bytes) where T : struct, Enum + { + var underlyingType = typeof(T).GetEnumUnderlyingType(); + var size = underlyingType.GetManagedSize(); + + for (var i = 0; i < bytes.Length; i += size) + yield return BytesToEnum(bytes, i, underlyingType); + } }