diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d4b2ee7..a329d5d 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:SolutionDir=$PWD -p:Configuration=Release + dotnet build Assembly-CSharp --runtime $RID -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 ae54a49..3529973 100644 --- a/Assembly-CSharp/Assembly-CSharp.csproj +++ b/Assembly-CSharp/Assembly-CSharp.csproj @@ -13,7 +13,7 @@ - + @@ -26,7 +26,12 @@ - $(SolutionDir)/OutputFinal + ../OutputFinal + + C:/Program Files (x86)/Steam/steamapps/common/Hollow Knight + $(HOME)/.local/share/Steam/steamapps/common/Hollow Knight + $(GamePath)/hollow_knight_Data/Managed + mono @@ -36,7 +41,7 @@ - + @@ -48,16 +53,16 @@ - - + + - + - - + + - + @@ -68,14 +73,34 @@ - - - + + + + + + + .dll + lib + .so + lib + .dylib + + + + + + + + + + + + @@ -99,6 +124,8 @@ all + + @@ -109,6 +136,8 @@ + + @@ -135,6 +164,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 b26b4c7..12ab9fc 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 /// @@ -44,6 +66,11 @@ public class ModHooksGlobalSettings /// public int PreloadBatchSize = 5; + /// + /// Determines the strategy used for preloading game objects. + /// + [JsonConverter(typeof(StringEnumConverter))] + public PreloadMode PreloadMode = PreloadMode.RepackAssets; /// /// Maximum number of days to preserve modlogs for. diff --git a/Assembly-CSharp/ModLoader.cs b/Assembly-CSharp/ModLoader.cs index 886beaf..6a0a19e 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 6822337..5d36344 100644 --- a/Assembly-CSharp/Preloader.cs +++ b/Assembly-CSharp/Preloader.cs @@ -1,34 +1,24 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; 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; 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 float _secondsSinceLastSet; + private ProgressBar progressBar; + + private void Start() { + progressBar = gameObject.AddComponent(); + } public IEnumerator Preload ( @@ -37,131 +27,167 @@ public IEnumerator Preload Dictionary>> sceneHooks ) { + var stopwatch = Stopwatch.StartNew(); MuteAllAudio(); - CreateBlanker(); - - CreateLoadingBarBackground(); - - CreateLoadingBar(); - - yield return DoPreload(toPreload, preloadedObjects, sceneHooks); - + 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: + yield return DoPreloadScenes(toPreload, preloadedObjects, sceneHooks); + break; + case PreloadMode.RepackScene: + 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 \"{nameof(PreloadMode.RepackScene)}\" preload mode"); + yield return DoPreloadRepackedScenes(toPreload, preloadedObjects, sceneHooks); + break; + } + + 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(); - } - - public void Update() - { - _secondsSinceLastSet += Time.unscaledDeltaTime; - _shownProgress = Mathf.Lerp(_shownProgress, _commandedProgress, _secondsSinceLastSet / 10.0f); - } - - public void LateUpdate() - { - _loadingBarRect.sizeDelta = new Vector2( - _shownProgress * LoadingBarWidth, - _loadingBarRect.sizeDelta.y - ); + Logger.APILogger.LogError($"Finished preloading in {stopwatch.ElapsedMilliseconds/1000:F2}s"); } /// /// 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)); + + private IEnumerator DoPreloadAssetbundle + ( + Dictionary Preloads)>> toPreload, + IDictionary>> preloadedObjects + ) { + const string PreloadBundleName = "modding_api_asset_bundle"; - DontDestroyOnLoad(_blanker); + 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); - GameObject panel = CanvasUtil.CreateImagePanel - ( - _blanker, - CanvasUtil.NullSprite(new byte[] { 0x00, 0x00, 0x00, 0xFF }), - new CanvasUtil.RectData(Vector2.zero, Vector2.zero, Vector2.zero, Vector2.one) - ); + if (op == null) { + progressBar.Progress = 1; + yield break; + } - panel - .GetComponent() - .preserveAspect = false; - } + yield return op; + var bundle = op.assetBundle; - /// - /// 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; - } + 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; + progressBar.Progress = progress; + yield return null; + } + } + /// - /// Creates the loading bar with an initial width of 0. - /// It is centered in the canvas. + /// Preload using `DoPreloadScenes`, but first preprocess them to only contain relevant objects /// - 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) - ) + private IEnumerator DoPreloadRepackedScenes + ( + Dictionary Preloads)>> toPreload, + IDictionary>> preloadedObjects, + Dictionary>> sceneHooks + ) { + const string PreloadBundleName = "modding_api_scene_bundle"; + + 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) { + yield return DoPreloadScenes(toPreload, preloadedObjects, sceneHooks); + yield break; + } - _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) - { - if (Mathf.Abs(_commandedProgress - progress) < float.Epsilon) - return; + 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; + } - _commandedProgress = progress; - _secondsSinceLastSet = 0.0f; + 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 DoPreload + private IEnumerator DoPreloadScenes ( Dictionary Preloads)>> toPreload, IDictionary>> preloadedObjects, - Dictionary>> sceneHooks - ) - { + Dictionary>> sceneHooks, + string scenePrefix = "" + ) { List sceneNames = toPreload.Keys.Union(sceneHooks.Keys).ToList(); Dictionary scenePriority = new(); Dictionary sceneAsyncOperationHolder = new(); @@ -201,12 +227,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) @@ -267,8 +292,8 @@ IEnumerator GetPreloadObjectsOperation(string sceneName) 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); @@ -279,18 +304,9 @@ 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(sceneName, LoadSceneMode.Additive); + AsyncOperation loadOp = USceneManager.LoadSceneAsync(scenePrefix + sceneName, LoadSceneMode.Additive); StartCoroutine(DoLoad(loadOp)); @@ -299,6 +315,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; @@ -323,10 +350,10 @@ IEnumerator DoLoad(AsyncOperation load) .Select(x => (x.load?.progress ?? 0) * 0.5f + (x.unload?.progress ?? 0) * 0.5f) .Average(); - UpdateLoadingBarProgress(sceneProgressAverage); + progressBar.Progress = sceneProgressAverage; } - UpdateLoadingBarProgress(1.0f); + progressBar.Progress = 1; } /// @@ -347,10 +374,7 @@ private IEnumerator CleanUpPreloading() yield return new WaitForEndOfFrame(); } - // Remove the black screen - Destroy(_loadingBar); - Destroy(_loadingBarBackground); - Destroy(_blanker); + Destroy(progressBar); } /// diff --git a/Assembly-CSharp/ProgressBar.cs b/Assembly-CSharp/ProgressBar.cs new file mode 100644 index 0000000..cde4129 --- /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); + } +} diff --git a/Assembly-CSharp/UnitySceneRepacker.cs b/Assembly-CSharp/UnitySceneRepacker.cs new file mode 100644 index 0000000..298cc7e --- /dev/null +++ b/Assembly-CSharp/UnitySceneRepacker.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Modding; + +[StructLayout(LayoutKind.Sequential)] +internal struct RepackStats { + public int objectsBefore; + public int objectsAfter; +} + +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) { + byte[] monobehaviourDump = GetEmbeddedTypetreeDump(); + + export( + bundleName, + gamePath, + preloadsJson, + out IntPtr errorPtr, + out int bundleSize, + out IntPtr bundleData, + out RepackStats stats, + monobehaviourDump, + monobehaviourDump.Length, + (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[] monobehaviourTypetreeExport, + int monobehaviourTypetreeExportLen, + 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 new byte[] { }; + + byte[] managedArray = new byte[size]; + Marshal.Copy(ptr, managedArray, 0, size); + 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 0000000..88bf608 Binary files /dev/null and b/Assembly-CSharp/monobehaviour-typetree-dump.lz4 differ diff --git a/HollowKnight.Modding.API.sln.DotSettings b/HollowKnight.Modding.API.sln.DotSettings index 98201e2..0d6f08d 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