Multi select options (WIP)

This commit is contained in:
AlchlcDvl committed 2025-03-21 16:39:44 -04:00
1 parent 5eba1c9d7f
commit 28636e961d
8 files changed
+292 -9

No files matched your search

+59 -1
View File
@@ -6,18 +6,76 @@ using UnityEngine;
namespace MiraAPI.GameOptions;
/// <summary>
/// Represents a modded game option interface that can be used to create custom game settings.
/// </summary>
public interface IModdedOption
{
/// <summary>
/// Gets the unique identifier of the option.
/// </summary>
uint Id { get; }
/// <summary>
/// Gets the title of the option.
/// </summary>
string Title { get; }
/// <summary>
/// Gets the StringName object of the option.
/// </summary>
StringNames StringName { get; }
/// <summary>
/// Gets or sets the parent mod of the option.
/// </summary>
IMiraPlugin? ParentMod { get; set; }
BaseGameSetting Data { get; }
/// <summary>
/// Gets the BaseGameSetting data of the option.
/// </summary>
BaseGameSetting? Data { get; }
/// <summary>
/// Gets the option behaviour of the option.
/// </summary>
OptionBehaviour? OptionBehaviour { get; }
/// <summary>
/// Gets or sets the visibility of the option.
/// </summary>
Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets the config definition of the option.
/// </summary>
ConfigDefinition? ConfigDefinition { get; set; }
/// <summary>
/// Creates the option behaviour.
/// </summary>
/// <param name="toggleOpt">The ToggleOption prefab.</param>
/// <param name="numberOpt">The NumberOption prefab.</param>
/// <param name="stringOpt">The StringOption prefab.</param>
/// <param name="container">The options container.</param>
/// <returns>A new OptionBehaviour for this modded option.</returns>
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
/// <summary>
/// Gets the float data of the option.
/// </summary>
/// <returns>A float object representing the option's value.</returns>
float GetFloatData();
/// <summary>
/// Gets the net data of the option.
/// </summary>
/// <returns>A NetData object representing this option's data.</returns>
NetData GetNetData();
/// <summary>
/// Handles incoming net data.
/// </summary>
/// <param name="data">The NetData's byte array.</param>
void HandleNetData(byte[] data);
}
@@ -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;
}
@@ -116,7 +116,7 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : struct, Enum
/// Initializes a new instance of the <see cref="ModdedEnumOption{T}"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue)
{
@@ -167,19 +167,19 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : struct, Enum
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue((T)Enum.Parse(typeof(T), Encoding.Unicode.GetString(data)));
SetValue(Enum.Parse<T>(Encoding.Unicode.GetString(data)));
}
/// <inheritdoc />
public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
return Enum.Parse<T>(optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
}
/// <inheritdoc />
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;
@@ -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;
/// <summary>
/// An option for selecting an enum value.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
public class ModdedMultiSelectOption<T> : ModdedOption<MultiSelectValue<T>> where T : struct, Enum
{
/// <summary>
/// Gets the string values of the enum.
/// </summary>
public string[]? Values { get; }
/// <summary>
/// Gets a value indicating "all of the above".
/// </summary>
public T All { get; }
/// <summary>
/// Gets a value indicating "none of the above".
/// </summary>
public T None { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedMultiSelectOption{T}"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="all">The all value.</param>
/// <param name="none">The none value.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedMultiSelectOption(string title, T defaultValue, T all, T none, string[]? values = null) : base(title, defaultValue)
{
Values = values ?? Enum.GetNames<T>();
All = all;
None = none;
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = (values is null
? Enum.GetNames<T>()
: values)
.Select(CustomStringName.CreateAndRegister).ToArray();
data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
}
/// <inheritdoc />
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<OptionBehaviour>)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<StringNames>(0);
stringOption.Value = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
OptionBehaviour = stringOption;
return stringOption;
}
/// <inheritdoc />
public override float GetFloatData()
{
return Convert.ToSingle(Value, NumberFormatInfo.InvariantInfo);
}
/// <inheritdoc />
public override NetData GetNetData()
{
return new NetData(Id, Value.ToBytes());
}
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue(MultiSelectValue<T>.FromBytes(data));
}
/// <inheritdoc />
public override MultiSelectValue<T> GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return []; // TODO: Add implementation
}
/// <inheritdoc />
protected override void OnValueChanged(MultiSelectValue<T> 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);
}
}
}
@@ -12,7 +12,7 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// Represents a modded option.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
public abstract class ModdedOption<T> : IModdedOption
public abstract class ModdedOption<T> : IModdedOption where T : struct
{
private IMiraPlugin? _parentMod;
@@ -34,7 +34,7 @@ public abstract class ModdedOption<T> : IModdedOption
/// <summary>
/// Gets the BaseGameSetting data of the option.
/// </summary>
public BaseGameSetting Data { get; protected init; }
public BaseGameSetting? Data { get; protected init; }
/// <summary>
/// Gets or sets the parent mod of the option.
@@ -120,7 +120,7 @@ public abstract class ModdedOption<T> : IModdedOption
var oldVal = Value;
Value = newValue;
if (Value?.Equals(oldVal) == false)
if (!Value.Equals(oldVal))
{
ChangedEvent?.Invoke(Value);
}
+22
View File
@@ -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<StringNames> Values;
}
+20 -1
View File
@@ -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;
/// <typeparam name="T">The enum type that this collection will store.</typeparam>
/// <param name="values">One or more enum values to initialize the collection with.</param>
[Serializable]
public struct MultiSelectValue<T>(params T[] values) : IEnumerable<T>, IEquatable<MultiSelectValue<T>> where T : struct, Enum
public struct MultiSelectValue<T>(params T[] values) : IEnumerable<T>, IEquatable<MultiSelectValue<T>>, IDisposable where T : struct, Enum
{
/// <summary>
/// Gets or sets the underlying comma-separated string representation of enum values.
@@ -105,6 +108,12 @@ public struct MultiSelectValue<T>(params T[] values) : IEnumerable<T>, IEquatabl
/// <returns>Returns the first value.</returns>
public readonly T First() => values.First();
/// <summary>
/// Serializes the current instance to an array of bytes.
/// </summary>
/// <returns>An array of bytes representing the current instance.</returns>
public readonly byte[] ToBytes() => [.. values.Select(Helpers.EnumToBytes).SelectMany(x => x)];
/// <summary>
/// Converts the collection to its string representation.
/// </summary>
@@ -126,6 +135,16 @@ public struct MultiSelectValue<T>(params T[] values) : IEnumerable<T>, IEquatabl
/// <inheritdoc/>
readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
public readonly void Dispose() => values.Clear();
/// <summary>
/// Creates an instance of <see cref="MultiSelectValue{T}"/> from the provided byte array.
/// </summary>
/// <param name="bytes">The bytes to deserialize from.</param>
/// <returns>An instance of <see cref="MultiSelectValue{T}"/>.</returns>
public static MultiSelectValue<T> FromBytes(byte[] bytes) => new([.. Helpers.EnumsFromBytes<T>(bytes)]);
/// <summary>
/// Converts the current instance to its string representation.
/// </summary>
+66
View File
@@ -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..];
}
/// <summary>
/// Serializes an enum to an array of bytes.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="value">The value to serialize.</param>
/// <returns>An array of bytes representing the enum value.</returns>
/// <exception cref="ArgumentException">The enum type had an unknown underlying type.</exception>
public static byte[] EnumToBytes<T>(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<T>(byte[] bytes, int offset) where T : struct, Enum => BytesToEnum<T>(bytes, offset, typeof(T).GetEnumUnderlyingType());
public static T BytesToEnum<T>(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<T> EnumsFromBytes<T>(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<T>(bytes, i, underlyingType);
}
}