Merge branch 'dev' into controller-support

This commit is contained in:
Atony authored and GitHub committed 2026-10-13 21:06:27 -05:00
commit 503b7cec51
66 files changed
+2254 -346

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
<ItemGroup>
<PackageReference Include="Reactor" Version="2.5.0-ci.371" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.8.18" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.9.29" 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,5 +1,4 @@
using Il2CppSystem;
using MiraAPI.Example.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.Modifiers.Types;
using MiraAPI.Translation;
using UnityEngine;
@@ -28,7 +27,7 @@ public class FreezeModifier : TimedModifier
if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole)
{
Player.cosmetics.SetOutline(true, new Nullable<Color>(Palette.LightBlue));
Player.cosmetics.SetOutline(true, new Il2CppSystem.Nullable<Color>(Palette.LightBlue));
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ public static class GameModeOption
}
}
internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("Gamemode");
internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode");
internal static readonly StringNames CustomName = MiraLocaleManager.GetOrCreateLocaleString("Custom");
internal static readonly Dictionary<uint, StringNames> Values = new()
{
+20
View File
@@ -121,4 +121,24 @@ public interface IModdedOption
/// </summary>
public interface IModdedOptionList : IReadOnlyList<IModdedOption>
{
/// <summary>
/// Gets the function to name the keys of the individual options in the configuration file.
/// <para/>
/// Return <see langword="null"/> to use the default of the property's name with its index appended.
/// </summary>
Func<int, string>? ConfigName { get; init; }
}
/// <summary>
/// Interface for list of <typeparamref name="T"/> options.
/// </summary>
/// <typeparam name="T">The type of <see cref="IModdedOption"/>.</typeparam>
public interface IModdedOptionList<out T> : IModdedOptionList
where T : IModdedOption
{
/// <inheritdoc cref="IReadOnlyList{T}.this[int]"/>
new T this[int index] { get; }
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
new IEnumerator<T> GetEnumerator();
}
+21
View File
@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace MiraAPI.GameOptions;
/// <summary>
/// Allows an option group to compress noisy option blocks in the wiki "Options" section.
/// Implementers can hide specific option keys and replace them with one or more summary lines.
/// </summary>
public interface ISummarizedOptions
{
/// <summary>
/// Gets the option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options).
/// </summary>
IReadOnlySet<StringNames> WikiHiddenOptionKeys { get; }
/// <summary>
/// Gets summary lines to insert when the first hidden option would have appeared.
/// Lines should already be formatted as "Title: Value".
/// </summary>
IEnumerable<string> GetWikiOptionSummaryLines();
}
+4 -2
View File
@@ -198,9 +198,11 @@ public static class ModdedOptionsManager
return;
}
Func<int, string> configName = optionList.ConfigName ?? (i => property.Name + (i + 1));
for (int i = 0; i < optionList.Count; i++)
{
RegisterOption(optionList[i], group, property.Name + i, pluginInfo);
RegisterOption(optionList[i], group, configName(i), pluginInfo);
}
}
@@ -277,7 +279,7 @@ public static class ModdedOptionsManager
option.Visible = () => visibilityFunc(i);
}
RegisterOption(option, group, property.Name + i, pluginInfo);
RegisterOption(option, group, property.Name + (i + 1), pluginInfo);
}
}
@@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// Represents a modded option list.
/// </summary>
/// <typeparam name="T">The type of options.</typeparam>
public class ModdedOptionList<T> : IModdedOptionList
public class ModdedOptionList<T> : IModdedOptionList<T>
where T : IModdedOption
{
/// <inheritdoc/>
@@ -20,6 +20,9 @@ public class ModdedOptionList<T> : IModdedOptionList
/// </summary>
public IReadOnlyList<T> Options { get; }
/// <inheritdoc/>
public Func<int, string>? ConfigName { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOptionList{T}"/> class.
/// </summary>
@@ -41,10 +44,10 @@ public class ModdedOptionList<T> : IModdedOptionList
Options = [.. options];
}
/// <inheritdoc/>
public IEnumerator<IModdedOption> GetEnumerator()
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
return Options.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
@@ -52,6 +55,11 @@ public class ModdedOptionList<T> : IModdedOptionList
return Options.GetEnumerator();
}
IEnumerator<IModdedOption> IEnumerable<IModdedOption>.GetEnumerator()
{
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
}
/// <summary>
/// Indexes the option of type <typeparamref name="T"/>.
/// </summary>
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Networking;
using MiraAPI.Translation;
using MiraAPI.Utilities;
@@ -43,7 +43,7 @@ public class ModdedPlayerOption : ModdedOption<int>
data.Title = StringName;
data.Type = global::OptionTypes.Player;
Values = new List<NetworkedPlayerInfo>();
Values = new();
AllowNone = allowNone;
Value = DefaultValue;
}
@@ -60,17 +60,16 @@ public class ModdedPlayerOption : ModdedOption<int>
playerOption.name =
$"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}";
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
Values = GetFilteredPlayers();
playerOption.SetUpFromData(Data, 20);
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
playerOption.Title = StringName;
playerOption.TitleText.text = Title.Translate();
playerOption.Values = Values;
playerOption.Values = Values.ToIl2CppList();
playerOption.Value = Value;
playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
playerOption.playerIndex = Values.FindIndex(p => p.PlayerId == Value);
OptionBehaviour = playerOption;
ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
@@ -85,7 +84,7 @@ public class ModdedPlayerOption : ModdedOption<int>
/// Returns a player ran through the filter.
/// </summary>
/// <returns>A list of filtered players.</returns>
public System.Collections.Generic.List<NetworkedPlayerInfo> GetFilteredPlayers()
public List<NetworkedPlayerInfo> GetFilteredPlayers()
{
return [.. GameData.Instance.AllPlayers.ToArray().Where(x => PlayerFilter?.Invoke(x) ?? true)];
}
+22
View File
@@ -4,6 +4,7 @@ using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.Patches;
using MiraAPI.Patches.Roles;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
@@ -63,9 +64,30 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
public static ConfigEntry<SortingGroups> WikiSortingGroup { get; private set; }
public static ConfigEntry<SortingMethod> WikiSortingMethod { get; private set; }
public static ConfigEntry<SortingFilter> WikiSortingFilter { get; private set; }
/// <inheritdoc />
public override void Load()
{
WikiSortingGroup = Config.Bind(
"Wiki",
"WikiSortingGroup",
SortingGroups.Ungrouped,
"Determines how clusters of roles / modifiers are grouped together, if at all.");
WikiSortingMethod = Config.Bind(
"Wiki",
"WikiSortingMethod",
SortingMethod.Alphabetical,
"Determines if the list of roles / modifiers are organized in ascending or descending order.");
WikiSortingFilter = Config.Bind(
"Wiki",
"WikiSortingFilter",
SortingFilter.AllRoles,
"Determines what roles / modifiers are displayed at once.");
Harmony.PatchAll();
RoleOptionsCollectionPatch.PatchRoleMethods(Harmony);
UiResetEvents.Initialize();
+94 -1
View File
@@ -1,7 +1,13 @@
using System;
using System.Collections.Generic;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -44,16 +50,98 @@ public abstract class BaseModifier : IOptionable
x => x.InternalModifiers.Exists(y => y.TypeId == TypeId))
?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary>
/// Gets the id part used to build the modifier's translation keys. It is recommended to simply call it what the modifier is, as other mods may utilize it.
/// </summary>
public virtual string IdPart => GetType().Name;
/// <summary>
/// Gets the id part used to build the modifier's translation keys. It is recommended to call it ModGuid.Modifier.Type.
/// </summary>
public virtual string IdPrefix => GetType().Namespace!;
/// <summary>
/// Gets the modifier name.
/// </summary>
public abstract string ModifierName { get; }
public virtual string ModifierName => MiraLocaleManager.Get(ModifierNameLocale);
/// <summary>
/// Gets the modifier's name id for localization.
/// </summary>
public virtual string ModifierNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart);
/// <summary>
/// Gets the medium description of the modifier. Used in the modifier guide and options menu.
/// </summary>
public virtual string ModifierMedDescription => MiraLocaleManager.Get(ModifierMedDescriptionLocale, GetDescription());
/// <summary>
/// Gets the modifier's medium description id for localization.
/// </summary>
public virtual string ModifierMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription");
/// <summary>
/// Gets the wiki description of the modifier. Used in the wiki and normally appends the options text as well.
/// </summary>
public virtual string ModifierWikiDescription => MiraLocaleManager.Get(ModifierWikiDescriptionLocale, GetDescription()) +
Helpers.GetOptionsText(GetType());
/// <summary>
/// Gets the modifier's wiki description id for localization.
/// </summary>
public virtual string ModifierWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription");
/// <summary>
/// Gets the category of the modifier. Used in the wiki.
/// </summary>
public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier");
/// <summary>
/// Gets whether the modifier is forcibly shown or disabled in the wiki screen.
/// </summary>
/// <returns><see langword="true"/> if the modifier is always displayed, otherwise <see langword="false"/> if it is never displayable, or <see langword="null"/> if it is dictated by amount and chance.</returns>
public virtual bool? ForceShowModifierOnWiki => false;
/// <summary>
/// Gets the information to display in the wiki.
/// </summary>
/// <param name="guide">The guide object.</param>
/// <param name="titleText">The title text object.</param>
/// <param name="titleIcon">The title's icon.</param>
/// <param name="parent">The scroller parent.</param>
/// <returns>The <see cref="GameObject"/> of the wiki.</returns>
public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent)
{
var abilities = WikiAbilities;
return Helpers.CreateAdvancedWikiPage(
guide,
titleText,
titleIcon,
parent,
ModifierNameLocale,
$"{ModifierName} ({ModifierCategoryTitle})".TrimStart(),
ModifierIcon ?? MiraAssets.IconNeutral.LoadAsset(),
ModifierWikiDescription,
abilities);
}
/// <summary>
/// Gets the list of abilities or other similar information to display below the text of a modifier's wiki page.
/// </summary>
[HideFromIl2Cpp]
public virtual List<AdvancedWikiAbilityDescription> WikiAbilities => [];
/// <summary>
/// Gets the modifier icon. Useless if <see cref="HideOnUi"/> is <see langword="true"/>.
/// </summary>
public virtual LoadableAsset<Sprite>? ModifierIcon => null;
/// <summary>
/// Gets the <see cref="TMP_SpriteAsset"/> for the Modifier Icon.
/// </summary>
[HideFromIl2Cpp]
public virtual TMP_SpriteAsset IconTmp => null!;
/// <summary>
/// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided.
/// </summary>
@@ -69,6 +157,11 @@ public abstract class BaseModifier : IOptionable
/// </summary>
public virtual Color FreeplayFileColor => Color.gray;
/// <summary>
/// Gets the <see cref="Color"/> of the modifier, used in the wiki and other UI outside freeplay.
/// </summary>
public virtual Color GeneralColor => FreeplayFileColor;
/// <summary>
/// Gets a value indicating whether the modifier is unique. If <see langword="true"/>, the player can only have one instance of this modifier.
/// </summary>
+3
View File
@@ -9,6 +9,9 @@ namespace MiraAPI.Modifiers.Types;
[MiraIgnore]
public abstract class GameModifier : BaseModifier
{
/// <inheritdoc />
public override bool? ForceShowModifierOnWiki => null;
/// <inheritdoc />
public override bool ShowInFreeplay => true;
@@ -23,10 +23,18 @@ public static class VanillaButtonPatches
{
playerTarget = role.Cast<TrackerRole>().currentTarget;
}
else if (role.Role is RoleTypes.Detective)
{
playerTarget = role.Cast<DetectiveRole>().currentTarget;
}
else if (role.Role is RoleTypes.GuardianAngel)
{
playerTarget = role.Cast<GuardianAngelRole>().currentTarget;
}
else if (role.Role is RoleTypes.SpiritGuide)
{
playerTarget = role.Cast<SpiritGuideRole>().currentTarget;
}
else if (role.Role is RoleTypes.Engineer)
{
ventTarget = role.Cast<EngineerRole>().currentTarget;
@@ -53,6 +61,10 @@ public static class VanillaButtonPatches
{
playerTarget = role.Cast<DetectiveRole>().currentTarget;
}
else if (role.Role is RoleTypes.SpiritGuide)
{
playerTarget = role.Cast<SpiritGuideRole>().currentTarget;
}
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget);
MiraEventManager.InvokeEvent(genericEvent);
+3 -4
View File
@@ -1,6 +1,5 @@
using System.Collections;
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.GameModes;
using MiraAPI.Translation;
using Reactor.Utilities;
@@ -16,7 +15,7 @@ internal static class HudPatches
[HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))]
public static void HudUpdatePatch(HudManager __instance)
{
if (GameManager.Instance != null && GameManager.Instance.GameHasStarted &&
if (GameManager.Instance && GameManager.Instance.GameHasStarted &&
CustomGameModeManager.ActiveMode != null)
{
CustomGameModeManager.ActiveMode.HudUpdate(__instance);
@@ -32,7 +31,7 @@ internal static class HudPatches
private static IEnumerator CoPostHudStart(HudManager instance)
{
yield return new WaitUntil((Func<bool>)(() => GameManager.Instance != null));
yield return new WaitUntil((Il2CppSystem.Func<bool>)(() => GameManager.Instance != null));
if (GameManager.Instance.IsHideAndSeek())
yield return null;
var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane");
@@ -42,7 +41,7 @@ internal static class HudPatches
var modelText = modeLabel!.Find("Text_TMP").gameObject;
var modelTextClone = Object.Instantiate(modelText, modeLabel);
Object.Destroy(modelTextClone.GetComponent<TextTranslatorTMP>());
modelTextClone.GetComponent<TextMeshPro>().text = "Gamemode";
modelTextClone.GetComponent<TextMeshPro>().text = MiraLocaleManager.Get("MiraApi.Gamemode", "Gamemode");
var gmText = modeValue!.Find("GameModeText").gameObject;
var gmTextClone = Object.Instantiate(gmText, modeValue);
gmText.SetActive(false);
+52 -78
View File
@@ -1,10 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSystem.Collections.Generic;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
@@ -28,9 +28,9 @@ internal static class GameOptionsMenuPatch
private static readonly List<CategoryHeaderMasked> VanillaHeaders = new();
private static readonly List<OptionBehaviour> VanillaOptions = new();
private static readonly List<OptionBehaviour> MainOptions = new();
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<AbstractOptionGroup>> GameModeGroups = [];
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<OptionBehaviour>> GameModeOptions = [];
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<CategoryHeaderMasked>> GameModeHeaders = [];
private static readonly Dictionary<AbstractGameMode, List<AbstractOptionGroup>> GameModeGroups = [];
private static readonly Dictionary<AbstractGameMode, List<OptionBehaviour>> GameModeOptions = [];
private static readonly Dictionary<AbstractGameMode, List<CategoryHeaderMasked>> GameModeHeaders = [];
private static TextMeshPro _gamemodeDescription = null!;
private static SpriteRenderer _modIcon = null!;
@@ -38,7 +38,7 @@ internal static class GameOptionsMenuPatch
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
public static bool InitPatch(GameOptionsMenu __instance)
{
__instance.Children ??= new List<OptionBehaviour>();
__instance.Children ??= new();
__instance.Children.Clear();
if (MenuState.Instance.CurrentModIdx == 0)
@@ -324,8 +324,8 @@ internal static class GameOptionsMenuPatch
.ToList();
GameModeGroups.Add(mode, [.. filteredGroups]);
var optionBehaviours = new System.Collections.Generic.List<OptionBehaviour>();
var categoryHeaders = new System.Collections.Generic.List<CategoryHeaderMasked>();
var optionBehaviours = new List<OptionBehaviour>();
var categoryHeaders = new List<CategoryHeaderMasked>();
var newNum = mode.ShowNormalGameSettings ? num : num - additionalVanillaScrollNum;
foreach (var group in filteredGroups)
{
@@ -406,7 +406,7 @@ internal static class GameOptionsMenuPatch
__instance.scrollBar.SetYBoundsMax(-num - 1.65f);
}
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref System.Collections.Generic.List<OptionBehaviour> optionBehaviours, ref System.Collections.Generic.List<CategoryHeaderMasked> categoryHeaders)
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref List<OptionBehaviour> optionBehaviours, ref List<CategoryHeaderMasked> categoryHeaders)
{
var categoryHeaderMasked = Object.Instantiate(
menu.categoryHeaderOrigin,
@@ -618,6 +618,8 @@ internal static class GameOptionsMenuPatch
{
menu.MapPicker.gameObject.SetActive(false);
var mod = MenuState.Instance.CurrentMod;
var container = MenuState.Instance.CurrentContainer.transform;
PresetManager.DefaultPresets.TryGetValue(mod, out var defaultPreset);
var filteredGroups = MenuState.Instance.CurrentMenu switch
{
@@ -643,15 +645,13 @@ internal static class GameOptionsMenuPatch
{
foreach (var group in filteredGroups)
{
yield return CoCreateGroup(menu, group);
yield return CoCreateGroup(menu, group, container, defaultPreset);
}
}
}
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group)
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, OptionPreset? defaultPreset)
{
var container = MenuState.Instance.CurrentContainer.transform;
group.Ready = false;
if (group.Header == null || !group.Header)
{
@@ -713,12 +713,6 @@ internal static class GameOptionsMenuPatch
menu.playerOptionOrigin,
container));
OptionPreset? defaultPreset = null;
if (PresetManager.DefaultPresets.TryGetValue(MenuState.Instance.CurrentMod, out var preset))
{
defaultPreset = preset;
}
yield return options.CoLoopWithBudget(newOpt =>
{
newOpt.SetClickMask(menu.ButtonClickMask);
@@ -765,73 +759,53 @@ internal static class GameOptionsMenuPatch
}
menu.Children.Add(newOpt);
var resetBtn = new GameObject("ResetOption")
if (defaultPreset != null && defaultPreset.IsOptionInPreset(newOpt) &&
newOpt is ToggleOption or NumberOption or StringOption)
{
transform =
var resetBtn = new GameObject("ResetOption")
{
parent = newOpt.transform,
localScale = new(.5f, .5f, 1),
localPosition = new Vector3(-3.1f, 0f, -2f),
},
layer = LayerMask.NameToLayer("UI"),
};
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
if (newOpt is ToggleOption toggleOpt)
{
transform =
{
parent = newOpt.transform,
localScale = new(.5f, .5f, 1),
localPosition = new Vector3(-3.1f, 0f, -2f),
},
layer = LayerMask.NameToLayer("UI"),
};
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(toggleOpt);
defaultPreset.ResetOption(newOpt);
}));
}
else if (newOpt is NumberOption numOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(numOpt);
}));
}
else if (newOpt is StringOption strOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(strOpt);
}));
}
if (!defaultPreset!.IsOptionInPreset(newOpt))
{
resetBtn.Destroy();
}
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
+3 -5
View File
@@ -1,11 +1,9 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.GameOptions;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
@@ -165,7 +163,7 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>()).Translate();
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate();
return false;
}
@@ -314,8 +312,8 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>()).Translate();
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate();
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value]);
return false;
}
@@ -26,8 +26,8 @@ public static class PlayerOptionPatches
{
filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId));
moddedPlrOpt.Values = filteredPlayers;
__instance.Values = filteredPlayers.ToIl2CppList();
moddedPlrOpt.Values = __instance.Values;
__instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value);
__instance.SetValueText();
}
File diff suppressed because it is too large. Load diff
+2 -3
View File
@@ -1,5 +1,4 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.Networking;
using MiraAPI.Roles;
using UnityEngine;
@@ -31,13 +30,13 @@ public static class KillButtonPatches
if (__instance.currentTarget && __instance.currentTarget != target)
{
__instance.currentTarget.cosmetics.SetOutline(false, new Nullable<Color>(Color.clear));
__instance.currentTarget.cosmetics.SetOutline(false, new Il2CppSystem.Nullable<Color>(Color.clear));
}
__instance.currentTarget = target;
if (__instance.currentTarget)
{
__instance.currentTarget.cosmetics.SetOutline(true, new Nullable<Color>(customRole.Configuration.KillButtonOutlineColor));
__instance.currentTarget.cosmetics.SetOutline(true, new Il2CppSystem.Nullable<Color>(customRole.Configuration.KillButtonOutlineColor));
__instance.SetEnabled();
return false;
}
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
@@ -14,7 +14,7 @@ public static class LogicRoleSelectionHnsPatch
[HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))]
public static bool AssignRolesForTeam(
LogicRoleSelectionHnS __instance,
List<NetworkedPlayerInfo> players,
Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo> players,
IGameOptions opts,
RoleTeamTypes team,
int teamMax,
@@ -58,7 +58,7 @@ public static class LogicRoleSelectionHnsPatch
case RoleTeamTypes.Impostor:
Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
var newImpostors = new List<NetworkedPlayerInfo>();
var newImpostors = new Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo>();
// Specified Seeker
if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() &&
@@ -113,7 +113,7 @@ public static class LogicRoleSelectionHnsPatch
return false;
}
public static void AssignRolesFromList(List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
public static void AssignRolesFromList(Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
{
while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax)
{
@@ -128,7 +128,7 @@ public static class LogicRoleSelectionHnsPatch
}
private static void AddGuaranteedRoles(
System.Collections.Generic.IEnumerable<RoleBehaviour> guaranteedRoles,
IEnumerable<RoleBehaviour> guaranteedRoles,
IGameOptions opts,
List<RoleTypes> list)
{
@@ -146,7 +146,7 @@ public static class LogicRoleSelectionHnsPatch
}
}
private static void AddPotentialRoles(System.Collections.Generic.IEnumerable<RoleBehaviour> source, IGameOptions opts, List<RoleTypes> list)
private static void AddPotentialRoles(IEnumerable<RoleBehaviour> source, IGameOptions opts, List<RoleTypes> list)
{
var roleOptions = opts.RoleOptions;
var potentialRoles = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData(
+2 -3
View File
@@ -2,7 +2,6 @@
using System.Runtime.CompilerServices;
using HarmonyLib;
using Hazel;
using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
@@ -45,9 +44,9 @@ public static class RoleBehaviourStubs
/// <param name="instance">The <see cref="RoleBehaviour"/> object.</param>
/// <param name="taskStringBuilder">The <see cref="StringBuilder"/> to append the task hint to.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(StringBuilder))]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(Il2CppSystem.Text.StringBuilder))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder)
public static void AppendTaskHint(RoleBehaviour instance, Il2CppSystem.Text.StringBuilder taskStringBuilder)
{
// nothing needed
}
+8 -3
View File
@@ -70,7 +70,12 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
/// <param name="baseOption">The option to reset.</param>
public void ResetOption(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
if (selectedOpt == null)
{
return;
}
selectedOpt.LoadFromPreset(PresetConfig);
/*ModdedOptionsManager.SyncAllOptions();
@@ -84,7 +89,7 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
/// <returns>The value of whether the option is in the preset.</returns>
public bool IsOptionInPreset(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
return selectedOpt != null && selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
}
}
+3
View File
@@ -9,7 +9,10 @@ using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
// TODO: for the love of fucking god please add a namespace on the asset bundle AND here - Atony.
/*
namespace MiraAPI.Presets;
*/
[RegisterInIl2Cpp]
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")]
+16
View File
@@ -17,6 +17,7 @@
<string name="MiraApi.CustomCategory2">Custom Category 2</string>
<string name="MiraApi.ModifierMenuDescription">Configure modifiers and their settings here!</string>
<string name="MiraApi.CustomOptionMenuDescription">Apply game settings for this mod!</string>
<string name="MiraApi.Gamemode">Gamemode</string>
<string name="MiraApi.Gamemode.Classic">Classic</string>
<string name="MiraApi.Gamemode.Classic.Description">The classic Among Us experience!
Find the Impostors or deceive the Crew.</string>
@@ -43,4 +44,19 @@ Outlive the timer or eliminate the crew.</string>
<string name="MiraApi.ModifierDisplay.Page">Page</string>
<string name="MiraApi.Page">Page</string>
<string name="MiraApi.Main">Main</string>
<string name="MiraApi.AbilityType.Basic">Ability</string>
<string name="MiraApi.AbilityType.Indirect">Indirect Ability</string>
<string name="MiraApi.AbilityType.Interaction">Interaction Ability</string>
<string name="MiraApi.AbilityType.Radius">Radius Ability</string>
<string name="MiraApi.AbilityType.Menu">Menu Ability</string>
<string name="MiraApi.AbilityType.Meeting">Meeting Ability</string>
<string name="MiraApi.AbilityType.SelectiveMeeting">Selective Meeting Ability</string>
<string name="MiraApi.AbilityType.MeetingMenu">Meeting Menu Ability</string>
<string name="Options">Options</string>
<string name="MiraApi.Wiki.PlayersTab">Player List</string>
<string name="MiraApi.Wiki.SettingsTab">Game Settings</string>
<string name="MiraApi.Wiki.SearchPlaceholder">Search...</string>
<string name="MiraApi.Wiki.FilterLabel">Filter</string>
<string name="MiraApi.Wiki.GroupingLabel">Grouping</string>
<string name="MiraApi.Wiki.OrderLabel">Order</string>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

+11 -1
View File
@@ -45,6 +45,11 @@ public static class CustomRoleManager
/// </summary>
public static IReadOnlyList<RoleBehaviour> CustomRoleBehaviours { get; private set; } = [];
/// <summary>
/// Gets the <see cref="IReadOnlyList{T}"/> of all roles as <see cref="RoleBehaviour"/> objects.
/// </summary>
public static IReadOnlyList<RoleBehaviour> AllStoredRoleBehaviours { get; private set; } = [];
/// <summary>
/// Gets the <see cref="IReadOnlyList{T}"/> of custom roles as <see cref="ICustomRole"/> objects.
/// </summary>
@@ -52,6 +57,7 @@ public static class CustomRoleManager
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal static readonly Dictionary<Type, ushort> RoleIds = [];
internal static readonly Dictionary<Type, MiraPluginInfo> RoleParentMods = [];
private static CppCollections.List<BaseGameSetting>? _emptySettings;
private static Il2CppReferenceArray<OverlayKillAnimation>? _emptyKillAnimations;
@@ -77,6 +83,7 @@ public static class CustomRoleManager
CustomRoleBehaviours = [.. CustomRoles.Values];
CustomMiraRoles = [.. CustomRoles.Values.OfType<ICustomRole>()];
AllStoredRoleBehaviours = [.. RoleManager.Instance.AllRoles];
}
internal static void RegisterRoleTypes(List<Type> roles, MiraPluginInfo pluginInfo)
@@ -99,6 +106,7 @@ public static class CustomRoleManager
}
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
RoleParentMods[roleType] = pluginInfo;
}
catch (Exception ex)
{
@@ -205,7 +213,9 @@ public static class CustomRoleManager
/// <returns>A <see cref="MiraPluginInfo"/> object representing the parent mod of the role.</returns>
public static MiraPluginInfo FindParentMod(ICustomRole role)
{
return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
return RoleParentMods.TryGetValue(role.GetType(), out var mod)
? mod
: MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
}
/// <summary>
+25 -13
View File
@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using BepInEx.Configuration;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
@@ -68,9 +71,10 @@ public interface ICustomRole : IOptionable
string RoleLongDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "TabDescription");
/// <summary>
/// Gets the wiki description of the role. Used in the wiki, but not currently required.
/// Gets the wiki description of the role. Used in the wiki and normally appends the options text as well.
/// </summary>
string RoleWikiDescription => MiraLocaleManager.Get(RoleWikiDescriptionLocale, RoleLongDescription);
string RoleWikiDescription => MiraLocaleManager.Get(RoleWikiDescriptionLocale, RoleLongDescription) +
Helpers.GetOptionsText(GetType());
/// <summary>
/// Gets the role's wiki description id for localization.
@@ -314,26 +318,34 @@ public interface ICustomRole : IOptionable
/// </summary>
/// <param name="guide">The guide object.</param>
/// <param name="titleText">The title text object.</param>
/// <param name="titleIcon">The title's icon.</param>
/// <param name="parent">The scroller parent.</param>
/// <returns>The <see cref="GameObject"/> of the wiki.</returns>
GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent)
GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent)
{
parent.ScrollToTop();
var obj = Helpers.CreateAdvancedWikiTab(
var abilities = WikiAbilities;
return Helpers.CreateAdvancedWikiPage(
guide,
RoleNameLocale,
RoleName + $" ({RoleFactionTitle})",
RoleWikiDescription,
titleText,
out var desc);
obj.transform.SetParent(parent.Inner.transform);
obj.transform.localPosition = new Vector3(0f, 0f, 0f);
parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2, 0f, 999f));
return obj;
titleIcon,
parent,
RoleNameLocale,
$"{RoleName} ({RoleFactionTitle})",
Configuration.Icon ?? MiraAssets.IconNeutral.LoadAsset(),
RoleWikiDescription,
abilities);
}
/// <summary>
/// Gets the function that determines whether the role should be toggled on or off in the game settings.
/// </summary>
Func<bool> VisibleInSettings => () => true;
/// <summary>
/// Gets the list of abilities or other similar information to display below the text of a role's wiki page.
/// </summary>
[HideFromIl2Cpp]
public List<AdvancedWikiAbilityDescription> WikiAbilities => [];
}
public record struct AdvancedWikiAbilityDescription(string Name, string AbilityType, string Description, LoadableAsset<Sprite> Icon);
+176 -1
View File
@@ -19,7 +19,7 @@ public static class MiraAssets
new Vector2(0.5f, 0.5f),
100f,
0U,
SpriteMeshType.Tight,
SpriteMeshType.FullRect,
new Vector4(20, 20, 20, 20));
RoundedBox = new LoadableAssetWrapper<Sprite>(boxSprite);
@@ -160,5 +160,180 @@ public static class MiraAssets
/// </summary>
public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png");
/// <summary>
/// Gets the sprite used for players button while hovering.
/// </summary>
public static LoadableResourceAsset WikiPlayersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonHover.png");
/// <summary>
/// Gets the sprite used for the players button while hovering.
/// </summary>
public static LoadableResourceAsset WikiPlayersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonIdle.png");
/// <summary>
/// Gets the sprite used for the players button while hovering.
/// </summary>
public static LoadableResourceAsset WikiPlayersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonOpen.png");
/// <summary>
/// Gets the sprite used for the settings button while hovering.
/// </summary>
public static LoadableResourceAsset WikiSettingsButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonHover.png");
/// <summary>
/// Gets the sprite used for the settings button while hovering.
/// </summary>
public static LoadableResourceAsset WikiSettingsButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonIdle.png");
/// <summary>
/// Gets the sprite used for the settings button while hovering.
/// </summary>
public static LoadableResourceAsset WikiSettingsButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonOpen.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset WikiRolesButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset WikiRolesButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupCountHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupCountIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupFactionHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupFactionIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupNoneHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupNoneIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchClearHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchClearIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchIconHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchIconIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingAzHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingAzIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingZaHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingZaIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleAllHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleAllIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOffHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOffIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffIdle.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOnHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnHover.png");
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOnIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnIdle.png");
/// <summary>
/// Gets the sprite used for crewmates.
/// </summary>
public static LoadableResourceAsset IconCrewmate { get; } = new("MiraAPI.Resources.Wiki.IconCrewmate.png");
/// <summary>
/// Gets the sprite used for impostors.
/// </summary>
public static LoadableResourceAsset IconImpostor { get; } = new("MiraAPI.Resources.Wiki.IconImpostor.png");
/// <summary>
/// Gets the sprite used for neutrals.
/// </summary>
public static LoadableResourceAsset IconNeutral { get; } = new("MiraAPI.Resources.Wiki.IconNeutral.png");
/// <summary>
/// Gets the sprite used for the player list.
/// </summary>
public static LoadableResourceAsset IconPlayerList { get; } = new("MiraAPI.Resources.Wiki.IconPlayerList.png");
/// <summary>
/// Gets the sprite used for settings.
/// </summary>
public static LoadableResourceAsset IconSettings { get; } = new("MiraAPI.Resources.Wiki.IconSettings.png");
/// <summary>
/// Gets the sprite used for terminology.
/// </summary>
public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png");
internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png");
}
+7 -1
View File
@@ -48,7 +48,13 @@ public static class SpriteTools
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
{
var tex = LoadTextureFromResourcePath(resourcePath, assembly);
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
var sprite = Sprite.Create(
tex,
new Rect(0, 0, tex.width, tex.height),
new Vector2(0.5f, 0.5f),
pixelsPerUnit,
0,
SpriteMeshType.FullRect);
sprite.name = resourcePath;
return sprite;
}
+3 -3
View File
@@ -226,7 +226,7 @@ public static class Extensions
public static IEnumerator CoLoopWithBudget<T>(this IEnumerable<T> collection, Action<T> action)
{
var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60;
long budget = 1 / (fps * 2); // default is half of current frame time.
var budget = 1000L / (fps * 2); // default is half of current frame time.
yield return collection.CoLoopWithBudget(budget, action);
}
@@ -355,8 +355,8 @@ public static class Extensions
public static void ClearGarbageCollector()
{
Resources.UnloadUnusedAssets();
Il2CppSystem.GC.Collect();
GC.Collect();
/*Il2CppSystem.GC.Collect();
GC.Collect();*/
}
/// <summary>
+304 -29
View File
@@ -1,17 +1,27 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities.Assets;
using Rewired;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using MethodBase = System.Reflection.MethodBase;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
namespace MiraAPI.Utilities;
@@ -20,6 +30,125 @@ namespace MiraAPI.Utilities;
/// </summary>
public static class Helpers
{
private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
public static ReadOnlyCollection<IModdedOption>? GetModdedOptionsForType(Type classType)
{
var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType);
return group?.Children;
}
public static string GetOptionsText(Type classType)
{
var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType);
var options = group?.Children;
var summaryProvider = group as ISummarizedOptions;
var hiddenKeys = summaryProvider?.WikiHiddenOptionKeys;
if (options == null)
{
return string.Empty;
}
var lines = new List<string>();
var insertedSummary = false;
foreach (var option in options)
{
StringNames? key = option switch
{
ModdedToggleOption t => t.StringName,
ModdedEnumOption e => e.StringName,
ModdedNumberOption n => n.StringName,
_ => null,
};
if (!insertedSummary && summaryProvider != null && hiddenKeys != null &&
key.HasValue && hiddenKeys.Contains(key.Value))
{
foreach (var line in summaryProvider.GetWikiOptionSummaryLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
lines.Add(line);
}
}
insertedSummary = true;
}
if (!option.Visible())
{
continue;
}
if (key.HasValue && hiddenKeys?.Contains(key.Value) == true)
{
continue;
}
switch (option)
{
case ModdedToggleOption toggleOption:
lines.Add(
TranslationController.Instance.GetString(toggleOption.StringName) + ": " +
toggleOption.Value);
break;
/*case ModdedMultiSelectOption<Enum> enumOption:
if (!enumOption.Visible())
{
continue;
}
builder.AppendLine(enumOption.Title + ": " + enumOption.Values[enumOption.Value]);
break;*/
case ModdedEnumOption enumOption:
lines.Add(
TranslationController.Instance.GetString(enumOption.StringName) + ": " +
MiraLocaleManager.Get(
enumOption.Values[enumOption.Value],
enumOption.Values[enumOption.Value]));
break;
case ModdedNumberOption numberOption:
var optionStr = TrailingZeroRegex.Replace(
numberOption.Data.GetValueString(numberOption.Value),
string.Empty);
var title = TranslationController.Instance.GetString(numberOption.StringName);
if (numberOption is { NegativeWordValue: not "#", Value: -1 })
{
lines.Add(title + $": {numberOption.NegativeWordValue}");
}
else if (numberOption is { ZeroWordValue: not "#", Value: 0 })
{
lines.Add(title + $": {numberOption.ZeroWordValue}");
}
else
{
lines.Add(title + ": " + optionStr);
}
break;
}
}
if (lines.Count == 0)
{
return string.Empty;
}
var builder = new StringBuilder();
builder.AppendLine(
MiraApiPlugin.Culture,
$"\n<size=50%> \n</size><b><color=#29D7B5>{MiraLocaleManager.Get("Options")}</color></b>");
foreach (var line in lines)
{
builder.AppendLine(line);
}
return builder.ToString();
}
/// <summary>
/// Get all living players.
/// </summary>
@@ -228,7 +357,7 @@ public static class Helpers
var popper = HudManager.Instance.Notifier;
var newMessage = Object.Instantiate(popper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, popper.transform);
newMessage.transform.localPosition = localPos;
newMessage.SetUp(text, spr ?? null, color, new System.Action(() => popper.OnMessageDestroy(newMessage)));
newMessage.SetUp(text, spr ?? null, color, new Action(() => popper.OnMessageDestroy(newMessage)));
popper.lastMessageKey = -1;
popper.ShiftMessages();
popper.AddMessageToQueue(newMessage);
@@ -280,9 +409,12 @@ public static class Helpers
{
var results = new CppCollections.List<Collider2D>();
Physics2D.OverlapCircle(source, radius, filter, results);
return [.. results.ToArray()
.Where(collider2D => collider2D.CompareTag("DeadBody"))
.Select(collider2D => collider2D.GetComponent<DeadBody>())];
return
[
.. results.ToArray()
.Where(collider2D => collider2D.CompareTag("DeadBody"))
.Select(collider2D => collider2D.GetComponent<DeadBody>())
];
}
/// <summary>
@@ -299,9 +431,12 @@ public static class Helpers
{
var results = new CppCollections.List<Collider2D>();
Physics2D.OverlapCircle(source, radius, filter, results);
return [.. results.ToArray()
.Where(collider2D => colliderTag == null || collider2D.CompareTag(colliderTag))
.Select(collider2D => collider2D.GetComponent<T>())];
return
[
.. results.ToArray()
.Where(collider2D => colliderTag == null || collider2D.CompareTag(colliderTag))
.Select(collider2D => collider2D.GetComponent<T>())
];
}
/// <summary>
@@ -320,15 +455,18 @@ public static class Helpers
return !ignoreColliders
? newList
: [.. from player in newList
let vector = player.GetTruePosition() - source
let magnitude = vector.magnitude
where !PhysicsHelpers.AnyNonTriggersBetween(
source,
vector.normalized,
magnitude,
Constants.ShipAndObjectsMask)
select player];
:
[
.. from player in newList
let vector = player.GetTruePosition() - source
let magnitude = vector.magnitude
where !PhysicsHelpers.AnyNonTriggersBetween(
source,
vector.normalized,
magnitude,
Constants.ShipAndObjectsMask)
select player
];
}
/// <summary>
@@ -389,17 +527,16 @@ public static class Helpers
Constants.ShipAndObjectsMask))
select playerControl);
outputList.Sort(
(a, b) =>
{
var magnitude2 = (a.GetTruePosition() - source).magnitude;
var magnitude3 = (b.GetTruePosition() - source).magnitude;
return magnitude2 > magnitude3
? 1
: magnitude2 < magnitude3
? -1
: 0;
});
outputList.Sort((a, b) =>
{
var magnitude2 = (a.GetTruePosition() - source).magnitude;
var magnitude3 = (b.GetTruePosition() - source).magnitude;
return magnitude2 > magnitude3
? 1
: magnitude2 < magnitude3
? -1
: 0;
});
return outputList;
}
@@ -552,6 +689,40 @@ public static class Helpers
return role is ICustomRole custom ? custom.RoleLongDescription : role.BlurbLong;
}
/// <summary>
/// Returns the TextMeshPro sprite tag for a role, matching how role names are shown in-game.
/// Uses the role's <see cref="CustomRoleConfiguration.IconTmp"/> if set, otherwise falls back to the faction icon.
/// </summary>
/// <param name="role">The <see cref="RoleBehaviour"/>.</param>
/// <returns>A <c>&lt;sprite&gt;</c> rich text tag.</returns>
public static string GetTmpIcon(this RoleBehaviour role)
{
return role is ICustomRole custom ? custom.GetTmpIcon() : $"<sprite name=\"AmongUs.Role.{role.Role}\">";
}
/// <summary>
/// Returns the TextMeshPro sprite tag for a custom role, matching how role names are shown in-game.
/// Uses the role's <see cref="CustomRoleConfiguration.IconTmp"/> if set, otherwise falls back to the faction icon.
/// </summary>
/// <param name="role">The <see cref="ICustomRole"/>.</param>
/// <returns>A <c>&lt;sprite&gt;</c> rich text tag.</returns>
public static string GetTmpIcon(this ICustomRole role)
{
return role.Configuration.IconTmp
? $"<sprite name=\"{role.Configuration.IconTmp.name}\">"
: $"<sprite name=\"AmongUs.Role.{role.Team}\">";
}
/// <summary>
/// Returns the TextMeshPro sprite tag for a modifier, or an empty string if it has no <see cref="BaseModifier.IconTmp"/>.
/// </summary>
/// <param name="modifier">The <see cref="BaseModifier"/>.</param>
/// <returns>A <c>&lt;sprite&gt;</c> rich text tag, or <see cref="string.Empty"/>.</returns>
public static string GetTmpIcon(this BaseModifier modifier)
{
return modifier.IconTmp ? $"<sprite name=\"{modifier.IconTmp.name}\">" : string.Empty;
}
/// <summary>
/// Returns whether the <see cref="RoleBehaviour"/> is blacklisted from appearing and spawning. (Only applicable to vanilla roles).
/// </summary>
@@ -570,14 +741,19 @@ public static class Helpers
/// <param name="guide">The guide panel to attach the tab to.</param>
/// <param name="objName">The name of the tab.</param>
/// <param name="title">The title of the tab.</param>
/// <param name="icon">The icon of the page.</param>
/// <param name="description">The description of the tab.</param>
/// <param name="titleTmp">The <see cref="TextMeshPro"/> instance of the title.</param>
/// <param name="titleIcon">The <see cref="SpriteRenderer"/> instance for the title.</param>
/// <param name="descriptionTmp">The <see cref="TextMeshPro"/> instance of the description.</param>
/// <returns>The <see cref="GameObject"/> of the tab.</returns>
public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, string description, TextMeshPro titleTmp, out TextMeshPro descriptionTmp)
public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, Sprite icon, string description, TextMeshPro titleTmp, SpriteRenderer titleIcon, out TextMeshPro descriptionTmp)
{
var obj = new GameObject(objName);
titleTmp.richText = true;
titleTmp.text = title;
titleIcon.sprite = icon;
titleIcon.SetSizeLimit(0.55f);
descriptionTmp = Object.Instantiate(guide.MatchInfoRolePanelPrefab.roleCount, obj.transform);
descriptionTmp.fontSizeMin = descriptionTmp.fontSizeMax = descriptionTmp.fontSize = 2f;
var sourceTmp = HudManager.Instance.TaskPanel.taskText;
@@ -591,4 +767,103 @@ public static class Helpers
descriptionTmp.ForceMeshUpdate();
return obj;
}
/// <summary>
/// Creates a full advanced wiki page with a title, a description, and an optional grid of ability panels.
/// </summary>
/// <param name="guide">The guide object.</param>
/// <param name="titleTmp">The <see cref="TextMeshPro"/> instance of the title.</param>
/// <param name="titleIcon">The <see cref="SpriteRenderer"/> instance for the title.</param>
/// <param name="parent">The <see cref="Scroller"/> the page should be parented to.</param>
/// <param name="objName">The name of the page object.</param>
/// <param name="title">The title of the page.</param>
/// <param name="icon">The icon of the page.</param>
/// <param name="description">The description of the page.</param>
/// <param name="abilities">The abilities to display in a grid below the description.</param>
/// <param name="variableTextSizing">Whether to use variable text sizing for ability descriptions (allows different heights).</param>
/// <returns>The <see cref="GameObject"/> of the wiki page.</returns>
public static GameObject CreateAdvancedWikiPage(
MatchInfoGuide guide,
TextMeshPro titleTmp,
SpriteRenderer titleIcon,
Scroller parent,
string objName,
string title,
Sprite icon,
string description,
IReadOnlyCollection<AdvancedWikiAbilityDescription> abilities,
bool variableTextSizing = false)
{
parent.ScrollToTop();
var obj = CreateAdvancedWikiTab(
guide,
objName,
title,
icon,
description,
titleTmp,
titleIcon,
out var desc);
var num = 0;
var maxTextSize = 0f;
if (abilities.Count != 0)
{
var grid = Object.Instantiate(parent.Inner, obj.transform);
var layoutGroup = grid.GetComponent<GridLayoutGroup>();
layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical;
layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
layoutGroup.spacing = new Vector2(0.75f, 0.4f);
grid.DestroyChildren();
foreach (var ability in abilities)
{
num++;
var panel = Object.Instantiate(guide.MatchInfoRolePanelPrefab, grid);
SetupAbilityPanel(panel, ability, variableTextSizing);
if (variableTextSizing)
{
var descSize = panel.roleDescription.textBounds.size.y;
if (maxTextSize < descSize)
{
maxTextSize = descSize;
}
}
}
grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f);
grid.localScale = new Vector3(1.3f, 1.3f, 1);
}
obj.transform.SetParent(parent.Inner.transform);
obj.transform.localPosition = new Vector3(0f, 0.5f, 0f);
var scrollHeight = variableTextSizing
? (desc.textBounds.size.y - 2.5f) + maxTextSize + 0.475f
: desc.textBounds.size.y - 2.5f + Mathf.Ceil(num / 2f) * 1.45f;
parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f));
return obj;
}
private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false)
{
panel.roleCount.text = ability.AbilityType;
panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleName.text = ability.Name;
panel.roleName.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleDescription.text = ability.Description;
panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f);
panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f);
if (variableTextSizing)
{
panel.roleDescription.alignment = TextAlignmentOptions.Top;
panel.roleDescription.fontSizeMin = 1.5f;
panel.roleDescription.ForceMeshUpdate();
}
panel.roleIcon.sprite = ability.Icon.LoadAsset();
panel.roleIcon.SetSizeLimit(0.13f);
panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50);
panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f);
}
}
+88
View File
@@ -0,0 +1,88 @@
using System;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.Utilities;
/// <summary>
/// A spinning ring used to indicate that content is loading.
/// </summary>
[RegisterInIl2Cpp]
public class LoadingRing(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
private static Sprite? _ringSprite;
/// <summary>
/// Gets or sets the rotation speed in degrees per second.
/// </summary>
public float Speed { get; set; } = 270f;
/// <summary>
/// Creates a <see cref="LoadingRing"/> on the UI layer.
/// </summary>
/// <param name="parent">The transform to parent the spinner to.</param>
/// <param name="localPosition">The local position of the spinner.</param>
/// <param name="scale">The uniform scale of the spinner.</param>
/// <returns>The created <see cref="LoadingRing"/>.</returns>
public static LoadingRing Create(Transform parent, Vector3 localPosition, float scale)
{
if (!_ringSprite)
{
_ringSprite = CreateRingSprite();
}
var obj = new GameObject("LoadingRing")
{
transform =
{
parent = parent,
localPosition = localPosition,
localScale = Vector3.one * scale,
},
layer = LayerMask.NameToLayer("UI"),
};
var renderer = obj.AddComponent<SpriteRenderer>();
renderer.sprite = _ringSprite;
return obj.AddComponent<LoadingRing>();
}
/// <summary>
/// Rotates the ring.
/// </summary>
public void Update()
{
transform.Rotate(0f, 0f, -Speed * Time.deltaTime);
}
private static Sprite CreateRingSprite()
{
const int size = 128;
const float innerRadius = 46f;
const float outerRadius = 60f;
var texture = new Texture2D(size, size, TextureFormat.RGBA32, false)
{
filterMode = FilterMode.Bilinear,
wrapMode = TextureWrapMode.Clamp,
};
var pixels = new Color[size * size];
for (var y = 0; y < size; y++)
{
for (var x = 0; x < size; x++)
{
var dx = x + 0.5f - (size / 2f);
var dy = y + 0.5f - (size / 2f);
var radius = Mathf.Sqrt((dx * dx) + (dy * dy));
var ring = Mathf.Clamp01(radius - innerRadius) * Mathf.Clamp01(outerRadius - radius);
var angle = (Mathf.Atan2(dy, dx) + Mathf.PI) / (2f * Mathf.PI);
pixels[(y * size) + x] = new Color(1f, 1f, 1f, ring * Mathf.Lerp(1f, 0.1f, angle));
}
}
texture.SetPixels(pixels);
texture.Apply();
texture.hideFlags = HideFlags.HideAndDontSave;
var sprite = Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f);
sprite.hideFlags = HideFlags.HideAndDontSave;
return sprite;
}
}
+1 -4
View File
@@ -300,10 +300,7 @@ public static class VotingUtils
// If modified, these will visually change.
votes = @event.Votes;
MeetingHud.Instance.TitleText.text =
TranslationController.Instance.GetString(
StringNames.MeetingVotingResults,
Il2CppSystem.Array.Empty<Il2CppSystem.Object>());
MeetingHud.Instance.TitleText.text = TranslationController.Instance.GetString(StringNames.MeetingVotingResults);
var delays = new Dictionary<byte, int>();
var num = 0;