/// <summary> /// Register a new EverestModule (mod) dynamically. Invokes LoadSettings and Load. /// </summary> /// <param name="module">Mod to register.</param> public static void Register(this EverestModule module) { lock (_Modules) { _Modules.Add(module); _ModuleTypes.Add(module.GetType()); } LuaLoader.Precache(module.GetType().Assembly); foreach (Type type in module.GetType().Assembly.GetTypes()) { // Search for all entities marked with the CustomEntityAttribute. foreach (CustomEntityAttribute attrib in type.GetCustomAttributes <CustomEntityAttribute>()) { 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 <RegisterStrawberryAttribute>()) { List <string> names = new List <string>(); foreach (CustomEntityAttribute nameAttrib in type.GetCustomAttributes <CustomEntityAttribute>()) { 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); } } NoDefinedBerryNames :; } module.LoadSettings(); module.Load(); if (_Initialized) { module.Initialize(); } InvalidateInstallationHash(); EverestModuleMetadata meta = module.Metadata; meta.Hash = GetChecksum(meta); Logger.Log(LogLevel.Info, "core", $"Module {module.Metadata} registered."); // Attempt to load mods after their dependencies have been loaded. // Only load and lock the delayed list if we're not already loading delayed mods. if (Interlocked.CompareExchange(ref Loader.DelayedLock, 1, 0) == 0) { lock (Loader.Delayed) { for (int i = Loader.Delayed.Count - 1; i > -1; i--) { Tuple <EverestModuleMetadata, Action> entry = Loader.Delayed[i]; if (!Loader.DependenciesLoaded(entry.Item1)) { continue; } Loader.LoadMod(entry.Item1); Loader.Delayed.RemoveAt(i); entry.Item2?.Invoke(); } } Interlocked.Decrement(ref Loader.DelayedLock); } }
public static void InvokeTyped(string methodName, Type[] argsTypes, params object[] args) { if (args == null) { args = _EmptyObjectArray; if (argsTypes == null) { argsTypes = _EmptyTypeArray; } } else if (argsTypes == null) { argsTypes = Type.GetTypeArray(args); } if (!Debugger.IsAttached) { // Fast codepath: DynamicMethodDelegate // Unfortunately prevents us from stepping into invoked methods. for (int i = 0; i < _Modules.Count; i++) { EverestModule module = _Modules[i]; IDictionary <string, DynamicMethodDelegate> moduleMethods = _ModuleMethodDelegates[i]; DynamicMethodDelegate method; if (moduleMethods.TryGetValue(methodName, out method)) { if (method == null) { continue; } method(module, args); continue; } MethodInfo methodInfo = _ModuleTypes[i].GetMethod(methodName, argsTypes); if (methodInfo != null) { method = methodInfo.GetDelegate(); } moduleMethods[methodName] = method; if (method == null) { continue; } method(module, args); } } else { // Slow codepath: MethodInfo.Invoke // Doesn't hinder us from stepping into the invoked methods. for (int i = 0; i < _Modules.Count; i++) { EverestModule module = _Modules[i]; IDictionary <string, MethodInfo> moduleMethods = _ModuleMethods[i]; MethodInfo methodInfo; if (moduleMethods.TryGetValue(methodName, out methodInfo)) { if (methodInfo == null) { continue; } methodInfo.Invoke(module, args); continue; } methodInfo = _ModuleTypes[i].GetMethod(methodName, argsTypes); moduleMethods[methodName] = methodInfo; if (methodInfo == null) { continue; } methodInfo.Invoke(module, args); } } }
public ModuleSettingsKeyboardConfigUI(EverestModule module) { Module = module; // Base already reloads too early before the module has been set. Reload(); }
/// <summary> /// Register a new EverestModule (mod) dynamically. Invokes LoadSettings and Load. /// </summary> /// <param name="module">Mod to register.</param> public static void Register(this EverestModule module) { lock (_Modules) { _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 <CustomEntityAttribute>()) { 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 <RegisterStrawberryAttribute>()) { List <string> names = new List <string>(); foreach (CustomEntityAttribute nameAttrib in type.GetCustomAttributes <CustomEntityAttribute>()) { 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 <CustomEventAttribute>()) { 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; } } } module.LoadSettings(); module.Load(); if (_ContentLoaded) { module.LoadContent(true); } if (_Initialized) { Tracker.Initialize(); module.Initialize(); Input.Initialize(); if (SaveData.Instance != null) { // we are in a save. we are expecting the save data to already be loaded at this point Logger.Log("core", $"Loading save data slot {SaveData.Instance.FileSlot} for {module.Metadata}"); module.LoadSaveData(SaveData.Instance.FileSlot); if (SaveData.Instance.CurrentSession?.InArea ?? false) { // we are in a level. we are expecting the session to already be loaded at this point Logger.Log("core", $"Loading session slot {SaveData.Instance.FileSlot} for {module.Metadata}"); module.LoadSession(SaveData.Instance.FileSlot, false); } } // 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("core", $"Module {module.Metadata} has custom strawberries or map data processors: reloading maps."); AssetReloadHelper.ReloadAllMaps(); } } if (Engine.Instance != null && Engine.Scene is Overworld overworld) { // we already are in the overworld. Register new Ouis real quick! Type[] types = FakeAssembly.GetFakeEntryAssembly().GetTypesSafe(); foreach (Type type in types) { if (typeof(Oui).IsAssignableFrom(type) && !type.IsAbstract && !overworld.UIs.Any(ui => ui.GetType() == type)) { Logger.Log("core", $"Instanciating UI from {module.Metadata}: {type.FullName}"); Oui oui = (Oui)Activator.CreateInstance(type); oui.Visible = false; overworld.Add(oui); overworld.UIs.Add(oui); } } } InvalidateInstallationHash(); EverestModuleMetadata meta = module.Metadata; meta.Hash = GetChecksum(meta); // Audio banks are cached, and as such use the module's hash. We can only ingest those now. if (patch_Audio.AudioInitialized) { patch_Audio.IngestNewBanks(); } Logger.Log(LogLevel.Info, "core", $"Module {module.Metadata} registered."); // Attempt to load mods after their dependencies have been loaded. // Only load and lock the delayed list if we're not already loading delayed mods. if (Interlocked.CompareExchange(ref Loader.DelayedLock, 1, 0) == 0) { try { lock (Loader.Delayed) { for (int i = 0; i < Loader.Delayed.Count; i++) { Tuple <EverestModuleMetadata, Action> entry = Loader.Delayed[i]; if (!Loader.DependenciesLoaded(entry.Item1)) { continue; // dependencies are still missing! } Logger.Log(LogLevel.Info, "core", $"Dependencies of mod {entry.Item1} are now satisfied: loading"); if (Everest.Modules.Any(mod => mod.Metadata.Name == entry.Item1.Name)) { // a duplicate of the mod was loaded while it was sitting in the delayed list. Logger.Log(LogLevel.Warn, "core", $"Mod {entry.Item1.Name} already loaded!"); } else { entry.Item2?.Invoke(); Loader.LoadMod(entry.Item1); } Loader.Delayed.RemoveAt(i); // we now loaded an extra mod, consider all delayed mods again to deal with transitive dependencies. i = -1; } } } finally { Interlocked.Decrement(ref Loader.DelayedLock); } } }
internal static void ReloadModAssembly(object source, FileSystemEventArgs e, bool retrying = false) { if (!File.Exists(e.FullPath)) { return; } 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; using (FileStream stream = File.OpenRead(e.FullPath)) asm = Relinker.GetRelinkedAssembly(module.Metadata, Path.GetFileNameWithoutExtension(e.FullPath), stream); if (asm == null) { if (!retrying) { // Retry. QueuedTaskHelper.Do("ReloadModAssembly:" + e.FullPath, () => { ReloadModAssembly(source, e, true); }); } return; } ((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("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("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); }); AssetReloadHelper.ReloadLevel(); }); }
/// <summary> /// Find and load all EverestModules in the given assembly. /// </summary> /// <param name="meta">The mod metadata, preferably from the mod metadata.yaml file.</param> /// <param name="asm">The mod assembly, preferably relinked.</param> public static void LoadModAssembly(EverestModuleMetadata meta, Assembly asm) { if (!Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (string.IsNullOrEmpty(meta.PathArchive) && File.Exists(meta.DLL) && meta.SupportsCodeReload && CoreModule.Settings.CodeReload) { try { FileSystemWatcher watcher = meta.DevWatcher = new FileSystemWatcher { Path = Path.GetDirectoryName(meta.DLL), NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite, }; watcher.Changed += (s, e) => { if (e.FullPath != meta.DLL) { return; } ReloadModAssembly(s, e); // FIXME: Should we dispose the old .dll watcher? }; watcher.EnableRaisingEvents = true; } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed watching folder: {Path.GetDirectoryName(meta.DLL)}"); e.LogDetailed(); meta.DevWatcher?.Dispose(); meta.DevWatcher = null; } } ApplyModHackfixes(meta, asm); Content.Crawl(new AssemblyModContent(asm) { Mod = meta, Name = meta.Name }); Type[] types; try { types = asm.GetTypesSafe(); } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed reading assembly: {e}"); e.LogDetailed(); return; } bool foundModule = false; for (int i = 0; i < types.Length; i++) { Type type = types[i]; if (typeof(EverestModule).IsAssignableFrom(type) && !type.IsAbstract) { foundModule = true; if (!typeof(NullModule).IsAssignableFrom(type)) { EverestModule mod = (EverestModule)type.GetConstructor(_EmptyTypeArray).Invoke(_EmptyObjectArray); 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!"); } }