Compare commits

..
Author SHA1 Message Date
XtraCube 32b368c1ef update other addressable related things 2025-03-29 17:58:05 -04:00
XtraCube 103c32f386 finish AddressablesLoader.cs 2025-03-29 17:56:50 -04:00
XtraCube 662b867dc5 Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-03-29 17:40:17 -04:00
XtraCube adae0225e9 fix merge error 2025-03-29 17:40:00 -04:00
XtraCube 0d347b8b1b Merge branch 'dev' into feat-addressables-2
# Conflicts:
#	MiraAPI.Example/ExampleEventHandlers.cs
2025-03-29 17:39:46 -04:00
XtraCube 4fad586aa7 use new alternate color in lobby 2025-03-29 13:53:58 -04:00
XtraCube 6d51e6806a fix warning in role stubs 2025-03-29 13:37:06 -04:00
XtraCube b28a0a1253 cleanup CustomPlayerMenu.cs 2025-03-29 13:35:16 -04:00
XtraCube 51b54d4340 reorganize vent patch 2025-03-29 13:12:19 -04:00
XtraCube ea9bf162ea rework modifier component caching 2025-03-29 13:12:12 -04:00
XtraCube 57105f487b cleanup 2025-03-29 13:11:15 -04:00
XtraCube c5e424e8ff hide empty role groups in settings 2025-03-29 12:26:38 -04:00
MyDragonBreath d3b40aec5a remove tou and me specific example 2025-03-29 23:25:55 +11:00
XtraCube 521e5a12fd more accurate doc 2025-03-29 00:27:07 -04:00
XtraCube e6e4a86546 fix stupid innerscuff code 2025-03-28 15:20:31 -04:00
XtraCube bb0b678747 fix VERY stupid issue with LobbyViewPanePatches.cs 2025-03-24 18:57:06 -04:00
XtraCube f48143610c fix incorrect chunking algorithm 2025-03-23 15:15:21 -04:00
XtraCube 2cfc5ac039 update example 2025-03-23 14:00:12 -04:00
MyDragonBreath 71b537dba9 final addressables 2025-03-23 22:59:11 +11:00
XtraCube e9beb3b423 fix weird behaviour 2025-03-21 15:24:48 -04:00
MyDragonBreath 23c0397afe an approach to addressables 2025-03-19 23:07:09 +11:00
ang-xd 7917d4b5d9 Make some changes to role registration for sounds 2025-03-16 19:05:12 -04:00
34 changed files with 996 additions and 140 deletions

No files matched your search

