From f9eb16489fcf3f4c486df5f96a94edf16cf19a09 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Aug 2018 14:44:18 -0400 Subject: refactor some methods for reuse (#468) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9ac95fd4..09880d03 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; @@ -181,7 +180,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate ID format - if (Regex.IsMatch(mod.Manifest.UniqueID, "[^a-z0-9_.-]", RegexOptions.IgnoreCase)) + if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } -- cgit From 826dd53ab550e5b92796c510569118beee6bd044 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 18:28:16 -0400 Subject: move most SMAPI files into subfolder (#582) --- build/common.targets | 22 +++-- build/prepare-install-package.targets | 57 +++++------ docs/release-notes.md | 3 + docs/technical-docs.md | 36 +++---- src/SMAPI.Installer/InteractiveInstaller.cs | 115 ++++++++++++++--------- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- src/SMAPI.ModBuildConfig/package.nuspec | 5 +- src/SMAPI/Constants.cs | 7 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 1 + src/SMAPI/Program.cs | 85 +++++++++++------ 10 files changed, 197 insertions(+), 136 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/build/common.targets b/build/common.targets index 5b6511f8..90c477b6 100644 --- a/build/common.targets +++ b/build/common.targets @@ -99,14 +99,14 @@ - - - - - - + + + + + + @@ -114,12 +114,14 @@ - - + + + - - + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 79185896..35ff78a5 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -23,47 +23,50 @@ - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/docs/release-notes.md b/docs/release-notes.md index c7097f97..0ec842ef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,12 +1,15 @@ # Release notes ## 2.8 (upcoming) * For players: + * Moved most SMAPI files into a `smapi-internal` subfolder. * Updated compatibility list. * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added `IContentPack.WriteJsonFile` method. + * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. + * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mods, but you'll need to update the mod build config NuGet package when compiling mods. ## 2.7 * For players: diff --git a/docs/technical-docs.md b/docs/technical-docs.md index ed45871a..be809c3f 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -50,15 +50,15 @@ on the wiki for the first-time setup. 1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a [semantic version](http://semver.org). Recommended format: - build type | format | example - :--------- | :-------------------------------- | :------ - dev build | `-alpha.` | `2.0-alpha.20171230` - prerelease | `-prerelease.` | `2.0-prerelease.2` - release | `` | `2.0` + build type | format | example + :--------- | :----------------------- | :------ + dev build | `-alpha.` | `3.0-alpha.20171230` + prerelease | `-beta.` | `3.0-beta.2` + release | `` | `3.0` 2. In Windows: 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 3.0`). 2. Transfer the `SMAPI ` folder to Linux or Mac. _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, since we need to set Unix file permissions that Windows won't save._ @@ -69,36 +69,26 @@ on the wiki for the first-time setup. 3. If you did everything right so far, you should have a folder like this: ``` - SMAPI-2.x/ - install.exe - readme.txt + SMAPI 3.0 installer/ + install on Linux.sh + install on Mac.command + install on Windows.exe + README.txt internal/ Mono/ Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll + smapi-internal/* StardewModdingAPI - StardewModdingAPI.config.json - StardewModdingAPI.Internal.dll - StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml steam_appid.txt - System.Numerics.dll - System.Runtime.Caching.dll - System.ValueTuple.dll Windows/ Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI.config.json - StardewModdingAPI.Internal.dll - StardewModdingAPI.metadata.json + smapi-internal/* StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml - System.ValueTuple.dll steam_appid.txt ``` 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 0aac1da2..f9e1ff94 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -93,40 +93,39 @@ namespace StardewModdingApi.Installer { string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); - // common - yield return GetInstallPath("0Harmony.dll"); - yield return GetInstallPath("0Harmony.pdb"); - yield return GetInstallPath("Mono.Cecil.dll"); - yield return GetInstallPath("Newtonsoft.Json.dll"); + // current files + yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI.exe"); - yield return GetInstallPath("StardewModdingAPI.config.json"); - yield return GetInstallPath("StardewModdingAPI.metadata.json"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); - yield return GetInstallPath("System.ValueTuple.dll"); - yield return GetInstallPath("steam_appid.txt"); - - // Linux/Mac only - yield return GetInstallPath("libgdiplus.dylib"); - yield return GetInstallPath("StardewModdingAPI"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); - yield return GetInstallPath("System.Numerics.dll"); - yield return GetInstallPath("System.Runtime.Caching.dll"); - - // Windows only - yield return GetInstallPath("StardewModdingAPI.pdb"); + yield return GetInstallPath("smapi-internal"); // obsolete - yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 + yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) - yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 + yield return GetInstallPath("0Harmony.dll"); // moved in 2.8 + yield return GetInstallPath("0Harmony.pdb"); // moved in 2.8 + yield return GetInstallPath("Mono.Cecil.dll"); // moved in 2.8 + yield return GetInstallPath("Newtonsoft.Json.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.config.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.metadata.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.xml"); // moved in 2.8 + yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 + yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8 + yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8 + yield return GetInstallPath("steam_appid.txt"); // moved in 2.8 + if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) @@ -438,14 +437,13 @@ namespace StardewModdingApi.Installer { // copy SMAPI files to game dir this.PrintDebug("Adding SMAPI files..."); - foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo sourceEntry in paths.PackageDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { - if (sourceFile.Name == this.InstallerFileName) + if (sourceEntry.Name == this.InstallerFileName) continue; - string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name); - this.InteractivelyDelete(targetPath); - sourceFile.CopyTo(targetPath); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name)); + this.RecursiveCopy(sourceEntry, paths.GameDir); } // replace mod launcher (if possible) @@ -508,7 +506,7 @@ namespace StardewModdingApi.Installer targetDir.Create(); // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopy)) sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); } @@ -690,6 +688,31 @@ namespace StardewModdingApi.Installer } } + /// Recursively copy a directory or file. + /// The file or folder to copy. + /// The folder to copy into. + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder) + { + if (!targetFolder.Exists) + targetFolder.Create(); + + switch (source) + { + case FileInfo sourceFile: + sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); + break; + + case DirectoryInfo sourceDir: + DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); + foreach (var entry in sourceDir.EnumerateFileSystemInfos()) + this.RecursiveCopy(entry, targetSubfolder); + break; + + default: + throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); + } + } + /// Delete a file or folder regardless of file permissions, and block until deletion completes. /// The file or folder to reset. /// This method is mirred from FileUtilities.ForceDelete in the toolkit. @@ -871,7 +894,7 @@ namespace StardewModdingApi.Installer this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); // move mods if no conflicts (else warn) - foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { // get type bool isDir = entry is DirectoryInfo; @@ -928,22 +951,26 @@ namespace StardewModdingApi.Installer Directory.CreateDirectory(newPath); DirectoryInfo directory = (DirectoryInfo)entry; - foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopy)) this.Move(child, Path.Combine(newPath, child.Name)); directory.Delete(recursive: true); } } - /// Get whether a file should be copied when moving a folder. - /// The file info. - private bool ShouldCopyFile(FileSystemInfo file) + /// Get whether a file or folder should be copied from the installer files. + /// The file or folder info. + private bool ShouldCopy(FileSystemInfo entry) { - // ignore Mac symlink - if (file is FileInfo && file.Name == "mcs") - return false; - - return true; + switch (entry.Name) + { + case "mcs": + return false; // ignore Mac symlink + case "Mods": + return false; // Mods folder handled separately + default: + return true; + } } } } diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d1c8a4eb..db9fe8bd 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -136,7 +136,7 @@ true - $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll false true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 3d6f2598..04880101 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0 + 2.1.1 Build package for SMAPI mods Pathoschild Pathoschild @@ -19,6 +19,9 @@ - Added option to ignore files by regex pattern. - Added reference to new SMAPI DLL. - Fixed some game paths not detected by NuGet package. + + 2.1.1: + - Update for SMAPI 2.8. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bd512fb1..0e0ae239 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -64,11 +64,14 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; + /// The absolute path to the folder containing SMAPI's internal files. + internal static readonly string InternalFilesPath = Program.DllSearchPath; + /// The file path for the SMAPI configuration file. - internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json"); /// The file path for the SMAPI metadata file. - internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); + internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.metadata.json"); /// The filename prefix used for all SMAPI logs. internal static string LogNamePrefix { get; } = "SMAPI-"; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 37b1a378..e750c659 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -45,6 +45,7 @@ namespace StardewModdingAPI.Framework.ModLoading this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary(); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c40d2ff6..64eeb45a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -45,6 +45,11 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// The absolute path to search for SMAPI's internal DLLs. + /// We can't use directly, since depends on DLLs loaded from this folder. + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + internal static readonly string DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal"); + /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -111,6 +116,8 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { + // initial setup + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; Program.AssertMinimumCompatibility(); // get flags from arguments @@ -135,10 +142,48 @@ namespace StardewModdingAPI program.RunInteractively(); } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // skip if already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; + this.Monitor.Log("Disposing...", LogLevel.Trace); + + // dispose mod data + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { + try + { + (mod.Mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } + + // dispose core components + this.IsGameRunning = false; + this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); + this.CancellationTokenSource?.Dispose(); + this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); + + // end game (moved from Game1.OnExiting to let us clean up first) + Process.GetCurrentProcess().Kill(); + } + + + /********* + ** Private methods + *********/ /// Construct an instance. /// The path to search for mods. /// Whether to output log messages to the console. - public Program(string modsPath, bool writeToConsole) + private Program(string modsPath, bool writeToConsole) { // init paths this.VerifyPath(modsPath); @@ -189,7 +234,7 @@ namespace StardewModdingAPI /// Launch SMAPI. [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - public void RunInteractively() + private void RunInteractively() { // initialise SMAPI try @@ -320,44 +365,28 @@ namespace StardewModdingAPI } } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + /// Method called when assembly resolution fails, which may return a manually resolved assembly. + /// The event sender. + /// The event arguments. + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { - // skip if already disposed - if (this.IsDisposed) - return; - this.IsDisposed = true; - this.Monitor.Log("Disposing...", LogLevel.Trace); - - // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { try { - (mod.Mod as IDisposable)?.Dispose(); + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + return Assembly.LoadFrom(dll.FullName); } catch (Exception ex) { - mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting the smapi-lib folder and reinstalling SMAPI.", ex); } } - // dispose core components - this.IsGameRunning = false; - this.ConsoleManager?.Dispose(); - this.ContentCore?.Dispose(); - this.CancellationTokenSource?.Dispose(); - this.GameInstance?.Dispose(); - this.LogFile?.Dispose(); - - // end game (moved from Game1.OnExiting to let us clean up first) - Process.GetCurrentProcess().Kill(); + return null; } - - /********* - ** Private methods - *********/ /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. private static void AssertMinimumCompatibility() { -- cgit From 100e303b488a36e8410ff67e32c35bff80f21ba2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 20:27:28 -0400 Subject: add recursive mod search (#583) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +- .../Framework/ModScanning/ModFolder.cs | 15 ++--- .../Framework/ModScanning/ModScanner.cs | 64 ++++++++++++++++++++-- 4 files changed, 66 insertions(+), 18 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ec842ef..c742204e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ # Release notes ## 2.8 (upcoming) * For players: + * Added support for subfolders under `Mods`, for players who want to organise their mods. * Moved most SMAPI files into a `smapi-internal` subfolder. * Updated compatibility list. diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 09880d03..11518444 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName); + displayName = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); // apply defaults if (manifest != null && dataRecord != null) @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + yield return new ModMetadata(displayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 4aaa3f83..83c9c44d 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -11,11 +11,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /********* ** Accessors *********/ - /// The Mods subfolder containing this mod. - public DirectoryInfo SearchDirectory { get; } - - /// The folder containing manifest.json. - public DirectoryInfo ActualDirectory { get; } + /// The folder containing the mod's manifest.json. + public DirectoryInfo Directory { get; } /// The mod manifest. public Manifest Manifest { get; } @@ -28,14 +25,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ** Public methods *********/ /// Construct an instance. - /// The Mods subfolder containing this mod. - /// The folder containing manifest.json. + /// The folder containing the mod's manifest.json. /// The mod manifest. /// The error which occurred parsing the manifest, if any. - public ModFolder(DirectoryInfo searchDirectory, DirectoryInfo actualDirectory, Manifest manifest, string manifestParseError = null) + public ModFolder(DirectoryInfo directory, Manifest manifest, string manifestParseError = null) { - this.SearchDirectory = searchDirectory; - this.ActualDirectory = actualDirectory; + this.Directory = directory; this.Manifest = manifest; this.ManifestParseError = manifestParseError; } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index f1cce4a4..063ec2f4 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -16,6 +16,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The JSON helper with which to read manifests. private readonly JsonHelper JsonHelper; + /// A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod. + private readonly HashSet IgnoreFilesystemEntries = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ".DS_Store", + "mcs", + "Thumbs.db" + }; + /********* ** Public methods @@ -31,19 +39,23 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The root folder containing mods. public IEnumerable GetModFolders(string rootPath) { - foreach (DirectoryInfo folder in new DirectoryInfo(rootPath).EnumerateDirectories()) - yield return this.ReadFolder(rootPath, folder); + DirectoryInfo root = new DirectoryInfo(rootPath); + return this.GetModFolders(root, root); } /// Extract information from a mod folder. - /// The root folder containing mods. /// The folder to search for a mod. - public ModFolder ReadFolder(string rootPath, DirectoryInfo searchFolder) + public ModFolder ReadFolder(DirectoryInfo searchFolder) { // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); if (manifestFile == null) - return new ModFolder(searchFolder, null, null, "it doesn't have a manifest."); + { + bool isEmpty = !searchFolder.GetFileSystemInfos().Where(this.IsRelevant).Any(); + if (isEmpty) + return new ModFolder(searchFolder, null, "it's an empty folder."); + return new ModFolder(searchFolder, null, "it contains files, but none of them are manifest.json."); + } // read mod info Manifest manifest = null; @@ -64,13 +76,33 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - return new ModFolder(searchFolder, manifestFile.Directory, manifest, manifestError); + return new ModFolder(manifestFile.Directory, manifest, manifestError); } /********* ** Private methods *********/ + /// Recursively extract information about all mods in the given folder. + /// The root mod folder. + /// The folder to search for mods. + public IEnumerable GetModFolders(DirectoryInfo root, DirectoryInfo folder) + { + // recurse into subfolders + if (this.IsModSearchFolder(root, folder)) + { + foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) + { + foreach (ModFolder match in this.GetModFolders(root, subfolder)) + yield return match; + } + } + + // treat as mod folder + else + yield return this.ReadFolder(folder); + } + /// Find the manifest for a mod folder. /// The folder to search. private FileInfo FindManifest(DirectoryInfo folder) @@ -94,5 +126,25 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return null; } } + + /// Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods). + /// The root mod folder. + /// The folder to search for mods. + private bool IsModSearchFolder(DirectoryInfo root, DirectoryInfo folder) + { + if (root.FullName == folder.FullName) + return true; + + DirectoryInfo[] subfolders = folder.GetDirectories().Where(this.IsRelevant).ToArray(); + FileInfo[] files = folder.GetFiles().Where(this.IsRelevant).ToArray(); + return subfolders.Any() && !files.Any(); + } + + /// Get whether a file or folder is relevant when deciding how to process a mod folder. + /// The file or folder. + private bool IsRelevant(FileSystemInfo entry) + { + return !this.IgnoreFilesystemEntries.Contains(entry.Name); + } } } -- cgit From ca8699c68f238f3092966a550643859bce357a86 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 21:22:48 -0400 Subject: add display name field to ModFolder (#583) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 9 +-------- .../Framework/ModScanning/ModFolder.cs | 13 ++++++++++++- .../Framework/ModScanning/ModScanner.cs | 11 ++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 11518444..65a311dc 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -30,13 +30,6 @@ namespace StardewModdingAPI.Framework.ModLoading // parse internal data record (if any) ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); - // get display name - string displayName = manifest?.Name; - if (string.IsNullOrWhiteSpace(displayName)) - displayName = dataRecord?.DisplayName; - if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); - // apply defaults if (manifest != null && dataRecord != null) { @@ -48,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 83c9c44d..d2fea9e2 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.ModScanning { @@ -11,6 +12,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /********* ** Accessors *********/ + /// A suggested display name for the mod folder. + public string DisplayName { get; } + /// The folder containing the mod's manifest.json. public DirectoryInfo Directory { get; } @@ -25,14 +29,21 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ** Public methods *********/ /// Construct an instance. + /// The root folder containing mods. /// The folder containing the mod's manifest.json. /// The mod manifest. /// The error which occurred parsing the manifest, if any. - public ModFolder(DirectoryInfo directory, Manifest manifest, string manifestParseError = null) + public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null) { + // save info this.Directory = directory; this.Manifest = manifest; this.ManifestParseError = manifestParseError; + + // set display name + this.DisplayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(this.DisplayName)) + this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); } /// Get the update keys for a mod. diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 063ec2f4..71dc0cb3 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -44,8 +44,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } /// Extract information from a mod folder. + /// The root folder containing mods. /// The folder to search for a mod. - public ModFolder ReadFolder(DirectoryInfo searchFolder) + public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) { // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); @@ -53,8 +54,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { bool isEmpty = !searchFolder.GetFileSystemInfos().Where(this.IsRelevant).Any(); if (isEmpty) - return new ModFolder(searchFolder, null, "it's an empty folder."); - return new ModFolder(searchFolder, null, "it contains files, but none of them are manifest.json."); + return new ModFolder(root, searchFolder, null, "it's an empty folder."); + return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json."); } // read mod info @@ -76,7 +77,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - return new ModFolder(manifestFile.Directory, manifest, manifestError); + return new ModFolder(root, manifestFile.Directory, manifest, manifestError); } @@ -100,7 +101,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // treat as mod folder else - yield return this.ReadFolder(folder); + yield return this.ReadFolder(root, folder); } /// Find the manifest for a mod folder. -- cgit From ff8ffbdef0306254094258be326d75fcdc2bcbe1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Aug 2018 00:48:34 -0400 Subject: add paranoid warnings mode (#590) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 ++++++++ .../ModLoading/InstructionHandleResult.cs | 8 +++++- src/SMAPI/Framework/ModLoading/ModWarning.cs | 8 +++++- src/SMAPI/Framework/Models/SConfig.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 11 ++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 19 ++++++++++++-- src/SMAPI/StardewModdingAPI.config.json | 30 ++++++++++++++-------- 8 files changed, 75 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ff90aa3..5d0d8a4e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mods, but you'll need to update the mod build config NuGet package when compiling mods. * For SMAPI developers: + * Added a 'paranoid warnings' option which reports mods using potentially sensitive .NET APIs (like file or shell access) in the mod issues list. * Adjusted `SaveBackup` mod to make it easier to account for custom mod subfolders in the installer. * Installer no longer special-cases Omegasis' older `SaveBackup` mod (now named `AdvancedSaveBackup`). diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index e750c659..fdbfdd8d 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -350,6 +350,16 @@ namespace StardewModdingAPI.Framework.ModLoading mod.SetWarning(ModWarning.UsesDynamic); break; + case InstructionHandleResult.DetectedFilesystemAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesFilesystem); + break; + + case InstructionHandleResult.DetectedShellAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected shell or process access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesShell); + break; + case InstructionHandleResult.None: break; diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index cfa23d08..f3555c2d 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -24,6 +24,12 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedDynamic, /// The instruction is compatible, but references which may impact stability. - DetectedUnvalidatedUpdateTick + DetectedUnvalidatedUpdateTick, + + /// The instruction accesses the filesystem directly. + DetectedFilesystemAccess, + + /// The instruction accesses the OS shell or processes directly. + DetectedShellAccess } } diff --git a/src/SMAPI/Framework/ModLoading/ModWarning.cs b/src/SMAPI/Framework/ModLoading/ModWarning.cs index 0e4b2570..c62199b2 100644 --- a/src/SMAPI/Framework/ModLoading/ModWarning.cs +++ b/src/SMAPI/Framework/ModLoading/ModWarning.cs @@ -26,6 +26,12 @@ namespace StardewModdingAPI.Framework.ModLoading UsesUnvalidatedUpdateTick = 16, /// The mod has no update keys set. - NoUpdateKeys = 32 + NoUpdateKeys = 32, + + /// Uses .NET APIs for filesystem access. + AccessesFilesystem = 64, + + /// Uses .NET APIs for shell or process access. + AccessesShell = 128 } } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 15671af4..bdfaa670 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } + /// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access. + public bool ParanoidWarnings { get; set; } + /// Whether to show beta versions as valid updates. public bool UseBetaChannel { get; set; } = Constants.ApiVersion.IsPrerelease(); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 3e93760b..af8df8a0 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1026,6 +1026,17 @@ namespace StardewModdingAPI.Framework "These mods change the save serialiser. They may corrupt your save files, or make them unusable if", "you uninstall these mods." ); + if (this.Settings.ParanoidWarnings) + { + LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly", + "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be", + "legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); + LogWarningGroup(ModWarning.AccessesShell, LogLevel.Warn, "Accesses shell/process directly", + "These mods directly access the OS shell or processes, and you enabled paranoid warnings. (Note that", + "this may be legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); + } LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code", "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", "your game has issues, try removing these first. Otherwise you can ignore this warning." diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 2f0c1b15..5a47fb1a 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Metadata new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), /**** - ** detect incompatible code + ** detect mod issues ****/ // detect broken code new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies), @@ -61,7 +61,22 @@ namespace StardewModdingAPI.Metadata new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), - new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick) + new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick), + + /**** + ** detect paranoid issues + ****/ + // filesystem access + new TypeFinder(typeof(System.IO.File).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.FileStream).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.FileInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.Directory).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.DirectoryInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.DriveInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.FileSystemWatcher).FullName, InstructionHandleResult.DetectedFilesystemAccess), + + // shell access + new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess) }; } } diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 115997ba..d81292c8 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -9,10 +9,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ { /** - * Whether to enable features intended for mod developers. Currently this only makes TRACE-level - * messages appear in the console. + * The console color theme to use. The possible values are: + * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. + * - LightBackground: use darker text colors that look better on a white or light background. + * - DarkBackground: use lighter text colors that look better on a black or dark background. */ - "DeveloperMode": true, + "ColorScheme": "AutoDetect", /** * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new @@ -21,6 +23,20 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "CheckForUpdates": true, + /** + * Whether to enable features intended for mod developers. Currently this only makes TRACE-level + * messages appear in the console. + */ + "DeveloperMode": true, + + /** + * Whether to add a section to the 'mod issues' list for mods which directly use potentially + * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as + * part of their normal functionality, so these warnings are meaningless without further + * investigation. + */ + "ParanoidWarnings": false, + /** * Whether SMAPI should show newer beta versions as an available update. If not specified, SMAPI * will only show beta updates if the current version is beta. @@ -50,14 +66,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "DumpMetadata": false, - /** - * The console color theme to use. The possible values are: - * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. - * - LightBackground: use darker text colors that look better on a white or light background. - * - DarkBackground: use lighter text colors that look better on a black or dark background. - */ - "ColorScheme": "AutoDetect", - /** * The mod IDs SMAPI should ignore when performing update checks or validating update keys. */ -- cgit From c94f3e7c63a2f1aec89c68417db348d4e684fb79 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Sep 2018 19:19:13 -0400 Subject: only use valid update keys in update-check logic (#592) --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 10 ++++++++-- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 23 ++++++++++++++++------ src/SMAPI/Framework/SCore.cs | 13 +++++++----- .../Framework/UpdateData/UpdateKey.cs | 8 ++++++++ 5 files changed, 42 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5d0d8a4e..4fbb2e07 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ * Fixed installer duplicating bundled mods if you moved them after the last install. * Fixed crash when a mod manifest is corrupted. * Fixed error-handling when initialising paths. + * Fixed 'no update keys' warning not shown for mods with only invalid update keys. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 1a007297..85d1b619 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Framework { @@ -80,7 +82,11 @@ namespace StardewModdingAPI.Framework /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). bool HasID(); - /// Whether the mod has at least one update key set. - bool HasUpdateKeys(); + /// Get the defined update keys. + /// Only return valid update keys. + IEnumerable GetUpdateKeys(bool validOnly = true); + + /// Whether the mod has at least one valid update key set. + bool HasValidUpdateKeys(); } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 585debb4..c02f0830 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Framework.ModLoading { @@ -141,13 +143,22 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } - /// Whether the mod has at least one update key set. - public bool HasUpdateKeys() + /// Get the defined update keys. + /// Only return valid update keys. + public IEnumerable GetUpdateKeys(bool validOnly = false) { - return - this.HasManifest() - && this.Manifest.UpdateKeys != null - && this.Manifest.UpdateKeys.Any(key => !string.IsNullOrWhiteSpace(key)); + foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0]) + { + UpdateKey updateKey = UpdateKey.Parse(rawKey); + if (updateKey.LooksValid || !validOnly) + yield return updateKey; + } + } + + /// Whether the mod has at least one valid update key set. + public bool HasValidUpdateKeys() + { + return this.GetUpdateKeys(validOnly: true).Any(); } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index af8df8a0..59ce3be7 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -575,11 +575,14 @@ namespace StardewModdingAPI.Framework List searchMods = new List(); foreach (IModMetadata mod in mods) { - if (!mod.HasID()) + if (!mod.HasID() || suppressUpdateChecks.Contains(mod.Manifest.UniqueID)) continue; - string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; - searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + string[] updateKeys = mod + .GetUpdateKeys(validOnly: true) + .Select(p => p.ToString()) + .ToArray(); + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.ToArray())); } // fetch results @@ -699,7 +702,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)})...", LogLevel.Trace); // show warning for missing update key - if (metadata.HasManifest() && !metadata.HasUpdateKeys()) + if (metadata.HasManifest() && !metadata.HasValidUpdateKeys()) metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status @@ -745,7 +748,7 @@ namespace StardewModdingAPI.Framework : $" {metadata.DisplayName}...", LogLevel.Trace); // show warnings - if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) + if (metadata.HasManifest() && !metadata.HasValidUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status diff --git a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 49b7a272..865ebcf7 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -61,5 +61,13 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData return new UpdateKey(raw, repository, id); } + + /// Get a string that represents the current object. + public override string ToString() + { + return this.LooksValid + ? $"{this.Repository}:{this.ID}" + : this.RawText; + } } } -- cgit From ea3d3a6648f0a1c679a7593cfed0dacaed1ec045 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 19:13:03 -0400 Subject: make duplicate-mod more intuitive The error now shows the relative folder paths (instead of manifest IDs), and only one error will be logged instead of one per duplicate. --- docs/release-notes.md | 5 +---- src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 ++++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 5 +++-- src/SMAPI/Framework/SCore.cs | 10 +++++++--- 5 files changed, 20 insertions(+), 10 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 63db5dc3..8b644a43 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,10 +6,7 @@ * You can now group mods into subfolders to organise them. * Most SMAPI files are now tucked into a `smapi-internal` subfolder. * Save backups are now in a `save-backups` subfolder, so they're easier to access. Note that previous backups will be deleted when you update. - * Improved error messages when... - * an XNB mod is added to `Mods`; - * you install the wrong version of SMAPI for your OS; - * SMAPI can't prepare its folders. + * Improved various error messages to be more clear and intuitive. * Fixed transparency issues on Linux/Mac for some mod images. * Fixed error when a mod manifest is corrupted. * Fixed error when a mod adds an unnamed location. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 85d1b619..a62c9950 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -18,6 +18,9 @@ namespace StardewModdingAPI.Framework /// The mod's full directory path. string DirectoryPath { get; } + /// The relative to the game's Mods folder. + string RelativeDirectoryPath { get; } + /// Metadata about the mod from SMAPI's internal data (if any). ModDataRecordVersionedFields DataRecord { get; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index c02f0830..0a5f5d3f 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's full directory path. public string DirectoryPath { get; } + /// The relative to the game's Mods folder. + public string RelativeDirectoryPath { get; } + /// The mod manifest. public IManifest Manifest { get; } @@ -59,12 +62,14 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The mod's display name. /// The mod's full directory path. + /// The relative to the game's Mods folder. /// The mod manifest. /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) + public ModMetadata(string displayName, string directoryPath, string relativeDirectoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; + this.RelativeDirectoryPath = relativeDirectoryPath; this.Manifest = manifest; this.DataRecord = dataRecord; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 65a311dc..26ec82d7 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -41,7 +41,8 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } @@ -188,7 +189,7 @@ namespace StardewModdingAPI.Framework.ModLoading { 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))})."); + mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed ({string.Join(", ", group.Select(p => p.RelativeDirectoryPath).OrderBy(p => p))})."); } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2c5cd2bd..af517379 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -908,7 +908,7 @@ namespace StardewModdingAPI.Framework } // validate dependencies - // Although dependences are validated before mods are loaded, a dependency may have failed. + // Although dependences are validated before mods are loaded, a dependency may have failed to load. if (mod.Manifest.Dependencies?.Any() == true) { foreach (IManifestDependency dependency in mod.Manifest.Dependencies) @@ -955,7 +955,6 @@ namespace StardewModdingAPI.Framework catch (IncompatibleInstructionException) // details already in trace logs { string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."; return false; } @@ -1050,13 +1049,18 @@ namespace StardewModdingAPI.Framework this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); this.Monitor.Newline(); + HashSet logged = new HashSet(); foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; string errorReason = pair.Value.Item1; string errorDetails = pair.Value.Item2; + string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}"; + + if (!logged.Add($"{message}|{errorDetails}")) + continue; // skip duplicate messages (e.g. if multiple copies of the mod are installed) - this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}", LogLevel.Error); + this.Monitor.Log(message, LogLevel.Error); if (errorDetails != null) this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); } -- cgit From 88ea1eae13f3c5e3bfcedfb2ac9139c6dc829bac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Oct 2018 22:08:00 -0400 Subject: add support for ignored mod folders --- docs/release-notes.md | 4 +++- src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 ++++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 ++-- src/SMAPI/Framework/SCore.cs | 9 ++++++--- src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 7 ++++++- .../Framework/ModScanning/ModScanner.cs | 6 +++++- 7 files changed, 31 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 22c483c4..acbaef99 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,9 +3,11 @@ * For players: * Update checks now work even for mods without update keys in most cases. * Reorganised SMAPI files: - * You can now group mods into subfolders to organise them. * Most SMAPI files are now tucked into a `smapi-internal` subfolder. * Save backups are now in a `save-backups` subfolder, so they're easier to access. Note that previous backups will be deleted when you update. + * Added support for organising mods: + * You can now group mods into subfolders to organise them. + * You can now mark a mod folder ignored by starting the name with a dot (like `.disabled mods`). * Improved various error messages to be more clear and intuitive. * SMAPI now prevents a crash caused by mods adding dialogue the game can't parse. * When you have an older game version, SMAPI now recommends a compatible SMAPI version in its error. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index a62c9950..bda9429f 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework /// The reason the metadata is invalid, if any. string Error { get; } + /// Whether the mod folder should be ignored. This is true if it was found within a folder whose name starts with a dot. + bool IsIgnored { get; } + /// The mod instance (if loaded and is false). IMod Mod { get; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 0a5f5d3f..04aa679b 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The reason the metadata is invalid, if any. public string Error { get; private set; } + /// Whether the mod folder should be ignored. This is true if it was found within a folder whose name starts with a dot. + public bool IsIgnored { get; } + /// The mod instance (if loaded and is false). public IMod Mod { get; private set; } @@ -65,13 +68,15 @@ namespace StardewModdingAPI.Framework.ModLoading /// The relative to the game's Mods folder. /// The mod manifest. /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, string relativeDirectoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) + /// Whether the mod folder should be ignored. This should be true if it was found within a folder whose name starts with a dot. + public ModMetadata(string displayName, string directoryPath, string relativeDirectoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord, bool isIgnored) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; this.RelativeDirectoryPath = relativeDirectoryPath; this.Manifest = manifest; this.DataRecord = dataRecord; + this.IsIgnored = isIgnored; } /// Set the mod status. diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 26ec82d7..9992cc78 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -38,11 +38,11 @@ namespace StardewModdingAPI.Framework.ModLoading } // build metadata - ModMetadataStatus status = folder.ManifestParseError == null + ModMetadataStatus status = folder.ManifestParseError == null || !folder.ShouldBeLoaded ? ModMetadataStatus.Found : ModMetadataStatus.Failed; string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded).SetStatus(status, folder.ManifestParseError ?? "disabled by dot convention"); } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6c897382..a17af91e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -373,12 +373,15 @@ namespace StardewModdingAPI.Framework // load manifests IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); - // process dependencies - mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); + // filter out ignored mods + foreach (IModMetadata mod in mods.Where(p => p.IsIgnored)) + this.Monitor.Log($" Skipped {mod.RelativeDirectoryPath} (folder name starts with a dot).", LogLevel.Trace); + mods = mods.Where(p => !p.IsIgnored).ToArray(); // load mods + resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); + mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // write metadata file diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index d2fea9e2..bb467b36 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -24,6 +24,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The error which occurred parsing the manifest, if any. public string ManifestParseError { get; } + /// Whether the mod should be loaded by default. This is false if it was found within a folder whose name starts with a dot. + public bool ShouldBeLoaded { get; } + /********* ** Public methods @@ -33,12 +36,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The folder containing the mod's manifest.json. /// The mod manifest. /// The error which occurred parsing the manifest, if any. - public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null) + /// Whether the mod should be loaded by default. This should be false if it was found within a folder whose name starts with a dot. + public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) { // save info this.Directory = directory; this.Manifest = manifest; this.ManifestParseError = manifestParseError; + this.ShouldBeLoaded = shouldBeLoaded; // set display name this.DisplayName = manifest?.Name; diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 2c23a3ce..106c294f 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -102,8 +102,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The folder to search for mods. public IEnumerable GetModFolders(DirectoryInfo root, DirectoryInfo folder) { + // skip + if (folder.FullName != root.FullName && folder.Name.StartsWith(".")) + yield return new ModFolder(root, folder, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false); + // recurse into subfolders - if (this.IsModSearchFolder(root, folder)) + else if (this.IsModSearchFolder(root, folder)) { foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) { -- cgit From 6f23aaf2954f7eeb93b7cc5aad2d3f9b237883f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 2 Nov 2018 23:55:35 -0400 Subject: add internal mod.HasID helper (#480) --- src/SMAPI/Framework/IModMetadata.cs | 4 ++++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 9 +++++++++ src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/ModRegistry.cs | 2 +- src/SMAPI/Framework/SCore.cs | 6 +++--- 5 files changed, 18 insertions(+), 5 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index bda9429f..7ada7dea 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -88,6 +88,10 @@ namespace StardewModdingAPI.Framework /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). bool HasID(); + /// Whether the mod has the given ID. + /// The mod ID to check. + bool HasID(string id); + /// Get the defined update keys. /// Only return valid update keys. IEnumerable GetUpdateKeys(bool validOnly = true); diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 04aa679b..0cb62a75 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -153,6 +153,15 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } + /// Whether the mod has the given ID. + /// The mod ID to check. + public bool HasID(string id) + { + return + this.HasID() + && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.InvariantCultureIgnoreCase); + } + /// Get the defined update keys. /// Only return valid update keys. public IEnumerable GetUpdateKeys(bool validOnly = false) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9992cc78..3ff70d64 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -379,7 +379,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The loaded mods. private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) { - IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id)); // yield dependencies if (manifest.Dependencies != null) diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index e7d4f89a..da68fce3 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + return this.GetAll().FirstOrDefault(p => p.HasID(uniqueID)); } /// Get the mod metadata from one of its assemblies. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d59051fa..69b33699 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -750,7 +750,7 @@ namespace StardewModdingAPI.Framework // log loaded content packs if (loadedContentPacks.Any()) { - string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName; this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) @@ -907,7 +907,7 @@ namespace StardewModdingAPI.Framework if (this.ModRegistry.Get(dependency.UniqueID) == null) { string dependencyName = mods - .FirstOrDefault(otherMod => otherMod.HasID() && dependency.UniqueID.Equals(otherMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID)) ?.DisplayName ?? dependency.UniqueID; errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; return false; @@ -971,7 +971,7 @@ namespace StardewModdingAPI.Framework // get content packs IContentPack[] contentPacks = this.ModRegistry .GetAll(assemblyMods: false) - .Where(p => p.IsContentPack && mod.Manifest.UniqueID.Equals(p.Manifest.ContentPackFor.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID)) .Select(p => p.ContentPack) .ToArray(); -- cgit From 0717f926d879881e1f510a34942163bf275807dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:41:58 -0500 Subject: fix unit tests --- src/SMAPI.Tests/Core/ModResolverTests.cs | 1 + src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index a38621f8..51b839df 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -513,6 +513,7 @@ namespace StardewModdingAPI.Tests.Core mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); mod.Setup(p => p.Manifest).Returns(manifest); + mod.Setup(p => p.HasID(It.IsAny())).Returns((string id) => manifest.UniqueID == id); if (allowStatusChange) { mod diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 3ff70d64..0264f61f 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -42,7 +42,9 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded).SetStatus(status, folder.ManifestParseError ?? "disabled by dot convention"); + + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded) + .SetStatus(status, !folder.ShouldBeLoaded ? "disabled by dot convention" : folder.ManifestParseError); } } -- cgit From 724d3fccb1c7a5ec842314436126271c5697129a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:48:55 -0500 Subject: update mod compatibility page URLs --- src/SMAPI.Web/Controllers/ModsApiController.cs | 10 +++++----- src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs | 2 +- src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/appsettings.json | 2 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/SCore.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 6e517a97..05568d5e 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -47,8 +47,8 @@ namespace StardewModdingAPI.Web.Controllers /// The internal mod metadata list. private readonly ModDatabase ModDatabase; - /// The web URL for the wiki compatibility list. - private readonly string WikiCompatibilityPageUrl; + /// The web URL for the compatibility list. + private readonly string CompatibilityPageUrl; /********* @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Web.Controllers { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; - this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl; + this.CompatibilityPageUrl = config.CompatibilityPageUrl; this.Cache = cache; this.SuccessCacheMinutes = config.SuccessCacheMinutes; @@ -163,7 +163,7 @@ namespace StardewModdingAPI.Web.Controllers // get unofficial version if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, this.WikiCompatibilityPageUrl); + result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Web.Controllers if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) - ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, this.WikiCompatibilityPageUrl) + ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") : null; } else diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 5eef7c55..bde566c0 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -17,6 +17,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels public string SemanticVersionRegex { get; set; } /// The web URL for the wiki compatibility list. - public string WikiCompatibilityPageUrl { get; set; } + public string CompatibilityPageUrl { get; set; } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 82abf17d..60a16053 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -154,7 +154,7 @@ namespace StardewModdingAPI.Web // shortcut redirects redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1")); - redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility")); + redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://mods.smapi.io")); redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI")); diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 401b885f..aba8c448 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -48,6 +48,6 @@ "SuccessCacheMinutes": 60, "ErrorCacheMinutes": 5, "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", - "WikiCompatibilityPageUrl": "https://smapi.io/compat" + "CompatibilityPageUrl": "https://mods.smapi.io" } } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 0264f61f..ace84054 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModLoading updateUrls.Add(mod.DataRecord.AlternativeUrl); // default update URL - updateUrls.Add("https://smapi.io/compat"); + updateUrls.Add("https://mods.smapi.io"); // build error string error = $"{reasonPhrase}. Please check for a "; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f078acba..5f2e90aa 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -942,7 +942,7 @@ namespace StardewModdingAPI.Framework } catch (IncompatibleInstructionException) // details already in trace logs { - string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); + string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://mods.smapi.io" }.Where(p => p != null).ToArray(); errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."; return false; } -- cgit From fe56c6396a3d0817077ca9ae3b179f322957dfc4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Nov 2018 20:39:28 -0500 Subject: remove rewrite rules only used by mods that broke in SDV 1.3 --- .../Rewriters/VirtualEntryCallRemover.cs | 90 ---------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 9 --- src/SMAPI/StardewModdingAPI.csproj | 1 - 3 files changed, 100 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs deleted file mode 100644 index 322a7df1..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites virtual calls to the method. - internal class VirtualEntryCallRemover : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The type containing the method. - private readonly Type ToType; - - /// The name of the method. - private readonly string MethodName; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public VirtualEntryCallRemover() - { - this.ToType = typeof(Mod); - this.MethodName = nameof(Mod.Entry); - this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - // get instructions comprising method call - int index = cil.Body.Instructions.IndexOf(instruction); - Instruction loadArg0 = cil.Body.Instructions[index - 2]; - Instruction loadArg1 = cil.Body.Instructions[index - 1]; - if (loadArg0.OpCode != OpCodes.Ldarg_0) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); - if (loadArg1.OpCode != OpCodes.Ldarg_1) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); - - // remove method call - cil.Remove(loadArg0); - cil.Remove(loadArg1); - cil.Remove(instruction); - return InstructionHandleResult.Rewritten; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.ToType.FullName - && methodRef.Name == this.MethodName; - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 5a47fb1a..ff8d54e3 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -34,15 +34,6 @@ namespace StardewModdingAPI.Metadata // rewrite for crossplatform compatibility new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), - // rewrite for SMAPI 2.0 - new VirtualEntryCallRemover(), - - // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces) - new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 70fe2bed..5a098b8a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -227,7 +227,6 @@ - -- cgit