Compare commits

...
32 Commits
Author SHA1 Message Date
Atony 5049d16ac9 Fix errors 2025-10-26 14:17:11 -05:00
Atony dccd06eac3 Merge branch 'new-features' into options-changes 2025-10-26 13:54:56 -05:00
Atony bf9b34e728 Fix modded keybinds not being hidden on buttons 2025-10-26 13:36:35 -05:00
Atony baf80618bb Rename keybinds option 2025-10-26 13:19:22 -05:00
Atony 7d1976f772 Relocale keybinds toggle to local options for epic 2025-10-26 13:15:20 -05:00
Atony 4ce24c2a00 Fix warning on epic games 2025-10-26 12:20:02 -05:00
Atony 7fa75946fd Make ResetCooldowns callable 2025-10-25 18:51:14 -05:00
Atony 43f92d4dc2 Standadize button position 2025-10-25 12:17:44 -05:00
Atony ee22b776eb Fix Vanilla view pane 2025-10-20 18:23:13 -05:00
Atony f31349f6e8 Add shift-intervals for options 2025-10-20 11:17:51 -05:00
Atony 448e2f9152 Adjust size and scale 2025-10-18 13:27:33 -05:00
Atony dde7e70406 Fix category names 2025-10-18 12:32:34 -05:00
Atony 713a7a2bcc Work on adding custom game tabs 2025-10-18 12:19:15 -05:00
Atony 4b144a782d Fix Logger error 2025-10-14 13:53:02 -05:00
Atony 7a8d608b17 Bump Version (again) 2025-10-14 13:46:08 -05:00
XtraCube 6503bbecb7 use reactor static logging pattern 2025-10-14 14:37:20 -04:00
XtraCube cb794c3fc2 make kill anim patch update resilient 2025-10-14 14:35:20 -04:00
XtraCube 88329bef79 update freeplay patch to be version resistant 2025-10-14 14:14:25 -04:00
XtraCube eb7abecd75 update to latest among us 2025-10-14 14:06:43 -04:00
AlchlcDvl 2c2a2207c5 Fix build error 2025-04-19 10:20:18 -04:00
AS 960573243c Merge branch 'dev' into options-changes 2025-04-18 21:45:04 -04:00
AlchlcDvl 94bb5a7a37 Add settings size modifications 2025-03-22 00:04:39 -04:00
XtraCube 5eba77dba9 some improvements (still broken) 2025-03-21 18:01:42 -04:00
XtraCube ef329bfcbd cleanup 2025-03-21 17:40:07 -04:00
XtraCube a95cac055f Merge remote-tracking branch 'origin/options-changes' into options-changes 2025-03-21 17:12:44 -04:00
XtraCube 7909b8f13d improve example 2025-03-21 17:12:39 -04:00
AS 8ad2e220c9 Merge branch 'dev' into options-changes 2025-03-21 17:12:21 -04:00
AlchlcDvl 40d88750da Some renames and documentation and other things 2025-03-21 17:00:24 -04:00
AlchlcDvl 28636e961d Multi select options (WIP) 2025-03-21 16:39:44 -04:00
AlchlcDvl 5eba1c9d7f Some more modded enum option changes 2025-03-19 15:14:31 -04:00
AlchlcDvl 7cbf9ffe1b Fix up enum options a bit 2025-03-19 15:08:12 -04:00
AlchlcDvl 38ec50bcc7 Add new value type + number suffix 2025-03-19 14:53:36 -04:00
70 changed files with 1567 additions and 221 deletions

No files matched your search

