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/Assembly-CSharp/Mod.cs b/Assembly-CSharp/Mod.cs
index 3e476940..edef951f 100644
--- a/Assembly-CSharp/Mod.cs
+++ b/Assembly-CSharp/Mod.cs
@@ -110,13 +110,15 @@ private string GetGlobalSettingsPath()
string globalSettingsFileName = $"{GetType().Name}.GlobalSettings.json";
string location = GetType().Assembly.Location;
- string directory = Path.GetDirectoryName(location);
- string globalSettingsOverride = Path.Combine(directory, globalSettingsFileName);
+ if (location != "") { // TODO: not set while hot reloading
+ string directory = Path.GetDirectoryName(location);
+ string globalSettingsOverride = Path.Combine(directory, globalSettingsFileName);
- if (File.Exists(globalSettingsOverride))
- {
- Log("Overriding Global Settings path with Mod directory");
- return globalSettingsOverride;
+ if (File.Exists(globalSettingsOverride))
+ {
+ Log("Overriding Global Settings path with Mod directory");
+ return globalSettingsOverride;
+ }
}
return Path.Combine(Application.persistentDataPath, globalSettingsFileName);
diff --git a/Assembly-CSharp/ModHooksGlobalSettings.cs b/Assembly-CSharp/ModHooksGlobalSettings.cs
index b26b4c7b..23a53a97 100644
--- a/Assembly-CSharp/ModHooksGlobalSettings.cs
+++ b/Assembly-CSharp/ModHooksGlobalSettings.cs
@@ -44,6 +44,24 @@ public class ModHooksGlobalSettings
///
public int PreloadBatchSize = 5;
+ ///
+ /// When enabled, listens for filesystem changes to mod DLLs.
+ /// On modification, mods will be unloaded, and the new copy of the assembly gets loaded as well.
+ /// Limitations:
+ ///
+ /// -
+ /// The old assembly does not get unloaded. If you have created any unity game objects or components,
+ /// make sure to Destroy them in the mod's Unload function.
+ ///
k
+ /// -
+ /// Dependencies of mods cannot be hot reloaded. When you modify a dependency DLL, no change will be made to the mod depending on it.
+ ///
+ /// -
+ /// Assembly.location will return an empty string for hot-reloaded mods
+ ///
+ ///
+ ///
+ public bool EnableHotReload = false;
///
/// Maximum number of days to preserve modlogs for.
diff --git a/Assembly-CSharp/ModLoader.cs b/Assembly-CSharp/ModLoader.cs
index 886beaff..7b1b7fc4 100644
--- a/Assembly-CSharp/ModLoader.cs
+++ b/Assembly-CSharp/ModLoader.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -12,9 +13,17 @@
using UObject = UnityEngine.Object;
using USceneManager = UnityEngine.SceneManagement.SceneManager;
using Modding.Utils;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
namespace Modding
{
+ internal class ModLoaderObject: MonoBehaviour {
+ private void Update() {
+ ModLoader.HandleHotReloadEvents();
+ }
+ }
+
///
/// Handles loading of mods.
///
@@ -38,6 +47,19 @@ public enum ModLoadState
public static Dictionary ModInstanceNameMap { get; private set; } = new();
public static HashSet ModInstances { get; private set; } = new();
+ private static Dictionary> ModInstancesByAssembly = new();
+
+ private static DefaultAssemblyResolver hotReloadAssemblyResolver;
+
+ private static string ManagedPath = SystemInfo.operatingSystemFamily switch
+ {
+ OperatingSystemFamily.Windows => Path.Combine(Application.dataPath, "Managed"),
+ OperatingSystemFamily.MacOSX => Path.Combine(Application.dataPath, "Resources", "Data", "Managed"),
+ OperatingSystemFamily.Linux => Path.Combine(Application.dataPath, "Managed"),
+ OperatingSystemFamily.Other => null,
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+
///
/// Try to add a ModInstance to the internal dictionaries.
///
@@ -85,23 +107,10 @@ public static IEnumerator LoadModsInit(GameObject coroutineHolder)
Logger.APILogger.Log("Starting mod loading");
- string managed_path = SystemInfo.operatingSystemFamily switch
- {
- OperatingSystemFamily.Windows => Path.Combine(Application.dataPath, "Managed"),
- OperatingSystemFamily.MacOSX => Path.Combine(Application.dataPath, "Resources", "Data", "Managed"),
- OperatingSystemFamily.Linux => Path.Combine(Application.dataPath, "Managed"),
-
- OperatingSystemFamily.Other => null,
-
- _ => throw new ArgumentOutOfRangeException()
- };
-
- if (managed_path is null)
+ if (ManagedPath is null)
{
LoadState |= ModLoadState.Loaded;
-
UObject.Destroy(coroutineHolder);
-
yield break;
}
@@ -110,114 +119,26 @@ public static IEnumerator LoadModsInit(GameObject coroutineHolder)
Logger.APILogger.LogDebug($"Loading assemblies and constructing mods");
- string mods = Path.Combine(managed_path, "Mods");
-
- string[] files = Directory.GetDirectories(mods)
- .Except(new string[] { Path.Combine(mods, "Disabled") })
- .SelectMany(d => Directory.GetFiles(d, "*.dll"))
- .ToArray();
-
- Logger.APILogger.LogDebug(string.Join(",\n", files));
-
- Assembly Resolve(object sender, ResolveEventArgs args)
- {
- var asm_name = new AssemblyName(args.Name);
-
- if (files.FirstOrDefault(x => x.EndsWith($"{asm_name.Name}.dll")) is string path)
- return Assembly.LoadFrom(path);
-
- return null;
- }
-
- AppDomain.CurrentDomain.AssemblyResolve += Resolve;
-
- List asms = new(files.Length);
-
- // Load all the assemblies first to avoid dependency issues
- // Dependencies are lazy-loaded, so we won't have attempted loads
- // until the mod initialization.
- foreach (string path in files)
- {
- Logger.APILogger.LogDebug($"Loading assembly `{path}`");
-
- try
- {
- asms.Add(Assembly.LoadFrom(path));
- }
- catch (FileLoadException e)
- {
- Logger.APILogger.LogError($"Unable to load assembly - {e}");
- }
- catch (BadImageFormatException e)
- {
- Logger.APILogger.LogError($"Assembly is bad image. {e}");
- }
- catch (PathTooLongException)
- {
- Logger.APILogger.LogError("Unable to load, path to dll is too long!");
- }
+ string mods = Path.Combine(ManagedPath, "Mods");
+
+ string[] modDirectories = Directory.GetDirectories(mods)
+ .Except([Path.Combine(mods, "Disabled")])
+ .ToArray();
+ string[] modFiles = modDirectories
+ .SelectMany(d => Directory.GetFiles(d, "*.dll"))
+ .ToArray();
+ hotReloadAssemblyResolver = new DefaultAssemblyResolver();
+ foreach (string modDirectory in modDirectories) {
+ hotReloadAssemblyResolver.AddSearchDirectory(modDirectory);
}
+
+ List<(string, Assembly)> modAssemblies = GetModAssemblies(modFiles);
- foreach (Assembly asm in asms)
+ foreach ((string path, Assembly asm) in modAssemblies)
{
Logger.APILogger.LogDebug($"Loading mods in assembly `{asm.FullName}`");
-
- bool foundMod = false;
-
- try
- {
- foreach (Type ty in asm.GetTypesSafely())
- {
- if (!ty.IsClass || ty.IsAbstract || !ty.IsSubclassOf(typeof(Mod)))
- continue;
-
- foundMod = true;
-
- Logger.APILogger.LogDebug($"Constructing mod `{ty.FullName}`");
-
- try
- {
- if (ty.GetConstructor(Type.EmptyTypes)?.Invoke(Array.Empty