/// <summary> /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. /// If required, loads the mod after all of its dependencies have been loaded. /// </summary> /// <param name="meta">Metadata of the mod to load.</param> /// <param name="callback">Callback to be executed after the mod has been loaded. Executed immediately if meta == null.</param> public static void LoadModDelayed(EverestModuleMetadata meta, Action callback) { if (Flags.IsDisabled || !Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (meta == null) { callback?.Invoke(); return; } if (DependencyLoaded(meta)) { Logger.Log(LogLevel.Warn, "loader", $"Mod {meta} already loaded!"); return; } if (PermanentBlacklist.TryGetValue(meta.Name, out Version minver) && meta.Version < minver) { Logger.Log(LogLevel.Warn, "loader", $"Mod {meta} permanently blacklisted by Everest!"); return; } foreach (EverestModuleMetadata dep in meta.Dependencies) { if (!DependencyLoaded(dep)) { Logger.Log(LogLevel.Info, "loader", $"Dependency {dep} of mod {meta} not loaded! Delaying."); lock (Delayed) { Delayed.Add(Tuple.Create(meta, callback)); } return; } } callback?.Invoke(); LoadMod(meta); }
/// <summary> /// Checks if an dependency is loaded. /// Can be used by mods manually to f.e. activate / disable functionality. /// </summary> /// <param name="dep">Dependency to check for. Name and Version will be checked.</param> /// <returns>True if the dependency has already been loaded by Everest, false otherwise.</returns> public static bool DependencyLoaded(EverestModuleMetadata dep) { string depName = dep.Name; Version depVersion = dep.Version; foreach (EverestModule other in _Modules) { EverestModuleMetadata meta = other.Metadata; if (meta.Name != depName) { continue; } Version version = meta.Version; return(VersionSatisfiesDependency(depVersion, version)); } return(false); }
/// <summary> /// Cache the file and return a cached path. /// </summary> /// <returns>The cached file path.</returns> public virtual string GetCachedPath() { EverestModuleMetadata mod = Source?.Mod; if (mod == null) { throw new NullReferenceException("Cannot cache mod-less assets"); } string path = Path.Combine(Everest.Loader.PathCache, mod.Name, PathVirtual.Replace('/', Path.DirectorySeparatorChar)); string pathSum = path + ".sum"; byte[] hash = mod.Hash; if (hash == null) { // the mod we are looking at is not loaded - this must mean another one in the multimeta is, find it and take its hash. hash = mod.Multimeta.First(meta => meta.Hash != null).Hash; } string sum = hash.ToHexadecimalString(); if (File.Exists(path)) { if (File.Exists(pathSum) && File.ReadAllText(pathSum) == sum) { return(path); } File.Delete(pathSum); File.Delete(path); } string dir = Path.GetDirectoryName(path); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } WriteCache(path); File.WriteAllText(pathSum, sum); return(path); }
private static Stream OpenStream(EverestModuleMetadata meta, out string result, params string[] names) { if (!string.IsNullOrEmpty(meta.PathArchive)) { using (ZipFile zip = new ZipFile(meta.PathArchive)) { foreach (ZipEntry entry in zip.Entries) { if (!names.Contains(entry.FileName)) { continue; } result = entry.FileName; return(entry.ExtractStream()); } } result = null; return(null); } if (!string.IsNullOrEmpty(meta.PathDirectory)) { foreach (string name in names) { string path = name; if (!File.Exists(path)) { path = Path.Combine(meta.PathDirectory, name); } if (!File.Exists(path)) { continue; } result = path; return(File.OpenRead(path)); } } result = null; return(null); }
private static MissingDependencyResolver GenerateModDependencyResolver(EverestModuleMetadata meta) { if (!string.IsNullOrEmpty(meta.PathArchive)) { return(delegate(MonoModder mod, ModuleDefinition main, string name, string fullName) { string asmName = name + ".dll"; using (Stream zipStream = File.OpenRead(meta.PathArchive)) using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Read)) { foreach (ZipArchiveEntry entry in zip.Entries) { if (entry.FullName != asmName) { continue; } using (Stream stream = entry.Open()) using (MemoryStream ms = new MemoryStream()) { stream.CopyTo(ms); ms.Seek(0, SeekOrigin.Begin); return ModuleDefinition.ReadModule(ms, mod.GenReaderParameters(false)); } } } return null; }); } if (!string.IsNullOrEmpty(meta.PathDirectory)) { return(delegate(MonoModder mod, ModuleDefinition main, string name, string fullName) { string asmPath = Path.Combine(meta.PathDirectory, name + ".dll"); if (!File.Exists(asmPath)) { return null; } return ModuleDefinition.ReadModule(asmPath, mod.GenReaderParameters(false, asmPath)); }); } return(null); }
private static ResolveEventHandler GenerateModAssemblyResolver(EverestModuleMetadata meta) { if (!string.IsNullOrEmpty(meta.PathArchive)) { return((sender, args) => { string asmName = new AssemblyName(args.Name).Name + ".dll"; using (Stream zipStream = File.OpenRead(meta.PathArchive)) using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Read)) { foreach (ZipArchiveEntry entry in zip.Entries) { if (entry.FullName != asmName) { continue; } using (Stream stream = entry.Open()) using (MemoryStream ms = new MemoryStream()) { stream.CopyTo(ms); ms.Seek(0, SeekOrigin.Begin); return Assembly.Load(ms.GetBuffer()); } } } return null; }); } if (!string.IsNullOrEmpty(meta.PathDirectory)) { return((sender, args) => { string asmPath = Path.Combine(meta.PathDirectory, new AssemblyName(args.Name).Name + ".dll"); if (!File.Exists(asmPath)) { return null; } return Assembly.LoadFrom(asmPath); }); } return(null); }
/// <summary> /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. /// If required, loads the mod after all of its dependencies have been loaded. /// </summary> /// <param name="meta">Metadata of the mod to load.</param> /// <param name="callback">Callback to be executed after the mod has been loaded. Executed immediately if meta == null.</param> public static void LoadModDelayed(EverestModuleMetadata meta, Action callback) { if (meta == null) { callback?.Invoke(); return; } foreach (EverestModuleMetadata dep in meta.Dependencies) { if (!DependencyLoaded(dep)) { Logger.Log(LogLevel.Info, "loader", $"Dependency {dep} of mod {meta} not loaded! Delaying."); Delayed.Add(Tuple.Create(meta, callback)); return; } } LoadMod(meta); callback?.Invoke(); }
/// <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.IsDisabled || !Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } Content.Crawl(new AssemblyModContent(asm) { Mod = meta, Name = meta.Name }); Type[] types; try { types = asm.GetTypes(); } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed reading assembly: {e}"); e.LogDetailed(); return; } for (int i = 0; i < types.Length; i++) { Type type = types[i]; if (!typeof(EverestModule).IsAssignableFrom(type) || type.IsAbstract) { continue; } if (typeof(NullModule).IsAssignableFrom(type)) { continue; } EverestModule mod = (EverestModule)type.GetConstructor(_EmptyTypeArray).Invoke(_EmptyObjectArray); mod.Metadata = meta; mod.Register(); } }
/// <summary> /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. /// If required, loads the mod after all of its dependencies have been loaded. /// </summary> /// <param name="meta">Metadata of the mod to load.</param> /// <param name="callback">Callback to be executed after the mod has been loaded. Executed immediately if meta == null.</param> public static void LoadModDelayed(EverestModuleMetadata meta, Action callback) { if (Flags.IsDisabled || !Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (meta == null) { callback?.Invoke(); return; } if (DependencyLoaded(meta)) { Logger.Log(LogLevel.Warn, "loader", $"Mod {meta} already loaded!"); return; } foreach (EverestModuleMetadata dep in meta.Dependencies) { if (!DependencyLoaded(dep)) { Logger.Log(LogLevel.Info, "loader", $"Dependency {dep} of mod {meta} not loaded! Delaying."); lock (Delayed) { Delayed.Add(Tuple.Create(meta, callback)); } return; } } LoadMod(meta); callback?.Invoke(); }
private static MissingDependencyResolver GenerateModDependencyResolver(EverestModuleMetadata meta) { if (!string.IsNullOrEmpty(meta.PathArchive)) { return(delegate(MonoModder mod, ModuleDefinition main, string name, string fullName) { string asmName = name + ".dll"; using (ZipFile zip = new ZipFile(meta.PathArchive)) { foreach (ZipEntry entry in zip.Entries) { if (entry.FileName != asmName) { continue; } using (MemoryStream stream = entry.ExtractStream()) { return ModuleDefinition.ReadModule(stream, mod.GenReaderParameters(false)); } } } return null; }); } if (!string.IsNullOrEmpty(meta.PathDirectory)) { return(delegate(MonoModder mod, ModuleDefinition main, string name, string fullName) { string asmPath = Path.Combine(meta.PathDirectory, name + ".dll"); if (!File.Exists(asmPath)) { return null; } return ModuleDefinition.ReadModule(asmPath, mod.GenReaderParameters(false, asmPath)); }); } return(null); }
private static ResolveEventHandler GenerateModAssemblyResolver(EverestModuleMetadata meta) { if (!string.IsNullOrEmpty(meta.PathArchive)) { return((sender, args) => { string asmName = new AssemblyName(args.Name).Name + ".dll"; using (ZipFile zip = new ZipFile(meta.PathArchive)) { foreach (ZipEntry entry in zip.Entries) { if (entry.FileName != asmName) { continue; } using (MemoryStream stream = entry.ExtractStream()) { return Assembly.Load(stream.GetBuffer()); } } } return null; }); } if (!string.IsNullOrEmpty(meta.PathDirectory)) { return((sender, args) => { string asmPath = Path.Combine(meta.PathDirectory, new AssemblyName(args.Name).Name + ".dll"); if (!File.Exists(asmPath)) { return null; } return Assembly.LoadFrom(asmPath); }); } return(null); }
/// <summary> /// Cache the file and return a cached path. /// </summary> /// <returns>The cached file path.</returns> public virtual string GetCachedPath() { EverestModuleMetadata mod = Source?.Mod; if (mod == null) { throw new NullReferenceException("Cannot cache mod-less assets"); } string path = Path.Combine(Everest.Loader.PathCache, mod.Name, PathVirtual.Replace('/', Path.DirectorySeparatorChar)); string pathSum = path + ".sum"; string sum = mod.Hash.ToHexadecimalString(); if (File.Exists(path)) { if (File.Exists(pathSum) && File.ReadAllText(pathSum) == sum) { return(path); } File.Delete(pathSum); File.Delete(path); } string dir = Path.GetDirectoryName(path); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } WriteCache(path); File.WriteAllText(pathSum, sum); return(path); }
/// <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; } for (int i = 0; i < types.Length; i++) { Type type = types[i]; if (typeof(EverestModule).IsAssignableFrom(type) && !type.IsAbstract && !typeof(NullModule).IsAssignableFrom(type)) { EverestModule mod = (EverestModule)type.GetConstructor(_EmptyTypeArray).Invoke(_EmptyObjectArray); mod.Metadata = meta; mod.Register(); } } }
/// <summary> /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. /// </summary> /// <param name="meta">Metadata of the mod to load.</param> public static void LoadMod(EverestModuleMetadata meta) { if (!Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (meta == null) { return; } // Add an AssemblyResolve handler for all bundled libraries. AppDomain.CurrentDomain.AssemblyResolve += GenerateModAssemblyResolver(meta); // Load the actual assembly. Assembly asm = null; if (!string.IsNullOrEmpty(meta.PathArchive)) { bool returnEarly = false; using (ZipFile zip = new ZipFile(meta.PathArchive)) { foreach (ZipEntry entry in zip.Entries) { string entryName = entry.FileName.Replace('\\', '/'); if (entryName == meta.DLL) { using (MemoryStream stream = entry.ExtractStream()) asm = Relinker.GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(meta.DLL), stream); } if (entryName == "main.lua") { new LuaModule(meta).Register(); returnEarly = true; } } } if (returnEarly) { return; } } else { if (!string.IsNullOrEmpty(meta.DLL) && File.Exists(meta.DLL)) { using (FileStream stream = File.OpenRead(meta.DLL)) asm = Relinker.GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(meta.DLL), stream); } if (File.Exists(Path.Combine(meta.PathDirectory, "main.lua"))) { new LuaModule(meta).Register(); return; } } if (asm == null) { // Register a null module for content mods. new NullModule(meta).Register(); return; } LoadModAssembly(meta, asm); }
/// <summary> /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. /// If required, loads the mod after all of its dependencies have been loaded. /// </summary> /// <param name="meta">Metadata of the mod to load.</param> /// <param name="callback">Callback to be executed after the mod has been loaded. Executed immediately if meta == null.</param> public static void LoadModDelayed(EverestModuleMetadata meta, Action callback) { if (!Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (meta == null) { callback?.Invoke(); return; } if (Modules.Any(module => module.Metadata.Name == meta.Name)) { Logger.Log(LogLevel.Warn, "loader", $"Mod {meta.Name} already loaded!"); return; } if (PermanentBlacklist.TryGetValue(meta.Name, out Version minver) && meta.Version < minver) { Logger.Log(LogLevel.Warn, "loader", $"Mod {meta} permanently blacklisted by Everest!"); return; } Tuple <string, Version, string, Version> conflictRow = PermanentConflictlist.FirstOrDefault(row => (meta.Name == row.Item1 && meta.Version < row.Item2 && (_Modules.FirstOrDefault(other => other.Metadata.Name == row.Item3)?.Metadata.Version ?? _VersionInvalid) < row.Item4) || (meta.Name == row.Item3 && meta.Version < row.Item4 && (_Modules.FirstOrDefault(other => other.Metadata.Name == row.Item1)?.Metadata.Version ?? _VersionInvalid) < row.Item2) ); if (conflictRow != null) { throw new Exception($"CONFLICTING MODS: {conflictRow.Item1} VS {conflictRow.Item3}"); } foreach (EverestModuleMetadata dep in meta.Dependencies) { if (!DependencyLoaded(dep)) { Logger.Log(LogLevel.Info, "loader", $"Dependency {dep} of mod {meta} not loaded! Delaying."); lock (Delayed) { Delayed.Add(Tuple.Create(meta, callback)); } return; } } foreach (EverestModuleMetadata dep in meta.OptionalDependencies) { if (!DependencyLoaded(dep) && (enforceOptionalDependencies || Everest.Modules.Any(module => module.Metadata?.Name == dep.Name))) { Logger.Log(LogLevel.Info, "loader", $"Optional dependency {dep} of mod {meta} not loaded! Delaying."); lock (Delayed) { Delayed.Add(Tuple.Create(meta, callback)); } return; } } callback?.Invoke(); LoadMod(meta); }
/// <summary> /// Load a mod from a directory at runtime. /// </summary> /// <param name="dir">The path to the mod directory.</param> public static void LoadDir(string dir) { if (!Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (!Directory.Exists(dir)) // Relative path? { dir = Path.Combine(PathMods, dir); } if (!Directory.Exists(dir)) // It just doesn't exist. { return; } Logger.Log(LogLevel.Verbose, "loader", $"Loading mod directory: {dir}"); EverestModuleMetadata meta = null; EverestModuleMetadata[] multimetas = null; string metaPath = Path.Combine(dir, "metadata.yaml"); if (File.Exists(metaPath)) { using (StreamReader reader = new StreamReader(metaPath)) { try { meta = YamlHelper.Deserializer.Deserialize <EverestModuleMetadata>(reader); meta.PathDirectory = dir; meta.PostParse(); } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed parsing metadata.yaml in {dir}: {e}"); FilesWithMetadataLoadFailures.Add(dir); } } } metaPath = Path.Combine(dir, "multimetadata.yaml"); if (!File.Exists(metaPath)) { metaPath = Path.Combine(dir, "everest.yaml"); } if (!File.Exists(metaPath)) { metaPath = Path.Combine(dir, "everest.yml"); } if (File.Exists(metaPath)) { using (StreamReader reader = new StreamReader(metaPath)) { try { if (!reader.EndOfStream) { multimetas = YamlHelper.Deserializer.Deserialize <EverestModuleMetadata[]>(reader); foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.PathDirectory = dir; multimeta.PostParse(); } } } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed parsing everest.yaml in {dir}: {e}"); FilesWithMetadataLoadFailures.Add(dir); } } } FileSystemModContent contentMeta = new FileSystemModContent(dir); EverestModuleMetadata contentMetaParent = null; Action contentCrawl = () => { if (contentMeta == null) { return; } if (contentMetaParent != null) { contentMeta.Mod = contentMetaParent; contentMeta.Name = contentMetaParent.Name; } OnCrawlMod?.Invoke(dir, contentMetaParent); Content.Crawl(contentMeta); contentMeta = null; }; if (multimetas != null) { foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.Multimeta = multimetas; if (contentMetaParent == null) { contentMetaParent = multimeta; } LoadModDelayed(multimeta, contentCrawl); } } else { if (meta == null) { meta = new EverestModuleMetadata() { Name = "_dir_" + Path.GetFileName(dir), VersionString = "0.0.0-dummy", PathDirectory = dir }; meta.PostParse(); } contentMetaParent = meta; LoadModDelayed(meta, contentCrawl); } }
/// <summary> /// Load a mod from a .zip archive at runtime. /// </summary> /// <param name="archive">The path to the mod .zip archive.</param> public static void LoadZip(string archive) { if (!Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (!File.Exists(archive)) // Relative path? Let's just make it absolute. { archive = Path.Combine(PathMods, archive); } if (!File.Exists(archive)) // It just doesn't exist. { return; } Logger.Log(LogLevel.Verbose, "loader", $"Loading mod .zip: {archive}"); EverestModuleMetadata meta = null; EverestModuleMetadata[] multimetas = null; using (ZipFile zip = new ZipFile(archive)) { foreach (ZipEntry entry in zip.Entries) { if (entry.FileName == "metadata.yaml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) { try { meta = YamlHelper.Deserializer.Deserialize <EverestModuleMetadata>(reader); meta.PathArchive = archive; meta.PostParse(); } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed parsing metadata.yaml in {archive}: {e}"); FilesWithMetadataLoadFailures.Add(archive); } } continue; } if (entry.FileName == "multimetadata.yaml" || entry.FileName == "everest.yaml" || entry.FileName == "everest.yml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) { try { if (!reader.EndOfStream) { multimetas = YamlHelper.Deserializer.Deserialize <EverestModuleMetadata[]>(reader); foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.PathArchive = archive; multimeta.PostParse(); } } } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed parsing everest.yaml in {archive}: {e}"); FilesWithMetadataLoadFailures.Add(archive); } } continue; } } } ZipModContent contentMeta = new ZipModContent(archive); EverestModuleMetadata contentMetaParent = null; Action contentCrawl = () => { if (contentMeta == null) { return; } if (contentMetaParent != null) { contentMeta.Mod = contentMetaParent; contentMeta.Name = contentMetaParent.Name; } OnCrawlMod?.Invoke(archive, contentMetaParent); Content.Crawl(contentMeta); contentMeta = null; }; if (multimetas != null) { foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.Multimeta = multimetas; if (contentMetaParent == null) { contentMetaParent = multimeta; } LoadModDelayed(multimeta, contentCrawl); } } else { if (meta == null) { meta = new EverestModuleMetadata() { Name = "_zip_" + Path.GetFileNameWithoutExtension(archive), VersionString = "0.0.0-dummy", PathArchive = archive }; meta.PostParse(); } contentMetaParent = meta; LoadModDelayed(meta, contentCrawl); } }
/// <summary> /// Load a mod from a .zip archive at runtime. /// </summary> /// <param name="archive">The path to the mod .zip archive.</param> public static void LoadZip(string archive) { if (Flags.IsDisabled || !Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (!File.Exists(archive)) // Relative path? Let's just make it absolute. archive = Path.Combine(PathMods, archive); if (!File.Exists(archive)) // It just doesn't exist. return; Logger.Log(LogLevel.Verbose, "loader", $"Loading mod .zip: {archive}"); EverestModuleMetadata meta = null; EverestModuleMetadata[] multimetas = null; // In case the icon appears before the metadata in the .zip, store it temporarily, set it later. Texture2D icon = null; using (ZipFile zip = new ZipFile(archive)) { foreach (ZipEntry entry in zip.Entries) { if (entry.FileName == "metadata.yaml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) { try { meta = YamlHelper.Deserializer.Deserialize<EverestModuleMetadata>(reader); meta.PathArchive = archive; meta.PostParse(); } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed parsing metadata.yaml in {archive}: {e}"); } } continue; } if (entry.FileName == "multimetadata.yaml" || entry.FileName == "everest.yaml" || entry.FileName == "everest.yml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) { try { if (!reader.EndOfStream) { multimetas = YamlHelper.Deserializer.Deserialize<EverestModuleMetadata[]>(reader); foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.PathArchive = archive; multimeta.PostParse(); } } } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed parsing multimetadata.yaml in {archive}: {e}"); } } continue; } if (entry.FileName == "icon.png") { using (Stream stream = entry.ExtractStream()) icon = Texture2D.FromStream(Celeste.Instance.GraphicsDevice, stream); continue; } } } if (meta != null) { if (icon != null) meta.Icon = icon; } ZipModContent contentMeta = new ZipModContent(archive); EverestModuleMetadata contentMetaParent = null; Action contentCrawl = () => { if (contentMeta == null) return; if (contentMetaParent != null) { contentMeta.Mod = contentMetaParent; contentMeta.Name = contentMetaParent.Name; } Content.Crawl(contentMeta); contentMeta = null; }; if (multimetas != null) { foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.Multimeta = multimetas; if (contentMetaParent == null) contentMetaParent = multimeta; LoadModDelayed(multimeta, contentCrawl); } } else { if (meta == null) { meta = new EverestModuleMetadata() { Name = "_zip_" + Path.GetFileNameWithoutExtension(archive), VersionString = "0.0.0-dummy", PathArchive = archive }; meta.PostParse(); } contentMetaParent = meta; LoadModDelayed(meta, contentCrawl); } }
/// <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; } } } 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); } }
/// <summary> /// Get the cached path of a given mod's relinked .dll /// </summary> /// <param name="meta">The mod metadata.</param> /// <returns>The full path to the cached relinked .dll</returns> public static string GetCachedPath(EverestModuleMetadata meta) => Path.Combine(Loader.PathCache, meta.Name + "." + Path.GetFileNameWithoutExtension(meta.DLL) + ".dll");
internal static void LoadMod(EverestModuleMetadata meta) => OnLoadMod?.Invoke(meta);
public static void LoadDir(string dir) { if (!Directory.Exists(dir)) // Relative path? { dir = Path.Combine(PathMods, dir); } if (!Directory.Exists(dir)) // It just doesn't exist. { return; } Logger.Log("loader", $"Loading mod directory: {dir}"); EverestModuleMetadata meta = null; Assembly asm = null; // First read the metadata, ... string metaPath = Path.Combine(dir, "metadata.yaml"); if (File.Exists(metaPath)) { using (StreamReader reader = new StreamReader(metaPath)) meta = EverestModuleMetadata.Parse("", dir, reader); } if (meta != null) { // ... then check if the dependencies are loaded ... foreach (EverestModuleMetadata dep in meta.Dependencies) { if (!DependencyLoaded(dep)) { Logger.Log("loader", $"Dependency {dep} of mod {meta} not loaded!"); return; } } // ... then add an AssemblyResolve handler for all the .zip-ped libraries AppDomain.CurrentDomain.AssemblyResolve += GenerateModAssemblyResolver(meta); } // ... then handle the assembly and all assets. Content.Crawl(null, dir); if (meta == null || !File.Exists(meta.DLL)) { return; } if (meta.Prelinked) { asm = Assembly.LoadFrom(meta.DLL); } else { using (FileStream stream = File.OpenRead(meta.DLL)) asm = Relinker.GetRelinkedAssembly(meta, stream); } if (asm != null) { LoadMod(meta, asm); } }
public static void LoadZip(string archive) { if (!File.Exists(archive)) // Relative path? { archive = Path.Combine(PathMods, archive); } if (!File.Exists(archive)) // It just doesn't exist. { return; } Logger.Log("loader", $"Loading mod .zip: {archive}"); EverestModuleMetadata meta = null; Assembly asm = null; using (ZipFile zip = new ZipFile(archive)) { // In case the icon appears before the metadata in the .zip, store it temporarily. Texture2D icon = null; // First read the metadata, ... foreach (ZipEntry entry in zip.Entries) { if (entry.FileName == "metadata.yaml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) meta = EverestModuleMetadata.Parse(archive, "", reader); continue; } if (entry.FileName == "icon.png") { using (Stream stream = entry.ExtractStream()) icon = Texture2D.FromStream(Celeste.Instance.GraphicsDevice, stream); continue; } } if (meta != null) { if (icon != null) { meta.Icon = icon; } // ... then check if the dependencies are loaded ... // TODO: Enqueue the mod, reload it on Register of other mods, rechecking if deps loaded. foreach (EverestModuleMetadata dep in meta.Dependencies) { if (!DependencyLoaded(dep)) { Logger.Log("loader", $"Dependency {dep} of mod {meta} not loaded!"); return; } } // ... then add an AssemblyResolve handler for all the .zip-ped libraries AppDomain.CurrentDomain.AssemblyResolve += GenerateModAssemblyResolver(meta); } // ... then handle the assembly ... foreach (ZipEntry entry in zip.Entries) { string entryName = entry.FileName.Replace('\\', '/'); if (meta != null && entryName == meta.DLL) { using (MemoryStream stream = entry.ExtractStream()) { if (meta.Prelinked) { asm = Assembly.Load(stream.GetBuffer()); } else { asm = Relinker.GetRelinkedAssembly(meta, stream); } } } } // ... then tell the Content class to crawl through the zip. // (This also registers the zip for recrawls further down the line.) Content.Crawl(null, archive, zip); } if (meta != null && asm != null) { LoadMod(meta, asm); } }
/// <summary> /// Relink a .dll to point towards Celeste.exe and FNA / XNA properly at runtime, then load it. /// </summary> /// <param name="meta">The mod metadata, used for caching, among other things.</param> /// <param name="stream">The stream to read the .dll from.</param> /// <param name="depResolver">An optional dependency resolver.</param> /// <param name="checksumsExtra">Any optional checksums</param> /// <param name="prePatch">An optional step executed before patching, but after MonoMod has loaded the input assembly.</param> /// <returns>The loaded, relinked assembly.</returns> public static Assembly GetRelinkedAssembly(EverestModuleMetadata meta, string asmname, Stream stream, MissingDependencyResolver depResolver = null, string[] checksumsExtra = null, Action <MonoModder> prePatch = null) { if (!Flags.SupportRelinkingMods) { Logger.Log(LogLevel.Warn, "relinker", "Relinker disabled!"); return(null); } string cachedPath = GetCachedPath(meta, asmname); string cachedChecksumPath = cachedPath.Substring(0, cachedPath.Length - 4) + ".sum"; string[] checksums = new string[2 + (checksumsExtra?.Length ?? 0)]; if (GameChecksum == null) { GameChecksum = Everest.GetChecksum(Assembly.GetAssembly(typeof(Relinker)).Location).ToHexadecimalString(); } checksums[0] = GameChecksum; checksums[1] = Everest.GetChecksum(ref stream).ToHexadecimalString(); if (checksumsExtra != null) { for (int i = 0; i < checksumsExtra.Length; i++) { checksums[i + 2] = checksumsExtra[i]; } } if (File.Exists(cachedPath) && File.Exists(cachedChecksumPath) && ChecksumsEqual(checksums, File.ReadAllLines(cachedChecksumPath))) { Logger.Log(LogLevel.Verbose, "relinker", $"Loading cached assembly for {meta} - {asmname}"); try { Assembly asm = Assembly.LoadFrom(cachedPath); _RelinkedAssemblies.Add(asm); return(asm); } catch (Exception e) { Logger.Log(LogLevel.Warn, "relinker", $"Failed loading {meta} - {asmname}"); e.LogDetailed(); return(null); } } if (depResolver == null) { depResolver = GenerateModDependencyResolver(meta); } bool temporaryASM = false; try { MonoModder modder = Modder; modder.Input = stream; modder.OutputPath = cachedPath; modder.MissingDependencyResolver = depResolver; string symbolPath; modder.ReaderParameters.SymbolStream = OpenStream(meta, out symbolPath, meta.DLL.Substring(0, meta.DLL.Length - 4) + ".pdb", meta.DLL + ".mdb"); modder.ReaderParameters.ReadSymbols = modder.ReaderParameters.SymbolStream != null; if (modder.ReaderParameters.SymbolReaderProvider != null && modder.ReaderParameters.SymbolReaderProvider is RelinkerSymbolReaderProvider) { ((RelinkerSymbolReaderProvider)modder.ReaderParameters.SymbolReaderProvider).Format = string.IsNullOrEmpty(symbolPath) ? DebugSymbolFormat.Auto : symbolPath.EndsWith(".mdb") ? DebugSymbolFormat.MDB : symbolPath.EndsWith(".pdb") ? DebugSymbolFormat.PDB : DebugSymbolFormat.Auto; } try { modder.ReaderParameters.ReadSymbols = true; modder.Read(); } catch { modder.ReaderParameters.SymbolStream?.Dispose(); modder.ReaderParameters.SymbolStream = null; modder.ReaderParameters.ReadSymbols = false; stream.Seek(0, SeekOrigin.Begin); modder.Read(); } if (modder.ReaderParameters.SymbolReaderProvider != null && modder.ReaderParameters.SymbolReaderProvider is RelinkerSymbolReaderProvider) { ((RelinkerSymbolReaderProvider)modder.ReaderParameters.SymbolReaderProvider).Format = DebugSymbolFormat.Auto; } modder.MapDependencies(); if (!RuntimeRulesParsed) { RuntimeRulesParsed = true; InitMMSharedData(); string rulesPath = Path.Combine( Path.GetDirectoryName(typeof(Celeste).Assembly.Location), Path.GetFileNameWithoutExtension(typeof(Celeste).Assembly.Location) + ".Mod.mm.dll" ); if (!File.Exists(rulesPath)) { // Fallback if someone renamed Celeste.exe rulesPath = Path.Combine( Path.GetDirectoryName(typeof(Celeste).Assembly.Location), "Celeste.Mod.mm.dll" ); } if (File.Exists(rulesPath)) { ModuleDefinition rules = ModuleDefinition.ReadModule(rulesPath, new ReaderParameters(ReadingMode.Immediate)); modder.ParseRules(rules); rules.Dispose(); // Is this safe? } } prePatch?.Invoke(modder); modder.ParseRules(modder.Module); modder.AutoPatch(); RetryWrite: try { modder.WriterParameters.WriteSymbols = true; modder.Write(); } catch { try { modder.WriterParameters.WriteSymbols = false; modder.Write(); } catch when(!temporaryASM) { temporaryASM = true; long stamp = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; cachedPath = Path.Combine(Path.GetTempPath(), $"Everest.Relinked.{Path.GetFileNameWithoutExtension(cachedPath)}.{stamp}.dll"); modder.Module.Name += "." + stamp; modder.Module.Assembly.Name.Name += "." + stamp; modder.OutputPath = cachedPath; modder.WriterParameters.WriteSymbols = true; goto RetryWrite; } } } catch (Exception e) { Logger.Log(LogLevel.Warn, "relinker", $"Failed relinking {meta} - {asmname}"); e.LogDetailed(); return(null); } finally { Modder.ReaderParameters.SymbolStream?.Dispose(); if (SharedModder) { Modder.ClearCaches(moduleSpecific: true); Modder.Module.Dispose(); Modder.Module = null; } else { Modder.Dispose(); Modder = null; } } if (File.Exists(cachedChecksumPath)) { File.Delete(cachedChecksumPath); } if (!temporaryASM) { File.WriteAllLines(cachedChecksumPath, checksums); } Logger.Log(LogLevel.Verbose, "relinker", $"Loading assembly for {meta} - {asmname}"); try { Assembly asm = Assembly.LoadFrom(cachedPath); _RelinkedAssemblies.Add(asm); return(asm); } catch (Exception e) { Logger.Log(LogLevel.Warn, "relinker", $"Failed loading {meta} - {asmname}"); e.LogDetailed(); return(null); } }
/// <summary> /// Relink a .dll to point towards Celeste.exe and FNA / XNA properly at runtime, then load it. /// </summary> /// <param name="meta">The mod metadata, used for caching, among other things.</param> /// <param name="stream">The stream to read the .dll from.</param> /// <param name="depResolver">An optional dependency resolver.</param> /// <param name="checksumsExtra">Any optional checksums</param> /// <param name="prePatch">An optional step executed before patching, but after MonoMod has loaded the input assembly.</param> /// <returns>The loaded, relinked assembly.</returns> public static Assembly GetRelinkedAssembly(EverestModuleMetadata meta, Stream stream, MissingDependencyResolver depResolver = null, string[] checksumsExtra = null, Action <MonoModder> prePatch = null) => GetRelinkedAssembly(meta, Path.GetFileNameWithoutExtension(meta.DLL), stream, depResolver, checksumsExtra, prePatch);
public static Assembly GetRelinkedAssembly(EverestModuleMetadata meta, Stream stream, MissingDependencyResolver depResolver = null) { string name = Path.GetFileName(meta.DLL); string cachedName = meta.Name + "." + name.Substring(0, name.Length - 3) + "dll"; string cachedPath = Path.Combine(Loader.PathCache, cachedName); string cachedChecksumPath = Path.Combine(Loader.PathCache, cachedName + ".sum"); string[] checksums = new string[2]; using (MD5 md5 = MD5.Create()) { if (GameChecksum == null) { using (FileStream fs = File.OpenRead(Assembly.GetAssembly(typeof(Relinker)).Location)) GameChecksum = md5.ComputeHash(fs).ToHexadecimalString(); } checksums[0] = GameChecksum; string modPath = meta.PathArchive; if (modPath.Length == 0) { modPath = meta.DLL; } using (FileStream fs = File.OpenRead(modPath)) checksums[1] = md5.ComputeHash(fs).ToHexadecimalString(); } if (File.Exists(cachedPath) && File.Exists(cachedChecksumPath) && ChecksumsEqual(checksums, File.ReadAllLines(cachedChecksumPath))) { return(Assembly.LoadFrom(cachedPath)); } if (depResolver == null) { depResolver = GenerateModDependencyResolver(meta); } using (MonoModder modder = new MonoModder() { Input = stream, OutputPath = cachedPath, CleanupEnabled = false, RelinkModuleMap = SharedRelinkModuleMap, RelinkMap = SharedRelinkMap, DependencyDirs = { PathGame }, MissingDependencyResolver = depResolver }) try { modder.ReaderParameters.ReadSymbols = false; modder.WriterParameters.WriteSymbols = false; modder.WriterParameters.SymbolWriterProvider = null; modder.Read(); modder.MapDependencies(); modder.AutoPatch(); modder.Write(); } catch (Exception e) { Logger.Log("relinker", $"Failed relinking {meta}: {e}"); return(null); } if (File.Exists(cachedChecksumPath)) { File.Delete(cachedChecksumPath); } File.WriteAllLines(cachedChecksumPath, checksums); return(Assembly.LoadFrom(cachedPath)); }
/// <summary> /// Get the cached path of a given mod's relinked .dll /// </summary> /// <param name="meta">The mod metadata.</param> /// <returns>The full path to the cached relinked .dll</returns> public static string GetCachedPath(EverestModuleMetadata meta, string asmname) => Path.Combine(Loader.PathCache, meta.Name + "." + asmname + ".dll");
/// <summary> /// Load a mod .dll given its metadata at runtime. Doesn't load the mod content. /// </summary> /// <param name="meta">Metadata of the mod to load.</param> public static void LoadMod(EverestModuleMetadata meta) { if (!Flags.SupportRuntimeMods) { Logger.Log(LogLevel.Warn, "loader", "Loader disabled!"); return; } if (meta == null) { return; } // Add an AssemblyResolve handler for all bundled libraries. AppDomain.CurrentDomain.AssemblyResolve += GenerateModAssemblyResolver(meta); if (string.IsNullOrEmpty(meta.DLL)) { return; } // Load the actual assembly. Assembly asm = null; if (!string.IsNullOrEmpty(meta.PathArchive)) { using (ZipFile zip = new ZipFile(meta.PathArchive)) { foreach (ZipEntry entry in zip.Entries) { string entryName = entry.FileName.Replace('\\', '/'); if (entryName == meta.DLL) { using (MemoryStream stream = entry.ExtractStream()) { if (meta.Prelinked) { asm = Assembly.Load(stream.GetBuffer()); } else { asm = Relinker.GetRelinkedAssembly(meta, stream); } } } } } } else { if (meta.Prelinked) { asm = Assembly.LoadFrom(meta.DLL); } else { using (FileStream stream = File.OpenRead(meta.DLL)) asm = Relinker.GetRelinkedAssembly(meta, stream); } } if (asm != null) { LoadModAssembly(meta, asm); } }
/// <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."); CheckDependenciesOfDelayedMods(); }
/// <summary> /// Get the cached path of a given mod's relinked .dll /// </summary> /// <param name="meta">The mod metadata.</param> /// <returns>The full path to the cached relinked .dll</returns> public static string GetCachedPath(EverestModuleMetadata meta) => GetCachedPath(meta, Path.GetFileNameWithoutExtension(meta.DLL));