/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="displayName">The mod's display name.</param> /// <param name="directoryPath">The mod's full directory path.</param> /// <param name="manifest">The mod manifest.</param> /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; this.Manifest = manifest; this.Compatibility = compatibility; }
/// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary> /// <param name="mod">The mock mod metadata.</param> /// <param name="compatibility">The compatibility record to set.</param> private void SetupMetadataForValidation(Mock <IModMetadata> mod, ModCompatibility compatibility = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.Compatibility).Returns(() => null); mod.Setup(p => p.Manifest).Returns(this.GetManifest()); mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); mod.Setup(p => p.Compatibility).Returns(compatibility); }
private void OnGameLaunched(object sender, GameLaunchedEventArgs e) { var spaceCore = this.Helper.ModRegistry.GetApi <ISpaceCoreApi>("spacechase0.SpaceCore"); Type[] types = { typeof(BuildableGreenhouseBuilding), typeof(BuildableGreenhouseLocation) }; foreach (Type type in types) { spaceCore.RegisterSerializerType(type); } ModCompatibility.applyGMCMCompatibility(sender, e); }
public override void OnApplicationStart() // Runs after Game Initialization. { if (Environment.CommandLine.Contains("--ff.debug") || MelonDebug.IsEnabled()) { isDebug = true; MelonLogger.Msg("Debug mode is active"); } melon = MelonPreferences.CreateCategory(BuildInfo.Name, BuildInfo.Name); allowFrameLimit = (MelonPreferences_Entry <bool>)melon.CreateEntry("allowFrameLimit", false, "Toggle Frame Focus"); FrameLimit = (MelonPreferences_Entry <int>)melon.CreateEntry("FrameLimit", 90, "Max Focused Frame Limit"); FrameLimitUnfocused = (MelonPreferences_Entry <int>)melon.CreateEntry("FrameLimitUnfocused", 5, "Unfocused Frame Limit"); // suggested by ljoonal override_emmVRC = (MelonPreferences_Entry <bool>)melon.CreateEntry("override_emmVRC", false, "Make FrameFocus ignore emmVRC integration (only works if emmVRC is detected)"); MelonCoroutines.Start(ModCompatibility.RunCompatibilityCheck()); MelonCoroutines.Start(StartLate.Init()); MelonLogger.Msg("Initialized!"); }
public HitokoriRuleset() { void RegisterMods(IEnumerable <Mod> mods) { foreach (var mod in mods) { if (mod is MultiMod multi) { RegisterMods(multi.Mods); } else { ModCompatibility.RegisterMod(GetType(), mod.GetType()); } } } foreach (ModType type in Enum.GetValues(typeof(ModType))) { RegisterMods(GetModsFor(type)); } }
public override void Entry(IModHelper helper) { this.Config = helper.ReadConfig <ModConfig>(); this.graphicsDevice = Game1.graphics.GraphicsDevice; ModPatch.Initialize(helper, this.Monitor); ModCompatibility.Initialize(helper, this.Monitor, this.ModManifest); helper.Events.Content.AssetRequested += this.OnAssetRequested; helper.Events.Player.Warped += this.OnWarped; helper.Events.GameLoop.GameLaunched += this.OnGameLaunched; helper.Events.GameLoop.DayStarted += this.OnDayStarted; helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded; helper.Events.Display.MenuChanged += this.OnMenuChanged; var harmony = new Harmony(this.ModManifest.UniqueID); harmony.Patch( original: AccessTools.Method(typeof(GreenhouseBuilding), nameof(GreenhouseBuilding.drawInMenu)), prefix: new HarmonyMethod(typeof(ModPatch), nameof(ModPatch.drawInMenu_Prefix)) ); }
/// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> /// <param name="vendorModUrls">Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID).</param> public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion, IDictionary <string, string> vendorModUrls) { mods = mods.ToArray(); // validate each manifest foreach (IModMetadata mod in mods) { // skip if already failed if (mod.Status == ModMetadataStatus.Failed) { continue; } // validate compatibility ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); switch (compatibility?.Status) { case ModStatus.Obsolete: mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; // get update URLs List <string> updateUrls = new List <string>(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { string[] parts = key.Split(new[] { ':' }, 2); if (parts.Length != 2) { continue; } string vendorKey = parts[0].Trim(); string modID = parts[1].Trim(); if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) { updateUrls.Add(string.Format(urlTemplate, modID)); } } if (mod.DataRecord.AlternativeUrl != null) { updateUrls.Add(mod.DataRecord.AlternativeUrl); } // build error string error = $"{reasonPhrase}. Please check for a "; if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) { error += "newer version"; } else { error += $"version newer than {compatibility.UpperVersion}"; } error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); } continue; } // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } // validate DLL value if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll)) { mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field."); continue; } if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); continue; } // validate DLL path string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } // validate required fields { List <string> missingFields = new List <string>(3); if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) { missingFields.Add(nameof(IManifest.Name)); } if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") { missingFields.Add(nameof(IManifest.Version)); } if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) { missingFields.Add(nameof(IManifest.UniqueID)); } if (missingFields.Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); } } } // validate IDs are unique { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { foreach (IModMetadata mod in group) { if (mod.Status == ModMetadataStatus.Failed) { continue; // don't replace metadata error } mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); } } } }
/// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion) { mods = mods.ToArray(); // validate each manifest foreach (IModMetadata mod in mods) { // skip if already failed if (mod.Status == ModMetadataStatus.Failed) { continue; } // validate compatibility { ModCompatibility compatibility = mod.Compatibility; if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) { #if SMAPI_1_x bool hasOfficialUrl = mod.Compatibility.UpdateUrls.Length > 0; bool hasUnofficialUrl = mod.Compatibility.UpdateUrls.Length > 1; string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:"; if (hasOfficialUrl) { error += !hasUnofficialUrl ? $" {compatibility.UpdateUrls[0]}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrls[0]}"; } if (hasUnofficialUrl) { error += $"{Environment.NewLine}- unofficial update: {compatibility.UpdateUrls[1]}"; } #else string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; string error = $"{reasonPhrase}. Please check for a "; if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) { error += "newer version"; } else { error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; } error += " at " + string.Join(" or ", compatibility.UpdateUrls); #endif mod.SetStatus(ModMetadataStatus.Failed, error); continue; } } // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } // validate DLL path string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } // validate required fields #if !SMAPI_1_x { List <string> missingFields = new List <string>(3); if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) { missingFields.Add(nameof(IManifest.Name)); } if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") { missingFields.Add(nameof(IManifest.Version)); } if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) { missingFields.Add(nameof(IManifest.UniqueID)); } if (missingFields.Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); } } #endif } // validate IDs are unique #if !SMAPI_1_x { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { foreach (IModMetadata mod in group) { if (mod.Status == ModMetadataStatus.Failed) { continue; // don't replace metadata error } mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); } } } #endif }
/********* ** Public methods *********/ /// <summary>Get manifest metadata for each folder in the given root path.</summary> /// <param name="rootPath">The root path to search for mods.</param> /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> /// <param name="disabledMods">Metadata about mods that SMAPI should consider obsolete and not load.</param> /// <returns>Returns the manifests by relative folder.</returns> public IEnumerable <IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable <ModCompatibility> compatibilityRecords, IEnumerable <DisabledMod> disabledMods) { compatibilityRecords = compatibilityRecords.ToArray(); disabledMods = disabledMods.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file Manifest manifest = null; string path = Path.Combine(modDir.FullName, "manifest.json"); string error = null; try { // read manifest manifest = jsonHelper.ReadJsonFile <Manifest>(path); // validate if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) { error = "its manifest doesn't set an entry DLL."; } } catch (SParseException ex) { error = $"parsing its manifest failed: {ex.Message}"; } catch (Exception ex) { error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } // validate metadata ModCompatibility compatibility = null; if (manifest != null) { // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; // check if mod should be disabled DisabledMod disabledMod = disabledMods.FirstOrDefault(mod => mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)); if (disabledMod != null) { error = $"it's obsolete: {disabledMod.ReasonPhrase}"; } // get compatibility record compatibility = ( from mod in compatibilityRecords where mod.ID.Any(p => p.Matches(key, manifest)) && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) && !manifest.Version.IsNewerThan(mod.UpperVersion) select mod ).FirstOrDefault(); } // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name : modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); ModMetadataStatus status = error == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; yield return(new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error)); } }
public override void VRChat_OnUiManagerInit() { MelonCoroutines.Start(ModCompatibility.RunCompatibilityCheck()); MelonCoroutines.Start(StartLate.Init()); }
private void OnDayStarted(object sender, DayStartedEventArgs e) { ModCompatibility.applyGreenhouseUpgradesCompatibility(); }
/// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion) { mods = mods.ToArray(); // validate each manifest foreach (IModMetadata mod in mods) { // skip if already failed if (mod.Status == ModMetadataStatus.Failed) { continue; } // validate compatibility ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); switch (compatibility?.Status) { case ModStatus.Obsolete: mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); continue; case ModStatus.AssumeBroken: { string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; string error = $"{reasonPhrase}. Please check for a "; if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) { error += "newer version"; } else { error += $"version newer than {compatibility.UpperVersion}"; } error += " at " + string.Join(" or ", mod.DataRecord.UpdateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); } continue; } // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } // validate DLL path string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } // validate required fields { List <string> missingFields = new List <string>(3); if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) { missingFields.Add(nameof(IManifest.Name)); } if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") { missingFields.Add(nameof(IManifest.Version)); } if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) { missingFields.Add(nameof(IManifest.UniqueID)); } if (missingFields.Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); } } } // validate IDs are unique { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { foreach (IModMetadata mod in group) { if (mod.Status == ModMetadataStatus.Failed) { continue; // don't replace metadata error } mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); } } } }
/// <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>Find all mods in the given folder.</summary> /// <param name="rootPath">The root mod path to search.</param> /// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param> /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param> private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList <Action> deprecationWarnings) { this.Monitor.Log("Finding mods..."); void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); // load mod metadata List <ModMetadata> mods = new List <ModMetadata>(); foreach (string modRootPath in Directory.GetDirectories(rootPath)) { if (this.Monitor.IsExiting) { return(new ModMetadata[0]); // exit in progress } // init metadata string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); // passthrough empty directories DirectoryInfo directory = new DirectoryInfo(modRootPath); while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) { directory = directory.GetDirectories().First(); } // get manifest path string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); continue; } // read manifest Manifest manifest; try { // read manifest file string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { LogSkip(displayName, "its manifest is empty."); continue; } // parse manifest manifest = jsonHelper.ReadJsonFile <Manifest>(Path.Combine(directory.FullName, "manifest.json")); if (manifest == null) { LogSkip(displayName, "its manifest is invalid."); continue; } // validate manifest if (string.IsNullOrWhiteSpace(manifest.EntryDll)) { LogSkip(displayName, "its manifest doesn't set an entry DLL."); continue; } if (string.IsNullOrWhiteSpace(manifest.UniqueID)) { deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); } } catch (Exception ex) { LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); continue; } if (!string.IsNullOrWhiteSpace(manifest.Name)) { displayName = 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 error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; if (hasOfficialUrl) { error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; } if (hasUnofficialUrl) { error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; } LogSkip(displayName, error); } // validate SMAPI version if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) { try { ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } } catch (FormatException ex) when(ex.Message.Contains("not a valid semantic version")) { LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); 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)) { LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); continue; } } catch (Exception ex) { LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); continue; } } // validate DLL path string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); continue; } // add mod metadata mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); } return(mods.ToArray()); }