From 782879a4a274a331044e920e425ecb13aea8c876 Mon Sep 17 00:00:00 2001 From: Kalobi <46748261+Kalobi@users.noreply.github.com> Date: Sun, 5 Nov 2023 19:16:23 +0100 Subject: [PATCH] Fix module loading (#672) --- Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs | 281 ++++++++++++++++--- Celeste.Mod.mm/Mod/Everest/Everest.cs | 217 +------------- Celeste.Mod.mm/Mod/Module/EverestModule.cs | 8 + Celeste.Mod.mm/Mod/Module/LuaModule.cs | 8 + Celeste.Mod.mm/Mod/Module/NullModule.cs | 8 + 5 files changed, 281 insertions(+), 241 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs index bf98cc23d..fe583ea42 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs @@ -1,7 +1,11 @@ -using Celeste.Mod.Core; +using Celeste.Mod.Backdrops; +using Celeste.Mod.Core; +using Celeste.Mod.Entities; using Celeste.Mod.Helpers; using Ionic.Zip; using MAB.DotIgnore; +using Microsoft.Xna.Framework; +using Monocle; using MonoMod.Utils; using System; using System.Collections.Generic; @@ -544,9 +548,7 @@ public static void LoadModAssembly(EverestModuleMetadata meta, Assembly asm) { } bool foundModule = false; - for (int i = 0; i < types.Length; i++) { - Type type = types[i]; - + foreach (Type type in types) { EverestModule mod = null; try { if (typeof(EverestModule).IsAssignableFrom(type) && !type.IsAbstract) { @@ -560,15 +562,221 @@ public static void LoadModAssembly(EverestModuleMetadata meta, Assembly asm) { Logger.Log(LogLevel.Warn, "loader", $"Skipping type '{type.FullName}' likely depending on optional dependency: {e}"); } - if (mod != null) { + if (mod != null) { mod.Metadata = meta; mod.Register(); - } + } } // Warn if we didn't find a module, as that could indicate an oversight from the developer if (!foundModule) Logger.Log(LogLevel.Warn, "loader", "Assembly doesn't contain an EverestModule!"); + + ProcessAssembly(meta, asm, types); + } + + internal static void ProcessAssembly(EverestModuleMetadata meta, Assembly asm, Type[] types) { + LuaLoader.Precache(asm); + + bool newStrawberriesRegistered = false; + + foreach (Type type in types) { + // Search for all entities marked with the CustomEntityAttribute. + foreach (CustomEntityAttribute attrib in type.GetCustomAttributes()) { + foreach (string idFull in attrib.IDs) { + string id; + string genName; + string[] split = idFull.Split('='); + + if (split.Length == 1) { + id = split[0]; + genName = "Load"; + + } else if (split.Length == 2) { + id = split[0]; + genName = split[1]; + + } else { + Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom entity ID elements: {idFull} ({type.FullName})"); + continue; + } + + id = id.Trim(); + genName = genName.Trim(); + + patch_Level.EntityLoader loader = null; + + ConstructorInfo ctor; + MethodInfo gen; + + gen = type.GetMethod(genName, new Type[] { typeof(Level), typeof(LevelData), typeof(Vector2), typeof(EntityData) }); + if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Entity))) { + loader = (level, levelData, offset, entityData) => (Entity) gen.Invoke(null, new object[] { level, levelData, offset, entityData }); + goto RegisterEntityLoader; + } + + ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2), typeof(EntityID) }); + if (ctor != null) { + loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset, new EntityID(levelData.Name, entityData.ID) }); + goto RegisterEntityLoader; + } + + ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2) }); + if (ctor != null) { + loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset }); + goto RegisterEntityLoader; + } + + ctor = type.GetConstructor(new Type[] { typeof(Vector2) }); + if (ctor != null) { + loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { offset }); + goto RegisterEntityLoader; + } + + ctor = type.GetConstructor(_EmptyTypeArray); + if (ctor != null) { + loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(_EmptyObjectArray); + goto RegisterEntityLoader; + } + + RegisterEntityLoader: + if (loader == null) { + Logger.Log(LogLevel.Warn, "core", $"Found custom entity without suitable constructor / {genName}(Level, LevelData, Vector2, EntityData): {id} ({type.FullName})"); + continue; + } + patch_Level.EntityLoaders[id] = loader; + } + } + // Register with the StrawberryRegistry all entities marked with RegisterStrawberryAttribute. + foreach (RegisterStrawberryAttribute attrib in type.GetCustomAttributes()) { + List names = new List(); + foreach (CustomEntityAttribute nameAttrib in type.GetCustomAttributes()) + foreach (string idFull in nameAttrib.IDs) { + string[] split = idFull.Split('='); + if (split.Length == 0) { + Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom entity ID elements: {idFull} ({type.FullName})"); + continue; + } + names.Add(split[0]); + } + if (names.Count == 0) + goto NoDefinedBerryNames; // no customnames? skip out on registering berry + + foreach (string name in names) { + StrawberryRegistry.Register(type, name, attrib.isTracked, attrib.blocksNormalCollection); + newStrawberriesRegistered = true; + } + } + NoDefinedBerryNames: + ; + + // Search for all Entities marked with the CustomEventAttribute. + foreach (CustomEventAttribute attrib in type.GetCustomAttributes()) { + foreach (string idFull in attrib.IDs) { + string id; + string genName; + string[] split = idFull.Split('='); + + if (split.Length == 1) { + id = split[0]; + genName = "Load"; + + } else if (split.Length == 2) { + id = split[0]; + genName = split[1]; + + } else { + Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom cutscene ID elements: {idFull} ({type.FullName})"); + continue; + } + + id = id.Trim(); + genName = genName.Trim(); + + patch_EventTrigger.CutsceneLoader loader = null; + + ConstructorInfo ctor; + MethodInfo gen; + + gen = type.GetMethod(genName, new Type[] { typeof(EventTrigger), typeof(Player), typeof(string) }); + if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Entity))) { + loader = (trigger, player, eventID) => (Entity) gen.Invoke(null, new object[] { trigger, player, eventID }); + goto RegisterCutsceneLoader; + } + + ctor = type.GetConstructor(new Type[] { typeof(EventTrigger), typeof(Player), typeof(string) }); + if (ctor != null) { + loader = (trigger, player, eventID) => (Entity) ctor.Invoke(new object[] { trigger, player, eventID }); + goto RegisterCutsceneLoader; + } + + ctor = type.GetConstructor(_EmptyTypeArray); + if (ctor != null) { + loader = (trigger, player, eventID) => (Entity) ctor.Invoke(_EmptyObjectArray); + goto RegisterCutsceneLoader; + } + + RegisterCutsceneLoader: + if (loader == null) { + Logger.Log(LogLevel.Warn, "core", $"Found custom cutscene without suitable constructor / {genName}(EventTrigger, Player, string): {id} ({type.FullName})"); + continue; + } + patch_EventTrigger.CutsceneLoaders[id] = loader; + } + } + + // Search for all Backdrops marked with the CustomBackdropAttribute. + foreach (CustomBackdropAttribute attrib in type.GetCustomAttributes()) { + foreach (string idFull in attrib.IDs) { + string id; + string genName; + string[] split = idFull.Split('='); + + if (split.Length == 1) { + id = split[0]; + genName = "Load"; + } else if (split.Length == 2) { + id = split[0]; + genName = split[1]; + } else { + Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom backdrop ID elements: {idFull} ({type.FullName})"); + continue; + } + + id = id.Trim(); + genName = genName.Trim(); + + patch_MapData.BackdropLoader loader = null; + + ConstructorInfo ctor; + MethodInfo gen; + + gen = type.GetMethod(genName, new Type[] { typeof(BinaryPacker.Element) }); + if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Backdrop))) { + loader = data => (Backdrop) gen.Invoke(null, new object[] { data }); + goto RegisterBackdropLoader; + } + + ctor = type.GetConstructor(new Type[] { typeof(BinaryPacker.Element) }); + if (ctor != null) { + loader = data => (Backdrop) ctor.Invoke(new object[] { data }); + goto RegisterBackdropLoader; + } + + RegisterBackdropLoader: + if (loader == null) { + Logger.Log(LogLevel.Warn, "core", $"Found custom backdrop without suitable constructor / {genName}(BinaryPacker.Element): {id} ({type.FullName})"); + continue; + } + patch_MapData.BackdropLoaders[id] = loader; + } + } + } + // We should run the map data processors again if new berry types are registered, so that CoreMapDataProcessor assigns them checkpoint IDs and orders. + if (newStrawberriesRegistered && _Initialized) { + Logger.Log(LogLevel.Verbose, "core", $"Assembly {asm.FullName} for module {meta} has custom strawberries: reloading maps."); + AssetReloadHelper.ReloadAllMaps(); + } } internal static void ReloadModAssembly(object source, FileSystemEventArgs e, bool retrying = false) { @@ -577,16 +785,14 @@ internal static void ReloadModAssembly(object source, FileSystemEventArgs e, boo Logger.Log(LogLevel.Info, "loader", $"Reloading mod assembly: {e.FullPath}"); QueuedTaskHelper.Do("ReloadModAssembly:" + e.FullPath, () => { - EverestModule module = _Modules.FirstOrDefault(m => m.Metadata.DLL == e.FullPath); - if (module == null) - return; - AssetReloadHelper.Do($"{Dialog.Clean("ASSETRELOADHELPER_RELOADINGMODASSEMBLY")} {Path.GetFileName(e.FullPath)}", () => { - Assembly asm = null; + List modules = _Modules.FindAll(m => m.Metadata.DLL == e.FullPath); + + Assembly newAsm; using (FileStream stream = File.OpenRead(e.FullPath)) - asm = Relinker.GetRelinkedAssembly(module.Metadata, Path.GetFileNameWithoutExtension(e.FullPath), stream); + newAsm = Relinker.GetRelinkedAssembly(modules[0].Metadata, Path.GetFileNameWithoutExtension(e.FullPath), stream); - if (asm == null) { + if (newAsm == null) { if (!retrying) { // Retry. QueuedTaskHelper.Do("ReloadModAssembly:" + e.FullPath, () => { @@ -597,36 +803,43 @@ internal static void ReloadModAssembly(object source, FileSystemEventArgs e, boo } ((FileSystemWatcher) source).Dispose(); - - // be sure to save this module's save data and session before reloading it, so that they are not lost. - if (SaveData.Instance != null) { - Logger.Log(LogLevel.Verbose, "core", $"Saving save data slot {SaveData.Instance.FileSlot} for {module.Metadata} before reloading"); - if (module.SaveDataAsync) { - module.WriteSaveData(SaveData.Instance.FileSlot, module.SerializeSaveData(SaveData.Instance.FileSlot)); - } else { -#pragma warning disable CS0618 // Synchronous save / load IO is obsolete but some mods still override / use it. - if (CoreModule.Settings.SaveDataFlush ?? false) - module.ForceSaveDataFlush++; - module.SaveSaveData(SaveData.Instance.FileSlot); -#pragma warning restore CS0618 - } - - if (SaveData.Instance.CurrentSession?.InArea ?? false) { - Logger.Log(LogLevel.Verbose, "core", $"Saving session slot {SaveData.Instance.FileSlot} for {module.Metadata} before reloading"); + + foreach (EverestModule module in modules) { + // be sure to save this module's save data and session before reloading it, so that they are not lost. + if (SaveData.Instance != null) { + Logger.Log(LogLevel.Verbose, "core", $"Saving save data slot {SaveData.Instance.FileSlot} for {module.Metadata} before reloading"); if (module.SaveDataAsync) { - module.WriteSession(SaveData.Instance.FileSlot, module.SerializeSession(SaveData.Instance.FileSlot)); + module.WriteSaveData(SaveData.Instance.FileSlot, module.SerializeSaveData(SaveData.Instance.FileSlot)); } else { #pragma warning disable CS0618 // Synchronous save / load IO is obsolete but some mods still override / use it. if (CoreModule.Settings.SaveDataFlush ?? false) module.ForceSaveDataFlush++; - module.SaveSession(SaveData.Instance.FileSlot); + module.SaveSaveData(SaveData.Instance.FileSlot); #pragma warning restore CS0618 } + + if (SaveData.Instance.CurrentSession?.InArea ?? false) { + Logger.Log(LogLevel.Verbose, "core", $"Saving session slot {SaveData.Instance.FileSlot} for {module.Metadata} before reloading"); + if (module.SaveDataAsync) { + module.WriteSession(SaveData.Instance.FileSlot, module.SerializeSession(SaveData.Instance.FileSlot)); + } else { +#pragma warning disable CS0618 // Synchronous save / load IO is obsolete but some mods still override / use it. + if (CoreModule.Settings.SaveDataFlush ?? false) + module.ForceSaveDataFlush++; + module.SaveSession(SaveData.Instance.FileSlot); +#pragma warning restore CS0618 + } + } } - } - Unregister(module); - LoadModAssembly(module.Metadata, asm); + Unregister(module); + } + + Assembly oldAsm = modules[0].GetType().Assembly; + MainThreadHelper.Do(() => _DetourModManager.Unload(oldAsm)); + _RelinkedAssemblies.Remove(oldAsm); + + LoadModAssembly(modules[0].Metadata, newAsm); }); AssetReloadHelper.ReloadLevel(); }); diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.cs b/Celeste.Mod.mm/Mod/Everest/Everest.cs index 7548005a0..1b68a62b6 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.cs @@ -472,7 +472,11 @@ internal static void Boot() { STAThreadHelper.Instance = new STAThreadHelper(Celeste.Instance); // Register our core module and load any other modules. - new CoreModule().Register(); + CoreModule core = new CoreModule(); + core.Register(); + Assembly asm = typeof(CoreModule).Assembly; + Type[] types = asm.GetTypesSafe(); + Loader.ProcessAssembly(core.Metadata, asm, types); // Note: Everest fulfills some mod dependencies by itself. new NullModule(new EverestModuleMetadata() { @@ -585,203 +589,6 @@ public static void Register(this EverestModule module) { _Modules.Add(module); } - LuaLoader.Precache(module.GetType().Assembly); - - bool newStrawberriesRegistered = false; - - foreach (Type type in module.GetType().Assembly.GetTypesSafe()) { - // Search for all entities marked with the CustomEntityAttribute. - foreach (CustomEntityAttribute attrib in type.GetCustomAttributes()) { - foreach (string idFull in attrib.IDs) { - string id; - string genName; - string[] split = idFull.Split('='); - - if (split.Length == 1) { - id = split[0]; - genName = "Load"; - - } else if (split.Length == 2) { - id = split[0]; - genName = split[1]; - - } else { - Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom entity ID elements: {idFull} ({type.FullName})"); - continue; - } - - id = id.Trim(); - genName = genName.Trim(); - - patch_Level.EntityLoader loader = null; - - ConstructorInfo ctor; - MethodInfo gen; - - gen = type.GetMethod(genName, new Type[] { typeof(Level), typeof(LevelData), typeof(Vector2), typeof(EntityData) }); - if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Entity))) { - loader = (level, levelData, offset, entityData) => (Entity) gen.Invoke(null, new object[] { level, levelData, offset, entityData }); - goto RegisterEntityLoader; - } - - ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2), typeof(EntityID) }); - if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset, new EntityID(levelData.Name, entityData.ID) }); - goto RegisterEntityLoader; - } - - ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2) }); - if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset }); - goto RegisterEntityLoader; - } - - ctor = type.GetConstructor(new Type[] { typeof(Vector2) }); - if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { offset }); - goto RegisterEntityLoader; - } - - ctor = type.GetConstructor(_EmptyTypeArray); - if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(_EmptyObjectArray); - goto RegisterEntityLoader; - } - - RegisterEntityLoader: - if (loader == null) { - Logger.Log(LogLevel.Warn, "core", $"Found custom entity without suitable constructor / {genName}(Level, LevelData, Vector2, EntityData): {id} ({type.FullName})"); - continue; - } - patch_Level.EntityLoaders[id] = loader; - } - } - // Register with the StrawberryRegistry all entities marked with RegisterStrawberryAttribute. - foreach (RegisterStrawberryAttribute attrib in type.GetCustomAttributes()) { - List names = new List(); - foreach (CustomEntityAttribute nameAttrib in type.GetCustomAttributes()) - foreach (string idFull in nameAttrib.IDs) { - string[] split = idFull.Split('='); - if (split.Length == 0) { - Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom entity ID elements: {idFull} ({type.FullName})"); - continue; - } - names.Add(split[0]); - } - if (names.Count == 0) - goto NoDefinedBerryNames; // no customnames? skip out on registering berry - - foreach (string name in names) { - StrawberryRegistry.Register(type, name, attrib.isTracked, attrib.blocksNormalCollection); - newStrawberriesRegistered = true; - } - } - NoDefinedBerryNames: - ; - - // Search for all Entities marked with the CustomEventAttribute. - foreach (CustomEventAttribute attrib in type.GetCustomAttributes()) { - foreach (string idFull in attrib.IDs) { - string id; - string genName; - string[] split = idFull.Split('='); - - if (split.Length == 1) { - id = split[0]; - genName = "Load"; - - } else if (split.Length == 2) { - id = split[0]; - genName = split[1]; - - } else { - Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom cutscene ID elements: {idFull} ({type.FullName})"); - continue; - } - - id = id.Trim(); - genName = genName.Trim(); - - patch_EventTrigger.CutsceneLoader loader = null; - - ConstructorInfo ctor; - MethodInfo gen; - - gen = type.GetMethod(genName, new Type[] { typeof(EventTrigger), typeof(Player), typeof(string) }); - if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Entity))) { - loader = (trigger, player, eventID) => (Entity) gen.Invoke(null, new object[] { trigger, player, eventID }); - goto RegisterCutsceneLoader; - } - - ctor = type.GetConstructor(new Type[] { typeof(EventTrigger), typeof(Player), typeof(string) }); - if (ctor != null) { - loader = (trigger, player, eventID) => (Entity) ctor.Invoke(new object[] { trigger, player, eventID }); - goto RegisterCutsceneLoader; - } - - ctor = type.GetConstructor(_EmptyTypeArray); - if (ctor != null) { - loader = (trigger, player, eventID) => (Entity) ctor.Invoke(_EmptyObjectArray); - goto RegisterCutsceneLoader; - } - - RegisterCutsceneLoader: - if (loader == null) { - Logger.Log(LogLevel.Warn, "core", $"Found custom cutscene without suitable constructor / {genName}(EventTrigger, Player, string): {id} ({type.FullName})"); - continue; - } - patch_EventTrigger.CutsceneLoaders[id] = loader; - } - } - - // Search for all Backdrops marked with the CustomBackdropAttribute. - foreach (CustomBackdropAttribute attrib in type.GetCustomAttributes()) { - foreach (string idFull in attrib.IDs) { - string id; - string genName; - string[] split = idFull.Split('='); - - if (split.Length == 1) { - id = split[0]; - genName = "Load"; - } else if (split.Length == 2) { - id = split[0]; - genName = split[1]; - } else { - Logger.Log(LogLevel.Warn, "core", $"Invalid number of custom backdrop ID elements: {idFull} ({type.FullName})"); - continue; - } - - id = id.Trim(); - genName = genName.Trim(); - - patch_MapData.BackdropLoader loader = null; - - ConstructorInfo ctor; - MethodInfo gen; - - gen = type.GetMethod(genName, new Type[] { typeof(BinaryPacker.Element) }); - if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Backdrop))) { - loader = data => (Backdrop) gen.Invoke(null, new object[] { data }); - goto RegisterBackdropLoader; - } - - ctor = type.GetConstructor(new Type[] { typeof(BinaryPacker.Element) }); - if (ctor != null) { - loader = data => (Backdrop) ctor.Invoke(new object[] { data }); - goto RegisterBackdropLoader; - } - - RegisterBackdropLoader: - if (loader == null) { - Logger.Log(LogLevel.Warn, "core", $"Found custom backdrop without suitable constructor / {genName}(BinaryPacker.Element): {id} ({type.FullName})"); - continue; - } - patch_MapData.BackdropLoaders[id] = loader; - } - } - } - module.LoadSettings(); module.Load(); if (_ContentLoaded) { @@ -818,9 +625,8 @@ public static void Register(this EverestModule module) { } // Check if the module defines a PrepareMapDataProcessors method. If this is the case, we want to reload maps so that they are applied. - // We should also run the map data processors again if new berry types are registered, so that CoreMapDataProcessor assigns them checkpoint IDs and orders. - if (newStrawberriesRegistered || module.GetType().GetMethod("PrepareMapDataProcessors", new Type[] { typeof(MapDataFixup) })?.DeclaringType == module.GetType()) { - Logger.Log(LogLevel.Verbose, "core", $"Module {module.Metadata} has custom strawberries or map data processors: reloading maps."); + if (module.GetType().GetMethod("PrepareMapDataProcessors", new Type[] { typeof(MapDataFixup) })?.DeclaringType == module.GetType()) { + Logger.Log(LogLevel.Verbose, "core", $"Module {module.Metadata} has map data processors: reloading maps."); AssetReloadHelper.ReloadAllMaps(); } } @@ -850,7 +656,7 @@ public static void Register(this EverestModule module) { patch_Audio.IngestNewBanks(); } - Logger.Log(LogLevel.Info, "core", $"Module {module.Metadata} registered."); + module.LogRegistration(); Events.Everest.RegisterModule(module); CheckDependenciesOfDelayedMods(); @@ -946,10 +752,6 @@ internal static void Unregister(this EverestModule module) { module.OnInputDeregister(); module.Unload(); - Assembly asm = module.GetType().Assembly; - MainThreadHelper.Do(() => _DetourModManager.Unload(asm)); - _RelinkedAssemblies.Remove(asm); - // TODO: Unload from LuaLoader // TODO: Unload from EntityLoaders // TODO: Undo event listeners @@ -968,7 +770,8 @@ internal static void Unregister(this EverestModule module) { InvalidateInstallationHash(); - Logger.Log(LogLevel.Info, "core", $"Module {module.Metadata} unregistered."); + module.LogUnregistration(); + } /// diff --git a/Celeste.Mod.mm/Mod/Module/EverestModule.cs b/Celeste.Mod.mm/Mod/Module/EverestModule.cs index fcb02467f..ab2c05a32 100644 --- a/Celeste.Mod.mm/Mod/Module/EverestModule.cs +++ b/Celeste.Mod.mm/Mod/Module/EverestModule.cs @@ -892,5 +892,13 @@ TextMenu.Item CreateItem(PropertyInfo prop, string name = null, object settingsO public virtual void PrepareMapDataProcessors(MapDataFixup context) { } + public virtual void LogRegistration() { + Logger.Log(LogLevel.Info, "core", $"Registered code module {GetType().FullName} for module {Metadata}."); + } + + public virtual void LogUnregistration() { + Logger.Log(LogLevel.Info, "core", $"Unregistered code module {GetType().FullName} for module {Metadata}."); + } + } } diff --git a/Celeste.Mod.mm/Mod/Module/LuaModule.cs b/Celeste.Mod.mm/Mod/Module/LuaModule.cs index 33f743e3c..c602c06cb 100644 --- a/Celeste.Mod.mm/Mod/Module/LuaModule.cs +++ b/Celeste.Mod.mm/Mod/Module/LuaModule.cs @@ -22,6 +22,14 @@ public override void Unload() { public override void CreateModMenuSection(patch_TextMenu menu, bool inGame, EventInstance snapshot) { } + + public override void LogRegistration() { + Logger.Log(LogLevel.Info, "core", $"Lua module {Metadata} registered."); + } + + public override void LogUnregistration() { + Logger.Log(LogLevel.Info, "core", $"Lua module {Metadata} unregistered."); + } } } diff --git a/Celeste.Mod.mm/Mod/Module/NullModule.cs b/Celeste.Mod.mm/Mod/Module/NullModule.cs index 48529f881..5fcbed12f 100644 --- a/Celeste.Mod.mm/Mod/Module/NullModule.cs +++ b/Celeste.Mod.mm/Mod/Module/NullModule.cs @@ -20,5 +20,13 @@ public override void Unload() { public override void CreateModMenuSection(patch_TextMenu menu, bool inGame, EventInstance snapshot) { } + public override void LogRegistration() { + Logger.Log(LogLevel.Info, "core", $"Non-code module {Metadata} registered."); + } + + public override void LogUnregistration() { + Logger.Log(LogLevel.Info, "core", $"Non-code module {Metadata} unregistered."); + } + } }