From 4f9ff054379a17806e05d0dc46345824ab99ff67 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 25 Jun 2025 10:51:08 +0200 Subject: [PATCH 01/19] build: copy files top if set --- Assembly-CSharp/Assembly-CSharp.csproj | 12 ++++++++++++ HollowKnight.Modding.API.sln.DotSettings | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Assembly-CSharp/Assembly-CSharp.csproj b/Assembly-CSharp/Assembly-CSharp.csproj index ae54a496..364fd6ea 100644 --- a/Assembly-CSharp/Assembly-CSharp.csproj +++ b/Assembly-CSharp/Assembly-CSharp.csproj @@ -27,6 +27,11 @@ $(SolutionDir)/OutputFinal + + C:/Program Files (x86)/Steam/steamapps/common/Hollow Knight + $(HOME)/.local/share/Steam/steamapps/common/Hollow Knight + $(GamePath)/hollow_knight_Data/Managed + mono @@ -78,6 +83,13 @@ + + + + + + + full bin\$(Configuration)\Assembly-CSharp.mm.xml diff --git a/HollowKnight.Modding.API.sln.DotSettings b/HollowKnight.Modding.API.sln.DotSettings index 98201e24..0d6f08de 100644 --- a/HollowKnight.Modding.API.sln.DotSettings +++ b/HollowKnight.Modding.API.sln.DotSettings @@ -1,3 +1,4 @@  FSM - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="AaBb" /></Policy> \ No newline at end of file + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="AaBb" /></Policy> + True \ No newline at end of file From 605231938ee47625fb9d296ba04f5e025f417ec3 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 25 Jun 2025 12:13:34 +0200 Subject: [PATCH 02/19] preloader: implement optimization using unity-scene-repacker --- Assembly-CSharp/Assembly-CSharp.csproj | 18 ++++++ Assembly-CSharp/ModHooksGlobalSettings.cs | 5 ++ Assembly-CSharp/Preloader.cs | 42 +++++++++--- Assembly-CSharp/UnitySceneRepacker.cs | 79 +++++++++++++++++++++++ 4 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 Assembly-CSharp/UnitySceneRepacker.cs diff --git a/Assembly-CSharp/Assembly-CSharp.csproj b/Assembly-CSharp/Assembly-CSharp.csproj index 364fd6ea..7c45c19c 100644 --- a/Assembly-CSharp/Assembly-CSharp.csproj +++ b/Assembly-CSharp/Assembly-CSharp.csproj @@ -81,6 +81,19 @@ + + + + .dll + lib + .so + lib + .dylib + + + + + @@ -111,6 +124,8 @@ all + + @@ -147,6 +162,9 @@ ../Vanilla/UnityEngine.AnimationModule.dll + + ../Vanilla/UnityEngine.AssetBundleModule.dll + ../Vanilla/UnityEngine.AudioModule.dll diff --git a/Assembly-CSharp/ModHooksGlobalSettings.cs b/Assembly-CSharp/ModHooksGlobalSettings.cs index b26b4c7b..3aec9b04 100644 --- a/Assembly-CSharp/ModHooksGlobalSettings.cs +++ b/Assembly-CSharp/ModHooksGlobalSettings.cs @@ -44,6 +44,11 @@ public class ModHooksGlobalSettings /// public int PreloadBatchSize = 5; + /// + /// - `false`: Load the entire scene unmodified into memory + /// - `true`: Preprocess the scenes by filtering to only the objects we care about. + /// + public bool PreloadUsingSceneRepack = true; /// /// Maximum number of days to preserve modlogs for. diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 6822337b..51ce6931 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -2,12 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; -using UObject = UnityEngine.Object; using USceneManager = UnityEngine.SceneManagement.SceneManager; using Modding.Utils; +using Newtonsoft.Json; namespace Modding; @@ -160,8 +161,32 @@ private IEnumerator DoPreload Dictionary Preloads)>> toPreload, IDictionary>> preloadedObjects, Dictionary>> sceneHooks - ) - { + ) { + const string PreloadBundleName = "hk_api_repack"; + AssetBundle repackBundle = null; + Logger.APILogger.Log($"Using: {ModHooks.GlobalSettings.PreloadUsingSceneRepack}"); + if (ModHooks.GlobalSettings.PreloadUsingSceneRepack) { + string preloadJson = JsonConvert.SerializeObject(toPreload.ToDictionary( + k => k.Key, + v => v.Value.SelectMany(x => x.Preloads).Distinct())); + byte[] bundleData = null; + RepackStats repackStats; + Task task = Task.Run(() => { + try { + (bundleData, repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.SceneBundle); + Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB)"); + } catch (Exception e) { + Logger.APILogger.LogError($"Error trying to repack preloads into assetbundle: {e}"); + } + }); + yield return new WaitUntil(() => task.IsCompleted); + if (bundleData != null) { + repackBundle = AssetBundle.LoadFromMemory(bundleData); + } + } + + string scenePrefix = repackBundle != null ? $"{PreloadBundleName}_" : ""; + List sceneNames = toPreload.Keys.Union(sceneHooks.Keys).ToList(); Dictionary scenePriority = new(); Dictionary sceneAsyncOperationHolder = new(); @@ -201,12 +226,11 @@ out Dictionary modScenePreloadedObjects return modScenePreloadedObjects; } - var preloadOperationQueue = new List(5); + var preloadOperationQueue = new List(ModHooks.GlobalSettings.PreloadBatchSize); IEnumerator GetPreloadObjectsOperation(string sceneName) { - Scene scene = USceneManager.GetSceneByName(sceneName); - + Scene scene = USceneManager.GetSceneByName(scenePrefix + sceneName); GameObject[] rootObjects = scene.GetRootGameObjects(); foreach (var go in rootObjects) @@ -268,7 +292,7 @@ void CleanupPreloadOperation(string sceneName) { Logger.APILogger.LogFine($"Unloading scene \"{sceneName}\""); - AsyncOperation unloadOp = USceneManager.UnloadSceneAsync(sceneName); + AsyncOperation unloadOp = USceneManager.UnloadSceneAsync(scenePrefix + sceneName); sceneAsyncOperationHolder[sceneName] = (sceneAsyncOperationHolder[sceneName].load, unloadOp); @@ -290,7 +314,7 @@ IEnumerator DoLoad(AsyncOperation load) Logger.APILogger.LogFine($"Loading scene \"{sceneName}\""); - AsyncOperation loadOp = USceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); + AsyncOperation loadOp = USceneManager.LoadSceneAsync(scenePrefix + sceneName, LoadSceneMode.Additive); StartCoroutine(DoLoad(loadOp)); @@ -325,6 +349,8 @@ IEnumerator DoLoad(AsyncOperation load) UpdateLoadingBarProgress(sceneProgressAverage); } + + repackBundle?.Unload(true); UpdateLoadingBarProgress(1.0f); } diff --git a/Assembly-CSharp/UnitySceneRepacker.cs b/Assembly-CSharp/UnitySceneRepacker.cs new file mode 100644 index 00000000..5d3ca60e --- /dev/null +++ b/Assembly-CSharp/UnitySceneRepacker.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.InteropServices; + +namespace Modding; + +[StructLayout(LayoutKind.Sequential)] +internal struct RepackStats { + public int objectsBefore; + public int objectsAfter; +} + +internal class UnitySceneRepackerException(string message) : Exception(message); + +internal static class UnitySceneRepacker { + + public enum Mode { + SceneBundle, + AssetBundle, + } + + + public static (byte[], RepackStats) Repack(string bundleName, string gamePath, string preloadsJson, Mode mode) { + export( + bundleName, + gamePath, + preloadsJson, + out IntPtr errorPtr, + out int bundleSize, + out IntPtr bundleData, + out RepackStats stats, + (byte) mode + ); + + if (errorPtr != IntPtr.Zero) { + string error = PtrToStringAndFree(errorPtr)!; + throw new UnitySceneRepackerException(error); + } else { + byte[] bytes = PtrToByteArrayAndFree(bundleSize, bundleData); + return (bytes, stats); + } + } + + + [DllImport("unityscenerepacker", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private static extern void export( + [MarshalAs(UnmanagedType.LPUTF8Str)] string bundleName, + [MarshalAs(UnmanagedType.LPUTF8Str)] string gameDir, + [MarshalAs(UnmanagedType.LPUTF8Str)] string preloadJson, + out IntPtr error, + out int bundleSize, + out IntPtr bundleData, + out RepackStats repackStats, + byte mode + ); + + [DllImport("unityscenerepacker", CallingConvention = CallingConvention.Cdecl)] + private static extern void free_str(IntPtr str); + + [DllImport("unityscenerepacker", CallingConvention = CallingConvention.Cdecl)] + private static extern void free_array(int len, IntPtr data); + + private static string PtrToStringAndFree(IntPtr ptr) { + if (ptr == IntPtr.Zero) return null; + + string message = Marshal.PtrToStringAnsi(ptr); + free_str(ptr); + return message; + } + + private static byte[] PtrToByteArrayAndFree(int size, IntPtr ptr) { + if (ptr == IntPtr.Zero || size == 0) + return []; + + byte[] managedArray = new byte[size]; + Marshal.Copy(ptr, managedArray, 0, size); + free_array(size, ptr); + return managedArray; + } +} From d03305fa4b126b4b700be7ee87d506e4cc948d5a Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Fri, 27 Jun 2025 12:29:57 +0200 Subject: [PATCH 03/19] preloader: support preloading using non-scene asset bundles --- Assembly-CSharp/ModHooksGlobalSettings.cs | 2 +- Assembly-CSharp/ModLoader.cs | 3 +- Assembly-CSharp/Preloader.cs | 222 ++++++++++++++++++++-- analyze-logs.py | 49 +++++ 4 files changed, 255 insertions(+), 21 deletions(-) create mode 100644 analyze-logs.py diff --git a/Assembly-CSharp/ModHooksGlobalSettings.cs b/Assembly-CSharp/ModHooksGlobalSettings.cs index 3aec9b04..601e0d03 100644 --- a/Assembly-CSharp/ModHooksGlobalSettings.cs +++ b/Assembly-CSharp/ModHooksGlobalSettings.cs @@ -48,7 +48,7 @@ public class ModHooksGlobalSettings /// - `false`: Load the entire scene unmodified into memory /// - `true`: Preprocess the scenes by filtering to only the objects we care about. /// - public bool PreloadUsingSceneRepack = true; + public string PreloadMode = "full-scene"; /// /// Maximum number of days to preserve modlogs for. diff --git a/Assembly-CSharp/ModLoader.cs b/Assembly-CSharp/ModLoader.cs index 886beaff..6a0a19e6 100644 --- a/Assembly-CSharp/ModLoader.cs +++ b/Assembly-CSharp/ModLoader.cs @@ -158,8 +158,7 @@ Assembly Resolve(object sender, ResolveEventArgs args) } } - foreach (Assembly asm in asms) - { + foreach (Assembly asm in asms) { Logger.APILogger.LogDebug($"Loading mods in assembly `{asm.FullName}`"); bool foundMod = false; diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 51ce6931..bd1554ae 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; @@ -9,6 +10,7 @@ using USceneManager = UnityEngine.SceneManagement.SceneManager; using Modding.Utils; using Newtonsoft.Json; +using Object = UnityEngine.Object; namespace Modding; @@ -46,8 +48,22 @@ Dictionary>> sceneHooks CreateLoadingBar(); - yield return DoPreload(toPreload, preloadedObjects, sceneHooks); - + Logger.APILogger.Log($"Preloading using mode {ModHooks.GlobalSettings.PreloadMode}"); + switch (ModHooks.GlobalSettings.PreloadMode) { + case "full-scene": + yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, false); + break; + case "repack-scene": + yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, true); + break; + case "repack-assets": + yield return DoPreloadAssetbundle(toPreload, preloadedObjects); + break; + default: + Logger.APILogger.LogError($"Unknown preload mode {ModHooks.GlobalSettings.PreloadMode}. Expected one of: full-scene, repack-scene, repack-assets"); + break; + } + yield return CleanUpPreloading(); UnmuteAllAudio(); @@ -152,29 +168,195 @@ private void UpdateLoadingBarProgress(float progress) _secondsSinceLastSet = 0.0f; } + + private IEnumerator DoPreloadAssetbundle + ( + Dictionary Preloads)>> toPreload, + IDictionary>> preloadedObjects + ) { + const string PreloadBundleName = "mapi_preload_assetbundle"; + + string preloadJson = JsonConvert.SerializeObject(toPreload.ToDictionary( + k => k.Key, + v => v.Value.SelectMany(x => x.Preloads).Distinct() )); + byte[] bundleData = null; + try { + (bundleData, RepackStats repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.AssetBundle); + Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB)"); + } catch (Exception e) { + Logger.APILogger.LogError($"Error trying to repack preloads into assetbundle: {e}"); + } + AssetBundleCreateRequest op = AssetBundle.LoadFromMemoryAsync(bundleData); + + if (op == null) { + UpdateLoadingBarProgress(1); + yield break; + } + + yield return op; + var bundle = op.assetBundle; + + var queue = new HashSet(); + + foreach (var (sceneName, sceneToPreload) in toPreload) { + foreach (var (mod, toPreloadPaths) in sceneToPreload) { + if (!preloadedObjects.TryGetValue(mod, out var modPreloads)) { + modPreloads = new Dictionary>(); + preloadedObjects[mod] = modPreloads; + } + if (!modPreloads.TryGetValue(sceneName, out var modScenePreloads)) { + modScenePreloads = new Dictionary(); + modPreloads[sceneName] = modScenePreloads; + } + + foreach (string path in toPreloadPaths) { + if (modScenePreloads.ContainsKey(path)) continue; + + string assetName = $"{sceneName}/{path}.prefab"; + AssetBundleRequest request = bundle.LoadAssetAsync(assetName); + request.completed += _ => { + queue.Remove(request); + + var go = (GameObject) request.asset; + if (!go) { + Logger.APILogger.LogError($" could not load '{assetName}'"); + return; + } + if (modScenePreloads.ContainsKey(path)) { + Logger.APILogger.LogWarn($"Duplicate preload by {mod.Name}: '{path}' in '{sceneName}'"); + } else { + modScenePreloads.Add(path, go); + } + }; + queue.Add(request); + } + } + } + int total = queue.Count; + + while (queue.Count > 0) { + float progress = (total - queue.Count) / (float)total; + UpdateLoadingBarProgress(progress); + yield return null; + } + } + + + /*private IEnumerator DoPreloadAssetbundleSeparate + ( + Dictionary Preloads)>> toPreload, + IDictionary>> preloadedObjects, + Dictionary>> sceneHooks + ) { + const string basePath = "/home/jakob/dev/unity/unity-scene-repacker/out-separate"; + + + List sceneNames = toPreload.Keys.ToList(); + + + var preloadOperationQueue = new List(); + var allOperations = new List(); + + float sceneProgressAverage = 0; + int i = 0; + Logger.APILogger.Log("starting"); + while (sceneProgressAverage < 1.0f || i < sceneNames.Count) { + while (preloadOperationQueue.Count < 20 && i < sceneNames.Count) { + string sceneName = sceneNames[i++]; + + string bundlePath = basePath + $"/{sceneName}.bundle"; + if (File.Exists(bundlePath)) { + + int priority = 0; + + if (toPreload.TryGetValue(sceneName, out var requests)) + priority += requests.Select(x => x.Preloads.Count).Sum(); + + AssetBundleCreateRequest op = AssetBundle.LoadFromFileAsync(bundlePath); + op.priority = priority; + preloadOperationQueue.Add(op); + allOperations.Add(op); + + StartCoroutine(HandleBundle(sceneName, toPreload[sceneName], op)); + } else { + Logger.APILogger.LogWarn($"Not Found: {bundlePath}"); + } + } + + yield return null; + + // sceneProgressAverage = allOperations.Average(op => op.progress); + sceneProgressAverage = i / (float) sceneNames.Count; + UpdateLoadingBarProgress(sceneProgressAverage); + } + Logger.APILogger.Log($"Preload completed {i}"); + + yield break; + + IEnumerator HandleBundle(string sceneName, List<(ModLoader.ModInstance, List)> toPreloadScene, AssetBundleCreateRequest op) { + float bundleLoadStart = TimingDumper.Current; + yield return op; + TimingDumper.Record(bundleLoadStart, "LoadBundle", sceneName); + + float objectLoadStart = TimingDumper.Current; + Logger.APILogger.Log($"Scene: {sceneName}"); + foreach (var (mod, modPreloadPaths) in toPreloadScene) { + Logger.APILogger.Log($" {mod.Name}"); + + if (!preloadedObjects.TryGetValue(mod, out var modPreloads)) { + modPreloads = new Dictionary>(); + preloadedObjects[mod] = modPreloads; + } + if (!modPreloads.TryGetValue(sceneName, out var modScenePreloads)) { + modScenePreloads = new Dictionary(); + modPreloads[sceneName] = modScenePreloads; + } + + foreach (string path in modPreloadPaths) { + if (modScenePreloads.ContainsKey(path)) continue; + + Object go = op.assetBundle.LoadAsset($"{path}.prefab"); + if (!go) { + Logger.APILogger.LogError($" could not load '{path}'"); + continue; + } + Logger.APILogger.Log($" {path}"); + modScenePreloads.Add(path, (GameObject) go); + + } + } + + // op.assetBundle.Unload(false); + + TimingDumper.Record(objectLoadStart, "LoadObjects", sceneName); + + preloadOperationQueue.Remove(op); + } + }*/ + /// /// This is the actual preloading process. /// /// - private IEnumerator DoPreload + private IEnumerator DoPreloadClassic ( Dictionary Preloads)>> toPreload, IDictionary>> preloadedObjects, - Dictionary>> sceneHooks + Dictionary>> sceneHooks, + bool useRepack ) { const string PreloadBundleName = "hk_api_repack"; AssetBundle repackBundle = null; - Logger.APILogger.Log($"Using: {ModHooks.GlobalSettings.PreloadUsingSceneRepack}"); - if (ModHooks.GlobalSettings.PreloadUsingSceneRepack) { + if (useRepack) { string preloadJson = JsonConvert.SerializeObject(toPreload.ToDictionary( k => k.Key, v => v.Value.SelectMany(x => x.Preloads).Distinct())); byte[] bundleData = null; - RepackStats repackStats; Task task = Task.Run(() => { try { + RepackStats repackStats = default; (bundleData, repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.SceneBundle); - Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB)"); + Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB"); } catch (Exception e) { Logger.APILogger.LogError($"Error trying to repack preloads into assetbundle: {e}"); } @@ -291,7 +473,7 @@ IEnumerator GetPreloadObjectsOperation(string sceneName) void CleanupPreloadOperation(string sceneName) { Logger.APILogger.LogFine($"Unloading scene \"{sceneName}\""); - + AsyncOperation unloadOp = USceneManager.UnloadSceneAsync(scenePrefix + sceneName); sceneAsyncOperationHolder[sceneName] = (sceneAsyncOperationHolder[sceneName].load, unloadOp); @@ -303,15 +485,6 @@ void CleanupPreloadOperation(string sceneName) void StartPreloadOperation(string sceneName) { - IEnumerator DoLoad(AsyncOperation load) - { - yield return load; - - preloadOperationQueue.Remove(load); - yield return GetPreloadObjectsOperation(sceneName); - CleanupPreloadOperation(sceneName); - } - Logger.APILogger.LogFine($"Loading scene \"{sceneName}\""); AsyncOperation loadOp = USceneManager.LoadSceneAsync(scenePrefix + sceneName, LoadSceneMode.Additive); @@ -323,6 +496,17 @@ IEnumerator DoLoad(AsyncOperation load) loadOp.priority = scenePriority[sceneName]; preloadOperationQueue.Add(loadOp); + + return; + + IEnumerator DoLoad(AsyncOperation load) + { + yield return load; + + preloadOperationQueue.Remove(load); + yield return GetPreloadObjectsOperation(sceneName); + CleanupPreloadOperation(sceneName); + } } int i = 0; @@ -377,6 +561,8 @@ private IEnumerator CleanUpPreloading() Destroy(_loadingBar); Destroy(_loadingBarBackground); Destroy(_blanker); + + yield break; } /// diff --git a/analyze-logs.py b/analyze-logs.py new file mode 100644 index 00000000..a4795efd --- /dev/null +++ b/analyze-logs.py @@ -0,0 +1,49 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "pandas", +# "plotly", +# ] +# /// + +from datetime import datetime +import json +import pandas as pd +import plotly.express as px +import sys +import tempfile + +color_map = { + "LoadScene": "#636EFA", + "LoadAsset": "#636EFA", + "GetPreloadObjects": "#Ef553b", + "CleanPreload": "#00CC96", + "LoadMods": "#AB63FA", +} + +temp_dir = tempfile.gettempdir() + +with open(f"{temp_dir}/loadTimings.json", "r") as f: + data = json.load(f) + +if len(data) == 0: + sys.exit(1) + +for value in data: + value["Diff"] = round(value["End"] - value["Start"], 6) + value["Start"] = datetime.fromtimestamp(value["Start"]) + value["End"] = datetime.fromtimestamp(value["End"]) + name = value["Name"] + value["Color"] = int(name) % 2 if name.isdigit() else name +df = pd.DataFrame(data) + +fig = px.timeline(df, x_start="Start", x_end="End", y="Context", color="Color", hover_data=["Context", "Name", "Diff"], + color_discrete_map=color_map) +fig.update_layout( + xaxis=dict(title="Timestamp", tickformat="%S.%L"), + yaxis=dict(title="", autorange="reversed", showticklabels=False), +) +fig.for_each_trace(lambda trace: trace.update(text=None) if trace.name in ["1","2","3","LoadScene"] else None) + +fig.update_layout(dragmode="pan") +fig.show(config={"scrollZoom": True}) \ No newline at end of file From e2b0714c8e38c4a73293d5368b686ae34c7bfca9 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 25 Jun 2025 13:08:06 +0200 Subject: [PATCH 04/19] fix progress bar not being able to catch up --- Assembly-CSharp/Preloader.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index bd1554ae..83b6a395 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -31,7 +31,6 @@ internal class Preloader : MonoBehaviour private float _commandedProgress; private float _shownProgress; - private float _secondsSinceLastSet; public IEnumerator Preload ( @@ -69,10 +68,13 @@ Dictionary>> sceneHooks UnmuteAllAudio(); } + private static float ExpDecay(float a, float b, float decay) => b + (a - b) * Mathf.Exp(-decay * Time.deltaTime); + public void Update() { - _secondsSinceLastSet += Time.unscaledDeltaTime; - _shownProgress = Mathf.Lerp(_shownProgress, _commandedProgress, _secondsSinceLastSet / 10.0f); + // https://youtu.be/LSNQuFEDOyQ?si=GmrFzX94CRqDdVqO&t=2976 + const float decay = 16; + _shownProgress = ExpDecay(_shownProgress, _commandedProgress, decay); } public void LateUpdate() @@ -161,11 +163,7 @@ private void CreateLoadingBar() /// The progress that should be displayed. 0.0f - 1.0f private void UpdateLoadingBarProgress(float progress) { - if (Mathf.Abs(_commandedProgress - progress) < float.Epsilon) - return; - _commandedProgress = progress; - _secondsSinceLastSet = 0.0f; } From 3908ac23af4fc461ef70b71009448b3e9ebe0c81 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Mon, 30 Jun 2025 19:33:17 +0200 Subject: [PATCH 05/19] refactor: move ProgressBar to separate file --- Assembly-CSharp/Preloader.cs | 137 +++------------------------------ Assembly-CSharp/ProgressBar.cs | 134 ++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 127 deletions(-) create mode 100644 Assembly-CSharp/ProgressBar.cs diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 83b6a395..05a5020f 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -6,31 +6,19 @@ using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; -using UnityEngine.UI; using USceneManager = UnityEngine.SceneManagement.SceneManager; using Modding.Utils; using Newtonsoft.Json; -using Object = UnityEngine.Object; namespace Modding; internal class Preloader : MonoBehaviour { - private const int CanvasResolutionWidth = 1920; - private const int CanvasResolutionHeight = 1080; - private const int LoadingBarBackgroundWidth = 1000; - private const int LoadingBarBackgroundHeight = 100; - private const int LoadingBarMargin = 12; - private const int LoadingBarWidth = LoadingBarBackgroundWidth - 2 * LoadingBarMargin; - private const int LoadingBarHeight = LoadingBarBackgroundHeight - 2 * LoadingBarMargin; - - private GameObject _blanker; - private GameObject _loadingBarBackground; - private GameObject _loadingBar; - private RectTransform _loadingBarRect; - - private float _commandedProgress; - private float _shownProgress; + private ProgressBar progressBar; + + private void Start() { + progressBar = gameObject.AddComponent(); + } public IEnumerator Preload ( @@ -41,12 +29,6 @@ Dictionary>> sceneHooks { MuteAllAudio(); - CreateBlanker(); - - CreateLoadingBarBackground(); - - CreateLoadingBar(); - Logger.APILogger.Log($"Preloading using mode {ModHooks.GlobalSettings.PreloadMode}"); switch (ModHooks.GlobalSettings.PreloadMode) { case "full-scene": @@ -68,104 +50,10 @@ Dictionary>> sceneHooks UnmuteAllAudio(); } - private static float ExpDecay(float a, float b, float decay) => b + (a - b) * Mathf.Exp(-decay * Time.deltaTime); - - public void Update() - { - // https://youtu.be/LSNQuFEDOyQ?si=GmrFzX94CRqDdVqO&t=2976 - const float decay = 16; - _shownProgress = ExpDecay(_shownProgress, _commandedProgress, decay); - } - - public void LateUpdate() - { - _loadingBarRect.sizeDelta = new Vector2( - _shownProgress * LoadingBarWidth, - _loadingBarRect.sizeDelta.y - ); - } - /// /// Mutes all audio from AudioListeners. /// private static void MuteAllAudio() => AudioListener.pause = true; - - /// - /// Creates the canvas used to show the loading progress. - /// It is centered on the screen. - /// - private void CreateBlanker() - { - _blanker = CanvasUtil.CreateCanvas(RenderMode.ScreenSpaceOverlay, new Vector2(CanvasResolutionWidth, CanvasResolutionHeight)); - - DontDestroyOnLoad(_blanker); - - GameObject panel = CanvasUtil.CreateImagePanel - ( - _blanker, - CanvasUtil.NullSprite(new byte[] { 0x00, 0x00, 0x00, 0xFF }), - new CanvasUtil.RectData(Vector2.zero, Vector2.zero, Vector2.zero, Vector2.one) - ); - - panel - .GetComponent() - .preserveAspect = false; - } - - /// - /// Creates the background of the loading bar. - /// It is centered in the canvas. - /// - private void CreateLoadingBarBackground() - { - _loadingBarBackground = CanvasUtil.CreateImagePanel - ( - _blanker, - CanvasUtil.NullSprite(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }), - new CanvasUtil.RectData - ( - new Vector2(LoadingBarBackgroundWidth, LoadingBarBackgroundHeight), - Vector2.zero, - new Vector2(0.5f, 0.5f), - new Vector2(0.5f, 0.5f) - ) - ); - - _loadingBarBackground.GetComponent().preserveAspect = false; - } - - /// - /// Creates the loading bar with an initial width of 0. - /// It is centered in the canvas. - /// - private void CreateLoadingBar() - { - _loadingBar = CanvasUtil.CreateImagePanel - ( - _blanker, - CanvasUtil.NullSprite(new byte[] { 0x99, 0x99, 0x99, 0xFF }), - new CanvasUtil.RectData - ( - new Vector2(0, LoadingBarHeight), - Vector2.zero, - new Vector2(0.5f, 0.5f), - new Vector2(0.5f, 0.5f) - ) - ); - - _loadingBar.GetComponent().preserveAspect = false; - _loadingBarRect = _loadingBar.GetComponent(); - } - - /// - /// Updates the progress of the loading bar to the given progress. - /// - /// The progress that should be displayed. 0.0f - 1.0f - private void UpdateLoadingBarProgress(float progress) - { - _commandedProgress = progress; - } - private IEnumerator DoPreloadAssetbundle ( @@ -187,7 +75,7 @@ private IEnumerator DoPreloadAssetbundle AssetBundleCreateRequest op = AssetBundle.LoadFromMemoryAsync(bundleData); if (op == null) { - UpdateLoadingBarProgress(1); + progressBar.Progress = 1; yield break; } @@ -234,7 +122,7 @@ private IEnumerator DoPreloadAssetbundle while (queue.Count > 0) { float progress = (total - queue.Count) / (float)total; - UpdateLoadingBarProgress(progress); + progressBar.Progress = progress; yield return null; } } @@ -529,12 +417,12 @@ IEnumerator DoLoad(AsyncOperation load) .Select(x => (x.load?.progress ?? 0) * 0.5f + (x.unload?.progress ?? 0) * 0.5f) .Average(); - UpdateLoadingBarProgress(sceneProgressAverage); + progressBar.Progress = sceneProgressAverage; } repackBundle?.Unload(true); - UpdateLoadingBarProgress(1.0f); + progressBar.Progress = 1; } /// @@ -555,12 +443,7 @@ private IEnumerator CleanUpPreloading() yield return new WaitForEndOfFrame(); } - // Remove the black screen - Destroy(_loadingBar); - Destroy(_loadingBarBackground); - Destroy(_blanker); - - yield break; + Destroy(progressBar); } /// diff --git a/Assembly-CSharp/ProgressBar.cs b/Assembly-CSharp/ProgressBar.cs new file mode 100644 index 00000000..cde41291 --- /dev/null +++ b/Assembly-CSharp/ProgressBar.cs @@ -0,0 +1,134 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace Modding; + +internal class ProgressBar : MonoBehaviour +{ + private const int CanvasResolutionWidth = 1920; + private const int CanvasResolutionHeight = 1080; + private const int LoadingBarBackgroundWidth = 1000; + private const int LoadingBarBackgroundHeight = 100; + private const int LoadingBarMargin = 12; + private const int LoadingBarWidth = LoadingBarBackgroundWidth - 2 * LoadingBarMargin; + private const int LoadingBarHeight = LoadingBarBackgroundHeight - 2 * LoadingBarMargin; + + private GameObject _blanker; + private GameObject _loadingBarBackground; + private GameObject _loadingBar; + private RectTransform _loadingBarRect; + + private float _commandedProgress; + private float _shownProgress; + + + + /// + /// Updates the progress of the loading bar to the given progress. + /// + /// The progress that should be displayed. 0.0f-1.0f + public float Progress { + get => _commandedProgress; + set => _commandedProgress = value; + } + + public void Start() + { + CreateBlanker(); + + CreateLoadingBarBackground(); + + CreateLoadingBar(); + } + + private static float ExpDecay(float a, float b, float decay) => b + (a - b) * Mathf.Exp(-decay * Time.deltaTime); + + public void Update() + { + // https://youtu.be/LSNQuFEDOyQ?si=GmrFzX94CRqDdVqO&t=2976 + const float decay = 16; + _shownProgress = ExpDecay(_shownProgress, _commandedProgress, decay); + } + + public void LateUpdate() + { + _loadingBarRect.sizeDelta = new Vector2( + _shownProgress * LoadingBarWidth, + _loadingBarRect.sizeDelta.y + ); + } + + /// + /// Creates the canvas used to show the loading progress. + /// It is centered on the screen. + /// + private void CreateBlanker() + { + _blanker = CanvasUtil.CreateCanvas(RenderMode.ScreenSpaceOverlay, new Vector2(CanvasResolutionWidth, CanvasResolutionHeight)); + + DontDestroyOnLoad(_blanker); + + GameObject panel = CanvasUtil.CreateImagePanel + ( + _blanker, + CanvasUtil.NullSprite(new byte[] { 0x00, 0x00, 0x00, 0xFF }), + new CanvasUtil.RectData(Vector2.zero, Vector2.zero, Vector2.zero, Vector2.one) + ); + + panel + .GetComponent() + .preserveAspect = false; + } + + /// + /// Creates the background of the loading bar. + /// It is centered in the canvas. + /// + private void CreateLoadingBarBackground() + { + _loadingBarBackground = CanvasUtil.CreateImagePanel + ( + _blanker, + CanvasUtil.NullSprite(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }), + new CanvasUtil.RectData + ( + new Vector2(LoadingBarBackgroundWidth, LoadingBarBackgroundHeight), + Vector2.zero, + new Vector2(0.5f, 0.5f), + new Vector2(0.5f, 0.5f) + ) + ); + + _loadingBarBackground.GetComponent().preserveAspect = false; + } + + /// + /// Creates the loading bar with an initial width of 0. + /// It is centered in the canvas. + /// + private void CreateLoadingBar() + { + _loadingBar = CanvasUtil.CreateImagePanel + ( + _blanker, + CanvasUtil.NullSprite(new byte[] { 0x99, 0x99, 0x99, 0xFF }), + new CanvasUtil.RectData + ( + new Vector2(0, LoadingBarHeight), + Vector2.zero, + new Vector2(0.5f, 0.5f), + new Vector2(0.5f, 0.5f) + ) + ); + + _loadingBar.GetComponent().preserveAspect = false; + _loadingBarRect = _loadingBar.GetComponent(); + } + + private void OnDestroy() + { + Destroy(_loadingBar); + Destroy(_loadingBarBackground); + Destroy(_blanker); + } +} From 91f1840ac47dec2ef078b68647babf3b334ecb67 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Mon, 30 Jun 2025 20:48:36 +0200 Subject: [PATCH 06/19] build: remove SolutionDir to support `dotnet build Assembly-CSharp ...` --- .github/workflows/build.yaml | 2 +- Assembly-CSharp/Assembly-CSharp.csproj | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4e1a7020..16406405 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -67,7 +67,7 @@ jobs: dotnet build PrePatcher -o PrePatcher/Output -p:Configuration=Release - name: Build Assembly-CSharp run: | - dotnet build Assembly-CSharp -p:SolutionDir=$PWD -p:Configuration=Release + dotnet build Assembly-CSharp -p:Configuration=Release - name: Upload Binary if: inputs.upload-artifact uses: actions/upload-artifact@v4 diff --git a/Assembly-CSharp/Assembly-CSharp.csproj b/Assembly-CSharp/Assembly-CSharp.csproj index 7c45c19c..befecfae 100644 --- a/Assembly-CSharp/Assembly-CSharp.csproj +++ b/Assembly-CSharp/Assembly-CSharp.csproj @@ -13,7 +13,7 @@ - + @@ -26,7 +26,7 @@ - $(SolutionDir)/OutputFinal + ../OutputFinal C:/Program Files (x86)/Steam/steamapps/common/Hollow Knight $(HOME)/.local/share/Steam/steamapps/common/Hollow Knight @@ -41,7 +41,7 @@ - + @@ -53,16 +53,16 @@ - - + + - + - - + + - + @@ -73,9 +73,9 @@ - - - + + + From df17ebd3b4145f7923dc8125564cac9ddd116c8b Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Mon, 30 Jun 2025 21:32:28 +0200 Subject: [PATCH 07/19] disable by default --- Assembly-CSharp/Assembly-CSharp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assembly-CSharp/Assembly-CSharp.csproj b/Assembly-CSharp/Assembly-CSharp.csproj index befecfae..3bd747f8 100644 --- a/Assembly-CSharp/Assembly-CSharp.csproj +++ b/Assembly-CSharp/Assembly-CSharp.csproj @@ -30,7 +30,7 @@ C:/Program Files (x86)/Steam/steamapps/common/Hollow Knight $(HOME)/.local/share/Steam/steamapps/common/Hollow Knight - $(GamePath)/hollow_knight_Data/Managed + $(GamePath)/hollow_knight_Data/Managed mono From d9dc78ccb006eb584d059c6c9fb195ad90718d74 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Mon, 30 Jun 2025 21:51:17 +0200 Subject: [PATCH 08/19] actions: build assembly-csharp with specified RID --- .github/workflows/build.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 16406405..8d24bae1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -51,6 +51,14 @@ jobs: mkdir Vanilla cd ./Vanilla tar -xzf ../hk-binary-archives/${{ env.HK_VERSION }}/managed.${{ matrix.platform }}.tar.gz + + - name: Set RID + run: | + case "${{ matrix.platform }}" in + windows) echo "RID=win-x64" >> $GITHUB_ENV ;; + linux) echo "RID=linux-x64" >> $GITHUB_ENV ;; + macos) echo "RID=osx-x64" >> $GITHUB_ENV ;; + esac - uses: actions/setup-dotnet@v4 with: @@ -67,7 +75,7 @@ jobs: dotnet build PrePatcher -o PrePatcher/Output -p:Configuration=Release - name: Build Assembly-CSharp run: | - dotnet build Assembly-CSharp -p:Configuration=Release + dotnet build Assembly-CSharp --runtime $RID -p:Configuration=Release - name: Upload Binary if: inputs.upload-artifact uses: actions/upload-artifact@v4 From cfa88600fbe22da901da5d57d797a25bbd7cc2dc Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Mon, 30 Jun 2025 22:33:29 +0200 Subject: [PATCH 09/19] preloader: log total duration for preloading --- Assembly-CSharp/Preloader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 05a5020f..699bd305 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -27,6 +28,7 @@ public IEnumerator Preload Dictionary>> sceneHooks ) { + var stopwatch = Stopwatch.StartNew(); MuteAllAudio(); Logger.APILogger.Log($"Preloading using mode {ModHooks.GlobalSettings.PreloadMode}"); @@ -48,6 +50,7 @@ Dictionary>> sceneHooks yield return CleanUpPreloading(); UnmuteAllAudio(); + Logger.APILogger.LogError($"Finished preloading in {stopwatch.ElapsedMilliseconds/1000:F2}s"); } /// From 8ef02d04aec783c9eca9911adaec87b6aebecfe6 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Tue, 1 Jul 2025 22:30:57 +0200 Subject: [PATCH 10/19] switch `PreloadMode` setting to enum --- Assembly-CSharp/ModHooksGlobalSettings.cs | 28 ++++++++++++++++++++--- Assembly-CSharp/Preloader.cs | 7 +++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Assembly-CSharp/ModHooksGlobalSettings.cs b/Assembly-CSharp/ModHooksGlobalSettings.cs index 601e0d03..a22bee03 100644 --- a/Assembly-CSharp/ModHooksGlobalSettings.cs +++ b/Assembly-CSharp/ModHooksGlobalSettings.cs @@ -1,9 +1,31 @@ using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Modding { + + /// + /// Strategy preloading game objects + /// + [PublicAPI] + public enum PreloadMode + { + /// + /// Load the entire scene unmodified into memory + /// + FullScene, + /// + /// Preprocess the scenes into an assetbundle, containing filtered versions of the originals + /// + RepackScene, + /// + /// Preprocess the scenes into an assetbundle that contains individual game object assets + /// + RepackAssets, + } + /// /// Class to hold GlobalSettings for the Modding API /// @@ -45,10 +67,10 @@ public class ModHooksGlobalSettings public int PreloadBatchSize = 5; /// - /// - `false`: Load the entire scene unmodified into memory - /// - `true`: Preprocess the scenes by filtering to only the objects we care about. + /// Determines the strategy used for preloading game objects. /// - public string PreloadMode = "full-scene"; + [JsonConverter(typeof(StringEnumConverter))] + public PreloadMode PreloadMode = PreloadMode.FullScene; /// /// Maximum number of days to preserve modlogs for. diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 699bd305..e7ecde25 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; @@ -33,13 +32,13 @@ Dictionary>> sceneHooks Logger.APILogger.Log($"Preloading using mode {ModHooks.GlobalSettings.PreloadMode}"); switch (ModHooks.GlobalSettings.PreloadMode) { - case "full-scene": + case PreloadMode.FullScene: yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, false); break; - case "repack-scene": + case PreloadMode.RepackScene: yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, true); break; - case "repack-assets": + case PreloadMode.RepackAssets: yield return DoPreloadAssetbundle(toPreload, preloadedObjects); break; default: From 24db6bd030b8a096c536cedf6559c66047bbcfdd Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Tue, 1 Jul 2025 22:39:16 +0200 Subject: [PATCH 11/19] fall back to `RepackScene` when scene hooks are present --- Assembly-CSharp/Preloader.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index e7ecde25..e88e99a9 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -30,6 +30,8 @@ Dictionary>> sceneHooks var stopwatch = Stopwatch.StartNew(); MuteAllAudio(); + bool usesSceneHooks = sceneHooks.Sum(kvp => kvp.Value.Count) > 0; + Logger.APILogger.Log($"Preloading using mode {ModHooks.GlobalSettings.PreloadMode}"); switch (ModHooks.GlobalSettings.PreloadMode) { case PreloadMode.FullScene: @@ -39,6 +41,12 @@ Dictionary>> sceneHooks yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, true); break; case PreloadMode.RepackAssets: + if (usesSceneHooks) { + Logger.APILogger.LogWarn($"Some mods ({string.Join(", ", sceneHooks.Keys)}) use scene hooks, falling back to \"RepackScene\" preload mode"); + yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, true); + break; + } + yield return DoPreloadAssetbundle(toPreload, preloadedObjects); break; default: From f6e258f1df2d1f4c1f0ecb034e98d2b5212f9af8 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Tue, 1 Jul 2025 22:36:59 +0200 Subject: [PATCH 12/19] set default preload mode to `RepackAssets` --- Assembly-CSharp/ModHooksGlobalSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assembly-CSharp/ModHooksGlobalSettings.cs b/Assembly-CSharp/ModHooksGlobalSettings.cs index a22bee03..12ab9fc7 100644 --- a/Assembly-CSharp/ModHooksGlobalSettings.cs +++ b/Assembly-CSharp/ModHooksGlobalSettings.cs @@ -70,7 +70,7 @@ public class ModHooksGlobalSettings /// Determines the strategy used for preloading game objects. /// [JsonConverter(typeof(StringEnumConverter))] - public PreloadMode PreloadMode = PreloadMode.FullScene; + public PreloadMode PreloadMode = PreloadMode.RepackAssets; /// /// Maximum number of days to preserve modlogs for. From 08bc9d446922f1ef6a48741f440b3094aab4d44e Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Tue, 1 Jul 2025 22:44:19 +0200 Subject: [PATCH 13/19] don't use new c# languge features --- Assembly-CSharp/UnitySceneRepacker.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Assembly-CSharp/UnitySceneRepacker.cs b/Assembly-CSharp/UnitySceneRepacker.cs index 5d3ca60e..a0529f8a 100644 --- a/Assembly-CSharp/UnitySceneRepacker.cs +++ b/Assembly-CSharp/UnitySceneRepacker.cs @@ -9,16 +9,17 @@ internal struct RepackStats { public int objectsAfter; } -internal class UnitySceneRepackerException(string message) : Exception(message); +internal class UnitySceneRepackerException : Exception { + public UnitySceneRepackerException(string message) : base(message) { } +} internal static class UnitySceneRepacker { - public enum Mode { SceneBundle, AssetBundle, } - + public static (byte[], RepackStats) Repack(string bundleName, string gamePath, string preloadsJson, Mode mode) { export( bundleName, @@ -28,7 +29,7 @@ public static (byte[], RepackStats) Repack(string bundleName, string gamePath, s out int bundleSize, out IntPtr bundleData, out RepackStats stats, - (byte) mode + (byte)mode ); if (errorPtr != IntPtr.Zero) { @@ -69,11 +70,11 @@ private static string PtrToStringAndFree(IntPtr ptr) { private static byte[] PtrToByteArrayAndFree(int size, IntPtr ptr) { if (ptr == IntPtr.Zero || size == 0) - return []; + return new byte[] { }; byte[] managedArray = new byte[size]; Marshal.Copy(ptr, managedArray, 0, size); free_array(size, ptr); return managedArray; } -} +} \ No newline at end of file From afd2498e9098e6e00c7f754d51db3aa83a607815 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 2 Jul 2025 13:29:19 +0200 Subject: [PATCH 14/19] fix typo --- Assembly-CSharp/Preloader.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index e88e99a9..ae07a245 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -250,9 +250,8 @@ bool useRepack byte[] bundleData = null; Task task = Task.Run(() => { try { - RepackStats repackStats = default; - (bundleData, repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.SceneBundle); - Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB"); + (bundleData, RepackStats repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.SceneBundle); + Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB)"); } catch (Exception e) { Logger.APILogger.LogError($"Error trying to repack preloads into assetbundle: {e}"); } From 0ed58bfcbc056e4c67c7f462ac91ba33e75a0b61 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 2 Jul 2025 13:39:36 +0200 Subject: [PATCH 15/19] refactor: make preloading method names clearer By extracting `DoPreloadRepackedScenes` into a separate method, that just passes the scenePrefix to `DoPreloadScenes`. --- Assembly-CSharp/Preloader.cs | 153 +++++++++-------------------------- 1 file changed, 37 insertions(+), 116 deletions(-) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index ae07a245..0ef30dc5 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -35,15 +35,15 @@ Dictionary>> sceneHooks Logger.APILogger.Log($"Preloading using mode {ModHooks.GlobalSettings.PreloadMode}"); switch (ModHooks.GlobalSettings.PreloadMode) { case PreloadMode.FullScene: - yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, false); + yield return DoPreloadScenes(toPreload, preloadedObjects, sceneHooks); break; case PreloadMode.RepackScene: - yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, true); + yield return DoPreloadRepackedScenes(toPreload, preloadedObjects, sceneHooks); break; case PreloadMode.RepackAssets: if (usesSceneHooks) { Logger.APILogger.LogWarn($"Some mods ({string.Join(", ", sceneHooks.Keys)}) use scene hooks, falling back to \"RepackScene\" preload mode"); - yield return DoPreloadClassic(toPreload, preloadedObjects, sceneHooks, true); + yield return DoPreloadRepackedScenes(toPreload, preloadedObjects, sceneHooks); break; } @@ -137,133 +137,56 @@ private IEnumerator DoPreloadAssetbundle } } - - /*private IEnumerator DoPreloadAssetbundleSeparate + /// + /// Preload using `DoPreloadScenes`, but first preprocess them to only contain relevant objects + /// + private IEnumerator DoPreloadRepackedScenes ( Dictionary Preloads)>> toPreload, IDictionary>> preloadedObjects, Dictionary>> sceneHooks ) { - const string basePath = "/home/jakob/dev/unity/unity-scene-repacker/out-separate"; - - - List sceneNames = toPreload.Keys.ToList(); - - - var preloadOperationQueue = new List(); - var allOperations = new List(); - - float sceneProgressAverage = 0; - int i = 0; - Logger.APILogger.Log("starting"); - while (sceneProgressAverage < 1.0f || i < sceneNames.Count) { - while (preloadOperationQueue.Count < 20 && i < sceneNames.Count) { - string sceneName = sceneNames[i++]; - - string bundlePath = basePath + $"/{sceneName}.bundle"; - if (File.Exists(bundlePath)) { - - int priority = 0; - - if (toPreload.TryGetValue(sceneName, out var requests)) - priority += requests.Select(x => x.Preloads.Count).Sum(); - - AssetBundleCreateRequest op = AssetBundle.LoadFromFileAsync(bundlePath); - op.priority = priority; - preloadOperationQueue.Add(op); - allOperations.Add(op); - - StartCoroutine(HandleBundle(sceneName, toPreload[sceneName], op)); - } else { - Logger.APILogger.LogWarn($"Not Found: {bundlePath}"); - } + const string PreloadBundleName = "hk_api_repack"; + string preloadJson = JsonConvert.SerializeObject( + toPreload.ToDictionary(k => k.Key, v => v.Value.SelectMany(x => x.Preloads).Distinct()) + ); + byte[] bundleData = null; + Task task = Task.Run(() => { + try { + (bundleData, RepackStats repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.SceneBundle); + Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB)"); + } catch (Exception e) { + Logger.APILogger.LogError($"Error trying to repack preloads into assetbundle: {e}"); } - - yield return null; - - // sceneProgressAverage = allOperations.Average(op => op.progress); - sceneProgressAverage = i / (float) sceneNames.Count; - UpdateLoadingBarProgress(sceneProgressAverage); - } - Logger.APILogger.Log($"Preload completed {i}"); + }); + yield return new WaitUntil(() => task.IsCompleted); + if (bundleData == null) { + yield return DoPreloadScenes(toPreload, preloadedObjects, sceneHooks); + yield break; + } - yield break; - - IEnumerator HandleBundle(string sceneName, List<(ModLoader.ModInstance, List)> toPreloadScene, AssetBundleCreateRequest op) { - float bundleLoadStart = TimingDumper.Current; - yield return op; - TimingDumper.Record(bundleLoadStart, "LoadBundle", sceneName); - - float objectLoadStart = TimingDumper.Current; - Logger.APILogger.Log($"Scene: {sceneName}"); - foreach (var (mod, modPreloadPaths) in toPreloadScene) { - Logger.APILogger.Log($" {mod.Name}"); - - if (!preloadedObjects.TryGetValue(mod, out var modPreloads)) { - modPreloads = new Dictionary>(); - preloadedObjects[mod] = modPreloads; - } - if (!modPreloads.TryGetValue(sceneName, out var modScenePreloads)) { - modScenePreloads = new Dictionary(); - modPreloads[sceneName] = modScenePreloads; - } - - foreach (string path in modPreloadPaths) { - if (modScenePreloads.ContainsKey(path)) continue; - - Object go = op.assetBundle.LoadAsset($"{path}.prefab"); - if (!go) { - Logger.APILogger.LogError($" could not load '{path}'"); - continue; - } - Logger.APILogger.Log($" {path}"); - modScenePreloads.Add(path, (GameObject) go); - - } - } - - // op.assetBundle.Unload(false); - - TimingDumper.Record(objectLoadStart, "LoadObjects", sceneName); - - preloadOperationQueue.Remove(op); + AssetBundle repackBundle = AssetBundle.LoadFromMemory(bundleData); + if (repackBundle == null) { + Logger.APILogger.LogWarn($"Scene repacking during preloading produced an unloadable asset bundle"); + yield return DoPreloadScenes(toPreload, preloadedObjects, sceneHooks); + yield break; } - }*/ + + const string scenePrefix = $"{PreloadBundleName}_"; + yield return DoPreloadScenes(toPreload, preloadedObjects, sceneHooks, scenePrefix); + repackBundle.Unload(true); + } /// - /// This is the actual preloading process. + /// Preload original scenes using a queue bounded by GlobalSettings.PreloadBatchSize /// - /// - private IEnumerator DoPreloadClassic + private IEnumerator DoPreloadScenes ( Dictionary Preloads)>> toPreload, IDictionary>> preloadedObjects, Dictionary>> sceneHooks, - bool useRepack + string scenePrefix = "" ) { - const string PreloadBundleName = "hk_api_repack"; - AssetBundle repackBundle = null; - if (useRepack) { - string preloadJson = JsonConvert.SerializeObject(toPreload.ToDictionary( - k => k.Key, - v => v.Value.SelectMany(x => x.Preloads).Distinct())); - byte[] bundleData = null; - Task task = Task.Run(() => { - try { - (bundleData, RepackStats repackStats) = UnitySceneRepacker.Repack(PreloadBundleName, Application.dataPath, preloadJson, UnitySceneRepacker.Mode.SceneBundle); - Logger.APILogger.Log($"Repacked {toPreload.Count} preload scenes from {repackStats.objectsBefore} to {repackStats.objectsAfter} objects ({bundleData.Length / 1024f / 1024f:F2}MB)"); - } catch (Exception e) { - Logger.APILogger.LogError($"Error trying to repack preloads into assetbundle: {e}"); - } - }); - yield return new WaitUntil(() => task.IsCompleted); - if (bundleData != null) { - repackBundle = AssetBundle.LoadFromMemory(bundleData); - } - } - - string scenePrefix = repackBundle != null ? $"{PreloadBundleName}_" : ""; - List sceneNames = toPreload.Keys.Union(sceneHooks.Keys).ToList(); Dictionary scenePriority = new(); Dictionary sceneAsyncOperationHolder = new(); @@ -428,8 +351,6 @@ IEnumerator DoLoad(AsyncOperation load) progressBar.Progress = sceneProgressAverage; } - - repackBundle?.Unload(true); progressBar.Progress = 1; } From cc80fecebc55674fc0ec6f36ac4907595ef02d4c Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 2 Jul 2025 14:51:07 +0200 Subject: [PATCH 16/19] replace string with `nameof()` reference --- Assembly-CSharp/Preloader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 0ef30dc5..6626a1d4 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -42,7 +42,7 @@ Dictionary>> sceneHooks break; case PreloadMode.RepackAssets: if (usesSceneHooks) { - Logger.APILogger.LogWarn($"Some mods ({string.Join(", ", sceneHooks.Keys)}) use scene hooks, falling back to \"RepackScene\" preload mode"); + Logger.APILogger.LogWarn($"Some mods ({string.Join(", ", sceneHooks.Keys)}) use scene hooks, falling back to \"{nameof(PreloadMode.RepackScene)}\" preload mode"); yield return DoPreloadRepackedScenes(toPreload, preloadedObjects, sceneHooks); break; } From b57bdf8bd96b9063fc831676931feb24a1adf721 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 2 Jul 2025 14:29:07 +0200 Subject: [PATCH 17/19] pass embedded dump of monobehaviour typetrees to unity-scene-repacker previously this was hardcoded in the unityscenerepacker dll --- Assembly-CSharp/Assembly-CSharp.csproj | 4 +++- Assembly-CSharp/UnitySceneRepacker.cs | 17 ++++++++++++++++- .../monobehaviour-typetree-dump.lz4 | Bin 0 -> 122837 bytes 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 Assembly-CSharp/monobehaviour-typetree-dump.lz4 diff --git a/Assembly-CSharp/Assembly-CSharp.csproj b/Assembly-CSharp/Assembly-CSharp.csproj index 3bd747f8..3529973a 100644 --- a/Assembly-CSharp/Assembly-CSharp.csproj +++ b/Assembly-CSharp/Assembly-CSharp.csproj @@ -125,7 +125,7 @@ all - + @@ -136,6 +136,8 @@ + + diff --git a/Assembly-CSharp/UnitySceneRepacker.cs b/Assembly-CSharp/UnitySceneRepacker.cs index a0529f8a..298cc7e0 100644 --- a/Assembly-CSharp/UnitySceneRepacker.cs +++ b/Assembly-CSharp/UnitySceneRepacker.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Reflection; using System.Runtime.InteropServices; namespace Modding; @@ -19,8 +21,9 @@ public enum Mode { AssetBundle, } - public static (byte[], RepackStats) Repack(string bundleName, string gamePath, string preloadsJson, Mode mode) { + byte[] monobehaviourDump = GetEmbeddedTypetreeDump(); + export( bundleName, gamePath, @@ -29,6 +32,8 @@ public static (byte[], RepackStats) Repack(string bundleName, string gamePath, s out int bundleSize, out IntPtr bundleData, out RepackStats stats, + monobehaviourDump, + monobehaviourDump.Length, (byte)mode ); @@ -51,6 +56,8 @@ private static extern void export( out int bundleSize, out IntPtr bundleData, out RepackStats repackStats, + byte[] monobehaviourTypetreeExport, + int monobehaviourTypetreeExportLen, byte mode ); @@ -77,4 +84,12 @@ private static byte[] PtrToByteArrayAndFree(int size, IntPtr ptr) { free_array(size, ptr); return managedArray; } + + private static byte[] GetEmbeddedTypetreeDump() { + Assembly assembly = typeof(UnitySceneRepacker).Assembly; + using Stream stream = assembly.GetManifestResourceStream("Modding.monobehaviour-typetree-dump.lz4")!; + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } } \ No newline at end of file diff --git a/Assembly-CSharp/monobehaviour-typetree-dump.lz4 b/Assembly-CSharp/monobehaviour-typetree-dump.lz4 new file mode 100644 index 0000000000000000000000000000000000000000..88bf608897ee542f83e9e7df3964b40ec14404bb GIT binary patch literal 122837 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

