public void ProcessDependencies_Reorders_OverlappingDependencyChain() { // arrange // A ◀── B ◀── C ◀── D // ▲ ▲ // │ │ // E ◀── F Mock <IModMetadata> modA = this.GetMetadata("Mod A"); Mock <IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock <IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); Mock <IModMetadata> modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); Mock <IModMetadata> modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod B" }); Mock <IModMetadata> modF = this.GetMetadata("Mod F", dependencies: new[] { "Mod C", "Mod E" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }).ToArray(); // assert Assert.AreEqual(6, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs mod B, and is needed by mod D."); Assert.AreSame(modD.Object, mods[3], "The load order is incorrect: mod D should be fourth since it needs mod C."); Assert.AreSame(modE.Object, mods[4], "The load order is incorrect: mod E should be fifth since it needs mod B, but is specified after C which also needs mod B."); Assert.AreSame(modF.Object, mods[5], "The load order is incorrect: mod F should be last since it needs mods E and C."); }
public void ProcessDependencies_NoMods_DoesNothing() { // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0]).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods."); }
public void ReadBasicManifest_CanReadFile() { // create manifest data IDictionary <string, object> originalDependency = new Dictionary <string, object> { [nameof(IManifestDependency.UniqueID)] = Sample.String() }; IDictionary <string, object> original = new Dictionary <string, object> { [nameof(IManifest.Name)] = Sample.String(), [nameof(IManifest.Author)] = Sample.String(), [nameof(IManifest.Version)] = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), [nameof(IManifest.Description)] = Sample.String(), [nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}", [nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll", [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}", [nameof(IManifest.Dependencies)] = new[] { originalDependency }, ["ExtraString"] = Sample.String(), ["ExtraInt"] = Sample.Int() }; // write to filesystem string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); string filename = Path.Combine(modFolder, "manifest.json"); Directory.CreateDirectory(modFolder); File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest."); Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name."); Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match."); Assert.AreEqual(original[nameof(IManifest.Author)], mod.Manifest.Author, "The manifest's author doesn't match."); Assert.AreEqual(original[nameof(IManifest.Description)], mod.Manifest.Description, "The manifest's description doesn't match."); Assert.AreEqual(original[nameof(IManifest.EntryDll)], mod.Manifest.EntryDll, "The manifest's entry DLL doesn't match."); Assert.AreEqual(original[nameof(IManifest.MinimumApiVersion)], mod.Manifest.MinimumApiVersion?.ToString(), "The manifest's minimum API version doesn't match."); Assert.AreEqual(original[nameof(IManifest.Version)]?.ToString(), mod.Manifest.Version?.ToString(), "The manifest's version doesn't match."); Assert.IsNotNull(mod.Manifest.ExtraFields, "The extra fields should not be null."); Assert.AreEqual(2, mod.Manifest.ExtraFields.Count, "The extra fields should contain two values."); Assert.AreEqual(original["ExtraString"], mod.Manifest.ExtraFields["ExtraString"], "The manifest's extra fields should contain an 'ExtraString' value."); Assert.AreEqual(original["ExtraInt"], mod.Manifest.ExtraFields["ExtraInt"], "The manifest's extra fields should contain an 'ExtraInt' value."); Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null."); Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value."); Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match."); }
public void ProcessDependencies_IfOptional_SucceedsIfMissing() { // arrange // A ◀── B where A doesn't exist Mock <IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }).ToArray(); // assert Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod."); }
public void ReadBasicManifest_NoMods_ReturnsEmptyList() { // arrange string rootFolder = this.GetTempFolderPath(); Directory.CreateDirectory(rootFolder); // act IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); }
public void ReadBasicManifest_NoMods_ReturnsEmptyList() { // arrange string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(rootFolder); // act IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); }
public void ProcessDependencies_WithMinVersions_FailsIfNotMet() { // arrange // A 1.0 ◀── B (need A 1.1) Mock <IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); Mock <IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny <string>()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); }
public void ProcessDependencies_IfOptional() { // arrange // A ◀── B Mock <IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); Mock <IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }).ToArray(); // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); }
public void ProcessDependencies_WithMinVersions_SucceedsIfMet() { // arrange // A 1.0 ◀── B (need A 1.0-beta) Mock <IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); Mock <IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0-beta") }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); }
public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest() { // arrange string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(modFolder); // act IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed."); Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); }
public void ProcessDependencies_NoDependencies_DoesNothing() { // arrange // A B C Mock <IModMetadata> modA = this.GetMetadata("Mod A"); Mock <IModMetadata> modB = this.GetMetadata("Mod B"); Mock <IModMetadata> modC = this.GetMetadata("Mod C"); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object, modC.Object }).ToArray(); // assert Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order unexpectedly changed with no dependencies."); Assert.AreSame(modB.Object, mods[1], "The load order unexpectedly changed with no dependencies."); Assert.AreSame(modC.Object, mods[2], "The load order unexpectedly changed with no dependencies."); }
public void ProcessDependencies_Reorders_DependencyChain() { // arrange // A ◀── B ◀── C ◀── D Mock <IModMetadata> modA = this.GetMetadata("Mod A"); Mock <IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock <IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); Mock <IModMetadata> modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); // assert Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs mod B, and is needed by mod D."); Assert.AreSame(modD.Object, mods[3], "The load order is incorrect: mod D should be fourth since it needs mod C."); }
public void ProcessDependencies_Reorders_SimpleDependencies() { // arrange // A ◀── B // ▲ ▲ // │ │ // └─ C ─┘ Mock <IModMetadata> modA = this.GetMetadata("Mod A"); Mock <IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock <IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod A", "Mod B" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }).ToArray(); // assert Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since the other mods depend on it."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs both mod A and mod B."); }
public void ProcessDependencies_WithSomeFailedMods_Succeeds() { // arrange // A ◀── B ◀── C D (failed) Mock <IModMetadata> modA = this.GetMetadata("Mod A"); Mock <IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock <IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); Mock <IModMetadata> modD = new Mock <IModMetadata>(MockBehavior.Strict); modD.Setup(p => p.Manifest).Returns <IManifest>(null); modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); // assert Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modD.Object, mods[0], "The load order is incorrect: mod D should be first since it was already failed."); Assert.AreSame(modA.Object, mods[1], "The load order is incorrect: mod A should be second since it's needed by mod B."); Assert.AreSame(modB.Object, mods[2], "The load order is incorrect: mod B should be third since it needs mod A, and is needed by mod C."); Assert.AreSame(modC.Object, mods[3], "The load order is incorrect: mod C should be fourth since it needs mod B, and is needed by mod D."); }
public void ProcessDependencies_Skips_CircularDependentMods() { // arrange // A ◀── B ◀── C ──▶ D // ▲ │ // │ ▼ // └──── E Mock <IModMetadata> modA = this.GetMetadata("Mod A"); Mock <IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock <IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B", "Mod D" }, allowStatusChange: true); Mock <IModMetadata> modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod E" }, allowStatusChange: true); Mock <IModMetadata> modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }).ToArray(); // assert Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny <string>()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny <string>()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny <string>()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); }
/// <summary>Initialise SMAPI and mods after the game starts.</summary> private void InitialiseAfterGameStart() { // load settings this.Settings = JsonConvert.DeserializeObject <SConfig>(File.ReadAllText(Constants.ApiConfigPath)); this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components this.ModRegistry = new ModRegistry(); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); // redirect direct console output { Monitor monitor = this.GetSecondaryMonitor("Console.Out"); if (monitor.WriteToConsole) { this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); } } // add headers if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; this.Monitor.ShowFullStampInConsole = true; this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); } if (!this.Settings.CheckForUpdates) { this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); } if (!this.Monitor.WriteToConsole) { this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); } this.VerboseLog("Verbose logging enabled."); // validate XNB integrity if (!this.ValidateContentIntegrity()) { this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); } // load mods { this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); ModResolver resolver = new ModResolver(); // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); // load mods this.LoadMods(mods, new JsonHelper(), this.ContentManager); // check for updates this.CheckForUpdatesAsync(mods); } if (this.Monitor.IsExiting) { this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); return; } // update window titles int modsLoaded = this.ModRegistry.GetMods().Count(); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; // start SMAPI console new Thread(this.RunConsoleLoop).Start(); }
/// <summary>Initialise SMAPI and mods after the game starts.</summary> private void InitialiseAfterGameStart() { // load settings this.Settings = JsonConvert.DeserializeObject <SConfig>(File.ReadAllText(Constants.ApiConfigPath)); this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components this.ModRegistry = new ModRegistry(); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); #if SMAPI_1_x // inject compatibility shims #pragma warning disable 618 Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); Config.Shim(this.DeprecationManager); Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); Mod.Shim(this.DeprecationManager); GameEvents.Shim(this.DeprecationManager); PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); #pragma warning restore 618 #endif // redirect direct console output { Monitor monitor = this.GetSecondaryMonitor("Console.Out"); if (monitor.WriteToConsole) { this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); } } // add headers if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; #if !SMAPI_1_x this.Monitor.ShowFullStampInConsole = true; #endif this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); } if (!this.Settings.CheckForUpdates) { this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); } if (!this.Monitor.WriteToConsole) { this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); } if (this.Settings.VerboseLogging) { this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace); } // validate XNB integrity if (!this.ValidateContentIntegrity()) { this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); } // load mods { #if SMAPI_1_x this.Monitor.Log("Loading mod metadata..."); #else this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); #endif ModResolver resolver = new ModResolver(); // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility, this.Settings.DisabledMods).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion); // check for deprecated metadata #if SMAPI_1_x IList <Action> deprecationWarnings = new List <Action>(); foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) { // missing fields that will be required in SMAPI 2.0 { 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()) { deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} is missing some manifest fields ({string.Join(", ", missingFields)}) which will be required in an upcoming SMAPI version.", LogLevel.Warn)); } } // per-save directories if ((mod.Manifest as Manifest)?.PerSaveConfigs == true) { deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.DisplayName, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.PendingRemoval)); try { string psDir = Path.Combine(mod.DirectoryPath, "psconfigs"); Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { mod.SetStatus(ModMetadataStatus.Failed, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); } } catch (Exception ex) { mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); } } } #endif // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); // load mods #if SMAPI_1_x this.LoadMods(mods, new JsonHelper(), this.ContentManager, deprecationWarnings); foreach (Action warning in deprecationWarnings) { warning(); } #else this.LoadMods(mods, new JsonHelper(), this.ContentManager); #endif } if (this.Monitor.IsExiting) { this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); return; } // update window titles int modsLoaded = this.ModRegistry.GetMods().Count(); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; // start SMAPI console new Thread(this.RunConsoleLoop).Start(); }