/// <summary>Load and hook up the given mods.</summary> /// <param name="mods">The mods to load.</param> /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param> /// <param name="contentManager">The content manager to use for mod content.</param> private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) { this.Monitor.Log("Loading mods...", LogLevel.Trace); // load mod assemblies IDictionary <IModMetadata, string> skippedMods = new Dictionary <IModMetadata, string>(); { void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); foreach (IModMetadata metadata in mods) { // get basic info IManifest manifest = metadata.Manifest; string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) : null; this.Monitor.Log(assemblyPath != null ? $"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}..." : $"Loading {metadata.DisplayName}...", LogLevel.Trace); // validate status if (metadata.Status == ModMetadataStatus.Failed) { this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); TrackSkip(metadata, metadata.Error); continue; } // preprocess & load mod assembly Assembly modAssembly; try { modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); continue; } catch (SAssemblyLoadFailedException ex) { TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}"); continue; } catch (Exception ex) { TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } // validate assembly try { int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); if (modEntries == 0) { TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); continue; } if (modEntries > 1) { TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); continue; } } catch (Exception ex) { TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } // initialise mod try { // get implementation TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { TrackSkip(metadata, "its entry class couldn't be instantiated."); continue; } // inject data { IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); mod.Monitor = monitor; } // track mod metadata.SetMod(mod); this.ModRegistry.Add(metadata); } catch (Exception ex) { TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); } } } IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods this.Monitor.Newline(); if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; string reason = pair.Value; if (mod.Manifest?.Version != null) { this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); } else { this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); } } this.Monitor.Newline(); } // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; this.Monitor.Log( $" {metadata.DisplayName} {manifest.Version}" + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), LogLevel.Info ); } this.Monitor.Newline(); // initialise translations this.ReloadTranslations(); // initialise loaded mods foreach (IModMetadata metadata in loadedMods) { // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) { this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; } // call entry method try { IMod mod = metadata.Mod; mod.Entry(mod.Helper); } catch (Exception ex) { this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); } } // invalidate cache entries when needed // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) { helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(e.NewItems.Cast <IAssetEditor>().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast <IAssetLoader>().ToArray()); } }; } } // reset cache now if any editors or loaders were added during entry IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); if (editors.Any() || loaders.Any()) { this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(editors, loaders); } }
private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) #endif { #if SMAPI_1_x this.Monitor.Log("Loading mods..."); #else this.Monitor.Log("Loading mods...", LogLevel.Trace); #endif // load mod assemblies IDictionary <IModMetadata, string> skippedMods = new Dictionary <IModMetadata, string>(); { void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); foreach (IModMetadata metadata in mods) { // get basic info IManifest manifest = metadata.Manifest; string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) : null; this.Monitor.Log(assemblyPath != null ? $"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}..." : $"Loading {metadata.DisplayName}...", LogLevel.Trace); // validate status if (metadata.Status == ModMetadataStatus.Failed) { this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); TrackSkip(metadata, metadata.Error); continue; } // preprocess & load mod assembly Assembly modAssembly; try { modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { #if SMAPI_1_x TrackSkip(metadata, $"it's not compatible with the latest version of the game or SMAPI (detected {ex.NounPhrase}). Please check for a newer version of the mod."); #else TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); #endif continue; } catch (Exception ex) { TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } // validate assembly try { int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); if (modEntries == 0) { TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); continue; } if (modEntries > 1) { TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); continue; } } catch (Exception ex) { TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } // initialise mod try { // get implementation TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { TrackSkip(metadata, "its entry class couldn't be instantiated."); continue; } #if SMAPI_1_x // prevent mods from using SMAPI 2.0 content interception before release // ReSharper disable SuspiciousTypeConversion.Global if (mod is IAssetEditor || mod is IAssetLoader) { TrackSkip(metadata, $"its entry class implements {nameof(IAssetEditor)} or {nameof(IAssetLoader)}. These are part of a prototype API that isn't available for mods to use yet."); } // ReSharper restore SuspiciousTypeConversion.Global #endif // inject data { IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); mod.Monitor = monitor; #if SMAPI_1_x mod.PathOnDisk = metadata.DirectoryPath; #endif } // track mod metadata.SetMod(mod); this.ModRegistry.Add(metadata); } catch (Exception ex) { TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); } } } IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods #if !SMAPI_1_x this.Monitor.Newline(); #endif if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; string reason = pair.Value; if (mod.Manifest?.Version != null) { this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); } else { this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); } } #if !SMAPI_1_x this.Monitor.Newline(); #endif } // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; this.Monitor.Log( $" {metadata.DisplayName} {manifest.Version}" + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), LogLevel.Info ); } #if !SMAPI_1_x this.Monitor.Newline(); #endif // initialise translations this.ReloadTranslations(); // initialise loaded mods foreach (IModMetadata metadata in loadedMods) { // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) { this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; } // call entry method try { IMod mod = metadata.Mod; mod.Entry(mod.Helper); #if SMAPI_1_x (mod as Mod)?.Entry(); // deprecated since 1.0 // raise deprecation warning for old Entry() methods if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) { deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.PendingRemoval)); } #else if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(IModHelper) })) { this.Monitor.Log($"{metadata.DisplayName} doesn't implement Entry() and may not work correctly.", LogLevel.Error); } #endif } catch (Exception ex) { this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); } } // reset cache when needed // only register listeners after Entry to avoid repeatedly reloading assets during load foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) { // TODO: optimise by only reloading assets the new editors/loaders can intercept helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); this.ContentManager.InvalidateCache((key, type) => true); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); this.ContentManager.InvalidateCache((key, type) => true); } }; } } this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); this.ContentManager.InvalidateCache((key, type) => true); }