public static void LoadMods() { LogColour(ConsoleColor.Green, "LOADING MODS"); foreach (String s in Directory.GetFiles(ModPath, "*.dll")) { LogColour(ConsoleColor.Green, "Found DLL: " + s); Assembly mod = Assembly.LoadFile(s); if (mod.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) > 0) { LogColour(ConsoleColor.Green, "Loading Mod DLL..."); TypeInfo tar = mod.DefinedTypes.First(x => x.BaseType == typeof(Mod)); Mod m = (Mod)mod.CreateInstance(tar.ToString()); Console.WriteLine("LOADED MOD: {0} by {1} - Version {2} | Description: {3}", m.Name, m.Authour, m.Version, m.Description); m.Entry(); } else { LogError("Invalid Mod DLL"); } } }
/// <summary>Load and hook up all mods in the mod directory.</summary> private void LoadMods() { this.Monitor.Log("Loading mods..."); // get JSON helper JsonHelper jsonHelper = new JsonHelper(); // get assembly loader AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // load mod assemblies int modsLoaded = 0; List <Action> deprecationWarnings = new List <Action>(); // queue up deprecation warnings to show after mod list foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath)) { // passthrough empty directories DirectoryInfo directory = new DirectoryInfo(directoryPath); while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) { directory = directory.GetDirectories().First(); } // check for cancellation if (this.CancellationTokenSource.IsCancellationRequested) { this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); return; } // get manifest path string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}"; // read manifest Manifest manifest; try { // read manifest text string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error); continue; } // deserialise manifest manifest = jsonHelper.ReadJsonFile <Manifest>(Path.Combine(directory.FullName, "manifest.json")); if (manifest == null) { this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); continue; } if (string.IsNullOrEmpty(manifest.EntryDll)) { this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } } catch (Exception ex) { this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } if (!string.IsNullOrWhiteSpace(manifest.Name)) { skippedPrefix = $"Skipped {manifest.Name}"; } // validate compatibility ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest); if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) { bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; if (hasOfficialUrl) { warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; } if (hasUnofficialUrl) { warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; } this.Monitor.Log(warning, LogLevel.Error); continue; } // validate SMAPI version if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) { try { ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); continue; } } catch (FormatException ex) when(ex.Message.Contains("not a valid semantic version")) { this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); continue; } } // create per-save directory if (manifest.PerSaveConfigs) { deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); try { string psDir = Path.Combine(directory.FullName, "psconfigs"); Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error); continue; } } catch (Exception ex) { this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } // validate mod path to simplify errors string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error); continue; } // preprocess & load mod assembly Assembly modAssembly; try { modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error); continue; } catch (Exception ex) { this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } // validate assembly try { int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); if (modEntries == 0) { this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error); continue; } if (modEntries > 1) { this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error); continue; } } catch (Exception ex) { this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); 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) { this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated."); continue; } // inject data // get helper mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; // track mod this.ModRegistry.Add(mod); modsLoaded += 1; this.Monitor.Log($"Loaded {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error); } } // initialise mods foreach (Mod mod in this.ModRegistry.GetMods()) { try { // call entry methods mod.Entry(); // deprecated since 1.0 mod.Entry(mod.Helper); // 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(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info)); } } catch (Exception ex) { this.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); } } // print result this.Monitor.Log($"Loaded {modsLoaded} mods."); foreach (Action warning in deprecationWarnings) { warning(); } Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods"; }
/// <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> /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param> /// <returns>Returns the number of mods successfully loaded.</returns> private int LoadMods(ModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList <Action> deprecationWarnings) { this.Monitor.Log("Loading mods..."); void LogSkip(ModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level); // load mod assemblies int modsLoaded = 0; AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); foreach (ModMetadata metadata in mods) { IManifest manifest = metadata.Manifest; string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll); // preprocess & load mod assembly Assembly modAssembly; try { modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { LogSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version})."); continue; } catch (Exception ex) { LogSkip(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) { LogSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); continue; } if (modEntries > 1) { LogSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); continue; } } catch (Exception ex) { LogSkip(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) { LogSkip(metadata, "its entry class couldn't be instantiated."); continue; } // inject data mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = metadata.DirectoryPath; // track mod this.ModRegistry.Add(mod); modsLoaded += 1; this.Monitor.Log($"Loaded {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { LogSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); } } // initialise loaded mods foreach (IMod mod in this.ModRegistry.GetMods()) { try { // call entry methods (mod as Mod)?.Entry(); // deprecated since 1.0 mod.Entry(mod.Helper); // 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(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info)); } } catch (Exception ex) { this.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); } } // print result this.Monitor.Log($"Loaded {modsLoaded} mods."); return(modsLoaded); }