Merge branch 'dev' into controller-support

This commit is contained in:
Atony authored and GitHub committed 2026-10-13 22:50:50 -05:00
commit 51efa74811
12 files changed
+225 -1062

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ internal static class AmongUsClientSyncPatch
GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value);
}
[HarmonyPostfix]
[HarmonyPrefix]
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))]
public static void PlayerLeftPatch(ClientData data, DisconnectReasons reason)
{
-193
View File
@@ -1,193 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(HatsTab))]
public static class HatsTabPatches
{
private static readonly Dictionary<int, string> StoreNames = [];
private static SortedList<string, List<HatData>> sortedHats = [];
private static int currentPage;
private static void PreviousPage(HatsTab hatsTab)
{
currentPage--;
currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
GenerateHats(hatsTab, currentPage);
}
private static void NextPage(HatsTab hatsTab)
{
currentPage++;
currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
GenerateHats(hatsTab, currentPage);
}
[HarmonyPatch(nameof(HatsTab.OnEnable))]
[HarmonyPrefix]
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
public static bool OnEnablePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return true;
}
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
if (sortedHats.Count == 0)
{
var num = 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);
if (!StoreNames.ContainsValue(hat.StoreName))
{
StoreNames.Add(num, hat.StoreName);
num++;
}
}
}
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return;
}
if (sortedHats.Count == 0) return;
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static IEnumerator? loadRoutine;
private static int hatIndex;
private static void GenerateHats(HatsTab __instance, int page)
{
if (loadRoutine != null) Coroutines.Stop(loadRoutine);
hatIndex = 0;
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var group = sortedHats.Where(x => x.Key == StoreNames[page]);
foreach (var (groupName, hats) in group)
{
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} ({currentPage + 1}/{sortedHats.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
loadRoutine = Coroutines.Start(CoGenerateChips(__instance, hats));
}
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentHatIsEquipped = true;
}
private static IEnumerator CoGenerateChips(HatsTab __instance, List<HatData> hats)
{
__instance.scroller.ScrollToTop();
var batchSize = 5;
for (var i = 0; i < hats.Count; i += batchSize)
{
var batch = hats.Skip(i).Take(batchSize).ToList();
foreach (var hat in batch.OrderBy(HatManager.Instance.allHats.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), hat);
hatIndex += 1;
yield return null;
}
__instance.SetScrollerBounds();
yield return new WaitForSeconds(0.01f);
}
__instance.currentHatIsEquipped = true;
loadRoutine = null;
}
private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = hat.ProductId;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
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))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectHat(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);
}
}
-55
View File
@@ -1,55 +0,0 @@
using System;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
public static class InventoryUtility
{
public static void CreateNextBackButtons<T>(T tab, Action<T> previousPage, Action<T> nextPage)
where T : InventoryTab
{
var title = tab.transform.FindChild("Text");
title.localPosition = new Vector3(.9123f, -.23f, -55);
title.GetComponent<TextMeshPro>().alignment = TextAlignmentOptions.Center;
tab.scroller.Hitbox.transform.localScale -= new Vector3(0, 0.1f, 0);
tab.scroller.Hitbox.transform.localPosition -= new Vector3(0, 0.1f, 0);
var leftButtonElement = Object.Instantiate(PlayerCustomizationMenu.Instance.BackButton.gameObject, tab.transform);
var leftSpriteRenderer = leftButtonElement.GetComponent<SpriteRenderer>();
var leftButton = leftButtonElement.GetComponent<PassiveButton>();
leftButtonElement.transform.localPosition = new Vector3(-1.1f, -0.2f, -55);
leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
leftSpriteRenderer.flipX = true;
leftButton.OnMouseOver = new UnityEvent();
leftButton.OnMouseOver.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
leftButton.OnMouseOut = new UnityEvent();
leftButton.OnMouseOut.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
leftButton.OnClick = new Button.ButtonClickedEvent();
leftButton.OnClick.AddListener((Action)(() => previousPage(tab)));
var rightButtonElement = Object.Instantiate(leftButtonElement, tab.transform);
var rightSpriteRenderer = rightButtonElement.GetComponent<SpriteRenderer>();
var rightButton = rightButtonElement.GetComponent<PassiveButton>();
rightButtonElement.transform.localPosition = new Vector3(2.8f, -0.2f, -55);
rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
rightSpriteRenderer.flipX = false;
rightButton.OnMouseOver = new UnityEvent();
rightButton.OnMouseOver.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
rightButton.OnMouseOut = new UnityEvent();
rightButton.OnMouseOut.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
rightButton.OnClick = new Button.ButtonClickedEvent();
rightButton.OnClick.AddListener((Action)(() => nextPage(tab)));
}
}
@@ -2,7 +2,6 @@
using System.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
@@ -23,10 +22,9 @@ public static class MainMenuManagerPatches
[HarmonyPostfix]
public static void AwakePostfix()
{
var requiredVersion = new Version(2026, 6, 5);
var requiredVersion = new Version(2026, 9, 29);
var version = Version.Parse(Application.version);
NeedsDeepDestroy = version >= requiredVersion;
AddressablesLoader.LoadAll();
Coroutines.Start(SetFps());
}
@@ -1,161 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using Innersloth.Assets;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(NameplatesTab))]
public static class NameplatesTabPatches
{
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
private static readonly SortedList<string, List<NamePlateData>> SortedNameplates = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
private static int currentPage;
internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
{
foreach (var (key, visor) in data)
{
if (!SortedNameplates.ContainsKey(key)) SortedNameplates.Add(key, []);
SortedNameplates[key].Add(visor);
}
}
private static void PreviousPage(NameplatesTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(NameplatesTab tab)
{
currentPage++;
currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return true;
}
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static void GenerateHats(NameplatesTab __instance, int page)
{
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var hatIndex = 0;
var (groupName, nameplates) = SortedNameplates.ToArray()[page];
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} ({currentPage + 1}/{SortedNameplates.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 2;
foreach (var visor in nameplates.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), visor);
hatIndex += 1;
}
__instance.SetScrollerBounds();
__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;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
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))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = namePlate.ProdId;
__instance.StartCoroutine(__instance.CoLoadAssetAsync<NamePlateViewData>(namePlate.GetAssetReference(), (Action<NamePlateViewData>?)LoadNameplate));
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Tag = namePlate;
__instance.ColorChips.Add(colorChip);
return;
void LoadNameplate(NamePlateViewData viewData)
{
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewData.Image;
// (colorChip as NameplateChip).image.sprite = viewData?.Image;
}
}
}
-155
View File
@@ -1,155 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
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 = [];
private static int currentPage;
private static void PreviousPage(SkinsTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(SkinsTab tab)
{
currentPage++;
currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
[HarmonyPrefix]
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
public static bool OnEnablePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return true;
}
__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);
}
}
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(SkinsTab), nameof(SkinsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static void GenerateHats(SkinsTab __instance, int page)
{
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var hatIndex = 0;
var (groupName, skins) = sortedSkins.ToArray()[page];
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} ({currentPage + 1}/{sortedSkins.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), skin);
hatIndex += 1;
}
__instance.SetScrollerBounds();
__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;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
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))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectSkin(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);
}
}
-154
View File
@@ -1,154 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(VisorsTab))]
public static class VisorsTabPatches
{
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
private static readonly SortedList<string, List<VisorData>> SortedVisors = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
private static int currentPage;
internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
{
foreach (var (key, visor) in data)
{
if (!SortedVisors.ContainsKey(key)) SortedVisors.Add(key, []);
SortedVisors[key].Add(visor);
}
}
private static void PreviousPage(VisorsTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(VisorsTab tab)
{
currentPage++;
currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return true;
}
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static void GenerateHats(VisorsTab __instance, int page)
{
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var hatIndex = 0;
var (groupName, visors) = SortedVisors.ToArray()[page];
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} ({currentPage + 1}/{SortedVisors.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), visor);
hatIndex += 1;
}
__instance.SetScrollerBounds();
__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;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
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))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectVisor(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);
}
}
+20
View File
@@ -32,6 +32,11 @@ public static class OptionsPatches
__instance.roleChance = 0;
}
if (__instance.roleChance == 0)
{
__instance.roleMaxCount = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -58,6 +63,11 @@ public static class OptionsPatches
__instance.roleChance = 100;
}
if (__instance.roleChance == 0)
{
__instance.roleMaxCount = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -82,6 +92,11 @@ public static class OptionsPatches
__instance.roleMaxCount = 0;
}
if (__instance.roleMaxCount == 0)
{
__instance.roleChance = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -106,6 +121,11 @@ public static class OptionsPatches
__instance.roleMaxCount = __instance.role.MaxCount;
}
if (__instance.roleMaxCount == 0)
{
__instance.roleChance = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
+18 -188
View File
@@ -1,48 +1,39 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Menu;
using Reactor.Utilities;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System;
using System.Diagnostics.CodeAnalysis;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// The component for loading Addressables, and cosmetics.
/// </summary>
[Obsolete("This system has been deprecated in favour of CorsacCosmetics. This will be removed in a future version.", true)]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Part of obsolete code.")]
public static class AddressablesLoader
{
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 = [];
private static bool _isInitialized;
/// <summary>
/// Gets a value indicating whether hats have been loaded by the addressables system.
/// </summary>
public static bool AddressableHatsExist { get; private set; }
public static bool AddressableHatsExist => false;
/// <summary>
/// Gets a value indicating whether visors have been loaded by the addressables system.
/// </summary>
public static bool AddressableVisorsExist { get; private set; }
public static bool AddressableVisorsExist => false;
/// <summary>
/// Gets a value indicating whether nameplates have been loaded by the addressables system.
/// </summary>
public static bool AddressableNameplatesExist { get; private set; }
public static bool AddressableNameplatesExist => false;
/// <summary>
/// Gets a value indicating whether skins have been loaded by the addressables system.
/// </summary>
public static bool AddressableSkinsExist { get; private set; }
public static bool AddressableSkinsExist => false;
/// <summary>
/// Gets a value indicating whether catalogs have been loaded by the addressables system.
/// </summary>
public static bool CatalogsExist => false;
/// <summary>
/// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
@@ -51,13 +42,7 @@ public static class AddressablesLoader
/// <param name="providerSuffix">The suffix of the provider for an addressables package.</param>
public static void RegisterCatalog(string location, string providerSuffix = "")
{
if (_isInitialized)
{
Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
return;
}
CatalogLocations.Add((location, providerSuffix));
Warning("The addressables system has been deprecated. None of the registered assets will be added.");
}
/// <summary>
@@ -66,14 +51,7 @@ public static class AddressablesLoader
/// <param name="addressablesKey">The key/label/group for a List <see cref="HatData"/>.</param>
public static void RegisterHats(string addressablesKey)
{
if (_isInitialized)
{
Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
return;
}
AddressableHatsExist = true;
RegisteredHatKeys.Add(addressablesKey);
Warning("The addressables system has been deprecated. None of the registered assets will be added.");
}
/// <summary>
@@ -82,14 +60,7 @@ public static class AddressablesLoader
/// <param name="addressablesKey">The key/label/group for a List <see cref="SkinData"/>.</param>
public static void RegisterSkins(string addressablesKey)
{
if (_isInitialized)
{
Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
return;
}
AddressableSkinsExist = true;
RegisteredSkinKeys.Add(addressablesKey);
Warning("The addressables system has been deprecated. None of the registered assets will be added.");
}
/// <summary>
@@ -99,14 +70,7 @@ public static class AddressablesLoader
/// /// <param name="groupTitle">The title of the group for visors.</param>
public static void RegisterNameplates(string addressablesKey, string groupTitle)
{
if (_isInitialized)
{
Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
return;
}
AddressableNameplatesExist = true;
RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
Warning("The addressables system has been deprecated. None of the registered assets will be added.");
}
/// <summary>
@@ -116,140 +80,6 @@ public static class AddressablesLoader
/// <param name="groupTitle">The title of the group for visors.</param>
public static void RegisterVisors(string addressablesKey, string groupTitle)
{
if (_isInitialized)
{
Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
return;
}
AddressableVisorsExist = true;
RegisteredVisorKeys.Add((addressablesKey, groupTitle));
}
internal static void LoadAll()
{
if (_isInitialized)
{
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)
{
Error($"Failed to load catalog {location}.");
}
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);
hatBehaviours = [.. hatBehaviours.OrderBy(x => x.StoreName)];
var skinBehaviours = DiscoverData<SkinData>(RegisteredSkinKeys);
skinBehaviours = [.. skinBehaviours.OrderBy(x => x.StoreName)];
var namePlateBehaviours = DiscoverAndReportData<NamePlateData>(RegisteredNameplateKeys);
namePlateBehaviours = [.. namePlateBehaviours.OrderBy(x => x.Category)];
var visorBehaviours = DiscoverAndReportData<VisorData>(RegisteredVisorKeys);
visorBehaviours = [.. visorBehaviours.OrderBy(x => x.Category)];
var hatData = new List<HatData>();
hatData.AddRange(HatManager.Instance.allHats);
hatData.ForEach(x => x.StoreName = "Vanilla");
HatManager.Instance.allHats = PrepareArray(hatData, hatBehaviours);
var skinData = new List<SkinData>();
skinData.AddRange(HatManager.Instance.allSkins);
skinData.ForEach(x => x.StoreName = "Vanilla");
HatManager.Instance.allSkins = PrepareArray(skinData, skinBehaviours);
var visorData = new List<VisorData>();
visorData.AddRange(HatManager.Instance.allVisors);
VisorsTabPatches.AddRange(visorBehaviours);
HatManager.Instance.allVisors = PrepareArray(visorData, [.. visorBehaviours.Select(x => x.Data)]);
var namePlateData = new List<NamePlateData>();
namePlateData.AddRange(HatManager.Instance.allNamePlates);
NameplatesTabPatches.AddRange(namePlateBehaviours);
HatManager.Instance.allNamePlates = PrepareArray(namePlateData, [.. namePlateBehaviours.Select(x => x.Data)]);
}
private static T[] PrepareArray<T>(List<T> data, List<T> behaviours)
where T : CosmeticData
{
var count = data.Count;
for (var i = 0; i < behaviours.Count; i++)
{
behaviours[i].displayOrder = count + i;
data.Add(behaviours[i]);
}
return [.. data];
}
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 CppCollections.List<T>(assets.Pointer);
behaviours.AddRange(array.ToArray());
}
catch
{
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 CppCollections.List<T>(assets.Pointer);
behaviours.AddRange(array.ToArray().Select(x => (tag.Category, x)));
}
catch
{
Error($"Failed to find tag {tag}");
}
}
return behaviours;
Warning("The addressables system has been deprecated. None of the registered assets will be added.");
}
}
+159
View File
@@ -0,0 +1,159 @@
using System;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for various audio-related operations.
/// </summary>
public static class AudioTools
{
/// <summary>
/// Loads and returns an audio clip from a resource path using the specified <see cref="Assembly"/>.
/// </summary>
/// <param name="resourcePath">The path to the resource within the <see cref="Assembly"/>.</param>
/// <param name="assembly">The <see cref="Assembly"/> from which to load the resource.</param>
/// <returns>A <see cref="AudioClip"/> object loaded from the specified resource path.</returns>
/// <exception cref="NotSupportedException">Attempted to load an Audio file in non WAV format.</exception>
/// <exception cref="MissingManifestResourceException">Stream failed to load. Check if the name of your asset was correct.</exception>
public static AudioClip LoadAudioFromResources(string resourcePath, Assembly assembly)
{
using var assetStream = assembly.GetManifestResourceStream(resourcePath)
?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {resourcePath}");
using var reader = new BinaryReader(assetStream);
// Read RIFF header
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
{
throw new NotSupportedException("Invalid WAV file (missing RIFF header).");
}
reader.ReadInt32(); // File size (ignore)
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
{
throw new NotSupportedException("Invalid WAV file (missing WAVE header).");
}
// Read chunks
int channels = 0, sampleRate = 0, bitsPerSample = 0;
byte[]? audioData = null;
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
var chunkSize = reader.ReadInt32();
// Format chunk
if (chunkId == "fmt ")
{
int audioFormat = reader.ReadInt16();
channels = reader.ReadInt16();
sampleRate = reader.ReadInt32();
reader.ReadInt32(); // Byte rate (ignore)
reader.ReadInt16(); // Block align (ignore)
bitsPerSample = reader.ReadInt16();
if (audioFormat != 1) // Only PCM supported
throw new NotSupportedException("Only PCM WAV files are supported.");
}
// Data chunk
else if (chunkId == "data")
{
audioData = reader.ReadBytes(chunkSize);
break; // No need to read further
}
else
{
reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks
}
}
if (audioData == null)
{
throw new InvalidOperationException("WAV file does not contain audio data.");
}
if (channels == 0 || sampleRate == 0 || bitsPerSample == 0)
{
throw new InvalidOperationException("WAV file does not contain valid format information.");
}
// Convert PCM data to float array
var samples = ConvertPcmToFloat(audioData, bitsPerSample);
// Create the AudioClip
var audioClip = AudioClip.Create(resourcePath, samples.Length / channels, channels, sampleRate, false);
audioClip.SetData(samples, 0);
return audioClip;
}
// This is somewhat magic to me, I just copied it from elsewhere.
// Could have been written better, so here - Az
private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample)
{
var sampleCount = pcmData.Length / (bitsPerSample / 8);
var floatData = new float[sampleCount];
ReadOnlySpan<byte> source = pcmData;
Span<float> destination = floatData;
switch (bitsPerSample)
{
case 8:
for (var i = 0; i < sampleCount; i++)
{
destination[i] = (source[i] - 128) / 128f; // 8-bit PCM is unsigned
}
break;
case 16:
var shortSource = MemoryMarshal.Cast<byte, short>(source);
for (var i = 0; i < sampleCount; i++)
{
destination[i] = shortSource[i] / 32768f;
}
break;
case 24:
for (int i = 0, byteIndex = 0; i < sampleCount; i++, byteIndex += 3)
{
var sample24 = source[byteIndex] | (source[byteIndex + 1] << 8) | (source[byteIndex + 2] << 16);
// Convert from unsigned to signed
if (sample24 > 0x7FFFFF)
sample24 -= 0x1000000;
destination[i] = sample24 / 8388608f;
}
break;
case 32:
var intSource = MemoryMarshal.Cast<byte, int>(source);
for (var i = 0; i < sampleCount; i++)
{
destination[i] = intSource[i] / 2147483648f;
}
break;
default:
throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}");
}
return floatData;
}
}
@@ -1,8 +1,4 @@
using System;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Text;
using UnityEngine;
namespace MiraAPI.Utilities.Assets;
@@ -15,118 +11,14 @@ public class LoadableAudioResourceAsset(string path) : LoadableAsset<AudioClip>
{
private readonly Assembly _assembly = Assembly.GetCallingAssembly();
/// <summary>
/// Loads the asset from embedded resources.
/// </summary>
/// <returns>The asset to load.</returns>
/// <exception cref="NotSupportedException">Attempted to load an Audio file in non WAV format.</exception>
/// <exception cref="MissingManifestResourceException">Stream failed to load. Check if the name of your asset was correct.</exception>
/// <inheritdoc />
public override AudioClip LoadAsset()
{
using var assetStream = _assembly.GetManifestResourceStream(path)
?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {path}");
using var reader = new BinaryReader(assetStream);
// Read RIFF header
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
if (!LoadedAsset)
{
throw new NotSupportedException("Invalid WAV file (missing RIFF header).");
LoadedAsset = AudioTools.LoadAudioFromResources(path, _assembly);
}
reader.ReadInt32(); // File size (ignore)
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
{
throw new NotSupportedException("Invalid WAV file (missing WAVE header).");
}
// Read chunks
int channels = 0, sampleRate = 0, bitsPerSample = 0;
byte[]? audioData = null;
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
var chunkSize = reader.ReadInt32();
// Format chunk
if (chunkId == "fmt ")
{
int audioFormat = reader.ReadInt16();
channels = reader.ReadInt16();
sampleRate = reader.ReadInt32();
reader.ReadInt32(); // Byte rate (ignore)
reader.ReadInt16(); // Block align (ignore)
bitsPerSample = reader.ReadInt16();
if (audioFormat != 1) // Only PCM supported
throw new NotSupportedException("Only PCM WAV files are supported.");
}
// Data chunk
else if (chunkId == "data")
{
audioData = reader.ReadBytes(chunkSize);
break; // No need to read further
}
else
{
reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks
}
}
if (audioData == null)
{
throw new InvalidOperationException("WAV file does not contain audio data.");
}
if (channels == 0 || sampleRate == 0 || bitsPerSample == 0)
{
throw new InvalidOperationException("WAV file does not contain valid format information.");
}
// Convert PCM data to float array
var samples = ConvertPcmToFloat(audioData, bitsPerSample);
// Create the AudioClip
var audioClip = AudioClip.Create(path, samples.Length / channels, channels, sampleRate, false);
audioClip.SetData(samples, 0);
return audioClip;
}
// This is somewhat magic to me, I just copied it from elsewhere.
private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample)
{
var sampleCount = pcmData.Length / (bitsPerSample / 8);
var floatData = new float[sampleCount];
for (var i = 0; i < sampleCount; i++)
{
var byteIndex = i * (bitsPerSample / 8);
switch (bitsPerSample)
{
case 8:
floatData[i] = (pcmData[byteIndex] - 128) / 128f; // 8-bit PCM is unsigned
break;
case 16:
floatData[i] = BitConverter.ToInt16(pcmData, byteIndex) / 32768f;
break;
case 24:
var sample24 = pcmData[byteIndex] | (pcmData[byteIndex + 1] << 8) | (pcmData[byteIndex + 2] << 16);
if (sample24 > 0x7FFFFF) sample24 -= 0x1000000; // Convert from unsigned to signed
floatData[i] = sample24 / 8388608f;
break;
case 32:
floatData[i] = BitConverter.ToInt32(pcmData, byteIndex) / 2147483648f;
break;
default:
throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}");
}
}
return floatData;
return LoadedAsset!;
}
}
@@ -1,7 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using System.Collections.Generic;
using Il2CppInterop.Runtime;
using Reactor.Utilities.Extensions;
using UnityEngine;
@@ -11,71 +9,55 @@ namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading multiple assets from an <see cref="AssetBundle"/>.
/// </summary>
public class LoadableBundleSubAssetHolder
/// <param name="names">The name of the assets to pull from.</param>
/// <param name="bundle">The <see cref="AssetBundle"/> that contains the assets.</param>
public class LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle)
{
private readonly string[] spriteNames;
private readonly AssetBundle bundle;
/// <summary>
/// Gets the sprites contained within the asset.
/// </summary>
public Sprite[] SubSprites { get; private set; } = [];
/// <summary>
/// Initializes a new instance of the <see cref="LoadableBundleSubAssetHolder"/> class.
/// </summary>
/// <param name="names">The name of the assets to pull from.</param>
/// <param name="bundle">The <see cref="AssetBundle"/> that contains the assets.</param>
public LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle)
{
this.bundle = bundle;
spriteNames = names;
}
/// <summary>
/// Initializes a new instance of the <see cref="LoadableBundleSubAssetHolder"/> class.
/// </summary>
/// <param name="name">The name of the asset.</param>
/// <param name="bundle">The <see cref="AssetBundle"/> that contains the assets.</param>
public LoadableBundleSubAssetHolder(string name, AssetBundle bundle)
: this([name], bundle)
{
this.bundle = bundle;
spriteNames = [name];
}
/// <summary>
/// Attempts to load all of the sprite assets within the sprite sheet.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the asset is not actually a sprite sheet.</exception>
[SuppressMessage("Style", "IDE0270:Use coalesce expression", Justification = "Null coalescing bypasses Unity lifetime checks.")]
public void TryInit()
{
if (SubSprites.Length == 0)
if (SubSprites.Length != 0)
return;
var newSprites = new List<Sprite>();
var type = Il2CppType.From(typeof(Sprite));
foreach (var name in names)
{
var newSprites = Array.Empty<Sprite>();
var loadedAssets = bundle.LoadAssetWithSubAssets(name, type)
?? throw new InvalidOperationException($"INVALID ASSETS: {name}");
foreach (var name in spriteNames)
foreach (var obj in loadedAssets)
{
var loadedAssets = bundle.LoadAssetWithSubAssets(name, Il2CppType.From(typeof(Sprite))).ToArray();
var img = obj.TryCast<Sprite>();
if (loadedAssets == null)
{
throw new InvalidOperationException($"INVALID ASSETS: {name}");
}
if (img == null)
continue;
foreach (var obj in loadedAssets)
{
var img = obj.TryCast<Sprite>();
if (img != null)
{
img.DontDestroy().DontUnload();
newSprites = newSprites.AddToArray(img);
}
}
img.DontDestroy().DontUnload();
newSprites.Add(img);
}
SubSprites = newSprites;
}
SubSprites = [.. newSprites];
}
/// <summary>