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