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);
+ }
}