From e292b6b99551bf76b602fa3618ebb0e9fbcef1af Mon Sep 17 00:00:00 2001 From: Flowaria Date: Sat, 23 Dec 2023 17:46:54 +0900 Subject: [PATCH] Adding Async Bundle Loading and API to block base game's loading process until done --- GTFO-API/API/AssetAPI.AssetLoadHandle.cs | 36 ++++++++ GTFO-API/API/AssetAPI.cs | 83 ++++++++++++++---- GTFO-API/API/Impl/AssetAPI_Impl.cs | 104 +++++++++++++++++++++++ GTFO-API/Patches/GS_Offline_Patches.cs | 25 ++++++ 4 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 GTFO-API/API/AssetAPI.AssetLoadHandle.cs create mode 100644 GTFO-API/Patches/GS_Offline_Patches.cs diff --git a/GTFO-API/API/AssetAPI.AssetLoadHandle.cs b/GTFO-API/API/AssetAPI.AssetLoadHandle.cs new file mode 100644 index 0000000..105c0ef --- /dev/null +++ b/GTFO-API/API/AssetAPI.AssetLoadHandle.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GTFO.API.API +{ + /// + /// Handle that allocated when you called + /// This blocks game loading until every allocated AssetLoadHandle as marked as completed + /// + public sealed class AssetLoadHandle + { + /// + /// true if current load job has ended + /// + public bool IsCompleted { get; private set; } + + /// + /// Mark this specific Asset loading job as completed + /// + public void SetCompleted() + { + IsCompleted = true; + } + + /// + /// Add Line to loading text + /// + public void AddLoadingText(string text) + { + MainMenuGuiLayer.Current.PageIntro.m_textCenter.AddLine($"{text}"); + } + } +} diff --git a/GTFO-API/API/AssetAPI.cs b/GTFO-API/API/AssetAPI.cs index 864fc14..9ccae30 100644 --- a/GTFO-API/API/AssetAPI.cs +++ b/GTFO-API/API/AssetAPI.cs @@ -4,6 +4,7 @@ using System.Linq; using AssetShards; using BepInEx; +using GTFO.API.API; using GTFO.API.Attributes; using GTFO.API.Impl; using GTFO.API.Resources; @@ -20,7 +21,7 @@ public static class AssetAPI public static ApiStatusInfo Status => APIStatus.Asset; /// - /// Invoked when the game's startup assets have been fully loaded + /// Invoked when the base game's startup assets have been fully loaded /// public static event Action OnStartupAssetsLoaded; @@ -29,11 +30,43 @@ public static class AssetAPI /// public static event Action OnAssetBundlesLoaded; + /// + /// Invoken when loading custom assets are about to start + /// + public static event Action OnCustomAssetsLoading; + + /// + /// Invoked when startup asset has fully loaded (Including custom bundles and base game assets) + /// + public static event Action OnStartupAssetsFullyLoaded; + /// /// Invoked when the internal handler is ready /// public static event Action OnImplReady; + /// + /// Return true If every assetbundle and startup assets are loaded + /// + public static bool IsReadyForStartup + { + get + { + if (!Status.Ready) + return false; + + if (s_LoadBlockerHandles.Any(x => !x.IsCompleted)) + return false; + + if (!s_StartupAssetsFullyLoaded) + { + s_StartupAssetsFullyLoaded = true; + OnStartupAssetsFullyLoaded?.Invoke(); + } + return true; + } + } + /// /// Checks if an asset is already registered in the /// @@ -188,6 +221,26 @@ public static bool TryInstantiateAsset(string assetName, string copyName return clonedObj != null; } + /// + /// Create new Load Job Handle + /// + /// Created Load Job Handle + /// true if handle has created, false if custom asset loading process is already done (after has invoked) + public static bool WantToWorkForStartupAssets(out AssetLoadHandle newLoadHandle) + { + if (s_StartupAssetsFullyLoaded) + { + APILogger.Error($"Asset", "Startup Assets are already loaded! Try load them before "); + newLoadHandle = null; + return false; + } + + + newLoadHandle = new(); + s_LoadBlockerHandles.Add(newLoadHandle); + return true; + } + private static void OnAssetsLoaded() { if (!APIStatus.Asset.Created) @@ -199,20 +252,21 @@ private static void OnAssetsLoaded() internal static void InvokeImplReady() => OnImplReady?.Invoke(); + internal static void InvokeBundleLoaded() => OnAssetBundlesLoaded?.Invoke(); + internal static void Setup() { EventAPI.OnAssetsLoaded += OnAssetsLoaded; - OnImplReady += LoadAssetBundles; + OnImplReady += LoadCustomStartupAssets; } - private static void LoadAssetBundles() + private static void LoadCustomStartupAssets() { + OnCustomAssetsLoading?.Invoke(); string assetBundleDir = Path.Combine(Paths.BepInExRootPath, "Assets", "AssetBundles"); string assetBundlesDirOld = Path.Combine(Paths.ConfigPath, "Assets", "AssetBundles"); - bool anyLoaded = LoadAssetBundles(assetBundleDir); - anyLoaded |= LoadAssetBundles(assetBundlesDirOld, outdated: true); - if (anyLoaded) - OnAssetBundlesLoaded?.Invoke(); + LoadAssetBundles(assetBundleDir); + LoadAssetBundles(assetBundlesDirOld, outdated: true); } private static bool LoadAssetBundles(string assetBundlesDir, bool outdated = false) @@ -238,19 +292,16 @@ private static bool LoadAssetBundles(string assetBundlesDir, bool outdated = fal for (int i = 0; i < bundlePaths.Length; i++) { - try - { - LoadAndRegisterAssetBundle(bundlePaths[i]); - } - catch (Exception ex) - { - APILogger.Warn(nameof(AssetAPI), $"Failed to load asset bundle '{bundlePaths[i]}' ({ex.Message})"); - } + WantToWorkForStartupAssets(out var assetLoadHandle); + AssetAPI_Impl.Instance.LoadAssetBundle(bundlePaths[i], assetLoadHandle); } + AssetAPI_Impl.Instance.DEBUG_BundleLoadingStarted(bundlePaths.Length); return true; } - internal static ConcurrentDictionary s_RegistryCache = new(); + internal static readonly ConcurrentDictionary s_RegistryCache = new(); + internal static readonly ConcurrentBag s_LoadBlockerHandles = new(); + internal static bool s_StartupAssetsFullyLoaded = false; } } diff --git a/GTFO-API/API/Impl/AssetAPI_Impl.cs b/GTFO-API/API/Impl/AssetAPI_Impl.cs index e6f231c..9de7fb4 100644 --- a/GTFO-API/API/Impl/AssetAPI_Impl.cs +++ b/GTFO-API/API/Impl/AssetAPI_Impl.cs @@ -1,6 +1,12 @@ using System; +using System.Collections; +using System.Diagnostics; +using System.IO; using AssetShards; +using BepInEx.Unity.IL2CPP.Utils; +using GTFO.API.API; using GTFO.API.Resources; +using Il2CppInterop.Runtime.Attributes; using UnityEngine; namespace GTFO.API.Impl @@ -21,6 +27,103 @@ public static AssetAPI_Impl Instance return s_Instance; } } + + private int _LoadingCount = 0; + + private readonly Stopwatch _SW = new(); + private int _DebugLoadingCount; + + private static readonly string COMPRESSED_STR = "-compressed"; + private static readonly int COMPRESSED_STR_LENGTH = COMPRESSED_STR.Length; + + [Conditional("DEBUG")] + [HideFromIl2Cpp] + public void DEBUG_BundleLoadingStarted(int loadingCount) + { + _SW.Restart(); + _DebugLoadingCount = loadingCount; + } + + [Conditional("DEBUG")] + [HideFromIl2Cpp] + public void DEBUG_BundleLoadingFinished() + { + _SW.Stop(); + APILogger.Verbose($"Asset", $"Elapsed Time to loading {_DebugLoadingCount} bundles:"); + APILogger.Verbose($"Asset", $" - {_SW.Elapsed}! (or {_SW.ElapsedMilliseconds}ms)"); + } + + [HideFromIl2Cpp] + public void LoadAssetBundle(string filePath, AssetLoadHandle loadHandle) + { + _LoadingCount++; + this.StartCoroutine(DoLoadAssetBundle(filePath, loadHandle)); + } + + [HideFromIl2Cpp] + private IEnumerator DoLoadAssetBundle(string filePath, AssetLoadHandle loadHandle) + { + AssetBundleCreateRequest loadReq = AssetBundle.LoadFromFileAsync(filePath); + + yield return loadReq; + + AssetBundle loadedBundle = loadReq.assetBundle; + if (loadedBundle == null) + { + _LoadingCount--; + APILogger.Warn($"Asset", $"Failed to load asset bundle: [{filePath}]"); + yield break; + } + + + APILogger.Warn($"Asset", $"Start Loading Bundle!"); + string[] assetNames = loadedBundle.AllAssetNames(); + int remainingAssets = assetNames.Length; + int loadedCount = 0; + + foreach (string assetName in assetNames) + { + AssetBundleRequest loadAssetReq = loadedBundle.LoadAssetAsync(assetName); + loadAssetReq.add_completed((Action)((x) => + { + remainingAssets--; + loadedCount++; + + UnityEngine.Object loadedAsset = loadAssetReq.asset; + if (loadedAsset == null) + { + APILogger.Warn("Asset", $"Skipping asset {assetName}"); + } + + RegisterAsset(assetName, loadedAsset); + })); + yield return null; + } + + yield return new WaitUntil((Il2CppSystem.Func)(() => + { + return remainingAssets <= 0; + })); + + _LoadingCount--; + loadHandle.SetCompleted(); + + string fileName = Path.GetFileNameWithoutExtension(filePath); + if (fileName.EndsWith(COMPRESSED_STR, StringComparison.InvariantCultureIgnoreCase)) + fileName = fileName[^COMPRESSED_STR_LENGTH..]; //Trim "-Compressed" from bundle name + + loadHandle.AddLoadingText($"[Assets] {fileName} has loaded!"); + + if (_LoadingCount <= 0) + { + _LoadingCount = 0; + loadHandle.AddLoadingText($"[Assets] All bundle has loaded!"); + AssetAPI.InvokeBundleLoaded(); + DEBUG_BundleLoadingFinished(); + } + + yield return null; + } private void Awake() { @@ -34,6 +137,7 @@ private void Awake() AssetAPI.InvokeImplReady(); } + [HideFromIl2Cpp] public void RegisterAsset(string name, UnityEngine.Object gameObject) { string upperName = name.ToUpper(); diff --git a/GTFO-API/Patches/GS_Offline_Patches.cs b/GTFO-API/Patches/GS_Offline_Patches.cs new file mode 100644 index 0000000..1b14a42 --- /dev/null +++ b/GTFO-API/Patches/GS_Offline_Patches.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HarmonyLib; + +namespace GTFO.API.Patches +{ + [HarmonyPatch(typeof(GS_Offline))] + internal class GS_Offline_Patches + { + [HarmonyPrefix] + [HarmonyPatch(nameof(GS_Offline.Update))] + static bool Prefix() + { + if (AssetAPI.IsReadyForStartup) + { + return true; //Run Original + } + + return false; //Skip Original + } + } +}