mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-20 01:46:15 +00:00
an approach to addressables
This commit is contained in:
1 parent
86f970c989
commit
23c0397afe
7 files changed
+432
No files matched your search
@@ -3,9 +3,12 @@ using BepInEx.Configuration;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor;
|
||||
using Reactor.Networking;
|
||||
using Reactor.Networking.Attributes;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MiraAPI.Example;
|
||||
|
||||
@@ -23,5 +26,9 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin
|
||||
{
|
||||
ExampleEventHandlers.Initialize();
|
||||
Harmony.PatchAll();
|
||||
|
||||
var path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(ExamplePlugin))!.Location) + "\\touhats.catalog";
|
||||
AddressablesLoader.RegisterToLoad(path);
|
||||
AddressablesLoader.RegisterHats("touhats");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
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.Inner.transform.GetChild(0).GetComponent<SpriteRenderer>().maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
|
||||
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;
|
||||
using Reactor.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(MainMenuManager))]
|
||||
public static class MainMenuManagerPatches
|
||||
{
|
||||
[HarmonyPatch(nameof(MainMenuManager.Awake))]
|
||||
[HarmonyPostfix]
|
||||
public static void AwakePostfix()
|
||||
{
|
||||
AddressablesLoader.LoadAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using BepInEx.Unity.IL2CPP.Utils;
|
||||
using Il2CppInterop.Runtime.Attributes;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Attributes;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
|
||||
namespace MiraAPI.Utilities.Assets;
|
||||
|
||||
/// <summary>
|
||||
/// The component for loading Addressables, and cosmetics.
|
||||
/// </summary>
|
||||
public 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> RegisteredVisorKeys = [];
|
||||
private static readonly List<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 RegisterToLoad(string location, string providerSuffix = "")
|
||||
{
|
||||
CatalogLocations.Add((location, providerSuffix));
|
||||
}
|
||||
|
||||
public static void RegisterHats(string addressables_key)
|
||||
{
|
||||
RegisteredHatKeys.Add(addressables_key);
|
||||
}
|
||||
|
||||
public static void RegisterSkins(string addressables_key)
|
||||
{
|
||||
RegisteredSkinKeys.Add(addressables_key);
|
||||
}
|
||||
|
||||
public static void RegisterNameplates(string addressables_key)
|
||||
{
|
||||
RegisteredNameplateKeys.Add(addressables_key);
|
||||
}
|
||||
|
||||
public static void RegisterVisors(string addressables_key)
|
||||
{
|
||||
RegisteredVisorKeys.Add(addressables_key);
|
||||
}
|
||||
|
||||
internal static void LoadAll()
|
||||
{
|
||||
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 = DiscoverData<NamePlateData>(RegisteredNameplateKeys);
|
||||
var visorBehaviours = DiscoverData<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);
|
||||
DestroyableSingleton<HatManager>.Instance.allSkins = PrepareArray(skinData, skinBehaviours);
|
||||
|
||||
var visorData = new List<VisorData>();
|
||||
visorData.AddRange(DestroyableSingleton<HatManager>.Instance.allVisors);
|
||||
DestroyableSingleton<HatManager>.Instance.allVisors = PrepareArray(visorData, visorBehaviours);
|
||||
|
||||
var namePlateData = new List<NamePlateData>();
|
||||
namePlateData.AddRange(DestroyableSingleton<HatManager>.Instance.allNamePlates);
|
||||
DestroyableSingleton<HatManager>.Instance.allNamePlates = PrepareArray(namePlateData, namePlateBehaviours);
|
||||
}
|
||||
|
||||
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 all_locations = Addressables.LoadResourceLocationsAsync(tag).WaitForCompletion();
|
||||
var assets = Addressables.LoadAssetsAsync<T>(all_locations, 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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}");
|
||||
}
|
||||
|
||||
if (gcAction != null)
|
||||
{
|
||||
gcAction.Invoke(this);
|
||||
}
|
||||
GC.EndNoGCRegion();
|
||||
|
||||
return LoadedAsset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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();
|
||||
|
||||
if (gcAction != null)
|
||||
{
|
||||
gcAction.Invoke(this);
|
||||
}
|
||||
GC.EndNoGCRegion();
|
||||
|
||||
return LoadedAssets.AsReadOnly();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user