+5 -5
View File
@@ -9,7 +9,10 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
namespace MiraAPI.Example;
@@ -38,12 +41,9 @@ public static class ExampleEventHandlers
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
{
foreach (var plr in PlayerControl.AllPlayerControls)
foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player.Data.Role is MayorRole))
{
if (plr.Data.Role is MayorRole)
{
plr.GetVoteData().IncreaseRemainingVotes(1);
}
plr.GetVoteData().IncreaseRemainingVotes(1);
}
}
+4 -2
View File
@@ -1,4 +1,5 @@
using MiraAPI.Roles;
using AmongUs.GameOptions;
using MiraAPI.Roles;
using TMPro;
using UnityEngine;
@@ -12,9 +13,10 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this)
public CustomRoleConfiguration Configuration => new(this)
{
OptionsScreenshot = ExampleAssets.Banner,
IntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Shapeshifter),
};
public void PlayerControlFixedUpdate(PlayerControl playerControl)
@@ -79,17 +79,7 @@ public abstract class ModdedOption<T> : IModdedOption
/// <summary>
/// Gets or sets the config definition of the option.
/// </summary>
public ConfigDefinition? ConfigDefinition
{
get => _configDefinition;
set
{
if (_configDefinition is not null) return;
_configDefinition = value;
}
}
private ConfigDefinition? _configDefinition;
public ConfigDefinition? ConfigDefinition { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
+27 -27
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
@@ -8,6 +9,8 @@ using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace MiraAPI.Hud;
@@ -16,22 +19,18 @@ namespace MiraAPI.Hud;
/// </summary>
/// <param name="il2CppPtr">Used by Il2Cpp. Do not use constructor, this is a MonoBehaviour.</param>
[RegisterInIl2Cpp]
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
public ShapeshifterPanel PanelPrefab;
public float XStart = -0.8f;
public float YStart = 2.15f;
public float XOffset = 1.95f;
public float YOffset = -0.65f;
public UiElement BackButton;
public UiElement DefaultButtonSelected;
public ShapeshifterPanel panelPrefab;
public float xStart = -0.8f;
public float yStart = 2.15f;
public float xOffset = 1.95f;
public float yOffset = -0.65f;
public UiElement backButton;
public UiElement defaultButtonSelected;
public List<ShapeshifterPanel> potentialVictims;
/// <summary>
@@ -42,17 +41,17 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>().ShapeshifterMenu;
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>()!.ShapeshifterMenu;
var newMenu = Instantiate(ogMenu);
var customMenu = newMenu.gameObject.AddComponent<CustomPlayerMenu>();
customMenu.PanelPrefab = newMenu.PanelPrefab;
customMenu.XStart = newMenu.XStart;
customMenu.YStart = newMenu.YStart;
customMenu.XOffset = newMenu.XOffset;
customMenu.YOffset = newMenu.YOffset;
customMenu.BackButton = newMenu.BackButton;
var back = customMenu.BackButton.GetComponent<PassiveButton>();
customMenu.panelPrefab = newMenu.PanelPrefab;
customMenu.xStart = newMenu.XStart;
customMenu.yStart = newMenu.YStart;
customMenu.xOffset = newMenu.XOffset;
customMenu.yOffset = newMenu.YOffset;
customMenu.backButton = newMenu.BackButton;
var back = customMenu.backButton.GetComponent<PassiveButton>();
back.OnClick.RemoveAllListeners();
back.OnClick.AddListener((UnityAction)(() =>
{
@@ -65,7 +64,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
newMenu.DestroyImmediate();
customMenu.transform.SetParent(Camera.main.transform, false);
customMenu.transform.SetParent(Camera.main!.transform, false);
customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
return customMenu;
}
@@ -75,6 +74,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
ControllerManager.Instance.CloseOverlayMenu(name);
}
/// <inheritdoc />
public override void Begin(PlayerTask task)
{
throw new NotImplementedException("Use the other Begin method.");
@@ -90,7 +90,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
MinigameStubs.Begin(this, null);
var back = BackButton.GetComponent<PassiveButton>();
var back = backButton.GetComponent<PassiveButton>();
back.OnClick.AddListener((UnityAction)(() =>
{
onClick(null);
@@ -107,13 +107,13 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
var num = i % 3;
var num2 = i / 3;
var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
var shapeshifterPanel = Instantiate(PanelPrefab, transform);
shapeshifterPanel.transform.localPosition = new Vector3(XStart + num * XOffset, YStart + num2 * YOffset, -1f);
var shapeshifterPanel = Instantiate(panelPrefab, transform);
shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
potentialVictims.Add(shapeshifterPanel);
list2.Add(shapeshifterPanel.Button);
}
ControllerManager.Instance.OpenOverlayMenu(name, BackButton, DefaultButtonSelected, list2);
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}
}
+10 -9
View File
@@ -1,19 +1,12 @@
using System;
using BepInEx;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Il2CppInterop.Runtime;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MonoMod.Utils;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.ProBuilder;
using Delegate = Il2CppSystem.Delegate;
namespace MiraAPI;
@@ -26,8 +19,16 @@ namespace MiraAPI;
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
/// <summary>
/// Gets the branding Mira API color.
/// </summary>
public static Color MiraColor { get; } = new Color32(238, 154, 112, 255);
public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255);
/// <summary>
/// Gets the default color for headers in the options menu.
/// </summary>
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
+22 -7
View File
@@ -29,9 +29,9 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
private List<BaseModifier> Modifiers { get; set; } = [];
private PlayerControl? _player;
private PlayerControl _player = null!;
private TextMeshPro? _modifierText;
private TextMeshPro _modifierText = null!;
private readonly List<BaseModifier> _toRemove = [];
@@ -42,9 +42,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toRemove.AddRange(Modifiers);
}
private void Start()
private void Awake()
{
_player = GetComponent<PlayerControl>();
if (_player == null)
{
Logger<MiraApiPlugin>.Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Destroy(this);
return;
}
ModifierExtensions.ModifierComponents.Add(_player, this);
Modifiers = [];
if (!_player.AmOwner)
@@ -78,9 +87,9 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
if (_toAdd.Count > 0 || _toRemove.Count > 0)
{
if (_player?.AmOwner == true)
if (_player.AmOwner && HudManager.InstanceExists)
{
HudManager.Instance?.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled);
HudManager.Instance.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled);
}
_toAdd.Clear();
_toRemove.Clear();
@@ -101,7 +110,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
modifier.Update();
}
if (!_modifierText || _player?.AmOwner == false)
if (!_modifierText || !_player.AmOwner)
{
return;
}
@@ -125,7 +134,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
builder.AppendLine(modifier.GetHudString());
}
_modifierText!.text = showText ? builder.ToString() : string.Empty;
_modifierText.text = showText ? builder.ToString() : string.Empty;
}
private void OnDestroy()
{
// Remove this component from the global list when destroyed
ModifierExtensions.ModifierComponents.Remove(_player);
}
/// <summary>
+101
View File
@@ -0,0 +1,101 @@
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(HatsTab))]
public static class HatsTabPatches
{
private static SortedList<string, List<HatData>> sortedHats = [];
[HarmonyPatch(nameof(HatsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(HatsTab __instance)
{
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
if (sortedHats.Count == 0)
{
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
sortedHats = new SortedList<string, List<HatData>>(comparer);
foreach (var hat in allHats)
{
if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = [];
sortedHats[hat.StoreName].Add(hat);
}
}
GenerateHats(__instance);
return false;
}
private static void GenerateHats(HatsTab __instance)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
foreach ((string groupName, List<HatData> hats) in sortedHats)
{
hatIndex = (hatIndex + 4) / 5 * 5; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var hat in hats.OrderBy(HatManager.Instance.allHats.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), hat);
hatIndex += 1;
}
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentHatIsEquipped = true;
}
private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = hat.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = hat;
__instance.ColorChips.Add(colorChip);
}
}
@@ -0,0 +1,21 @@
using HarmonyLib;
using MiraAPI.Utilities.Assets;
namespace MiraAPI.Patches.Menu;
/// <summary>
/// General MainMenuManage patches.
/// </summary>
[HarmonyPatch(typeof(MainMenuManager))]
public static class MainMenuManagerPatches
{
/// <summary>
/// A postifix on Awake to load all the addressables registered.
/// </summary>
[HarmonyPatch(nameof(MainMenuManager.Awake))]
[HarmonyPostfix]
public static void AwakePostfix()
{
AddressablesLoader.LoadAll();
}
}
@@ -0,0 +1,102 @@
using AmongUs.Data;
using HarmonyLib;
using Innersloth.Assets;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(NameplatesTab))]
public static class NameplatesTabPatches
{
private static readonly SortedList<string, List<NamePlateData>> SortedNameplates = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
{
foreach (var item in data)
{
if (!SortedNameplates.ContainsKey(item.Key)) SortedNameplates.Add(item.Key, []);
SortedNameplates[item.Key].Add(item.Visor);
}
}
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
{
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
GenerateHats(__instance);
return false;
}
private static void GenerateHats(NameplatesTab __instance)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
foreach ((string groupName, List<NamePlateData> visors) in SortedNameplates)
{
hatIndex = (hatIndex + 1) / 2 * 2; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 2;
foreach (var visor in visors.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
hatIndex += 1;
}
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentNameplateIsEquipped = true;
}
private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = namePlate.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectNameplate(HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = namePlate.ProdId;
var x = (NamePlateViewData viewdata) =>
{
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewdata?.Image;
//(colorChip as NameplateChip).image.sprite = viewdata?.Image;
};
__instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync<NamePlateViewData>(__instance, namePlate.GetAssetReference(), x));
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Tag = namePlate;
__instance.ColorChips.Add(colorChip);
}
}
+101
View File
@@ -0,0 +1,101 @@
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(SkinsTab))]
public static class SkinsTabPatches
{
private static SortedList<string, List<SkinData>> sortedSkins = [];
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(SkinsTab __instance)
{
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
if (sortedSkins.Count == 0)
{
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
sortedSkins = new SortedList<string, List<SkinData>>(comparer);
foreach (var skin in allSkins)
{
if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = [];
sortedSkins[skin.StoreName].Add(skin);
}
}
GenerateHats(__instance);
return false;
}
private static void GenerateHats(SkinsTab __instance)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
foreach ((string groupName, List<SkinData> skins) in sortedSkins)
{
hatIndex = (hatIndex + 4) / 5 * 5; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), skin);
hatIndex += 1;
}
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentSkinIsEquipped = true;
}
private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = skin.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = skin.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin);
skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = skin;
__instance.ColorChips.Add(colorChip);
}
}
+98
View File
@@ -0,0 +1,98 @@
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(VisorsTab))]
public static class VisorsTabPatches
{
private static readonly SortedList<string, List<VisorData>> SortedVisors = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
{
foreach (var item in data)
{
if (!SortedVisors.ContainsKey(item.Key)) SortedVisors.Add(item.Key, []);
SortedVisors[item.Key].Add(item.Visor);
}
}
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
{
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
GenerateHats(__instance);
return false;
}
private static void GenerateHats(VisorsTab __instance)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
foreach ((string groupName, List<VisorData> visors) in SortedVisors)
{
hatIndex = (hatIndex + 4) / 5 * 5; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
hatIndex += 1;
}
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentVisorIsEquipped = true;
}
private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = visor.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = visor.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor);
visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = visor;
__instance.ColorChips.Add(colorChip);
}
}
@@ -4,7 +4,6 @@ using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
@@ -250,14 +250,9 @@ public static class LobbyViewPanePatches
var list = new List<Type>();
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
var roleGroups = SelectedMod.CustomRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
if (roleGroups is null)
{
return;
}
// sort the groups by priority
var sortedRoleGroups = roleGroups
.OrderBy(x => x.Key.Priority)
@@ -265,7 +260,7 @@ public static class LobbyViewPanePatches
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any())
if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings))
{
continue;
}
@@ -340,7 +335,7 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
viewSettingsInfoPanelRoleVariant.chanceBackground.color =
viewSettingsInfoPanelRoleVariant.background.color =
customRole.RoleColor.GetAlternateColor();
customRole.RoleColor.FindAlternateColor();
instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
@@ -83,10 +83,9 @@ public static class RoleSettingMenuPatches
var quotaThing = __instance.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader");
var template = __instance.transform.parent.parent.GetComponent<GameSettingMenu>().GameSettingsTab.categoryHeaderOrigin;
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any())
if (!grouping.Any() || grouping.All(x=>x.Configuration.HideSettings))
{
continue;
}
+6 -35
View File
@@ -15,14 +15,11 @@ namespace MiraAPI.Patches;
/// General patches for the PlayerControl class.
/// </summary>
[HarmonyPatch(typeof(PlayerControl))]
public static class PlayerControlPatches
internal static class PlayerControlPatches
{
/// <summary>
/// Adds the modifier component to the player on start.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Start))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlStartPostfix(PlayerControl __instance)
{
if (__instance.gameObject.TryGetComponent<ModifierComponent>(out var modifierComp))
@@ -39,12 +36,9 @@ public static class PlayerControlPatches
__instance.gameObject.AddComponent<PlayerVoteData>();
}
/// <summary>
/// Calls the OnDeath method for all active modifiers.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Die))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{
var deathEvent = new PlayerDeathEvent(__instance, reason);
@@ -58,13 +52,9 @@ public static class PlayerControlPatches
}
}
/// <summary>
/// Used to trigger the <see cref="CompleteTaskEvent"/>.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
/// <param name="idx">The task id.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.CompleteTask))]
// ReSharper disable once InconsistentNaming
public static void PlayerCompleteTaskPostfix(PlayerControl __instance, uint idx)
{
var playerTask = __instance.myTasks.ToArray().First(playerTask => playerTask.Id == idx);
@@ -75,14 +65,9 @@ public static class PlayerControlPatches
}
}
/// <summary>
/// Used to trigger the <see cref="BeforeMurderEvent"/>.
/// </summary>
/// <param name="__instance">The source player.</param>
/// <param name="target">The target.</param>
/// <param name="didSucceed">Whether the kill succeeded.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
{
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
@@ -91,12 +76,9 @@ public static class PlayerControlPatches
didSucceed = beforeMurderEvent.IsCancelled;
}
/// <summary>
/// FixedUpdate handler for custom roles and custom buttons.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
{
if (__instance.Data?.Role is ICustomRole customRole)
@@ -124,15 +106,4 @@ public static class PlayerControlPatches
button.FixedUpdateHandler(__instance);
}
}
/// <summary>
/// Clear from modifier component cache.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.OnDestroy))]
public static void PlayerControlOnDestroyPrefix(PlayerControl __instance)
{
ModifierExtensions.ModifierComponents.Remove(__instance);
}
}
@@ -1,4 +1,5 @@
using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
@@ -21,6 +22,14 @@ public static class GameStartupPatch
}
_runOnce = true;
if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) return;
foreach (var queue in MiraPluginManager.Instance.QueuedRoleRegistrations)
{
CustomRoleManager.RegisterRoleTypes(queue.Value, queue.Key);
}
MiraPluginManager.Instance.QueuedRoleRegistrations.Clear();
CustomRoleManager.RegisterInRoleManager();
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ public static class RoleBehaviourPatch
__result = behaviour.RoleColor;
return false;
}
}
}
@@ -5,6 +5,7 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
@@ -15,6 +16,43 @@ public static class RoleManagerPatches
[HarmonyPatch(nameof(RoleManager.SetRole))]
public static void SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
{
if (!targetPlayer)
{
return;
}
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.");
return;
}
if (data.Role)
{
data.Role.Deinitialize(targetPlayer);
Object.Destroy(data.Role.gameObject);
}
var roleBehaviour = Object.Instantiate<RoleBehaviour>(__instance.AllRoles.First(r => r.Role == roleType), data.gameObject.transform);
roleBehaviour.Initialize(targetPlayer);
targetPlayer.Data.Role = roleBehaviour;
targetPlayer.Data.RoleType = roleType;
// basically everything is the same except this one if statement
// innersloth decided to check the roleType manually instead of the role behaviour.
if (!roleBehaviour.IsDead)
{
targetPlayer.Data.RoleWhenAlive = new Il2CppSystem.Nullable<RoleTypes>(roleType);
}
roleBehaviour.AdjustTasks(targetPlayer);
switch (roleBehaviour.IsDead)
{
case true when !targetPlayer.Data.IsDead:
targetPlayer.Die(DeathReason.Kill, false);
return;
case false when targetPlayer.Data.IsDead:
targetPlayer.Revive();
break;
}
var @event = new SetRoleEvent(targetPlayer, roleType);
MiraEventManager.InvokeEvent(@event);
}
+8 -5
View File
@@ -4,6 +4,9 @@ using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
/// <summary>
/// Stub methods for the RoleBehaviour class. Needed because of Il2Cpp limitations on injected classes.
/// </summary>
[HarmonyPatch]
public static class RoleBehaviourStubs
{
@@ -57,7 +60,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CanUse(RoleBehaviour instance, IUsable console)
{
return default;
return false;
}
/// <summary>
@@ -84,7 +87,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool DidWin(RoleBehaviour instance, GameOverReason gameOverReason)
{
return default;
return false;
}
/// <summary>
@@ -97,7 +100,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static PlayerControl FindClosestTarget(RoleBehaviour instance)
{
return null;
return null!;
}
/// <summary>
@@ -110,7 +113,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static float GetAbilityDistance(RoleBehaviour instance)
{
return default;
return 0;
}
/// <summary>
@@ -124,7 +127,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsValidTarget(RoleBehaviour instance, NetworkedPlayerInfo target)
{
return default;
return false;
}
/// <summary>
@@ -8,17 +8,13 @@ using UnityEngine;
namespace MiraAPI.Patches;
/// <summary>
/// Vent patches to make sure the player is able to use the vent.
/// Used to change vent behaviour for the event system, custom roles, and modifiers.
/// </summary>
[HarmonyPatch(typeof(Vent))]
public static class VentPatches
[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))]
internal static class VentCanUsePatch
{
/// <summary>
/// CanUse patch.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(Vent.CanUse))]
public static void VentCanUsePostfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
// ReSharper disable InconsistentNaming
public static void Postfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
{
var @event = new PlayerCanUseEvent(__instance.Cast<IUsable>());
MiraEventManager.InvokeEvent(@event);
+2 -2
View File
@@ -27,7 +27,7 @@ public sealed class MiraPluginManager
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values];
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = new();
internal static MiraPluginManager Instance { get; private set; } = new();
internal void Initialize()
@@ -97,7 +97,7 @@ public sealed class MiraPluginManager
}
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
CustomRoleManager.RegisterRoleTypes(roles, info);
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
+3 -2
View File
@@ -1,8 +1,6 @@
using AmongUs.GameOptions;
using Il2CppSystem.Web.Util;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using Helpers = MiraAPI.Utilities.Helpers;
namespace MiraAPI.Roles;
@@ -34,6 +32,9 @@ public record struct CustomRoleConfiguration
TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate;
HideSettings = roleBehaviour?.IsDead == true;
ShowInFreeplay = roleBehaviour?.IsDead == false;
IntroSound = role.Team is ModdedRoleTeams.Crewmate
? CustomRoleManager.CrewmateIntroSound
: CustomRoleManager.ImpostorIntroSound;
}
/// <summary>
+13 -1
View File
@@ -7,13 +7,13 @@ using Il2CppInterop.Runtime.Injection;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.TextCore;
using Object = UnityEngine.Object;
namespace MiraAPI.Roles;
@@ -23,6 +23,18 @@ namespace MiraAPI.Roles;
/// </summary>
public static class CustomRoleManager
{
/// <summary>
/// The default Among Us Crewmate Intro Sound.
/// </summary>
public static readonly LoadableAsset<AudioClip> CrewmateIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Crewmate)!;
/// <summary>
/// The default Among Us Impostor Intro Sound.
/// </summary>
public static readonly LoadableAsset<AudioClip> ImpostorIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!;
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal static readonly Dictionary<Type, ushort> RoleIds = [];
+13
View File
@@ -3,6 +3,7 @@ using System.Globalization;
using System.Linq;
using System.Text;
using AmongUs.GameOptions;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Roles;
@@ -46,4 +47,16 @@ public static class CustomRoleUtils
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
return taskStringBuilder;
}
/// <summary>
/// Returns an intro sound from a role.
/// </summary>
/// <param name="roleType">The role type.</param>
/// <returns>The intro sound.</returns>
public static LoadableAsset<AudioClip>? GetIntroSound(RoleTypes roleType)
{
var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType);
if (role is ICustomRole customRole) return customRole.Configuration.IntroSound;
return new PreloadedAsset<AudioClip>(role!.IntroSound);
}
}
-1
View File
@@ -3,7 +3,6 @@ using BepInEx.Configuration;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
using Reactor.Utilities;
using UnityEngine;
@@ -0,0 +1,222 @@
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Menu;
using Reactor.Utilities;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// The component for loading Addressables, and cosmetics.
/// </summary>
public static class AddressablesLoader
{
private static bool _isInitialized;
private static readonly List<(string Location, string ProviderSuffix)> CatalogLocations = [];
private static readonly List<string> LoadedLocations = [];
private static readonly List<string> RegisteredHatKeys = [];
private static readonly List<(string, string)> RegisteredVisorKeys = [];
private static readonly List<(string, string)> RegisteredNameplateKeys = [];
private static readonly List<string> RegisteredSkinKeys = [];
/// <summary>
/// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
/// </summary>
/// <param name="location">The location, remote or otherwise, of the addressables.</param>
/// <param name="providerSuffix">The suffix of the provider for an addressables package.</param>
public static void RegisterCatalog(string location, string providerSuffix = "")
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
return;
}
CatalogLocations.Add((location, providerSuffix));
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="HatData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="HatData"/>.</param>
public static void RegisterHats(string addressablesKey)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
return;
}
RegisteredHatKeys.Add(addressablesKey);
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="SkinData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="SkinData"/>.</param>
public static void RegisterSkins(string addressablesKey)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
return;
}
RegisteredSkinKeys.Add(addressablesKey);
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="NamePlateData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="NamePlateData"/>.</param>
/// /// <param name="groupTitle">The title of the group for visors.</param>
public static void RegisterNameplates(string addressablesKey, string groupTitle)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
return;
}
RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="VisorData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="VisorData"/>.</param>
/// <param name="groupTitle">The title of the group for visors.</param>
public static void RegisterVisors(string addressablesKey, string groupTitle)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
return;
}
RegisteredVisorKeys.Add((addressablesKey, groupTitle));
}
internal static void LoadAll()
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized.");
return;
}
_isInitialized = true;
foreach (var (location, providerSuffix) in CatalogLocations)
{
Coroutines.Start(CoLoadAddressables(location, providerSuffix));
}
Coroutines.Start(LoadCosmetics());
}
internal static IEnumerator CoLoadAddressables(string location, string suffix = "")
{
while (!AmongUsClient.Instance) yield return null;
// Load the local/remote content catalog
var catalogOperation = Addressables.LoadContentCatalog(location, suffix);
yield return catalogOperation;
// Check for errors
if (catalogOperation.Status != AsyncOperationStatus.Succeeded)
{
Logger<MiraApiPlugin>.Error($"Failed to load catalog {location}.");
}
Logger<MiraApiPlugin>.Info($"Loaded addressables {location}.");
LoadedLocations.Add(location);
}
[HideFromIl2Cpp]
internal static IEnumerator LoadCosmetics()
{
while (!AmongUsClient.Instance || CatalogLocations.Select(x=>x.Location).Any(x=>!LoadedLocations.Contains(x))) yield return null;
var hatBehaviours = DiscoverData<HatData>(RegisteredHatKeys);
var skinBehaviours = DiscoverData<SkinData>(RegisteredSkinKeys);
var namePlateBehaviours = DiscoverAndReportData<NamePlateData>(RegisteredNameplateKeys);
var visorBehaviours = DiscoverAndReportData<VisorData>(RegisteredVisorKeys);
var hatData = new List<HatData>();
hatData.AddRange(DestroyableSingleton<HatManager>.Instance.allHats);
hatData.ForEach(x => x.StoreName = "Vanilla");
DestroyableSingleton<HatManager>.Instance.allHats = PrepareArray(hatData, hatBehaviours);
var skinData = new List<SkinData>();
skinData.AddRange(DestroyableSingleton<HatManager>.Instance.allSkins);
skinData.ForEach(x => x.StoreName = "Vanilla");
DestroyableSingleton<HatManager>.Instance.allSkins = PrepareArray(skinData, skinBehaviours);
var visorData = new List<VisorData>();
visorData.AddRange(DestroyableSingleton<HatManager>.Instance.allVisors);
VisorsTabPatches.AddRange(visorBehaviours);
DestroyableSingleton<HatManager>.Instance.allVisors = PrepareArray(visorData, visorBehaviours.Select(x=>x.Data).ToList());
var namePlateData = new List<NamePlateData>();
namePlateData.AddRange(DestroyableSingleton<HatManager>.Instance.allNamePlates);
NameplatesTabPatches.AddRange(namePlateBehaviours);
DestroyableSingleton<HatManager>.Instance.allNamePlates = PrepareArray(namePlateData, namePlateBehaviours.Select(x => x.Data).ToList());
}
private static T[] PrepareArray<T>(List<T> data, List<T> behaviours) where T : CosmeticData
{
var count = data.Count;
for (int i = 0; i < behaviours.Count; i++)
{
behaviours[i].displayOrder = count + i;
data.Add(behaviours[i]);
}
return data.ToArray();
}
private static List<T> DiscoverData<T>(List<string> tags)
{
var behaviours = new List<T>();
foreach (var tag in tags)
{
try
{
var allLocations = Addressables.LoadResourceLocationsAsync(tag).WaitForCompletion();
var assets = Addressables.LoadAssetsAsync<T>(allLocations, null, false).WaitForCompletion();
var array = new Il2CppSystem.Collections.Generic.List<T>(assets.Pointer);
behaviours.AddRange(array.ToArray());
}
catch
{
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
}
}
return behaviours;
}
private static List<(string Category, T Data)> DiscoverAndReportData<T>(List<(string Tag, string Category)> tags)
{
var behaviours = new List<(string, T)>();
foreach (var tag in tags)
{
try
{
var allLocations = Addressables.LoadResourceLocationsAsync(tag.Tag).WaitForCompletion();
var assets = Addressables.LoadAssetsAsync<T>(allLocations, null, false).WaitForCompletion();
var array = new Il2CppSystem.Collections.Generic.List<T>(assets.Pointer);
behaviours.AddRange(array.ToArray().Select(x => (tag.Category, x)));
}
catch
{
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
}
}
return behaviours;
}
}
@@ -0,0 +1,55 @@
using System;
using Reactor.Utilities;
using UnityEngine.AddressableAssets;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading an asset from ALL addressable locations.
/// </summary>
/// <param name="uid">The guid of the asset.</param>
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
public class LoadableAddressableAsset<T>(string uid) : LoadableAsset<T> where T : UnityEngine.Object
{
private readonly Action<LoadableAddressableAsset<T>>? _gcAction;
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAddressableAsset{T}"/> class, with added garbageCollector logic,
/// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
/// </summary>
/// <param name="uid">The guid of the asset.</param>
/// <param name="garbageCollection">A lambda that allows GCHandle.Alloc calls without garbage collection interferences.</param>
public LoadableAddressableAsset(string uid, Action<LoadableAddressableAsset<T>> garbageCollection) : this(uid)
{
_gcAction = garbageCollection;
}
/// <summary>
/// Loads the asset from addressables.
/// </summary>
/// <returns>The asset.</returns>
/// <exception cref="Exception">The asset did not load properly.</exception>
public override T LoadAsset()
{
if (LoadedAsset != null)
{
return LoadedAsset;
}
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");
LoadedAsset = remoteReference.LoadAssetAsync<T>().WaitForCompletion();
if (LoadedAsset == null)
{
GC.EndNoGCRegion();
throw new InvalidOperationException($"INVALID ASSET: {uid}");
}
_gcAction?.Invoke(this);
GC.EndNoGCRegion();
return LoadedAsset;
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Reactor.Utilities;
using UnityEngine.AddressableAssets;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading groups of assets from ALL addressable locations.
/// </summary>
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
public class LoadableAddressableAssets<T>(string key) where T : UnityEngine.Object
{
private readonly Action<LoadableAddressableAssets<T>>? _gcAction;
/// <summary>
/// Gets or sets reference to the loaded asset. Intended to be used for caching purposes.
/// </summary>
protected List<T>? LoadedAssets { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAddressableAssets{T}"/> class, with added garbageCollector logic,
/// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
/// </summary>
/// <param name="key">The key of the assets.</param>
/// <param name="garbageCollection">A lambda that allows GCHandle.Alloc calls without garbage collection interferences.</param>
public LoadableAddressableAssets(string key, Action<LoadableAddressableAssets<T>> garbageCollection) : this(key)
{
_gcAction = garbageCollection;
}
/// <summary>
/// Loads the asset from addressables.
/// </summary>
/// <returns>The asset.</returns>
/// <exception cref="Exception">The asset did not load properly.</exception>
public ReadOnlyCollection<T> LoadAssets()
{
if (LoadedAssets != null)
{
return LoadedAssets.AsReadOnly();
}
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");
var assetsIList = Addressables.LoadAssetsAsync<T>(locations, null, false).WaitForCompletion();
if (assetsIList == null)
{
GC.EndNoGCRegion();
throw new InvalidOperationException($"INVALID ASSET/s: {key}");
}
var assetsList = new Il2CppSystem.Collections.Generic.List<T>(assetsIList.Pointer);
LoadedAssets = assetsList.ToArray().ToList();
_gcAction?.Invoke(this);
GC.EndNoGCRegion();
return LoadedAssets.AsReadOnly();
}
}
@@ -0,0 +1,19 @@
using System;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// An implementation for creating loadable assets from existing assets.
/// </summary>
/// <inheritdoc cref="LoadableAsset{T}"/>
/// <inheritdoc />
public class PreloadedAsset<T>(T asset) : LoadableAsset<T> where T : UnityEngine.Object
{
private readonly T _loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset));
/// <inheritdoc />
public override T LoadAsset()
{
return _loadedAsset;
}
}
-1
View File
@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Reflection;
using Reactor.Utilities.Extensions;
using UnityEngine;
+2 -2
View File
@@ -8,10 +8,10 @@ namespace MiraAPI.Utilities;
public static class ColorExtensions
{
/// <summary>
/// Gets the relative luminance of a color (WCAG 2.1 specification).
/// Gets the relative luminance of a color (WCAG 2.1 specification). 0 is black, 1 is white.
/// </summary>
/// <param name="color">Color to calculate luminance for.</param>
/// <returns>Relative luminance value.</returns>
/// <returns>Luminance value between 0 and 1.</returns>
public static float GetRelativeLuminance(this Color color)
{
var r = color.r <= 0.03928f ? color.r / 12.92f : Mathf.Pow((color.r + 0.055f) / 1.055f, 2.4f);
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.Utilities;
internal sealed class ControllableComparer<T> : IComparer<T> where T : IComparable
{
private readonly T[] _forcedToBottom;
private readonly T[] _forcedToTop;
private readonly IComparer<T> _fallbackComparer;
public ControllableComparer(T[] forcedToBottom, T[] forcedToTop, IComparer<T> fallbackComparer)
{
_forcedToBottom = forcedToBottom;
_forcedToTop = forcedToTop;
_fallbackComparer = fallbackComparer;
}
/// <inheritdoc/>
public int Compare(T? x, T? y)
{
if ((_forcedToBottom.Contains(x) && _forcedToBottom.Contains(y)) || (_forcedToTop.Contains(x) && _forcedToTop.Contains(y)))
return _fallbackComparer.Compare(x, y);
if (_forcedToBottom.Contains(x))
return 1;
if (_forcedToBottom.Contains(y))
return -1;
if (_forcedToTop.Contains(x))
return -1;
if (_forcedToTop.Contains(y))
return 1;
return _fallbackComparer.Compare(x, y);
}
}
+2 -1
View File
@@ -283,12 +283,13 @@ public static class Extensions
if (count + length > chunkSize)
{
chunks.Enqueue(current.ToArray());
chunks.Enqueue([.. current]);
current.Clear();
count = 0;
}
current.Add(netData);
count += length;
}
if (current.Count > 0)
-8
View File
@@ -1,17 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.Roles;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.TextCore;
using Object = UnityEngine.Object;
namespace MiraAPI.Utilities;