+2 -1
View File
@@ -6,4 +6,5 @@ bin/
.qodo/
*.user
*.lock.json
*.lock.json
.qodo
+1 -1
View File
@@ -2,7 +2,7 @@
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.9.9" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.10.14" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
+1 -1
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.3.2</VersionPrefix>
<VersionPrefix>0.3.3</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
+6 -3
View File
@@ -19,11 +19,14 @@ public class ExampleOptions : AbstractOptionGroup
[ModdedNumberOption("Number Opt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)]
public float NumberOpt { get; set; } = 4f;
[ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
public BestApi Opt { get; set; } = BestApi.MiraAPI;
[ModdedEnumOption("Best API", typeof(AuApis), ["Mira API", "Mitochondria", "Reactor"])]
public AuApis BestApiOpt { get; set; } = AuApis.MiraAPI;
[ModdedEnumOption<AuApis>("Worst API", ["Mira API", "Mitochondria", "Reactor"])]
public AuApis WorstApiOpt { get; set; } = AuApis.Mitochondria;
}
public enum BestApi
public enum AuApis
{
MiraAPI,
Mitochondria,
@@ -23,6 +23,12 @@ public class ExampleOptions2 : AbstractOptionGroup
{
ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"),
};
public ModdedMultiSelectOption<TestingData> MultiSelectOption { get; } = new(
"Emotions",
TestingData.Happy,
TestingData.All,
TestingData.None);
}
public enum TestingData : ulong
@@ -30,4 +36,6 @@ public enum TestingData : ulong
Happy,
Sad,
Neutral,
All,
None,
}
+2 -2
View File
@@ -34,7 +34,7 @@ public static class MiraEventManager
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
}
}
@@ -63,7 +63,7 @@ public static class MiraEventManager
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
}
}
+2 -2
View File
@@ -30,13 +30,13 @@ public static class GameOverManager
if (gameOverType.IsAbstract)
{
Logger<MiraApiPlugin>.Error("The type must not be abstract.");
Error("The type must not be abstract.");
return false;
}
if (gameOverType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error("The type must have a parameterless constructor.");
Error("The type must have a parameterless constructor.");
return false;
}
+4 -4
View File
@@ -37,7 +37,7 @@ public static class CustomGameModeManager
return;
}
Logger<MiraApiPlugin>.Error($"No gamemode with id {id} found!");
Error($"No gamemode with id {id} found!");
}
/// <summary>
@@ -48,7 +48,7 @@ public static class CustomGameModeManager
{
if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType))
{
Logger<MiraApiPlugin>.Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
return;
}
@@ -56,13 +56,13 @@ public static class CustomGameModeManager
if (modeObj is not CustomGameMode gameMode)
{
Logger<MiraApiPlugin>.Error($"Failed to create instance of {gameModeType.Name}");
Error($"Failed to create instance of {gameModeType.Name}");
return;
}
if (GameModes.Any(x => x.Key == gameMode.Id))
{
Logger<MiraApiPlugin>.Error($"ID for gamemode {gameMode.Name} already exists!");
Error($"ID for gamemode {gameMode.Name} already exists!");
return;
}
+36 -1
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MiraAPI.Modifiers;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -28,11 +29,16 @@ public abstract class AbstractOptionGroup
public virtual Type? OptionableType => null;
/// <summary>
/// Gets a value indicating whether the group should be shown in the modifiers menu.
/// Gets a value indicating whether the group should be shown in the modifiers menu. This is deprecated, please use ParentMenu!
/// </summary>
// TODO: make this not a boolean
public virtual bool ShowInModifiersMenu => false;
/// <summary>
/// Gets a value indicating which menu the group is in.
/// </summary>
public virtual MenuCategory ParentMenu => MenuCategory.Roles;
/// <summary>
/// Gets the function that determines whether the group should be visible or not.
/// </summary>
@@ -62,4 +68,33 @@ public abstract class AbstractOptionGroup<T> : AbstractOptionGroup where T : IOp
{
/// <inheritdoc />
public override Type OptionableType => typeof(T);
/// <summary>
/// Gets a value indicating which menu the group is in.
/// </summary>
public override MenuCategory ParentMenu
{
get
{
var type = typeof(T);
if (type == typeof(BaseModifier))
{
return MenuCategory.Modifiers;
}
else if (type == typeof(RoleBehaviour))
{
return MenuCategory.Roles;
}
return MenuCategory.Game;
}
}
}
public enum MenuCategory
{
Game,
Roles,
Modifiers,
CustomOne,
CustomTwo
}
@@ -32,3 +32,33 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption");
}
}
/// <summary>
/// Generic attribute for creating an enum option.
/// </summary>
/// <typeparam name="T">An enum type.</typeparam>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
: ModdedOptionAttribute(title) where T : struct, Enum
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var opt = new ModdedEnumOption<T>(Title, (T)(value ?? 0), values);
return opt;
}
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedEnumOption;
opt?.SetValue((int)value);
}
/// <inheritdoc />
public override object GetValue()
{
return HolderOption is ModdedEnumOption<T> opt
? Enum.ToObject(typeof(T), opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedEnumOption<{typeof(T).Name}>");
}
}
@@ -21,7 +21,15 @@ public class ModdedNumberOptionAttribute(
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity);
return new ModdedNumberOption(
Title,
(float)(value ?? min + increment),
min,
max,
increment,
suffixType,
formatString,
zeroInfinity);
}
/// <inheritdoc />
+5
View File
@@ -92,6 +92,11 @@ public interface IModdedOption
/// <param name="saveDefault">Indicates whether to save the default value instead of the current value.</param>
void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
/// <summary>
/// Changes the scales and positions of the option if it's attached to the game options menu.
/// </summary>
void ChangeGameSetting();
/// <summary>
/// Binds the option to a configuration file.
/// </summary>
@@ -0,0 +1,28 @@
using System;
using MiraAPI.GameOptions.OptionTypes;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.GameOptions;
/// <summary>
/// Component added onto options generated via MiraAPI. This allows developers to hook patches onto options in general, without using a list.
/// </summary>
[RegisterInIl2Cpp]
public class MiraNumberOptionComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets or sets the modded option associated with the object.
/// </summary>
public ModdedNumberOption NumberOption { get; set; }
/// <summary>
/// Gets or sets the default increment.
/// </summary>
public float DefaultIncrement { get; set; }
/// <summary>
/// Gets or sets the value of whether or not holding shift will split the increment in half.
/// </summary>
public bool ShiftIncrementToggle { get; set; }
}
+8 -6
View File
@@ -39,7 +39,7 @@ public static class ModdedOptionsManager
if (TypeToGroup.ContainsKey(type))
{
Logger<MiraApiPlugin>.Error($"Group {type.Name} already exists.");
Error($"Group {type.Name} already exists.");
return false;
}
@@ -47,11 +47,13 @@ public static class ModdedOptionsManager
TypeToGroup.Add(type, group);
pluginInfo.InternalOptionGroups.Add(group);
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
typeof(OptionGroupSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, group);
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
return true;
}
@@ -60,13 +62,13 @@ public static class ModdedOptionsManager
{
if (!TypeToGroup.TryGetValue(type, out var group))
{
Logger<MiraApiPlugin>.Error($"Failed to get group for {type.Name}");
Error($"Failed to get group for {type.Name}");
return;
}
if (property.GetValue(group) is not IModdedOption option)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
Error($"Failed to get option for {property.Name}");
return;
}
@@ -81,13 +83,13 @@ public static class ModdedOptionsManager
{
if (OptionAttributes.ContainsKey(property))
{
Logger<MiraApiPlugin>.Error($"Property {property.Name} already has an attribute registered.");
Error($"Property {property.Name} already has an attribute registered.");
return;
}
if (!TypeToGroup.TryGetValue(type, out var group))
{
Logger<MiraApiPlugin>.Error($"Failed to get group for {type.Name}");
Error($"Failed to get group for {type.Name}");
return;
}
@@ -95,7 +97,7 @@ public static class ModdedOptionsManager
if (option == null)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
Error($"Failed to get option for {property.Name}");
return;
}
@@ -47,11 +47,17 @@ public class ModdedEnumOption : ModdedOption<int>
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.name =
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
// SetUpFromData method doesnt work correctly so we must set the values manually
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// 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 = Value;
@@ -61,6 +67,32 @@ public class ModdedEnumOption : ModdedOption<int>
return stringOption;
}
/// <inheritdoc />
public override void ChangeGameSetting()
{
var stringOption = OptionBehaviour as StringOption;
stringOption!.MinusBtn.transform.localPosition += new Vector3(0.6f, 0f, 0f);
stringOption.PlusBtn.transform.localPosition += new Vector3(1.5f, 0f, 0f);
// The value box itself becomes the button
stringOption.PlusBtn.gameObject.SetActive(false);
stringOption.MinusBtn.gameObject.SetActive(false);
var background = stringOption.transform.GetChild(0);
background.localPosition += new Vector3(-0.8f, 0f, 0f);
background.localScale += new Vector3(1f, 0f, 0f);
var title = stringOption.TitleText;
title.transform.localPosition = new(-2.0466f, 0f, -2.9968f);
title.GetComponent<RectTransform>().sizeDelta = new(5.8f, 0.458f);
title.fontSize = 2.9f; // Why is it different for string options??
var valueBox = stringOption.transform.GetChild(5);
valueBox.localPosition += new Vector3(1.05f, 0f, 0f);
valueBox.localScale += new Vector3(0.2f, 0f, 0f);
}
/// <inheritdoc />
public override float GetFloatData()
{
@@ -105,7 +137,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// An option for selecting an enum value.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
public class ModdedEnumOption<T> : ModdedOption<T> where T : struct, Enum
{
/// <summary>
/// Gets the string values of the enum.
@@ -116,12 +148,12 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : 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>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(typeof(T));
Values = values ?? Enum.GetNames<T>();
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
@@ -138,11 +170,17 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.name =
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
// SetUpFromData method doesnt work correctly so we must set the values manually
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// 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);
@@ -152,6 +190,32 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
return stringOption;
}
/// <inheritdoc />
public override void ChangeGameSetting()
{
var stringOption = OptionBehaviour as StringOption;
stringOption!.MinusBtn.transform.localPosition += new Vector3(0.6f, 0f, 0f);
stringOption.PlusBtn.transform.localPosition += new Vector3(1.5f, 0f, 0f);
// The value box itself becomes the button
stringOption.PlusBtn.gameObject.SetActive(false);
stringOption.MinusBtn.gameObject.SetActive(false);
var background = stringOption.transform.GetChild(0);
background.localPosition += new Vector3(-0.8f, 0f, 0f);
background.localScale += new Vector3(1f, 0f, 0f);
var title = stringOption.TitleText;
title.transform.localPosition = new(-2.0466f, 0f, -2.9968f);
title.GetComponent<RectTransform>().sizeDelta = new(5.8f, 0.458f);
title.fontSize = 2.9f; // Why is it different for string options??
var valueBox = stringOption.transform.GetChild(5);
valueBox.localPosition += new Vector3(1.05f, 0f, 0f);
valueBox.localScale += new Vector3(0.2f, 0f, 0f);
}
/// <inheritdoc />
public override float GetFloatData()
{
@@ -167,19 +231,19 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : 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,182 @@
using AmongUs.GameOptions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Modules;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
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<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>
/// Gets the list of buttons that relate to the current instance.
/// </summary>
public List<MonoBehaviour> Buttons { 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>
/// <exception cref="ArgumentException">The enum type doesn't have the flags attribute.</exception>
public ModdedMultiSelectOption(string title, T defaultValue, T all, T none, string[]? values = null) : base(title, defaultValue)
{
var tType = typeof(T);
if (tType.GetCustomAttribute<FlagsAttribute>() == null)
throw new ArgumentException($"{tType.Name} is not a flag enum type");
Values = values ?? Enum.GetNames<T>();
All = all;
None = none;
Data = ScriptableObject.CreateInstance<MultiSelectGameSetting>();
Buttons = [];
var data = (MultiSelectGameSetting)Data;
data.Title = StringName;
data.OptionName = Int32OptionNames.Invalid;
data.Type = global::OptionTypes.MultipleChoice; // How nice of the game to provide an enum value themselves
data.EnumNames = []; // We don't initialise the translation here, we just add one as and when needed
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
var toggle = Object.Instantiate(toggleOpt.GetComponentInChildren<PassiveButton>(), stringOption.transform);
toggle.name = "Button";
toggle.transform.DestroyChildren();
toggle.OverrideOnClickListeners(ToggleButtons);
var box = toggle.GetComponent<BoxCollider2D>();
var prevColliderSize = box.size;
prevColliderSize.x *= 4.97f;
box.size = prevColliderSize;
var rend = stringOption.transform.GetChild(5).GetComponent<SpriteRenderer>();
toggle.OverrideOnMouseOverListeners(() => rend.color = Buttons.Count > 0 ? Color.white : MiraAssets.AcceptedTeal);
toggle.OverrideOnMouseOutListeners(() => rend.color = Buttons.Count > 0 ? MiraAssets.AcceptedTeal : Color.white);
// SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName;
stringOption.Values = (Data as MultiSelectGameSetting)?.EnumNames?.Values?.ToArray() ?? new Il2CppStructArray<StringNames>(0);
stringOption.Value = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
OptionBehaviour = stringOption;
return stringOption;
}
/// <inheritdoc />
public override void ChangeGameSetting()
{
var stringOption = OptionBehaviour as StringOption;
// The value box itself becomes the button
stringOption!.PlusBtn.gameObject.SetActive(false);
stringOption.MinusBtn.gameObject.SetActive(false);
var background = stringOption.transform.GetChild(0);
background.localPosition += new Vector3(-0.8f, 0f, 0f);
background.localScale += new Vector3(1f, 0f, 0f);
var title = stringOption.TitleText;
title.transform.localPosition = new(-2.0466f, 0f, -2.9968f);
title.GetComponent<RectTransform>().sizeDelta = new(5.8f, 0.458f);
title.fontSize = 2.9f; // Why is it different for string options??
var valueBox = stringOption.transform.GetChild(5);
valueBox.localPosition += new Vector3(1.05f, 0f, 0f);
valueBox.localScale += new Vector3(0.2f, 0f, 0f);
}
private void ToggleButtons()
{
if (Buttons.Count > 0)
{
Buttons.ForEach(x => x.gameObject.Destroy());
return;
}
// TODO: Implement this
}
/// <inheritdoc />
public override float GetFloatData()
{
return Convert.ToSingle(Value, NumberFormatInfo.InvariantInfo);
}
/// <inheritdoc />
public override NetData GetNetData()
{
return new NetData(Id, EnumSerializer.EnumToBytes(Value));
}
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue(EnumSerializer.EnumFromBytes<T>(data, 0));
}
/// <inheritdoc />
public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
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);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is StringOption opt)
{
opt.Value = Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo);
}
}
}
@@ -37,6 +37,11 @@ public class ModdedNumberOption : ModdedOption<float>
/// </summary>
public bool ZeroInfinity { get; }
/// <summary>
/// Gets a value indicating whether holding shift will divide the increment.
/// </summary>
public bool ShiftIncrement { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOption"/> class.
/// </summary>
@@ -49,6 +54,7 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
/// <param name="shiftIncrement">Whether holding shift will divide the increment or not.</param>
public ModdedNumberOption(
string title,
float defaultValue,
@@ -65,6 +71,7 @@ public class ModdedNumberOption : ModdedOption<float>
Increment = increment;
SuffixType = suffixType;
ZeroInfinity = zeroInfinity;
ShiftIncrement = increment != 1f;
Value = Mathf.Clamp(defaultValue, min, max);
@@ -97,6 +104,12 @@ public class ModdedNumberOption : ModdedOption<float>
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
numberOption.name =
$"{ParentMod!.OptionsTitleText}.NumberOption.{TranslationController.Instance.GetString(StringName)}";
var optionComponent = numberOption.gameObject.AddComponent<MiraNumberOptionComponent>();
optionComponent.NumberOption = this;
optionComponent.DefaultIncrement = Increment;
optionComponent.ShiftIncrementToggle = ShiftIncrement;
numberOption.SetUpFromData(Data, 20);
numberOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
@@ -115,6 +128,27 @@ public class ModdedNumberOption : ModdedOption<float>
return numberOption;
}
/// <inheritdoc />
public override void ChangeGameSetting()
{
var numberOption = OptionBehaviour as NumberOption;
numberOption!.PlusBtn.transform.localPosition += new Vector3(0.6f, 0f, 0f);
numberOption.MinusBtn.transform.localPosition += new Vector3(1.5f, 0f, 0f);
var background = numberOption.transform.GetChild(0);
background.localPosition += new Vector3(-0.8f, 0f, 0f);
background.localScale += new Vector3(1f, 0f, 0f);
var title = numberOption.TitleText;
title.transform.localPosition = new(-2.0466f, 0f, -2.9968f);
title.GetComponent<RectTransform>().sizeDelta = new(5.8f, 0.458f);
var valueBox = numberOption.transform.GetChild(5);
valueBox.localPosition += new Vector3(1.05f, 0f, 0f);
valueBox.localScale += new Vector3(0.2f, 0f, 0f);
}
/// <inheritdoc />
public override float GetFloatData()
{
@@ -145,7 +179,7 @@ public class ModdedNumberOption : ModdedOption<float>
Value = Mathf.Clamp(newValue, Min, Max);
HudManager.Instance.Notifier.AddSettingsChangeMessage(
StringName,
Data.GetValueString(Value),
Data!.GetValueString(Value),
false);
if (OptionBehaviour is NumberOption opt)
@@ -125,7 +125,7 @@ public abstract class ModdedOption<T> : IModdedOption
{
if (ConfigDefinition is null)
{
Logger<MiraApiPlugin>.Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
return;
}
Bind(presetConfig);
@@ -147,7 +147,7 @@ public abstract class ModdedOption<T> : IModdedOption
}
else
{
Logger<MiraApiPlugin>.Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
}
}
@@ -181,6 +181,23 @@ public abstract class ModdedOption<T> : IModdedOption
PlayerOption playerOpt,
Transform container);
/// <summary>
/// Visually adjust the game option as needed.
/// </summary>
public abstract void ChangeGameSetting();
/// <summary>
/// Binds the option to the configuration file of the parent mod, if available.
/// </summary>
public virtual void BindConfig()
{
var entry = ParentMod?.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
if (entry != null)
{
Value = entry.Value;
}
}
/// <summary>
/// Implicitly converts the option to type of <typeparamref name="T"/>.
/// </summary>
@@ -28,6 +28,27 @@ public class ModdedPlayerOption : ModdedOption<int>
/// </summary>
public bool AllowNone { get; set; }
/// <inheritdoc />
public override void ChangeGameSetting()
{
var numberOption = OptionBehaviour as PlayerOption;
numberOption!.PlusBtn.transform.localPosition += new Vector3(0.6f, 0f, 0f);
numberOption.MinusBtn.transform.localPosition += new Vector3(1.5f, 0f, 0f);
var background = numberOption.transform.GetChild(0);
background.localPosition += new Vector3(-0.8f, 0f, 0f);
background.localScale += new Vector3(1f, 0f, 0f);
var title = numberOption.TitleText;
title.transform.localPosition = new(-2.0466f, 0f, -2.9968f);
title.GetComponent<RectTransform>().sizeDelta = new(5.8f, 0.458f);
var valueBox = numberOption.transform.GetChild(5);
valueBox.localPosition += new Vector3(1.05f, 0f, 0f);
valueBox.localScale += new Vector3(0.2f, 0f, 0f);
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
/// </summary>
@@ -55,6 +76,8 @@ public class ModdedPlayerOption : ModdedOption<int>
Transform container)
{
var playerOption = Object.Instantiate(playerOpt, container);
playerOption.name =
$"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}";
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
@@ -29,6 +29,8 @@ public class ModdedToggleOption : ModdedOption<bool>
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
toggleOption.name =
$"{ParentMod!.OptionsTitleText}.ToggleOption.{TranslationController.Instance.GetString(StringName)}";
toggleOption.SetUpFromData(Data, 20);
@@ -42,6 +44,21 @@ public class ModdedToggleOption : ModdedOption<bool>
return toggleOption;
}
/// <inheritdoc />
public override void ChangeGameSetting()
{
var toggleOption = OptionBehaviour as ToggleOption;
toggleOption!.transform.GetChild(1).localPosition += new Vector3(2.2f, 0f, 0f); // Checkbox
var title = toggleOption.TitleText;
title.transform.localPosition = new(-2.0466f, 0f, -2.9968f);
title.GetComponent<RectTransform>().sizeDelta = new(5.8f, 0.458f);
var background = toggleOption.transform.GetChild(2);
background.localPosition += new Vector3(-0.8f, 0f, 0f);
background.localScale += new Vector3(1f, 0f, 0f);
}
/// <inheritdoc />
public override float GetFloatData()
{
+3 -2
View File
@@ -217,6 +217,7 @@ public abstract class CustomActionButton
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
HudManagerPatches.moddedKeybindIcons.Add(KeybindText);
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
}
}
@@ -233,7 +234,7 @@ public abstract class CustomActionButton
return;
}
this.Location = location;
Location = location;
if (!moveButton)
{
@@ -494,7 +495,7 @@ public abstract class CustomActionButton
if (Keybind != null && KeybindText != null)
{
KeybindText.text = Keybind.CurrentKey.ToString();
KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
KeybindIcon?.SetActive(ActiveInputManager.currentControlType is ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value &&
Keybind.CurrentKey != KeyboardKeyCode.None);
}
+3 -3
View File
@@ -26,11 +26,11 @@ public static class KeybindManager
try
{
var instance = KeybindUtils.RewiredInputManager!;
foreach (var keybind in KeybindManager.Keybinds)
foreach (var keybind in Keybinds)
{
if (instance.userData.actions.ToArray().Any(x => x.name == keybind.Id))
{
Logger<MiraApiPlugin>.Warning($"Keybind of id {keybind.Id} already exists. Skipping it");
Warning($"Keybind of id {keybind.Id} already exists. Skipping it");
continue;
}
@@ -44,7 +44,7 @@ public static class KeybindManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while registering keybinds in Rewired: {e}");
Error($"Error while registering keybinds in Rewired: {e}");
}
}
@@ -24,7 +24,7 @@ public static class LocalSettingsManager
if (TypeToTab.ContainsKey(type))
{
Logger<MiraApiPlugin>.Error($"Local settings tab {type.Name} already exists.");
Error($"Local settings tab {type.Name} already exists.");
return false;
}
+6 -1
View File
@@ -1,4 +1,6 @@
using BepInEx;
global using static Reactor.Utilities.Logger<MiraAPI.MiraApiPlugin>;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
@@ -30,6 +32,7 @@ public partial class MiraApiPlugin : BasePlugin
/// </summary>
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
internal static ConfigEntry<string>? DistanceSuffix { get; private set; }
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
@@ -40,6 +43,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();
}
+4 -2
View File
@@ -28,9 +28,11 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
public ConfigEntry<bool> ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false);
/// <summary>
/// Gets whether to show keybinds in the control mapper.
/// Gets whether to show keybinds on buttons.
/// </summary>
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true);
[LocalToggleSetting]
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds on Buttons", true);
// This would be placed in the keybinds menu, but it crashes for Epic Games users. - Atony
/// <summary>
/// Gets whether to apply cosmetic changes to the TaskAdder.
+11 -11
View File
@@ -47,7 +47,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_player = GetComponent<PlayerControl>();
if (_player == null)
{
Logger<MiraApiPlugin>.Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Destroy(this);
return;
}
@@ -77,7 +77,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
}
Modifiers.Remove(modifier);
@@ -95,7 +95,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
}
if (modifier is TimedModifier { AutoStart: true } timer)
@@ -122,7 +122,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
@@ -153,7 +153,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
}
@@ -336,7 +336,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var modifier = modifiers.FirstOrDefault();
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {type.Name} because it is not active.");
Error($"Cannot remove modifier {type.Name} because it is not active.");
return;
}
@@ -352,7 +352,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
if (!ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
return;
}
@@ -382,7 +382,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var modifier = GetModifier(uniqueId);
if (modifier == null)
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier with unique id {uniqueId} because it is not active.");
Error($"Cannot remove modifier with unique id {uniqueId} because it is not active.");
return;
}
@@ -412,19 +412,19 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
// TODO: Make a proper synchronization system.
if (LobbyBehaviour.Instance)
{
Logger<MiraApiPlugin>.Warning($"Modifiers added in the lobby won't sync to new players!");
Warning($"Modifiers added in the lobby won't sync to new players!");
}
var id = modifier.TypeId;
if (modifier.Unique && ActiveModifiers.Find(x => x.TypeId == id) != null)
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {id}!");
Error($"Player already has modifier with id {id}!");
return null;
}
if (ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Player already has this modifier!");
Error($"Player already has this modifier!");
return null;
}
+1 -1
View File
@@ -83,7 +83,7 @@ public static class ModifierExtensions
var modifier = target.GetModifier(typeId, predicate);
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}.");
Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}.");
return;
}
+4 -4
View File
@@ -97,7 +97,7 @@ public static class ModifierManager
if (modifierType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
return false;
}
@@ -137,7 +137,7 @@ public static class ModifierManager
var validPlayers = plrs.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId)).ToList();
if (validPlayers.Count == 0)
{
Logger<MiraApiPlugin>.Warning($"No valid players for modifier {modifier.ModifierName}");
Warning($"No valid players for modifier {modifier.ModifierName}");
continue;
}
@@ -158,7 +158,7 @@ public static class ModifierManager
if (candidates.Count == 0)
{
Logger<MiraApiPlugin>.Warning(
Warning(
$"No available players for modifier {modifier.ModifierName} at assignment {i + 1}");
break;
}
@@ -166,7 +166,7 @@ public static class ModifierManager
var plr = candidates.Random();
if (plr == null)
{
Logger<MiraApiPlugin>.Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
continue;
}
+1 -1
View File
@@ -80,7 +80,7 @@ public abstract class TimedModifier : BaseModifier
{
if (TimerActive)
{
Logger<MiraApiPlugin>.Error("Can't start a timer that has already been started.");
Error("Can't start a timer that has already been started.");
return;
}
@@ -0,0 +1,9 @@
using System;
namespace MiraAPI.Modules;
/// <summary>
/// Denotes a failure in conversion of values.
/// </summary>
/// <param name="message">The message to log.</param>
public class ConversionFailException(string message) : Exception(message);
+19
View File
@@ -0,0 +1,19 @@
// TODO: Remove later before completing the pr
#pragma warning disable
using System;
using System.Collections.Generic;
using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Reactor.Utilities.Attributes;
namespace MiraAPI.Modules;
[RegisterInIl2Cpp, Serializable]
public class MultiSelectGameSetting : BaseGameSetting
{
public Int32OptionNames OptionName;
public Dictionary<int, StringNames> EnumNames;
public override string GetValueString(float value) => TranslationController.Instance.GetString(EnumNames[(int)value]);
}
+202
View File
@@ -0,0 +1,202 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities;
namespace MiraAPI.Modules;
/// <summary>
/// Represents a collection of enum values that can be stored and manipulated as a comma-separated string.
/// </summary>
/// <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>>, IDisposable where T : struct, Enum
{
/// <summary>
/// Gets or sets the underlying comma-separated string representation of enum values.
/// </summary>
public string Values
{
readonly get => Helpers.Join(',', _values);
set => _values = [.. value.TrueSplit(',').Select(Enum.Parse<T>)];
}
private HashSet<T> _values = [.. values];
/// <summary>
/// Gets the number of values contained within.
/// </summary>
public readonly int Count => _values.Count;
/// <summary>
/// Adds a single enum value to the end of the collection.
/// </summary>
/// <param name="item">The enum value to add.</param>
/// <returns><c>true</c> if successfully added; otherwise, <c>false</c>.</returns>
public readonly bool Add(T item) => _values.Add(item);
/// <summary>
/// Removes the first occurrence of the specified enum value.<br/>
/// Returns <c>true</c> if the value was found and removed, <c>false</c> otherwise.
/// </summary>
/// <param name="item">The enum value to remove.</param>
/// <returns><c>true</c> if successfully removed; otherwise, <c>false</c>.</returns>
public readonly bool Remove(T item) => _values.Remove(item);
/// <summary>
/// Adds multiple enum values to the end of the collection.
/// </summary>
/// <param name="items">The collection of enum values to add.</param>
public readonly void AddRange(IEnumerable<T> items)
{
foreach (var item in items)
_values.Add(item);
}
/// <summary>
/// Removes multiple enum values from the collection.
/// </summary>
/// <param name="items">The enum values to remove.</param>
/// <returns>The number of items successfully removed.</returns>
public readonly int RemoveRange(params T[] items) => RemoveRange((IEnumerable<T>)items);
/// <summary>
/// Removes multiple enum values from the collection.
/// </summary>
/// <param name="items">The collection of enum values to remove.</param>
/// <returns>The number of items successfully removed.</returns>
public readonly int RemoveRange(IEnumerable<T> items) => items.Count(Remove);
/// <summary>
/// Determines whether the collection contains the specified enum value.
/// </summary>
/// <param name="item">The enum value to locate.</param>
/// <returns>true if the item is found in the collection; otherwise, false.</returns>
public readonly bool Contains(T item) => _values.Contains(item);
/// <summary>
/// Removes all enum values that match the specified predicate condition.<br/>
/// Iterates through the collection and removes each matching value.
/// </summary>
/// <param name="predicate">A function that defines the condition for removing items.</param>
/// <returns>The number of items removed from the collection.</returns>
public readonly int RemoveAll(Func<T, bool> predicate)
{
var count = 0;
foreach (var item in this)
{
if (predicate(item) && Remove(item))
count++;
}
return count;
}
/// <summary>
/// Removes all enum values from the collection.
/// </summary>
public readonly void Clear() => _values.Clear();
/// <summary>
/// Gets the first value in the collection.
/// </summary>
/// <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() => EnumSerializer.EnumsToBytes(this);
/// <summary>
/// Converts the collection to its string representation.
/// </summary>
/// <returns>A comma-separated string of enum values.</returns>
public readonly override string ToString() => Values;
/// <inheritdoc/>
public readonly override bool Equals(object? obj) => obj is MultiSelectValue<T> other && Equals(other);
/// <inheritdoc/>
public readonly bool Equals(MultiSelectValue<T> other) => _values.SetEquals(other._values);
/// <inheritdoc/>
public readonly override int GetHashCode() => Values.GetHashCode();
/// <inheritdoc/>
public readonly IEnumerator<T> GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
/// <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>
#pragma warning disable CA1000 // Do not declare static members on generic types
public static MultiSelectValue<T> FromBytes(byte[] bytes) => new([.. EnumSerializer.EnumsFromBytes<T>(bytes)]);
#pragma warning restore CA1000 // Do not declare static members on generic types
/// <summary>
/// Converts the current instance to its string representation.
/// </summary>
/// <param name="value">The value to convert.</param>
public static implicit operator string(MultiSelectValue<T> value) => value.Values;
/// <summary>
/// Converts the current instance to its array representation of values.
/// </summary>
/// <param name="value">The value to convert.</param>
public static implicit operator T[](MultiSelectValue<T> value) => [.. value._values];
/// <summary>
/// Converts the current instance to one singular value.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <exception cref="ConversionFailException">There were either no values, or more than one value.</exception>
#pragma warning disable S3877 // Exceptions should not be thrown from unexpected methods
public static implicit operator T(MultiSelectValue<T> 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
/// <summary>
/// Converts the value to an instance of MultiSelectValue.
/// </summary>
/// <param name="value">The value to convert.</param>
public static implicit operator MultiSelectValue<T>(T value) => new(value);
/// <summary>
/// Converts the array of values to an instance of MultiSelectValue.
/// </summary>
/// <param name="values">The values to convert.</param>
public static implicit operator MultiSelectValue<T>(T[] values) => new(values);
/// <summary>
/// Converts the value string to an instance of MultiSelectValue.
/// </summary>
/// <param name="values">The values to convert.</param>
public static implicit operator MultiSelectValue<T>(string values) => new([.. values.TrueSplit(',').Select(Enum.Parse<T>)]);
/// <summary>
/// Equality comparison.
/// </summary>
/// <param name="left">Left.</param>
/// <param name="right">Right.</param>
public static bool operator ==(MultiSelectValue<T> left, MultiSelectValue<T> right) => left.Equals(right);
/// <summary>
/// Inequality comparison.
/// </summary>
/// <param name="left">Left.</param>
/// <param name="right">Right.</param>
public static bool operator !=(MultiSelectValue<T> left, MultiSelectValue<T> right) => !left.Equals(right);
}
+2 -2
View File
@@ -50,7 +50,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
{
if (!gameOver.VerifyCondition(innerNetObject, [.. data.Winners]))
{
Logger<MiraApiPlugin>.Info($"Game over condition not met for {gameOver.GetType().Name}");
Info($"Game over condition not met for {gameOver.GetType().Name}");
return;
}
@@ -64,7 +64,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
else
{
CustomGameOver.Instance = null;
Logger<MiraApiPlugin>.Error($"Unknown game over reason: {data.Reason}");
Error($"Unknown game over reason: {data.Reason}");
}
}
}
+1 -1
View File
@@ -178,7 +178,7 @@ public static class CustomMurderRpc
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error with kill animation: {e.ToString()}");
Error($"Error with kill animation: {e.ToString()}");
}
}
@@ -7,12 +7,12 @@ using UnityEngine;
namespace MiraAPI.Patches;
[HarmonyPatch(typeof(DetectiveNotesMinigame), nameof(DetectiveNotesMinigame.OpenBodyPopup))]
[HarmonyPatch(typeof(DetectiveNotesMinigame), nameof(DetectiveNotesMinigame.SetImpostorPopup))]
public static class DetectiveNotesMinigamePatch
{
public static void Prefix(DetectiveNotesMinigame __instance, bool active)
{
__instance.impostorTypePopup.SetActive(active);
__instance.impostorTypePopup.gameObject.SetActive(active);
if (!active)
{
ControllerManager.Instance.CloseOverlayMenu(__instance.ImpostorOverlay);
@@ -22,23 +22,21 @@ public static class DetectiveNotesMinigamePatch
}
if (__instance.impostorButton != null)
{
ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, null, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false);
ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, __instance.impostorTypePopup, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false);
return;
}
__instance.impostorButton = new List<GameObject>();
int num = 0;
var allRoles = DestroyableSingleton<RoleManager>.Instance.AllRoles;
var customRoles = CustomRoleManager.CustomRoleBehaviours;
foreach (var role in allRoles)
{
if (role.TeamType != RoleTeamTypes.Impostor || role.Role == RoleTypes.ImpostorGhost || customRoles.Contains(role) )
continue;
GameObject gameObject = Object.Instantiate<GameObject>(__instance.impostorTypePrefab, __instance.impostorTypeParent);
var gameObject = Object.Instantiate(__instance.impostorTypePrefab, __instance.impostorTypeParent);
__instance.impostorButton.Add(gameObject);
gameObject.GetComponent<DetectiveImpostorType>().Initialize(__instance, role);
num++;
__instance.ImpostorIconControllerSelectables.Add(gameObject.GetComponent<PassiveButton>());
}
ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, null, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false);
ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, __instance.impostorTypePopup, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false);
}
}
}
@@ -1,13 +1,20 @@
using HarmonyLib;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(TutorialManager._RunTutorial_d__3), nameof(TutorialManager._RunTutorial_d__3.MoveNext))]
[HarmonyPatch]
public static class FreeplayRoundStartPatch
{
public static void Postfix(TutorialManager._RunTutorial_d__3 __instance, ref bool __result)
public static MethodBase TargetMethod()
{
return Helpers.GetStateMachineMoveNext<TutorialManager>(nameof(TutorialManager.RunTutorial))!;
}
public static void Postfix(ref bool __result)
{
if (!__result)
{
@@ -0,0 +1,35 @@
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
/// <summary>
/// Patch to invoke AfterMurderEvent after a kill animation is performed.
/// </summary>
[HarmonyPatch]
public static class KillAnimationMurderEventPatch
{
public static MethodBase TargetMethod()
{
return Helpers.GetStateMachineMoveNext<KillAnimation>(nameof(KillAnimation.CoPerformKill))!;
}
public static void Postfix(Il2CppObjectBase __instance)
{
var wrapper = new StateMachineWrapper<KillAnimation>(__instance);
if (wrapper.GetState() != 1)
{
return;
}
var source = wrapper.GetParameter<PlayerControl>("source");
var target = wrapper.GetParameter<PlayerControl>("target");
var afterMurderEvent = new AfterMurderEvent(source, target, Helpers.GetBodyById(target.PlayerId));
MiraEventManager.InvokeEvent(afterMurderEvent);
}
}
@@ -1,30 +0,0 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
/// <summary>
/// General patches for the KillAnimation class.
/// </summary>
[HarmonyPatch]
public static class KillAnimationPatches
{
/// <summary>
/// Used to trigger the <see cref="AfterMurderEvent"/>.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(KillAnimation._CoPerformKill_d__2), nameof(KillAnimation._CoPerformKill_d__2.MoveNext))]
public static void KillAnimPostfix(KillAnimation._CoPerformKill_d__2 __instance)
{
// Checking if state is -1 to wait for the end of coroutine.
if (__instance.__1__state != -1)
{
return;
}
var afterMurderEvent = new AfterMurderEvent(__instance.source, __instance.target, Helpers.GetBodyById(__instance.target.PlayerId));
MiraEventManager.InvokeEvent(afterMurderEvent);
}
}
@@ -41,7 +41,7 @@ public static class ReportBodyEventPatch
var source = AmongUsClient.Instance.FindObjectByNetId<PlayerControl>(reportBodyMessage.rpcObjectNetId);
if (source == null)
{
Logger<MiraApiPlugin>.Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
return true;
}
+6 -1
View File
@@ -9,6 +9,11 @@ namespace MiraAPI.Patches.Hud;
public static class ButtonResetPatches
{
public static void Postfix()
{
ResetCooldowns();
}
public static void ResetCooldowns()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
@@ -18,7 +23,7 @@ public static class ButtonResetPatches
}
catch (System.Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
}
if (customActionButton.UsesMode == ButtonUsesMode.PerRound)
+14 -4
View File
@@ -27,6 +27,8 @@ public static class HudManagerPatches
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = new();
internal static List<TextMeshPro> moddedKeybindIcons = new();
/*
/// <summary>
/// Trigger hudstart on current custom gamemode
@@ -88,6 +90,7 @@ public static class HudManagerPatches
fakeButton.ToggleVisible(true);
fakeButton.Destroy();
}
moddedKeybindIcons = [];
foreach (var button in CustomButtonManager.CustomButtons)
{
@@ -109,9 +112,11 @@ public static class HudManagerPatches
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to create custom button {button.GetType().Name}: {e}");
Error($"Failed to create custom button {button.GetType().Name}: {e}");
}
}
__instance.ImpostorVentButton.transform.SetParent(null);
__instance.ImpostorVentButton.transform.SetParent(BottomRight.transform);
gridArrange.Start();
gridArrange.ArrangeChilds();
@@ -171,7 +176,7 @@ public static class HudManagerPatches
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to set custom button {button.GetType().Name} active: {e}");
Error($"Failed to set custom button {button.GetType().Name} active: {e}");
}
}
}
@@ -180,11 +185,16 @@ public static class HudManagerPatches
[HarmonyPostfix]
public static void UpdatePostfix()
{
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
foreach (var btnIcon in vanillaKeybindIcons)
{
btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString();
btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value);
btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
}
foreach (var btnIcon in moddedKeybindIcons)
{
btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
}
var player = ReInput.players.GetPlayer(0);
+5 -5
View File
@@ -38,7 +38,7 @@ public static class IntroCutscenePatches
{
public static MethodBase TargetMethod()
{
return Helpers.GetStateMachineMoveNext<IntroCutscene>("ShowRole")!;
return Helpers.GetStateMachineMoveNext<IntroCutscene>(nameof(IntroCutscene.ShowRole))!;
}
public static void Postfix(Il2CppObjectBase __instance)
@@ -52,7 +52,7 @@ public static class IntroCutscenePatches
var introCutscene = wrapper.Instance;
Logger<MiraApiPlugin>.Info("IntroCutscene ShowRole reached");
Info("IntroCutscene ShowRole reached");
var @event = new IntroRoleRevealEvent(introCutscene);
MiraEventManager.InvokeEvent(@event);
}
@@ -96,12 +96,12 @@ public static class IntroCutscenePatches
if (onDestroy != null)
{
_usedFallback = false;
Logger<MiraApiPlugin>.Info("Using OnDestroy for IntroCutsceneDestroyPatch");
Info("Using OnDestroy for IntroCutsceneDestroyPatch");
return onDestroy;
}
_usedFallback = true;
return Helpers.GetStateMachineMoveNext<IntroCutscene>("CoBegin")!;
return Helpers.GetStateMachineMoveNext<IntroCutscene>(nameof(IntroCutscene.CoBegin))!;
}
public static void Postfix(Il2CppObjectBase __instance)
@@ -124,7 +124,7 @@ public static class IntroCutscenePatches
}
Logger<MiraApiPlugin>.Info("IntroCutscene ended");
Info("IntroCutscene ended");
MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene));
@@ -15,7 +15,8 @@ namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(ControlMapper))]
public static class ControlMapperPatches
{
[HarmonyPrefix]
// Patching this crashes for Epic Games users
/*[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Start))]
private static void StartPrefix(ControlMapper __instance)
{
@@ -37,7 +38,7 @@ public static class ControlMapperPatches
entry.Value = !entry.Value;
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
}));
}
}*/
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Update))]
@@ -32,11 +32,20 @@ internal static class GameOptionsMenuPatch
{
ModifiersUpdate(ref num);
}
else if (__instance.name == "CUSTOM TAB 1")
{
CustomMenuOneUpdate(ref num);
}
else if (__instance.name == "CUSTOM TAB 2")
{
CustomMenuTwoUpdate(ref num);
}
else
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
.Where(x => x.OptionableType == null &&
!x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
@@ -99,6 +108,48 @@ internal static class GameOptionsMenuPatch
}
}
private static void CustomMenuOneUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var modGroup in groups)
{
UpdateGroup(modGroup, ref num);
}
}
private static void CustomMenuOneCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
}
private static void CustomMenuTwoUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var modGroup in groups)
{
UpdateGroup(modGroup, ref num);
}
}
private static void CustomMenuTwoCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
}
private static void ModifiersUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
@@ -137,9 +188,19 @@ internal static class GameOptionsMenuPatch
ModifiersCreate(__instance);
return false;
}
else if (__instance.name == "CUSTOM TAB 1")
{
CustomMenuOneCreate(__instance);
return false;
}
else if (__instance.name == "CUSTOM TAB 2")
{
CustomMenuTwoCreate(__instance);
return false;
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
@@ -223,6 +284,9 @@ internal static class GameOptionsMenuPatch
menu.playerOptionOrigin,
menu.settingsContainer));
// TODO: Reimplement this without breaking the menu.
// group.Options.ForEach(x => x.ChangeGameSetting());
foreach (var newOpt in options)
{
newOpt.SetClickMask(menu.ButtonClickMask);
@@ -117,7 +117,7 @@ internal static class GamePresetsTabPatches
// ReSharper disable once InconsistentNaming
public static void Postfix()
{
Logger<MiraApiPlugin>.Error("OnDisable called");
Error("OnDisable called");
if (_saveButton)
{
@@ -155,7 +155,7 @@ internal static class GamePresetsTabPatches
var prefab = GameSettingMenu.Instance.GameSettingsButton;
if (prefab == null)
{
Logger<MiraApiPlugin>.Error("GameSettingsButton prefab is null");
Error("GameSettingsButton prefab is null");
return;
}
@@ -318,10 +318,10 @@ internal static class GamePresetsTabPatches
private static void Refresh()
{
Logger<MiraApiPlugin>.Error("Refreshing presets");
Error("Refreshing presets");
if (GameSettingMenuPatches.SelectedMod == null)
{
Logger<MiraApiPlugin>.Error("Selected mod is null, cannot refresh presets");
Error("Selected mod is null, cannot refresh presets");
return;
}
lock (Lock)
+199 -33
View File
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
@@ -27,13 +28,23 @@ internal static class GameSettingMenuPatches
private static Vector3 _roleBtnOgPos;
private static Vector3 _smallRoleBtnOgPos;
private static Vector3 _modifierBtnOgPos;
private static Vector3 _customOneBtnOgPos;
private static Vector3 _customTwoBtnOgPos;
private static GameOptionsMenu? _modifiersTab;
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
private static GameOptionsMenu? _customOneTab;
private static PassiveButton? _customOneButton;
private static GameOptionsMenu? _customTwoTab;
private static PassiveButton? _customTwoButton;
private static GameObject? _nextModButton;
private static GameObject? _previousModButton;
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
private static Dictionary<int, Vector3> CustomOnePositions { get; } = [];
private static Dictionary<int, Vector3> CustomTwoPositions { get; } = [];
private static void SaveScrollPositions(GameSettingMenu gameSettingMenu)
{
@@ -41,6 +52,14 @@ internal static class GameSettingMenuPatches
{
ModifiersPositions[SelectedModIdx] = _modifiersTab!.scrollBar.Inner.localPosition;
}
else if (_customOneTab)
{
CustomOnePositions[SelectedModIdx] = _customOneTab!.scrollBar.Inner.localPosition;
}
else if (_customTwoTab)
{
CustomTwoPositions[SelectedModIdx] = _customTwoTab!.scrollBar.Inner.localPosition;
}
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = gameSettingMenu.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = gameSettingMenu.GameSettingsTab.scrollBar.Inner.localPosition;
@@ -65,9 +84,21 @@ internal static class GameSettingMenuPatches
_modifiersTab!.gameObject.SetActive(tabNum == 3);
}
if (_customOneTab)
{
_customOneTab!.gameObject.SetActive(tabNum == 4);
}
if (_customTwoTab)
{
_customTwoTab!.gameObject.SetActive(tabNum == 5);
}
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum == 3)
_customOneButton?.SelectButton(tabNum == 4);
_customTwoButton?.SelectButton(tabNum == 5);
if (tabNum >= 3 && tabNum <= 5)
{
if (__instance.RoleSettingsButton.gameObject.active)
{
@@ -75,7 +106,18 @@ internal static class GameSettingMenuPatches
}
else
{
_modifiersButton?.SelectButton(true);
switch (tabNum)
{
case 3:
_modifiersButton?.SelectButton(true);
break;
case 4:
_customOneButton?.SelectButton(true);
break;
case 5:
_customTwoButton?.SelectButton(true);
break;
}
}
}
}
@@ -87,19 +129,30 @@ internal static class GameSettingMenuPatches
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
_customOneButton?.SelectButton(tabNum == 4);
_customTwoButton?.SelectButton(tabNum == 5);
if (tabNum != 3)
if (tabNum >= 3 && tabNum <= 5)
{
return;
}
if (__instance.RoleSettingsButton.gameObject.active)
{
__instance.RoleSettingsButton.SelectButton(true);
}
else
{
_modifiersButton?.SelectButton(true);
if (__instance.RoleSettingsButton.gameObject.active)
{
__instance.RoleSettingsButton.SelectButton(true);
}
else
{
switch (tabNum)
{
case 3:
_modifiersButton?.SelectButton(true);
break;
case 4:
_customOneButton?.SelectButton(true);
break;
case 5:
_customTwoButton?.SelectButton(true);
break;
}
}
}
}
@@ -127,16 +180,16 @@ internal static class GameSettingMenuPatches
_text.alignment = TextAlignmentOptions.Center;
var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
nextButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
nextButton.name = "RightArrowButton";
nextButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().sprite =
_nextModButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
_nextModButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
_nextModButton.name = "RightArrowButton";
_nextModButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().sprite =
MiraAssets.NextButton.LoadAsset();
nextButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().sprite =
_nextModButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().sprite =
MiraAssets.NextButtonActive.LoadAsset();
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
_nextModButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
var passiveButton = nextButton.gameObject.GetComponent<PassiveButton>();
var passiveButton = _nextModButton.gameObject.GetComponent<PassiveButton>();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
@@ -151,13 +204,13 @@ internal static class GameSettingMenuPatches
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject;
backButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
backButton.name = "LeftArrowButton";
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().Destroy();
backButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX =
backButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
_previousModButton = Object.Instantiate(_nextModButton, __instance.BackButton.transform.parent).gameObject;
_previousModButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
_previousModButton.name = "LeftArrowButton";
_previousModButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().Destroy();
_previousModButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX =
_previousModButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
_previousModButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() =>
{
SaveScrollPositions(__instance);
@@ -174,6 +227,61 @@ internal static class GameSettingMenuPatches
_modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_modifiersTab.name = "MODIFIERS TAB";
// clone game settings tab for both custom tabs
_customOneTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_customOneTab.name = "CUSTOM TAB 1";
_customTwoTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_customTwoTab.name = "CUSTOM TAB 2";
// create button for the first custom category
var customPos = new Vector3(
__instance.RoleSettingsButton.transform.localPosition.x,
__instance.RoleSettingsButton.transform.localPosition.y,
__instance.RoleSettingsButton.transform.localPosition.z);
_customOneButton = Object.Instantiate(__instance.RoleSettingsButton, __instance.RoleSettingsButton.transform.parent);
_customOneButton.buttonText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
_customOneButton.OnClick = new ButtonClickedEvent();
_customOneButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(4, false);
}));
_customOneButton.OnMouseOver = new UnityEvent();
_customOneButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(4, true);
}));
_customOneButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameOne : "Custom Category 1";
customPos.y -= 0.637f;
_customOneButton.transform.localPosition = customPos;
_customOneButton.name = "CustomOneButton";
_customOneBtnOgPos = _customOneButton.transform.localPosition;
// create button for the first custom category
_customTwoButton = Object.Instantiate(_customOneButton, _customOneButton.transform.parent);
_customTwoButton.OnClick = new ButtonClickedEvent();
_customTwoButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(5, false);
}));
_customTwoButton.OnMouseOver = new UnityEvent();
_customTwoButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(5, true);
}));
_customTwoButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameTwo : "Custom Category 2";
customPos.y -= 0.637f;
_customTwoButton.transform.localPosition = customPos;
_customTwoButton.name = "CustomTwoButton";
_customTwoBtnOgPos = _customTwoButton.transform.localPosition;
// create button for modifiers
__instance.RoleSettingsButton.buttonText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
__instance.RoleSettingsButton.gameObject.SetActive(false);
@@ -304,20 +412,32 @@ internal static class GameSettingMenuPatches
bool replaceWithModifiers = true;
_smallRolesButton!.transform.localPosition = _smallRoleBtnOgPos;
_modifiersButton!.transform.localPosition = _modifierBtnOgPos;
_customOneButton!.transform.localPosition = _customOneBtnOgPos;
_customTwoButton!.transform.localPosition = _customTwoBtnOgPos;
menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
if (SelectedModIdx != 0)
{
_customOneButton.buttonText.text = SelectedMod!.MiraPlugin.CustomOptionMenuNameOne;
_customTwoButton.buttonText.text = SelectedMod!.MiraPlugin.CustomOptionMenuNameTwo;
var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
var modHasCustomOne = SelectedMod.InternalOptionGroups.Exists(
x => x.ParentMenu == MenuCategory.CustomOne);
var modHasCustomTwo = SelectedMod.InternalOptionGroups.Exists(
x => x.ParentMenu == MenuCategory.CustomTwo);
var modHasModifiers = SelectedMod.InternalOptionGroups.Exists(
x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
x => x.ShowInModifiersMenu || x.ParentMenu == MenuCategory.Modifiers || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
var modHasOptions =
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && (!x.ShowInModifiersMenu ||
(x.ParentMenu != MenuCategory.Modifiers && x.ParentMenu != MenuCategory.Roles)));
_modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true);
_customOneButton.gameObject.SetActive(true);
_customTwoButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(false);
var defaultButton = menu.RoleSettingsButton;
// If there are no registered custom roles in the selected mod, hide the button.
if (!modHasRoles)
@@ -334,6 +454,7 @@ internal static class GameSettingMenuPatches
{
_modifiersButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
defaultButton = _modifiersButton;
if (_modifiersTab!.gameObject.active)
{
@@ -356,6 +477,7 @@ internal static class GameSettingMenuPatches
// If the mod has roles, we can enable the bigger role button and disable the small one.
if (modHasRoles)
{
defaultButton = menu.RoleSettingsButton;
menu.RoleSettingsButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(false);
}
@@ -370,6 +492,7 @@ internal static class GameSettingMenuPatches
{
menu.ChangeTab(0, false);
}
defaultButton = menu.RoleSettingsButton;
// If the mod has roles and modifiers, we can move their buttons to the game settings button position, since nothing is there.
if (menu.RoleSettingsButton.gameObject.active)
@@ -389,16 +512,37 @@ internal static class GameSettingMenuPatches
_smallRolesButton.transform.localPosition.z);
}
}
// If the mod has no primary custom tab, we can go ahead and hide the button.
if (!modHasCustomOne)
{
_customOneButton.gameObject.SetActive(false);
if (_customOneTab!.gameObject.active)
{
defaultButton.SelectButton(true);
}
}
// If the mod has no secondary custom tab, we can go ahead and hide the button.
if (!modHasCustomTwo)
{
_customTwoButton.gameObject.SetActive(false);
if (_customTwoTab!.gameObject.active)
{
defaultButton.SelectButton(true);
}
}
}
else
{
_modifiersButton.gameObject.SetActive(false);
_smallRolesButton.gameObject.SetActive(false);
_customOneButton.gameObject.SetActive(false);
_customTwoButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
replaceWithModifiers = false;
if (_modifiersTab!.gameObject.active)
if (_modifiersTab!.gameObject.active || _customOneTab!.gameObject.active || _customTwoTab!.gameObject.active)
{
menu.ChangeTab(0, false);
}
@@ -446,7 +590,17 @@ internal static class GameSettingMenuPatches
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab, true);
CleanupSettings(_modifiersTab, MenuCategory.Modifiers);
}
if (_customOneTab?.Children?.Count > 0)
{
CleanupSettings(_customOneTab, MenuCategory.CustomOne);
}
if (_customTwoTab?.Children?.Count > 0)
{
CleanupSettings(_customTwoTab, MenuCategory.CustomTwo);
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
@@ -484,7 +638,7 @@ internal static class GameSettingMenuPatches
}
}
void CleanupSettings(GameOptionsMenu gameOptMenu, bool modifiers = false)
void CleanupSettings(GameOptionsMenu gameOptMenu, MenuCategory menuCategory = MenuCategory.Roles)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
@@ -495,7 +649,19 @@ internal static class GameSettingMenuPatches
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
var positions = modifiers ? ModifiersPositions : OptionsPositions;
var positions = OptionsPositions;
switch (menuCategory)
{
case MenuCategory.Modifiers:
positions = ModifiersPositions;
break;
case MenuCategory.CustomOne:
positions = CustomOnePositions;
break;
case MenuCategory.CustomTwo:
positions = CustomTwoPositions;
break;
}
if (positions.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
+103 -1
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
@@ -163,7 +164,108 @@ public static class LobbyViewPanePatches
{
if (SelectedModIdx == 0)
{
return true;
float num = 0.95f;
float num2 = -6.53f;
CategoryHeaderMasked categoryHeaderMasked =
Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer);
categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61);
categoryHeaderMasked.transform.localScale = Vector3.one;
categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f);
__instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
var list = new List<RoleBehaviour>();
for (int i = 0; i < 2; i++)
{
var categoryHeaderRoleVariant =
Object.Instantiate(__instance.categoryHeaderRoleOrigin, __instance.settingsContainer);
categoryHeaderRoleVariant.SetHeader(
(i == 0) ? StringNames.CrewmateRolesHeader : StringNames.ImpostorRolesHeader,
61);
categoryHeaderRoleVariant.transform.localScale = Vector3.one;
categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f);
__instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
num -= 0.696f;
var roles = RoleManager.Instance.AllRoles.ToArray().Where(x =>
x is not ICustomRole && x.Role != RoleTypes.Crewmate && x.Role != RoleTypes.Impostor &&
x.Role != RoleTypes.CrewmateGhost && x.Role != RoleTypes.ImpostorGhost).ToList();
for (int j = 0; j < roles.Count; j++)
{
var roleBehaviour = roles[j];
int chancePerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(
roleBehaviour.Role);
int numPerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions
.GetNumPerGame(roleBehaviour.Role);
bool flag = numPerGame == 0;
var viewSettingsInfoPanelRoleVariant =
Object.Instantiate(
__instance.infoPanelRoleOrigin,
__instance.settingsContainer);
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
if (!flag)
{
list.Add(roleBehaviour);
}
viewSettingsInfoPanelRoleVariant.SetInfo(
roleBehaviour.NiceName,
numPerGame,
chancePerGame,
61,
(i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed,
roleBehaviour.RoleIconSolid,
i == 0,
flag);
__instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
}
if (list.Count > 0)
{
var categoryHeaderMasked2 =
Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer);
categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61);
categoryHeaderMasked2.transform.localScale = Vector3.one;
categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f);
__instance.settingsInfo.Add(categoryHeaderMasked2.gameObject);
num -= 2.1f;
float num3 = 0f;
for (int k = 0; k < list.Count; k++)
{
float num4;
if (k % 2 == 0)
{
num4 = -5.8f;
if (k > 0)
{
num -= num3 + 0.85f;
num3 = 0f;
}
}
else
{
num4 = 0.14999962f;
}
var advancedRoleViewPanel =
Object.Instantiate(__instance.advancedRolePanelOrigin, __instance.settingsContainer);
advancedRoleViewPanel.transform.localScale = Vector3.one;
advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f);
float num5 = advancedRoleViewPanel.SetUp(list[k], 0.85f, 61);
if (num5 > num3)
{
num3 = num5;
}
__instance.settingsInfo.Add(advancedRoleViewPanel.gameObject);
}
}
__instance.scrollBar.SetYBoundsMax(-num);
return false;
}
if (__instance.currentTab == ModifiersTabName)
+20 -2
View File
@@ -216,7 +216,16 @@ public static class OptionsPatches
return true;
}
__instance.Value += __instance.Increment;
var increment = __instance.Increment;
if (Input.GetKey(KeyCode.LeftShift) &&
__instance.TryGetComponent<MiraNumberOptionComponent>(out var miraNumber) &&
miraNumber.ShiftIncrementToggle)
{
increment /= 2;
}
__instance.Value += increment;
if (__instance.Value > __instance.ValidRange.max)
{
__instance.Value = __instance.ValidRange.min;
@@ -235,7 +244,16 @@ public static class OptionsPatches
return true;
}
__instance.Value -= __instance.Increment;
var increment = __instance.Increment;
if (Input.GetKey(KeyCode.LeftShift) &&
__instance.TryGetComponent<MiraNumberOptionComponent>(out var miraNumber) &&
miraNumber.ShiftIncrementToggle)
{
increment /= 2;
}
__instance.Value -= increment;
if (__instance.Value < __instance.ValidRange.min)
{
__instance.Value = __instance.ValidRange.max;
@@ -392,7 +392,7 @@ public static class RoleSettingMenuPatches
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Warning(e);
Warning(e);
}
roleSetting.UpdateValuesAndText(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions);
@@ -466,7 +466,7 @@ public static class RoleSettingMenuPatches
{
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
Error($"Role {role.NiceName} is not a custom role.");
return;
}
@@ -560,7 +560,7 @@ public static class RoleSettingMenuPatches
{
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
Error($"Role {role.NiceName} is not a custom role.");
return null;
}
@@ -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)]),
+5 -3
View File
@@ -127,17 +127,19 @@ internal static class PlayerControlPatches
return;
}
var role = __instance.Data?.Role;
foreach (var button in CustomButtonManager.CustomButtons)
{
if (__instance.Data?.Role == null)
if (role == null)
{
continue;
}
try
{
if (!button.Enabled(__instance.Data?.Role))
if (!button.Enabled(role))
{
button.SetActive(false, role);
continue;
}
@@ -145,7 +147,7 @@ internal static class PlayerControlPatches
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to update custom button {button.GetType().Name}: {e}");
Error($"Failed to update custom button {button.GetType().Name}: {e}");
}
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ public static class RoleManagerPatches
var data = targetPlayer.Data;
if (data == null)
{
Debug.LogError("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection.");
Error("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection.");
return false;
}
if (data.Role)
@@ -51,7 +51,7 @@ public static class RoleOptionsCollectionPatch
var chance = customRole.GetChance();
if (chance == null)
{
Logger<MiraApiPlugin>.Error($"Chance is null, defaulting to zero.");
Error($"Chance is null, defaulting to zero.");
chance = 0;
}
@@ -81,7 +81,7 @@ public static class RoleOptionsCollectionPatch
var count = customRole.GetCount();
if (count == null)
{
Logger<MiraApiPlugin>.Error($"Count is null, defaulting to zero.");
Error($"Count is null, defaulting to zero.");
count = 0;
}
+10
View File
@@ -12,6 +12,16 @@ public interface IMiraPlugin
/// </summary>
string OptionsTitleText { get; }
/// <summary>
/// Gets the name for the first custom category in the game options menu, if any.
/// </summary>
public virtual string CustomOptionMenuNameOne => "Custom Category 1";
/// <summary>
/// Gets the name for the second custom category in the game options menu, if any.
/// </summary>
public virtual string CustomOptionMenuNameTwo => "Custom Category 2";
/// <summary>
/// Gets the BepInEx configuration file for the plugin.
/// </summary>
+23 -23
View File
@@ -70,14 +70,14 @@ public sealed class MiraPluginManager
if (!method.IsStatic)
{
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
@@ -131,7 +131,7 @@ public sealed class MiraPluginManager
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += () =>
@@ -167,7 +167,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register game over {type.Name}: {e}");
Error($"Failed to register game over {type.Name}: {e}");
return false;
}
}
@@ -190,7 +190,7 @@ public sealed class MiraPluginManager
{
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
@@ -211,14 +211,14 @@ public sealed class MiraPluginManager
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for options.");
Error($"{field.Name} is a field, not a property. Use properties for options.");
}
return true;
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}");
Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
}
@@ -235,7 +235,7 @@ public sealed class MiraPluginManager
if (!ModList.GetById(pluginInfo.PluginId).IsRequiredOnAllClients)
{
Logger<MiraApiPlugin>.Error("Custom roles are only supported on all clients.");
Error("Custom roles are only supported on all clients.");
return false;
}
@@ -244,7 +244,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register role for {type.Name}: {e}");
Error($"Failed to register role for {type.Name}: {e}");
}
return false;
}
@@ -260,7 +260,7 @@ public sealed class MiraPluginManager
if (!type.IsStatic())
{
Logger<MiraApiPlugin>.Error($"Color class {type.Name} must be static.");
Error($"Color class {type.Name} must be static.");
return;
}
@@ -273,7 +273,7 @@ public sealed class MiraPluginManager
if (property.GetValue(null) is not CustomColor color)
{
Logger<MiraApiPlugin>.Error($"Color property {property.Name} in {type.Name} is not a CustomColor.");
Error($"Color property {property.Name} in {type.Name} is not a CustomColor.");
continue;
}
@@ -282,12 +282,12 @@ public sealed class MiraPluginManager
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for colors.");
Error($"{field.Name} is a field, not a property. Use properties for colors.");
}
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register color class {type.Name}: {e}");
Error($"Failed to register color class {type.Name}: {e}");
}
}
@@ -299,7 +299,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register modifier {type.Name}: {e}");
Error($"Failed to register modifier {type.Name}: {e}");
return false;
}
}
@@ -312,7 +312,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register button {type.Name}: {e}");
Error($"Failed to register button {type.Name}: {e}");
}
return false;
@@ -355,13 +355,13 @@ public sealed class MiraPluginManager
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} has to be a config entry.");
Error($"Option property {property.Name} in {type.Name} has to be a config entry.");
continue;
}
@@ -376,14 +376,14 @@ public sealed class MiraPluginManager
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute<LocalSettingAttribute>() != null))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for local settings.");
Error($"{field.Name} is a field, not a property. Use properties for local settings.");
}
return true;
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}");
Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
@@ -400,7 +400,7 @@ public sealed class MiraPluginManager
if (!type.IsStatic())
{
Logger<MiraApiPlugin>.Error($"Keybinds class {type.Name} must be static.");
Error($"Keybinds class {type.Name} must be static.");
return;
}
@@ -413,7 +413,7 @@ public sealed class MiraPluginManager
if (property.GetValue(null) is not MiraKeybind keybind)
{
Logger<MiraApiPlugin>.Error($"Keybind property {property.Name} in {type.Name} is not a MiraKeybind.");
Error($"Keybind property {property.Name} in {type.Name} is not a MiraKeybind.");
continue;
}
@@ -430,12 +430,12 @@ public sealed class MiraPluginManager
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(MiraKeybind))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for keybinds.");
Error($"{field.Name} is a field, not a property. Use properties for keybinds.");
}
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register keybind class {type.Name}: {e}");
Error($"Failed to register keybind class {type.Name}: {e}");
}
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ public static class PresetManager
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
var fileName = Path.GetFileName(file);
Logger<MiraApiPlugin>.Info($"Loading preset file {fileName}");
Info($"Loading preset file {fileName}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
+3 -3
View File
@@ -108,7 +108,7 @@ public static class CustomRoleManager
{
if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType)))
{
Logger<MiraApiPlugin>.Error($"{roleType.Name} does not inherit from RoleBehaviour or ICustomRole.");
Error($"{roleType.Name} does not inherit from RoleBehaviour or ICustomRole.");
return null;
}
@@ -168,7 +168,7 @@ public static class CustomRoleManager
if (useTaskHint && !overridesTaskText)
{
Logger<MiraApiPlugin>.Error($"Role {customRole.RoleName} is using RoleHintType.TaskHint but does not override SpawnTaskHeader!");
Error($"Role {customRole.RoleName} is using RoleHintType.TaskHint but does not override SpawnTaskHeader!");
}
CustomRoles.Add(roleId, roleBehaviour);
@@ -298,7 +298,7 @@ public static class CustomRoleManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error(e);
Error(e);
}
}
+3 -3
View File
@@ -156,7 +156,7 @@ public interface ICustomRole : IOptionable
{
if (!Configuration.CanModifyChance)
{
Logger<MiraApiPlugin>.Error($"Cannot modify chance for role: {RoleName}");
Error($"Cannot modify chance for role: {RoleName}");
return;
}
@@ -166,7 +166,7 @@ public interface ICustomRole : IOptionable
return;
}
Logger<MiraApiPlugin>.Error($"Error getting chance configuration for role: {RoleName}");
Error($"Error getting chance configuration for role: {RoleName}");
}
/// <summary>
@@ -181,7 +181,7 @@ public interface ICustomRole : IOptionable
return;
}
Logger<MiraApiPlugin>.Error($"Error getting count configuration for role: {RoleName}");
Error($"Error getting count configuration for role: {RoleName}");
}
/// <summary>
+10 -10
View File
@@ -33,7 +33,7 @@ public static class AddressablesLoader
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
return;
}
@@ -48,7 +48,7 @@ public static class AddressablesLoader
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
return;
}
@@ -63,7 +63,7 @@ public static class AddressablesLoader
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
return;
}
@@ -79,7 +79,7 @@ public static class AddressablesLoader
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
return;
}
@@ -95,7 +95,7 @@ public static class AddressablesLoader
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
return;
}
@@ -106,7 +106,7 @@ public static class AddressablesLoader
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized.");
Error("AddressablesLoader has already been initialized.");
return;
}
@@ -129,10 +129,10 @@ public static class AddressablesLoader
// Check for errors
if (catalogOperation.Status != AsyncOperationStatus.Succeeded)
{
Logger<MiraApiPlugin>.Error($"Failed to load catalog {location}.");
Error($"Failed to load catalog {location}.");
}
Logger<MiraApiPlugin>.Info($"Loaded addressables {location}.");
Info($"Loaded addressables {location}.");
LoadedLocations.Add(location);
}
@@ -193,7 +193,7 @@ public static class AddressablesLoader
}
catch
{
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
Error($"Failed to find tag {tag}");
}
}
return behaviours;
@@ -214,7 +214,7 @@ public static class AddressablesLoader
}
catch
{
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
Error($"Failed to find tag {tag}");
}
}
return behaviours;
@@ -38,7 +38,7 @@ public class LoadableAddressableAsset<T>(string uid) : LoadableAsset<T> where T
var remoteReference = new AssetReference(uid);
if (!GC.TryStartNoGCRegion(4096, true)) Logger<MiraApiPlugin>.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
if (!GC.TryStartNoGCRegion(4096, true)) Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
LoadedAsset = remoteReference.LoadAssetAsync<T>().WaitForCompletion();
if (LoadedAsset == null)
@@ -45,7 +45,7 @@ public class LoadableAddressableAssets<T>(string key) where T : UnityEngine.Obje
var locations = Addressables.LoadResourceLocationsAsync(key).WaitForCompletion();
if (!GC.TryStartNoGCRegion(4096, true)) Logger<MiraApiPlugin>.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
if (!GC.TryStartNoGCRegion(4096, true)) Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
var assetsIList = Addressables.LoadAssetsAsync<T>(locations, null, false).WaitForCompletion();
if (assetsIList == null)
+145
View File
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.Utilities;
/// <summary>
/// Enum serializer for converting enums to and from byte arrays.
/// </summary>
public static class EnumSerializer
{
/// <summary>
/// Serializes an enum to an array of bytes.
/// </summary>
/// <param name="value">The enum value to serialize.</param>
/// <typeparam name="T">The enum type.</typeparam>
/// <returns>A byte array representing the enum value.</returns>
/// <exception cref="ArgumentException">Thrown when the underlying type is not recognized.</exception>
public static byte[] EnumToBytes<T>(T value) where T : struct, Enum
{
var underlyingType = typeof(T).GetEnumUnderlyingType();
return underlyingType switch
{
not null when underlyingType == typeof(byte) => [(byte)(object)value],
not null when underlyingType == typeof(sbyte) => [(byte)(sbyte)(object)value],
not null when underlyingType == typeof(short) => BitConverter.GetBytes((short)(object)value),
not null when underlyingType == typeof(ushort) => BitConverter.GetBytes((ushort)(object)value),
not null when underlyingType == typeof(int) => BitConverter.GetBytes((int)(object)value),
not null when underlyingType == typeof(uint) => BitConverter.GetBytes((uint)(object)value),
not null when underlyingType == typeof(long) => BitConverter.GetBytes((long)(object)value),
not null when underlyingType == typeof(ulong) => BitConverter.GetBytes((ulong)(object)value),
_ => throw new ArgumentException("Unknown underlying type for " + typeof(T).Name),
};
}
/// <summary>
/// Converts bytes to an enum using the underlying type.
/// </summary>
/// <param name="bytes">The byte array to convert.</param>
/// <param name="offset">The offset in the byte array to start reading from.</param>
/// <param name="underlyingType">The underlying type of the enum.</param>
/// <typeparam name="T">The enum type.</typeparam>
/// <returns>The enum value represented by the bytes.</returns>
/// <exception cref="ArgumentException">Thrown when the underlying type is not recognized.</exception>
public static T EnumFromBytes<T>(byte[] bytes, int offset, Type underlyingType) where T : struct, Enum
{
return underlyingType switch
{
{ } t when t == typeof(byte) || t == typeof(sbyte) => (T)(object)bytes[offset],
{ } t when t == typeof(short) => (T)(object)BitConverter.ToInt16(bytes, offset),
{ } t when t == typeof(ushort) => (T)(object)BitConverter.ToUInt16(bytes, offset),
{ } t when t == typeof(int) => (T)(object)BitConverter.ToInt32(bytes, offset),
{ } t when t == typeof(uint) => (T)(object)BitConverter.ToUInt32(bytes, offset),
{ } t when t == typeof(long) => (T)(object)BitConverter.ToInt64(bytes, offset),
{ } t when t == typeof(ulong) => (T)(object)BitConverter.ToUInt64(bytes, offset),
_ => throw new ArgumentException("Unknown underlying type for " + typeof(T).Name),
};
}
/// <inheritdoc cref="EnumFromBytes{T}(byte[], int, Type)"/>
public static T EnumFromBytes<T>(byte[] bytes, int offset) where T : struct, Enum => EnumFromBytes<T>(bytes, offset, typeof(T).GetEnumUnderlyingType());
/// <summary>
/// Converts a byte array into a collection of enum values.
/// </summary>
/// <param name="bytes">The byte array to convert.</param>
/// <typeparam name="T">The enum type.</typeparam>
/// <returns>An enumerable collection of enum values.</returns>
/// <exception cref="ArgumentException">Thrown when the byte array length is not a multiple of the enum's underlying type size.</exception>
public static IEnumerable<T> EnumsFromBytes<T>(byte[] bytes) where T : struct, Enum
{
var underlyingType = typeof(T).GetEnumUnderlyingType();
var size = GetManagedSize(underlyingType);
if (bytes.Length % size != 0)
{
throw new ArgumentException("Byte array length must be a multiple of " + size + ".");
}
return EnumsFromBytesIterator<T>(bytes, underlyingType, size);
}
private static IEnumerable<T> EnumsFromBytesIterator<T>(byte[] bytes, Type underlyingType, int size) where T : struct, Enum
{
var count = bytes.Length / size;
var underlyingArray = Array.CreateInstance(underlyingType, count);
Buffer.BlockCopy(bytes, 0, underlyingArray, 0, bytes.Length);
foreach (var value in underlyingArray)
yield return (T)value;
}
/// <summary>
/// Converts a collection of enum values to a byte array.
/// </summary>
/// <param name="values"> The collection of enum values to convert.</param>
/// <typeparam name="T">The enum type.</typeparam>
/// <returns>A byte array representing the collection of enum values.</returns>
public static byte[] EnumsToBytes<T>(IEnumerable<T> values) where T : struct, Enum
{
var underlyingType = typeof(T).GetEnumUnderlyingType();
var size = GetManagedSize(underlyingType);
var valuesArray = values.ToArray();
var count = valuesArray.Length;
var underlyingArray = Array.CreateInstance(underlyingType, count);
for (var i = 0; i < count; i++)
{
underlyingArray.SetValue(ConvertToUnderlyingType(valuesArray[i], underlyingType), i);
}
var bytes = new byte[count * size];
Buffer.BlockCopy(underlyingArray, 0, bytes, 0, bytes.Length);
return bytes;
}
private static object ConvertToUnderlyingType<T>(T value, Type underlyingType) where T : struct, Enum
{
return underlyingType switch
{
not null when underlyingType == typeof(byte) => (byte)(object)value,
not null when underlyingType == typeof(sbyte) => (sbyte)(object)value,
not null when underlyingType == typeof(short) => (short)(object)value,
not null when underlyingType == typeof(ushort) => (ushort)(object)value,
not null when underlyingType == typeof(int) => (int)(object)value,
not null when underlyingType == typeof(uint) => (uint)(object)value,
not null when underlyingType == typeof(long) => (long)(object)value,
not null when underlyingType == typeof(ulong) => (ulong)(object)value,
_ => throw new InvalidOperationException("Unsupported underlying type."),
};
}
private static int GetManagedSize(Type type)
{
return Type.GetTypeCode(type) switch
{
TypeCode.Byte or TypeCode.SByte => 1,
TypeCode.Int16 or TypeCode.UInt16 => 2,
TypeCode.Int32 or TypeCode.UInt32 => 4,
TypeCode.Int64 or TypeCode.UInt64 => 8,
_ => throw new ArgumentException("Unsupported underlying type: " + type.Name),
};
}
}
+3 -3
View File
@@ -29,13 +29,13 @@ public static class Extensions
if (count == null)
{
Logger<MiraApiPlugin>.Error("Couldn't get role count for NetData, defaulting to zero.");
Error("Couldn't get role count for NetData, defaulting to zero.");
count = 0;
}
if (chance == null)
{
Logger<MiraApiPlugin>.Error("Couldn't get role chance for NetData, defaulting to zero.");
Error("Couldn't get role chance for NetData, defaulting to zero.");
chance = 0;
}
@@ -398,7 +398,7 @@ public static class Extensions
if (length > chunkSize)
{
Logger<MiraApiPlugin>.Info($"NetData length is greater than chunk size: {length} > {chunkSize}");
Info($"NetData length is greater than chunk size: {length} > {chunkSize}");
continue;
}
+81 -6
View File
@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -12,6 +13,7 @@ using TMPro;
using UnityEngine;
using MethodBase = System.Reflection.MethodBase;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
namespace MiraAPI.Utilities;
@@ -49,18 +51,18 @@ public static class Helpers
if (showRoleStateMachine == null)
{
Logger<MiraApiPlugin>.Error($"Failed to find {methodName} state machine for {typeName}");
Error($"Failed to find {methodName} state machine for {typeName}");
return null;
}
var moveNext = AccessTools.Method(showRoleStateMachine, "MoveNext");
if (moveNext == null)
{
Logger<MiraApiPlugin>.Error($"Failed to find MoveNext method for {typeName}.{methodName}");
Error($"Failed to find MoveNext method for {typeName}.{methodName}");
return null;
}
Logger<MiraApiPlugin>.Info($"Found {methodName}.MoveNext");
Info($"Found {methodName}.MoveNext");
return moveNext;
}
@@ -139,7 +141,7 @@ public static class Helpers
return true;
default:
{
var num = UnityEngine.Random.RandomRangeInt(1, 101);
var num = Random.RandomRangeInt(1, 101);
return num <= probability;
}
}
@@ -500,7 +502,7 @@ public static class Helpers
public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
{
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray());
.Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray());
}
/// <summary>
@@ -522,6 +524,79 @@ public static class Helpers
(Il2CppSystem.Object[])[value.ToString(formatString, CultureInfo.InvariantCulture)]),
};
}
/// <summary>
/// Shorthand for <see cref="string.Split(char, StringSplitOptions)"/> that removes empty entries and trims leading and ending spaces.
/// </summary>
/// <param name="value">The string to split.</param>
/// <param name="separator">The separator.</param>
/// <returns>An array of substrings.</returns>
public static string[] TrueSplit(this string value, char separator) => value.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
/// <summary>
/// Joins a collection of items that are separated by a character.
/// </summary>
/// <param name="separator">The separator.</param>
/// <param name="items">The collection of items to string together.</param>
/// <typeparam name="T">The type of the items.</typeparam>
/// <returns>A string formed by concatenating the ToString results of each item, each separated by a separator.</returns>
public static string Join<T>(char separator, IEnumerable<T> items) => Join(separator.ToString(), items);
/// <inheritdoc cref="Join{T}(char, IEnumerable{T})"/>
public static string Join<T>(string separator, IEnumerable<T> items)
{
if (!items.Any())
return string.Empty;
var result = string.Empty;
items.Do(x => result += $"{separator}{x}");
return result[separator.Length..];
}
/// <summary>
/// Shorthand helper for overriding the on click listeners for PassiveButton.
/// </summary>
/// <param name="passive">The PassiveButton being modified.</param>
/// <param name="action">The action that's replacing the original listeners.</param>
public static void OverrideOnClickListeners(this PassiveButton passive, Action action)
{
if (!passive)
return;
passive.OnClick?.RemoveAllListeners();
passive.OnClick = new();
passive.OnClick.AddListener(action);
passive.enabled = true;
}
/// <summary>
/// Shorthand helper for overriding the on mouse over listeners for PassiveButton.
/// </summary>
/// <inheritdoc cref="OverrideOnClickListeners"/>
public static void OverrideOnMouseOverListeners(this PassiveButton passive, Action action)
{
if (!passive)
return;
passive.OnMouseOver?.RemoveAllListeners();
passive.OnMouseOver = new();
passive.OnMouseOver.AddListener(action);
passive.enabled = true;
}
/// <summary>
/// Shorthand helper for overriding the on mouse out listeners for PassiveButton.
/// </summary>
/// <inheritdoc cref="OverrideOnClickListeners"/>
public static void OverrideOnMouseOutListeners(this PassiveButton passive, Action action)
{
if (!passive)
return;
passive.OnMouseOut?.RemoveAllListeners();
passive.OnMouseOut = new();
passive.OnMouseOut.AddListener(action);
passive.enabled = true;
}
/// <summary>
/// Returns the string of a role.
+5
View File
@@ -24,4 +24,9 @@ public enum MiraNumberSuffixes
/// Percentage suffix (%).
/// </summary>
Percent,
/// <summary>
/// Display in units.
/// </summary>
Distance,
}
+34 -1
View File
@@ -1,4 +1,6 @@
using System.Reflection;
using System;
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
@@ -11,8 +13,11 @@ namespace MiraAPI.Utilities;
public class StateMachineWrapper<T> where T : Il2CppObjectBase
{
private readonly Il2CppObjectBase _stateMachine;
// normally it is fields, but IL2CPP turns them into properties
private readonly PropertyInfo _thisProperty;
private readonly PropertyInfo _stateProperty;
private readonly Dictionary<string, PropertyInfo> _propertyCache;
private T? _parentInstance;
@@ -32,6 +37,13 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
var type = _stateMachine.GetType();
_thisProperty = AccessTools.Property(type, "__4__this");
_stateProperty = AccessTools.Property(type, "__1__state");
if (_thisProperty == null || _stateProperty == null)
{
throw new MissingMemberException($"Could not find required properties in type '{type}'.");
}
_propertyCache = [];
}
/// <summary>
@@ -39,4 +51,25 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
/// </summary>
/// <returns>The current state as an integer.</returns>
public int GetState() => (int)_stateProperty.GetValue(_stateMachine)!;
/// <summary>
/// Gets a parameter from the state machine by its name.
/// </summary>
/// <param name="parameterName">The name of the parameter to retrieve.</param>
/// <typeparam name="TField">The type of the parameter to retrieve.</typeparam>
/// <returns>>The value of the specified parameter.</returns>
/// <exception cref="MissingFieldException">Thrown if the specified parameter does not exist.</exception>
public TField GetParameter<TField>(string parameterName)
{
if (_propertyCache.TryGetValue(parameterName, out var property))
{
return (TField)property.GetValue(_stateMachine)!;
}
property = AccessTools.Property(_stateMachine.GetType(), parameterName)
?? throw new MissingFieldException($"Could not find parameter '{parameterName}' in state machine of type '{_stateMachine.GetType()}'.");
_propertyCache[parameterName] = property;
return (TField)property.GetValue(_stateMachine)!;
}
}
+1 -1
View File
@@ -224,7 +224,7 @@ public static class VotingUtils
var playerById = GameData.Instance.GetPlayerById(vote.Voter);
if (playerById == null)
{
Logger<MiraApiPlugin>.Error($"Couldn't find player info for voter: {vote.Voter}");
Error($"Couldn't find player info for voter: {vote.Voter}");
}
else if (i == 0 && vote.Suspect == SkipVoteId)
{
+1
View File
@@ -6,5 +6,6 @@
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="Reactor" value="https://nuget.reactor.gg/v3/index.json" />
<add key="Samboy063" value="https://nuget.samboy.dev/v3/index.json" />
<add key="local" value="./packages" />
</packageSources>
</configuration>
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
here to ensure git doesn't delete empty folders
local packages are stored in this directory