<*W9k4?XpR;R}QfSsP4P*u0#C_At^HTimUsa0Ag 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 z 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 literal 0 HcmV?d00001 From 6f4808dae54d050ed72d782c9a04e19b2675f72b Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 2 Jul 2025 15:13:42 +0200 Subject: [PATCH 18/19] rename preload bundles for consistency --- Assembly-CSharp/Preloader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Assembly-CSharp/Preloader.cs b/Assembly-CSharp/Preloader.cs index 6626a1d4..5d363448 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -70,7 +70,7 @@ private IEnumerator DoPreloadAssetbundle Dictionary Preloads)>> toPreload, IDictionary>> preloadedObjects ) { - const string PreloadBundleName = "mapi_preload_assetbundle"; + const string PreloadBundleName = "modding_api_asset_bundle"; string preloadJson = JsonConvert.SerializeObject(toPreload.ToDictionary( k => k.Key, @@ -146,7 +146,8 @@ private IEnumerator DoPreloadRepackedScenes IDictionary>> preloadedObjects, Dictionary>> sceneHooks ) { - const string PreloadBundleName = "hk_api_repack"; + const string PreloadBundleName = "modding_api_scene_bundle"; + string preloadJson = JsonConvert.SerializeObject( toPreload.ToDictionary(k => k.Key, v => v.Value.SelectMany(x => x.Preloads).Distinct()) ); From e662c9be243944642d77e9f3245fba86e56fb26f Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 2 Jul 2025 15:15:48 +0200 Subject: [PATCH 19/19] remove leftover python script for timing analysis --- analyze-logs.py | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 analyze-logs.py diff --git a/analyze-logs.py b/analyze-logs.py deleted file mode 100644 index a4795efd..00000000 --- a/analyze-logs.py +++ /dev/null @@ -1,49 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "pandas", -# "plotly", -# ] -# /// - -from datetime import datetime -import json -import pandas as pd -import plotly.express as px -import sys -import tempfile - -color_map = { - "LoadScene": "#636EFA", - "LoadAsset": "#636EFA", - "GetPreloadObjects": "#Ef553b", - "CleanPreload": "#00CC96", - "LoadMods": "#AB63FA", -} - -temp_dir = tempfile.gettempdir() - -with open(f"{temp_dir}/loadTimings.json", "r") as f: - data = json.load(f) - -if len(data) == 0: - sys.exit(1) - -for value in data: - value["Diff"] = round(value["End"] - value["Start"], 6) - value["Start"] = datetime.fromtimestamp(value["Start"]) - value["End"] = datetime.fromtimestamp(value["End"]) - name = value["Name"] - value["Color"] = int(name) % 2 if name.isdigit() else name -df = pd.DataFrame(data) - -fig = px.timeline(df, x_start="Start", x_end="End", y="Context", color="Color", hover_data=["Context", "Name", "Diff"], - color_discrete_map=color_map) -fig.update_layout( - xaxis=dict(title="Timestamp", tickformat="%S.%L"), - yaxis=dict(title="", autorange="reversed", showticklabels=False), -) -fig.for_each_trace(lambda trace: trace.update(text=None) if trace.name in ["1","2","3","LoadScene"] else None) - -fig.update_layout(dragmode="pan") -fig.show(config={"scrollZoom": True}) \ No newline at end of file