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) --- .../Serialisation/JsonHelper.cs | 16 ++++++++++------ src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs | 8 ++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs index cc8eeb73..dcc0dac4 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs @@ -95,18 +95,14 @@ namespace StardewModdingAPI.Toolkit.Serialisation Directory.CreateDirectory(dir); // write file - string json = JsonConvert.SerializeObject(model, this.JsonSettings); + string json = this.Serialise(model); File.WriteAllText(fullPath, json); } - - /********* - ** Private methods - *********/ /// Deserialize JSON text if possible. /// The model type. /// The raw JSON text. - private TModel Deserialise(string json) + public TModel Deserialise(string json) { try { @@ -127,5 +123,13 @@ namespace StardewModdingAPI.Toolkit.Serialisation throw; } } + + /// Serialize a model to JSON text. + /// The model type. + /// The model to serialise. + public string Serialise(TModel model) + { + return JsonConvert.SerializeObject(model, this.JsonSettings); + } } } diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs index 2e74e7d9..b959f9b5 100644 --- a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.Contracts; using System.IO; using System.Linq; +using System.Text.RegularExpressions; namespace StardewModdingAPI.Toolkit.Utilities { @@ -61,5 +62,12 @@ namespace StardewModdingAPI.Toolkit.Utilities relative = "./"; return relative; } + + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// The string to check. + public static bool IsSlug(string str) + { + return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); + } } } -- cgit From 944b2995f1bf7719cfcfb9bafe713523dbd8883f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Aug 2018 23:33:38 -0400 Subject: no longer allow non-relative paths for IContentPack.Read/WriteJsonFile (#468) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentPack.cs | 8 ++++++++ src/SMAPI/IContentPack.cs | 2 ++ src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs | 12 ++++++++++++ 4 files changed, 23 insertions(+) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index fc6ea97f..09d444a3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For modders: * Added `IContentPack.WriteJsonFile` method. + * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. ## 2.7 * For players: diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index ccb2b9a0..49285388 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -51,8 +51,12 @@ namespace StardewModdingAPI.Framework /// The model type. /// The file path relative to the contnet directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). public TModel ReadJsonFile(string path) where TModel : class { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.ReadJsonFile)} with a relative path."); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model) ? model @@ -63,8 +67,12 @@ namespace StardewModdingAPI.Framework /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the mod folder. /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). public void WriteJsonFile(string path, TModel data) where TModel : class { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.WriteJsonFile)} with a relative path."); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, data); } diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index fa793b13..9ba32394 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -25,12 +25,14 @@ namespace StardewModdingAPI /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the content pack directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). TModel ReadJsonFile(string path) where TModel : class; /// Save data to a JSON file in the content pack's folder. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the mod folder. /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). void WriteJsonFile(string path, TModel data) where TModel : class; /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs index b959f9b5..79748c25 100644 --- a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs @@ -63,6 +63,18 @@ namespace StardewModdingAPI.Toolkit.Utilities return relative; } + /// Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain ../). + /// The path to check. + public static bool IsSafeRelativePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return true; + + return + !Path.IsPathRooted(path) + && PathUtilities.GetSegments(path).All(segment => segment.Trim() != ".."); + } + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). /// The string to check. public static bool IsSlug(string str) -- cgit From 417c04076634ea87d7b3030a1acf46825da6e3e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 01:53:35 -0400 Subject: add data API (#468) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/DataHelper.cs | 155 +++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 28 ++-- src/SMAPI/IDataHelper.cs | 61 ++++++++ src/SMAPI/IModHelper.cs | 11 +- src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 2 + .../Serialisation/JsonHelper.cs | 5 +- 8 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/DataHelper.cs create mode 100644 src/SMAPI/IDataHelper.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 09d444a3..c7097f97 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ * Updated compatibility list. * For modders: + * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added `IContentPack.WriteJsonFile` method. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs new file mode 100644 index 00000000..6ba099b4 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -0,0 +1,155 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for reading and storing local mod data. + internal class DataHelper : BaseHelper, IDataHelper + { + /********* + ** Properties + *********/ + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + /// The absolute path to the mod folder. + private readonly string ModFolderPath; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The absolute path to the mod folder. + /// The absolute path to the mod folder. + public DataHelper(string modID, string modFolderPath, JsonHelper jsonHelper) + : base(modID) + { + this.ModFolderPath = modFolderPath; + this.JsonHelper = jsonHelper; + } + + /**** + ** JSON file + ****/ + /// Read data from a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). + public TModel ReadJsonFile(string path) where TModel : class + { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IModHelper.Data)}.{nameof(this.ReadJsonFile)} with a relative path."); + + path = Path.Combine(this.ModFolderPath, PathUtilities.NormalisePathSeparators(path)); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + ? data + : null; + } + + /// Save data to a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). + public void WriteJsonFile(string path, TModel data) where TModel : class + { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path."); + + path = Path.Combine(this.ModFolderPath, PathUtilities.NormalisePathSeparators(path)); + this.JsonHelper.WriteJsonFile(path, data); + } + + /**** + ** Save file + ****/ + /// Read arbitrary data stored in the current save slot. This is only possible if a save has been loaded. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + /// The player hasn't loaded a save file yet. + public TModel ReadSaveData(string key) where TModel : class + { + if (!Context.IsSaveLoaded) + throw new InvalidOperationException($"Can't invoke {nameof(this.ReadSaveData)} when a save file isn't loaded."); + + return Game1.CustomData.TryGetValue(this.GetSaveFileKey(key), out string value) + ? this.JsonHelper.Deserialise(value) + : null; + } + + /// Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + /// The player hasn't loaded a save file yet. + public void WriteSaveData(string key, TModel data) where TModel : class + { + if (!Context.IsSaveLoaded) + throw new InvalidOperationException($"Can't invoke {nameof(this.WriteSaveData)} when a save file isn't loaded."); + + Game1.CustomData[this.GetSaveFileKey(key)] = this.JsonHelper.Serialise(data, Formatting.None); + } + + /**** + ** Global app data + ****/ + /// Read arbitrary data stored on the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + public TModel ReadGlobalData(string key) where TModel : class + { + string path = this.GetGlobalDataPath(key); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + ? data + : null; + } + + /// Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + public void WriteGlobalData(string key, TModel data) where TModel : class + { + string path = this.GetGlobalDataPath(key); + this.JsonHelper.WriteJsonFile(path, data); + } + + + /********* + ** Public methods + *********/ + /// Get the unique key for a save file data entry. + /// The unique key identifying the data. + private string GetSaveFileKey(string key) + { + this.AssertSlug(key, nameof(key)); + return $"smapi-mod-data/{this.ModID}/{key}".ToLower(); + } + + /// Get the absolute path for a global data file. + /// The unique key identifying the data. + private string GetGlobalDataPath(string key) + { + this.AssertSlug(key, nameof(key)); + return Path.Combine(Constants.SavesPath, ".smapi-mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); + } + + /// Assert that a key contains only characters that are safe in all contexts. + /// The key to check. + /// The argument name for any assertion error. + private void AssertSlug(string key, string paramName) + { + if (!PathUtilities.IsSlug(key)) + throw new ArgumentException("The data key is invalid (keys must only contain letters, numbers, underscores, periods, or hyphens).", paramName); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 0ba258b4..0ed3df12 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,9 +4,7 @@ using System.IO; using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; -using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -16,9 +14,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Properties *********/ - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper; - /// The content packs loaded for this mod. private readonly IContentPack[] ContentPacks; @@ -41,6 +36,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for loading content assets. public IContentHelper Content { get; } + /// An API for reading and writing persistent mod data. + public IDataHelper Data { get; } + /// An API for checking and changing input state. public IInputHelper Input { get; } @@ -66,11 +64,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The mod's unique ID. /// The full path to the mod's folder. - /// Encapsulate SMAPI's JSON parsing. /// Manages the game's input state. /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. + /// An API for reading and writing persistent mod data. /// an API for fetching metadata about loaded mods. /// An API for accessing private game code. /// Provides multiplayer utilities. @@ -80,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -91,8 +89,8 @@ namespace StardewModdingAPI.Framework.ModHelpers // initialise this.DirectoryPath = modDirectory; - this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper)); this.Input = new InputHelper(modID, inputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); @@ -113,7 +111,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public TConfig ReadConfig() where TConfig : class, new() { - TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); + TConfig config = this.Data.ReadJsonFile("config.json") ?? new TConfig(); this.WriteConfig(config); // create file or fill in missing fields return config; } @@ -124,7 +122,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public void WriteConfig(TConfig config) where TConfig : class, new() { - this.WriteJsonFile("config.json", config); + this.Data.WriteJsonFile("config.json", config); } /**** @@ -134,24 +132,22 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The model type. /// The file path relative to the mod directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. + [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] public TModel ReadJsonFile(string path) where TModel : class { - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); - return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) - ? data - : null; + return this.Data.ReadJsonFile(path); } /// Save to a JSON file. /// The model type. /// The file path relative to the mod directory. /// The model to save. + [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] public void WriteJsonFile(string path, TModel model) where TModel : class { - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); - this.JsonHelper.WriteJsonFile(path, model); + this.Data.WriteJsonFile(path, model); } /**** diff --git a/src/SMAPI/IDataHelper.cs b/src/SMAPI/IDataHelper.cs new file mode 100644 index 00000000..722d5062 --- /dev/null +++ b/src/SMAPI/IDataHelper.cs @@ -0,0 +1,61 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for reading and storing local mod data. + public interface IDataHelper + { + /********* + ** Public methods + *********/ + /**** + ** JSON file + ****/ + /// Read data from a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). + TModel ReadJsonFile(string path) where TModel : class; + + /// Save data to a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). + void WriteJsonFile(string path, TModel data) where TModel : class; + + /**** + ** Save file + ****/ + /// Read arbitrary data stored in the current save slot. This is only possible if a save has been loaded. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + /// The player hasn't loaded a save file yet. + TModel ReadSaveData(string key) where TModel : class; + + /// Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + /// The player hasn't loaded a save file yet. + void WriteSaveData(string key, TModel data) where TModel : class; + + + /**** + ** Global app data + ****/ + /// Read arbitrary data stored on the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + TModel ReadGlobalData(string key) where TModel : class; + + /// Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + void WriteGlobalData(string key, TModel data) where TModel : class; + } +} diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index d7b8c986..e4b5d390 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -17,9 +17,15 @@ namespace StardewModdingAPI [Obsolete("This is an experimental interface which may change at any time. Don't depend on this for released mods.")] IModEvents Events { get; } + /// An API for managing console commands. + ICommandHelper ConsoleCommands { get; } + /// An API for loading content assets. IContentHelper Content { get; } + /// An API for reading and writing persistent mod data. + IDataHelper Data { get; } + /// An API for checking and changing input state. IInputHelper Input { get; } @@ -32,9 +38,6 @@ namespace StardewModdingAPI /// Provides multiplayer utilities. IMultiplayerHelper Multiplayer { get; } - /// An API for managing console commands. - ICommandHelper ConsoleCommands { get; } - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). ITranslationHelper Translation { get; } @@ -61,12 +64,14 @@ namespace StardewModdingAPI /// The model type. /// The file path relative to the mod directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. + [Obsolete("Use " + nameof(IModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] TModel ReadJsonFile(string path) where TModel : class; /// Save to a JSON file. /// The model type. /// The file path relative to the mod directory. /// The model to save. + [Obsolete("Use " + nameof(IModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] void WriteJsonFile(string path, TModel model) where TModel : class; /**** diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 634c5066..c40d2ff6 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -865,6 +865,7 @@ namespace StardewModdingAPI IModEvents events = new ModEvents(metadata, this.EventManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.GameInstance.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IDataHelper dataHelper = new DataHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); @@ -877,7 +878,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index fc2d45ba..713b1b31 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -109,6 +109,7 @@ + @@ -136,6 +137,7 @@ + diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs index dcc0dac4..cf2ce0d1 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs @@ -127,9 +127,10 @@ namespace StardewModdingAPI.Toolkit.Serialisation /// Serialize a model to JSON text. /// The model type. /// The model to serialise. - public string Serialise(TModel model) + /// The formatting to apply. + public string Serialise(TModel model, Formatting formatting = Formatting.Indented) { - return JsonConvert.SerializeObject(model, this.JsonSettings); + return JsonConvert.SerializeObject(model, formatting, this.JsonSettings); } } } -- 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/StardewModdingAPI.Toolkit') 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/StardewModdingAPI.Toolkit') 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 d2b6a71aa4cc3383d55350e346e27c1ab2ff134b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 01:36:11 -0400 Subject: fix crash when a mod manifest is corrupted --- docs/release-notes.md | 1 + src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3ce7d6bb..34f7404e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Moved most SMAPI files into a `smapi-internal` subfolder. * Moved save backups into a `save-backups` subfolder (instead of `Mods/SaveBackup/backups`). Note that previous backups will be deleted when you update. * Fixed installer duplicating bundled mods if you moved them after the last install. + * Fixed crash when a mod manifest is corrupted. * Updated compatibility list. * For modders: diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 71dc0cb3..7512d5cb 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { try { - if (!this.JsonHelper.ReadJsonFileIfExists(manifestFile.FullName, out manifest)) + if (!this.JsonHelper.ReadJsonFileIfExists(manifestFile.FullName, out manifest) || manifest == null) manifestError = "its manifest is invalid."; } catch (SParseException ex) -- cgit From cd83782ef982b9791b233e384170e0064564c041 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Aug 2018 20:35:13 -0400 Subject: fetch mod update keys from wiki when available --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 53 ++++++++++++++++------ .../Framework/ModData/ModDataRecord.cs | 9 ++++ 3 files changed, 49 insertions(+), 14 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6af726a1..12feb68a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ * Added support for subfolders under `Mods`, for players who want to organise their mods. * Moved most SMAPI files into a `smapi-internal` subfolder. * Moved save backups into a `save-backups` subfolder (instead of `Mods/SaveBackup/backups`). Note that previous backups will be deleted when you update. + * Update checks now work even when the mod has no update keys in most cases. * 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. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 18d55665..3d05da16 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -115,15 +115,10 @@ namespace StardewModdingAPI.Web.Controllers /// Returns the mod data if found, else null. private async Task GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata) { - // resolve update keys - var updateKeys = new HashSet(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + // crossreference data ModDataRecord record = this.ModDatabase.Get(search.ID); - if (record?.Fields != null) - { - string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; - if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) - updateKeys.Add(defaultUpdateKey); - } + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + string[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; @@ -166,7 +161,6 @@ namespace StardewModdingAPI.Web.Controllers } // get unofficial version - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); @@ -229,7 +223,7 @@ namespace StardewModdingAPI.Web.Controllers private async Task GetWikiDataAsync() { ModToolkit toolkit = new ModToolkit(); - return await this.Cache.GetOrCreateAsync($"_wiki", async entry => + return await this.Cache.GetOrCreateAsync("_wiki", async entry => { try { @@ -273,11 +267,42 @@ namespace StardewModdingAPI.Web.Controllers }); } - /// Get the requested API version. - private ISemanticVersion GetApiVersion() + /// Get update keys based on the available mod metadata, while maintaining the precedence order. + /// The specified update keys. + /// The mod's entry in SMAPI's internal database. + /// The mod's entry in the wiki list. + public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiCompatibilityEntry entry) { - string actualVersion = (string)this.RouteData.Values["version"]; - return new SemanticVersion(actualVersion); + IEnumerable GetRaw() + { + // specified update keys + if (specifiedKeys != null) + { + foreach (string key in specifiedKeys) + yield return key?.Trim(); + } + + // default update key + string defaultKey = record?.GetDefaultUpdateKey(); + if (defaultKey != null) + yield return defaultKey; + + // wiki metadata + if (entry != null) + { + if (entry.NexusID.HasValue) + yield return $"Nexus:{entry.NexusID}"; + if (entry.ChucklefishID.HasValue) + yield return $"Chucklefish:{entry.ChucklefishID}"; + } + } + + HashSet seen = new HashSet(StringComparer.InvariantCulture); + foreach (string key in GetRaw()) + { + if (!string.IsNullOrWhiteSpace(key) && seen.Add(key)) + yield return key; + } } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 82ac8837..3949f7dc 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -96,6 +96,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData .Distinct(); } + /// Get the default update key for this mod, if any. + public string GetDefaultUpdateKey() + { + string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + return !string.IsNullOrWhiteSpace(updateKey) + ? updateKey + : null; + } + /// Get a parsed representation of the which match a given manifest. /// The manifest to match. public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) -- cgit From 9c7858a28a8ae7e14a6654c41c2b5981d88b10ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Aug 2018 17:44:03 -0400 Subject: tweak semantic version converter to avoid invalid cast errors --- .../Serialisation/Converters/SemanticVersionConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 9b2f5e7d..39e990d5 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// The object type. public override bool CanConvert(Type objectType) { - return typeof(ISemanticVersion).IsAssignableFrom(objectType); + return objectType == typeof(ISemanticVersion); } /// Reads the JSON representation of the object. @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters return null; if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); - return (SemanticVersion)version; + return version; } } } -- cgit From 093117d777a84e3f1e3aaa8a1337059fb805a7dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Sep 2018 19:06:37 -0400 Subject: add update key parsing to toolkit (#592) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 44 ++++----------- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 9 --- .../Framework/ModRepositories/BaseRepository.cs | 6 +- .../ModRepositories/ChucklefishRepository.cs | 7 +-- .../Framework/ModRepositories/GitHubRepository.cs | 7 +-- .../Framework/ModRepositories/IModRepository.cs | 3 +- .../Framework/ModRepositories/NexusRepository.cs | 7 +-- src/SMAPI.Web/appsettings.json | 5 -- .../Framework/UpdateData/ModRepositoryKey.cs | 18 ++++++ .../Framework/UpdateData/UpdateKey.cs | 65 ++++++++++++++++++++++ 10 files changed, 109 insertions(+), 62 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 3d05da16..5ca8c94c 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -12,6 +12,7 @@ using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -29,7 +30,7 @@ namespace StardewModdingAPI.Web.Controllers ** Properties *********/ /// The mod repositories which provide mod metadata. - private readonly IDictionary Repositories; + private readonly IDictionary Repositories; /// The cache in which to store mod metadata. private readonly IMemoryCache Cache; @@ -73,11 +74,11 @@ namespace StardewModdingAPI.Web.Controllers this.Repositories = new IModRepository[] { - new ChucklefishRepository(config.ChucklefishKey, chucklefish), - new GitHubRepository(config.GitHubKey, github), - new NexusRepository(config.NexusKey, nexus) + new ChucklefishRepository(chucklefish), + new GitHubRepository(github), + new NexusRepository(nexus) } - .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + .ToDictionary(p => p.VendorKey); } /// Fetch version metadata for the given mods. @@ -189,28 +190,6 @@ namespace StardewModdingAPI.Web.Controllers return result; } - /// Parse a namespaced mod ID. - /// The raw mod ID to parse. - /// The parsed vendor key. - /// The parsed mod ID. - /// Returns whether the value could be parsed. - private bool TryParseModKey(string raw, out string vendorKey, out string modID) - { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - { - vendorKey = null; - modID = null; - return false; - } - - // parse - vendorKey = parts[0].Trim(); - modID = parts[1].Trim(); - return true; - } - /// Get whether a version is newer than an version. /// The current version. /// The other version. @@ -244,17 +223,18 @@ namespace StardewModdingAPI.Web.Controllers private async Task GetInfoForUpdateKeyAsync(string updateKey) { // parse update key - if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) + UpdateKey parsed = UpdateKey.Parse(updateKey); + if (!parsed.LooksValid) return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) - return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository)) + return new ModInfoModel($"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); // fetch mod info - return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{parsed.ID}".ToLower(), async entry => { - ModInfoModel result = await repository.GetModInfoAsync(modID); + ModInfoModel result = await repository.GetModInfoAsync(parsed.ID); if (result.Error != null) { if (result.Version == null) diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index ce4f3cb5..5eef7c55 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -16,15 +16,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// Derived from SMAPI's SemanticVersion implementation. public string SemanticVersionRegex { get; set; } - /// The repository key for the Chucklefish mod site. - public string ChucklefishKey { get; set; } - - /// The repository key for Nexus Mods. - public string GitHubKey { get; set; } - - /// The repository key for Nexus Mods. - public string NexusKey { get; set; } - /// The web URL for the wiki compatibility list. public string WikiCompatibilityPageUrl { get; set; } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index 4a4a40cd..94256005 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.ModRepositories { @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Accessors *********/ /// The unique key for this vendor. - public string VendorKey { get; } + public ModRepositoryKey VendorKey { get; } /********* @@ -29,7 +29,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories *********/ /// Construct an instance. /// The unique key for this vendor. - protected RepositoryBase(string vendorKey) + protected RepositoryBase(ModRepositoryKey vendorKey) { this.VendorKey = vendorKey; } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index e6074a60..6e2a8814 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories @@ -19,10 +19,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Public methods *********/ /// Construct an instance. - /// The unique key for this vendor. /// The underlying HTTP client. - public ChucklefishRepository(string vendorKey, IChucklefishClient client) - : base(vendorKey) + public ChucklefishRepository(IChucklefishClient client) + : base(ModRepositoryKey.Chucklefish) { this.Client = client; } diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 1d7e4fff..7ff22d0e 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories @@ -19,10 +19,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Public methods *********/ /// Construct an instance. - /// The unique key for this vendor. /// The underlying GitHub API client. - public GitHubRepository(string vendorKey, IGitHubClient client) - : base(vendorKey) + public GitHubRepository(IGitHubClient client) + : base(ModRepositoryKey.GitHub) { this.Client = client; } diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 09c59a86..68f754ae 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.ModRepositories { @@ -10,7 +11,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Accessors *********/ /// The unique key for this vendor. - string VendorKey { get; } + ModRepositoryKey VendorKey { get; } /********* diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 4afcda10..1e242c60 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories @@ -19,10 +19,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Public methods *********/ /// Construct an instance. - /// The unique key for this vendor. /// The underlying Nexus Mods API client. - public NexusRepository(string vendorKey, INexusClient client) - : base(vendorKey) + public NexusRepository(INexusClient client) + : base(ModRepositoryKey.Nexus) { this.Client = client; } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index bd948aa0..2c6aa0cc 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -47,11 +47,6 @@ "SuccessCacheMinutes": 60, "ErrorCacheMinutes": 5, "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", - - "ChucklefishKey": "Chucklefish", - "GitHubKey": "GitHub", - "NexusKey": "Nexus", - "WikiCompatibilityPageUrl": "https://smapi.io/compat" } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs new file mode 100644 index 00000000..7ca32f04 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.UpdateData +{ + /// A mod repository which SMAPI can check for updates. + public enum ModRepositoryKey + { + /// An unknown or invalid mod repository. + Unknown, + + /// The Chucklefish mod repository. + Chucklefish, + + /// A GitHub project containing releases. + GitHub, + + /// The Nexus Mods mod repository. + Nexus + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs new file mode 100644 index 00000000..49b7a272 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -0,0 +1,65 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Framework.UpdateData +{ + /// A namespaced mod ID which uniquely identifies a mod within a mod repository. + public class UpdateKey + { + /********* + ** Accessors + *********/ + /// The raw update key text. + public string RawText { get; } + + /// The mod repository containing the mod. + public ModRepositoryKey Repository { get; } + + /// The mod ID within the repository. + public string ID { get; } + + /// Whether the update key seems to be valid. + public bool LooksValid { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The raw update key text. + /// The mod repository containing the mod. + /// The mod ID within the repository. + public UpdateKey(string rawText, ModRepositoryKey repository, string id) + { + this.RawText = rawText; + this.Repository = repository; + this.ID = id; + this.LooksValid = + repository != ModRepositoryKey.Unknown + && !string.IsNullOrWhiteSpace(id); + } + + /// Parse a raw update key. + /// The raw update key to parse. + public static UpdateKey Parse(string raw) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + return new UpdateKey(raw, ModRepositoryKey.Unknown, null); + + // extract parts + string repositoryKey = parts[0].Trim(); + string id = parts[1].Trim(); + if (string.IsNullOrWhiteSpace(id)) + id = null; + + // parse + if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository)) + return new UpdateKey(raw, ModRepositoryKey.Unknown, id); + if (id == null) + return new UpdateKey(raw, repository, null); + + return new UpdateKey(raw, repository, id); + } + } +} -- 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/StardewModdingAPI.Toolkit') 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 47101419f2cf4b3c532fd996840d7301e77e7785 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Sep 2018 20:22:42 -0400 Subject: fix SemanticVersionConverter no longer writing JSON in some cases --- .../Serialisation/Converters/SemanticVersionConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 39e990d5..070f2c97 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// The object type. public override bool CanConvert(Type objectType) { - return objectType == typeof(ISemanticVersion); + return typeof(ISemanticVersion).IsAssignableFrom(objectType); } /// Reads the JSON representation of the object. -- cgit From f2cb952dd1b3752bd172161afadf956b195ec73f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Sep 2018 21:41:02 -0400 Subject: add support for parallel stable/beta unofficial versions (#594) --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 17 ++++++ src/SMAPI/Framework/SCore.cs | 5 +- .../Framework/Clients/WebApi/ModEntryModel.cs | 6 ++ .../Clients/WebApi/ModExtendedMetadataModel.cs | 21 +++++++ .../Clients/Wiki/WikiCompatibilityClient.cs | 66 +++++++++++++++------- .../Clients/Wiki/WikiCompatibilityEntry.cs | 32 +++++++++-- 7 files changed, 121 insertions(+), 27 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index bf0abae3..b9cee4c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,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 support for parallel stable/beta unofficial updates in update checks. * 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.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 5ca8c94c..592c8f97 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -165,6 +165,23 @@ namespace StardewModdingAPI.Web.Controllers if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); + // get unofficial version for beta + if (wikiEntry?.HasBetaInfo == true) + { + result.HasBetaInfo = true; + if (wikiEntry.BetaStatus == WikiCompatibilityStatus.Unofficial) + { + if (wikiEntry.BetaUnofficialVersion != null) + { + result.UnofficialForBeta = (wikiEntry.BetaUnofficialVersion != null && this.IsNewer(wikiEntry.BetaUnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaUnofficialVersion, result.Optional?.Version)) + ? new ModEntryVersionModel(wikiEntry.BetaUnofficialVersion, this.WikiCompatibilityPageUrl) + : null; + } + else + result.UnofficialForBeta = result.Unofficial; + } + } + // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 59ce3be7..00e608c1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -609,10 +609,11 @@ namespace StardewModdingAPI.Framework } // parse versions + bool useBetaInfo = result.HasBetaInfo && Constants.ApiVersion.IsPrerelease(); ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; - ISemanticVersion unofficialVersion = result.Unofficial?.Version; + ISemanticVersion unofficialVersion = useBetaInfo ? result.UnofficialForBeta?.Version : result.Unofficial?.Version; // show update alerts if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) @@ -620,7 +621,7 @@ namespace StardewModdingAPI.Framework else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) - updates.Add(Tuple.Create(mod, unofficialVersion, result.Unofficial?.Url)); + updates.Add(Tuple.Create(mod, unofficialVersion, useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url)); } // show update errors diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 2aafe199..8a9c0a25 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -18,9 +18,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest unofficial version, if newer than and . public ModEntryVersionModel Unofficial { get; set; } + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + public ModEntryVersionModel UnofficialForBeta { get; set; } + /// Optional extended data which isn't needed for update checks. public ModExtendedMetadataModel Metadata { get; set; } + /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . + public bool HasBetaInfo { get; set; } + /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 21376b36..0f3cb26f 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -13,6 +13,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /********* ** Accessors *********/ + /**** + ** Mod info + ****/ /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). public string[] ID { get; set; } = new string[0]; @@ -34,6 +37,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The custom mod page URL (if applicable). public string CustomUrl { get; set; } + /**** + ** Stable compatibility + ****/ /// The compatibility status. [JsonConverter(typeof(StringEnumConverter))] public WikiCompatibilityStatus? CompatibilityStatus { get; set; } @@ -42,6 +48,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public string CompatibilitySummary { get; set; } + /**** + ** Beta compatibility + ****/ + /// The compatibility status for the Stardew Valley beta (if any). + [JsonConverter(typeof(StringEnumConverter))] + public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatitng. + public string BetaCompatibilitySummary { get; set; } + + /********* ** Public methods *********/ @@ -63,8 +80,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.GitHubRepo = wiki.GitHubRepo; this.CustomSourceUrl = wiki.CustomSourceUrl; this.CustomUrl = wiki.CustomUrl; + this.CompatibilityStatus = wiki.Status; this.CompatibilitySummary = wiki.Summary; + + this.BetaCompatibilityStatus = wiki.BetaStatus; + this.BetaCompatibilitySummary = wiki.BetaSummary; } // internal DB data diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index d0da42df..929284c3 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -73,26 +73,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { foreach (HtmlNode node in nodes) { - // parse status - WikiCompatibilityStatus status; - { - string rawStatus = node.GetAttributeValue("data-status", null); - if (rawStatus == null) - continue; // not a mod node? - if (!Enum.TryParse(rawStatus, true, out status)) - throw new InvalidOperationException($"Unknown status '{rawStatus}' when parsing compatibility list."); - } - - // parse unofficial version - ISemanticVersion unofficialVersion = null; - { - string rawUnofficialVersion = node.GetAttributeValue("data-unofficial-version", null); - SemanticVersion.TryParse(rawUnofficialVersion, out unofficialVersion); - } - - // parse other fields + // parse mod info string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim(); - string summary = node.Descendants("td").FirstOrDefault(p => p.GetAttributeValue("class", null) == "summary")?.InnerText.Trim(); string[] ids = this.GetAttribute(node, "data-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); @@ -100,23 +82,65 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string customSourceUrl = this.GetAttribute(node, "data-custom-source"); string customUrl = this.GetAttribute(node, "data-custom-url"); + // parse stable compatibility + WikiCompatibilityStatus status = this.GetStatusAttribute(node, "data-status") ?? WikiCompatibilityStatus.Ok; + ISemanticVersion unofficialVersion = this.GetSemanticVersionAttribute(node, "data-unofficial-version"); + string summary = node.Descendants().FirstOrDefault(p => p.HasClass("data-summary"))?.InnerText.Trim(); + + // parse beta compatibility + WikiCompatibilityStatus? betaStatus = this.GetStatusAttribute(node, "data-beta-status"); + ISemanticVersion betaUnofficialVersion = betaStatus.HasValue ? this.GetSemanticVersionAttribute(node, "data-beta-unofficial-version") : null; + string betaSummary = betaStatus.HasValue ? node.Descendants().FirstOrDefault(p => p.HasClass("data-beta-summary"))?.InnerText.Trim() : null; + // yield model yield return new WikiCompatibilityEntry { + // mod info ID = ids, Name = name, - Status = status, NexusID = nexusID, ChucklefishID = chucklefishID, GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, CustomUrl = customUrl, + + // stable compatibility + Status = status, + Summary = summary, UnofficialVersion = unofficialVersion, - Summary = summary + + // beta compatibility + BetaStatus = betaStatus, + BetaSummary = betaSummary, + BetaUnofficialVersion = betaUnofficialVersion }; } } + /// Get a compatibility status attribute value. + /// The HTML node. + /// The attribute name. + private WikiCompatibilityStatus? GetStatusAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + if (raw == null) + return null; // not a mod node? + if (!Enum.TryParse(raw, true, out WikiCompatibilityStatus status)) + throw new InvalidOperationException($"Unknown status '{raw}' when parsing compatibility list."); + return status; + } + + /// Get a semantic version attribute value. + /// The HTML node. + /// The attribute name. + private ISemanticVersion GetSemanticVersionAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + return SemanticVersion.TryParse(raw, out ISemanticVersion version) + ? version + : null; + } + /// Get a nullable integer attribute value. /// The HTML node. /// The attribute name. diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs index 8bc66e20..3cb9c97c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -3,6 +3,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// An entry in the mod compatibility list. public class WikiCompatibilityEntry { + /********* + ** Accessors + *********/ + /**** + ** Mod info + ****/ /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). public string[] ID { get; set; } @@ -24,13 +30,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The custom mod page URL (if applicable). public string CustomUrl { get; set; } - /// The version of the latest unofficial update, if applicable. - public ISemanticVersion UnofficialVersion { get; set; } - + /**** + ** Stable compatibility + ****/ /// The compatibility status. public WikiCompatibilityStatus Status { get; set; } - /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. public string Summary { get; set; } + + /// The version of the latest unofficial update, if applicable. + public ISemanticVersion UnofficialVersion { get; set; } + + /**** + ** Beta compatibility + ****/ + /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . + public bool HasBetaInfo => this.BetaStatus != null; + + /// The compatibility status for the Stardew Valley beta, if a beta is in progress. + public WikiCompatibilityStatus? BetaStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting. + public string BetaSummary { get; set; } + + /// The version of the latest unofficial update for the Stardew Valley beta (if any), if applicable. + public ISemanticVersion BetaUnofficialVersion { get; set; } } } -- cgit From b1db14094d77ab5bd61ac0e02917a2510074a0d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 12 Sep 2018 19:43:11 -0400 Subject: update internal dependencies --- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 2 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 8 ++++---- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index c6241ecb..ca962b6d 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 6d2b0bec..e993e914 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -31,7 +31,7 @@ - + diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 6761c7ad..dc87cc98 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 21c130b3..91aae3e6 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -12,7 +12,7 @@ - + -- cgit From 074f730329659d0db4be3a99fa8e5c09383ca3e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 27 Sep 2018 00:36:31 -0400 Subject: add separate error when player puts an XNB mod in Mods --- docs/release-notes.md | 1 + .../Framework/ModScanning/ModScanner.cs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3fea347e..12886019 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Moved most SMAPI files into a `smapi-internal` subfolder. * Moved save backups into a `save-backups` subfolder (instead of `Mods/SaveBackup/backups`). Note that previous backups will be deleted when you update. * Update checks now work even when the mod has no update keys in most cases. + * Improved error when you put an XNB mod in `Mods`. * Fixed error when mods add an invalid location with no name. * Fixed compatibility issues for some Linux players. SMAPI will now always use xterm if it's available. * Fixed some game install paths not detected on Windows. diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 7512d5cb..2c23a3ce 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -24,6 +24,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning "Thumbs.db" }; + /// The extensions for files which an XNB mod may contain. If a mod contains *only* these file extensions, it should be considered an XNB mod. + private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ".md", + ".png", + ".txt", + ".xnb" + }; + /********* ** Public methods @@ -50,11 +59,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); + + // set appropriate invalid-mod error if (manifestFile == null) { - bool isEmpty = !searchFolder.GetFileSystemInfos().Where(this.IsRelevant).Any(); - if (isEmpty) + FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).ToArray(); + if (!files.Any()) return new ModFolder(root, searchFolder, null, "it's an empty folder."); + if (files.All(file => this.PotentialXnbModExtensions.Contains(file.Extension))) + return new ModFolder(root, searchFolder, null, "it's an older XNB mod which replaces game files (not run through SMAPI)."); return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json."); } -- cgit From 91b3344feafc5c2da6f4560783575c27eb43a42e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Sep 2018 18:18:01 -0400 Subject: fix mod web API returning a concatenated name for mods with alternate names --- docs/release-notes.md | 1 + .../Framework/Clients/Wiki/WikiCompatibilityClient.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 12886019..7c25eba6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -35,6 +35,7 @@ * 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`). + * Fixed mod web API returning a concatenated name for mods with alternate names. ## 2.7 * For players: diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index 929284c3..4060ed36 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -74,7 +74,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki foreach (HtmlNode node in nodes) { // parse mod info - string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim(); + string name = node.Descendants("td").FirstOrDefault()?.Descendants("a")?.FirstOrDefault()?.InnerText?.Trim(); string[] ids = this.GetAttribute(node, "data-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); -- cgit From 6c39a31f72c5d6e1e202fe3fbed5f775976e448b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 1 Oct 2018 19:32:49 -0400 Subject: special-case '-unofficial' when comparing versions --- docs/release-notes.md | 1 + src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 1 + src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 22 ++++++++++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8573c175..beba403a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -27,6 +27,7 @@ * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. * Mods are no longer prevented from loading a PNG while the game is drawing. + * When comparing mod versions, SMAPI now considers `-unofficial` to be lower-precedence than any other value (e.g. `1.0-beta` is now considered newer than `1.0-unofficial` regardless of normal sorting). * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. * Fixed trace logs not showing path for invalid mods. * Fixed 'no update keys' warning not shown for mods with only invalid update keys. diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 35d74b60..1782308b 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -127,6 +127,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = -1)] [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = -1)] [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = -1)] + [TestCase("1.0-unofficial.1", "1.0-beta.1", ExpectedResult = -1)] // special case: 'unofficial' has lower priority than official releases // more than [TestCase("0.5.8", "0.5.7", ExpectedResult = 1)] diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index 156d58ce..2a78d2f0 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -4,7 +4,13 @@ using System.Text.RegularExpressions; namespace StardewModdingAPI.Toolkit { /// A semantic version with an optional release tag. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + /// + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/), with a few deviations: + /// - short-form "x.y" versions are supported (equivalent to "x.y.0"); + /// - hyphens are synonymous with dots in prerelease tags (like "-unofficial.3-pathoschild"); + /// - +build suffixes are not supported; + /// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial"). + /// public class SemanticVersion : ISemanticVersion { /********* @@ -17,13 +23,7 @@ namespace StardewModdingAPI.Toolkit internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?" + SemanticVersion.TagPattern + "))?"; /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// + /// This pattern is derived from the BNF documentation in the semver repo, with deviations to support the Stardew Valley mod conventions (see remarks on ). internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); @@ -255,6 +255,12 @@ namespace StardewModdingAPI.Toolkit // compare if different if (curParts[i] != otherParts[i]) { + // unofficial is always lower-precedence + if (otherParts[i].Equals("unofficial", StringComparison.InvariantCultureIgnoreCase)) + return curNewer; + if (curParts[i].Equals("unofficial", StringComparison.InvariantCultureIgnoreCase)) + return curOlder; + // compare numerically if possible { if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) -- cgit From f09befe24047de8187276c722557b6f0fddd6e35 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Oct 2018 14:55:13 -0400 Subject: expand metadata fetched from the wiki (#597) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 26 ++--- .../Clients/WebApi/ModExtendedMetadataModel.cs | 10 +- .../Clients/Wiki/WikiCompatibilityClient.cs | 123 ++++++++++++--------- .../Clients/Wiki/WikiCompatibilityEntry.cs | 60 ---------- .../Clients/Wiki/WikiCompatibilityInfo.cs | 21 ++++ .../Framework/Clients/Wiki/WikiModEntry.cs | 54 +++++++++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 2 +- 7 files changed, 162 insertions(+), 134 deletions(-) delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 592c8f97..5caa5758 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -90,7 +90,7 @@ namespace StardewModdingAPI.Web.Controllers return new ModEntryModel[0]; // fetch wiki data - WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); + WikiModEntry[] wikiData = await this.GetWikiDataAsync(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in model.Mods) { @@ -114,11 +114,11 @@ namespace StardewModdingAPI.Web.Controllers /// The wiki data. /// Whether to include extended metadata for each mod. /// Returns the mod data if found, else null. - private async Task GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata) + private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) { // crossreference data ModDataRecord record = this.ModDatabase.Get(search.ID); - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); string[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); // get latest versions @@ -162,19 +162,19 @@ namespace StardewModdingAPI.Web.Controllers } // get unofficial version - if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); + 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); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { result.HasBetaInfo = true; - if (wikiEntry.BetaStatus == WikiCompatibilityStatus.Unofficial) + if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { - if (wikiEntry.BetaUnofficialVersion != null) + if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { - result.UnofficialForBeta = (wikiEntry.BetaUnofficialVersion != null && this.IsNewer(wikiEntry.BetaUnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaUnofficialVersion, result.Optional?.Version)) - ? new ModEntryVersionModel(wikiEntry.BetaUnofficialVersion, this.WikiCompatibilityPageUrl) + 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) : null; } else @@ -216,21 +216,21 @@ namespace StardewModdingAPI.Web.Controllers } /// Get mod data from the wiki compatibility list. - private async Task GetWikiDataAsync() + private async Task GetWikiDataAsync() { ModToolkit toolkit = new ModToolkit(); return await this.Cache.GetOrCreateAsync("_wiki", async entry => { try { - WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync(); + WikiModEntry[] entries = await toolkit.GetWikiCompatibilityListAsync(); entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes); return entries; } catch { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes); - return new WikiCompatibilityEntry[0]; + return new WikiModEntry[0]; } }); } @@ -268,7 +268,7 @@ namespace StardewModdingAPI.Web.Controllers /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiCompatibilityEntry entry) + public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { IEnumerable GetRaw() { diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 0f3cb26f..e6f2e4b4 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -68,7 +68,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The mod metadata from the wiki (if available). /// The mod metadata from SMAPI's internal DB (if available). - public ModExtendedMetadataModel(WikiCompatibilityEntry wiki, ModDataRecord db) + public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) { // wiki data if (wiki != null) @@ -81,11 +81,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.CustomSourceUrl = wiki.CustomSourceUrl; this.CustomUrl = wiki.CustomUrl; - this.CompatibilityStatus = wiki.Status; - this.CompatibilitySummary = wiki.Summary; + this.CompatibilityStatus = wiki.Compatibility.Status; + this.CompatibilitySummary = wiki.Compatibility.Summary; - this.BetaCompatibilityStatus = wiki.BetaStatus; - this.BetaCompatibilitySummary = wiki.BetaSummary; + this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status; + this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary; } // internal DB data diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index 4060ed36..569e820b 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } /// Fetch mod compatibility entries. - public async Task FetchAsync() + public async Task FetchAsync() { // fetch HTML ResponseModel response = await this.Client @@ -69,100 +69,113 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki *********/ /// Parse valid mod compatibility entries. /// The HTML compatibility entries. - private IEnumerable ParseEntries(IEnumerable nodes) + private IEnumerable ParseEntries(IEnumerable nodes) { foreach (HtmlNode node in nodes) { - // parse mod info - string name = node.Descendants("td").FirstOrDefault()?.Descendants("a")?.FirstOrDefault()?.InnerText?.Trim(); - string[] ids = this.GetAttribute(node, "data-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; - int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); - int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); - string githubRepo = this.GetAttribute(node, "data-github"); - string customSourceUrl = this.GetAttribute(node, "data-custom-source"); - string customUrl = this.GetAttribute(node, "data-custom-url"); + // extract fields + string name = this.GetMetadataField(node, "mod-name"); + string alternateNames = this.GetMetadataField(node, "mod-name2"); + string author = this.GetMetadataField(node, "mod-author"); + string alternateAuthors = this.GetMetadataField(node, "mod-author2"); + string[] ids = this.GetMetadataField(node, "mod-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; + int? nexusID = this.GetNullableIntField(node, "mod-nexus-id"); + int? chucklefishID = this.GetNullableIntField(node, "mod-chucklefish-id"); + string githubRepo = this.GetMetadataField(node, "mod-github"); + string customSourceUrl = this.GetMetadataField(node, "mod-custom-source"); + string customUrl = this.GetMetadataField(node, "mod-url"); + string brokeIn = this.GetMetadataField(node, "mod-broke-in"); + string anchor = this.GetMetadataField(node, "mod-anchor"); // parse stable compatibility - WikiCompatibilityStatus status = this.GetStatusAttribute(node, "data-status") ?? WikiCompatibilityStatus.Ok; - ISemanticVersion unofficialVersion = this.GetSemanticVersionAttribute(node, "data-unofficial-version"); - string summary = node.Descendants().FirstOrDefault(p => p.HasClass("data-summary"))?.InnerText.Trim(); + WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo + { + Status = this.GetStatusField(node, "mod-status") ?? WikiCompatibilityStatus.Ok, + UnofficialVersion = this.GetSemanticVersionField(node, "mod-unofficial-version"), + UnofficialUrl = this.GetMetadataField(node, "mod-unofficial-url"), + Summary = this.GetMetadataField(node, "mod-summary")?.Trim() + }; // parse beta compatibility - WikiCompatibilityStatus? betaStatus = this.GetStatusAttribute(node, "data-beta-status"); - ISemanticVersion betaUnofficialVersion = betaStatus.HasValue ? this.GetSemanticVersionAttribute(node, "data-beta-unofficial-version") : null; - string betaSummary = betaStatus.HasValue ? node.Descendants().FirstOrDefault(p => p.HasClass("data-beta-summary"))?.InnerText.Trim() : null; + WikiCompatibilityInfo betaCompatibility = null; + { + WikiCompatibilityStatus? betaStatus = this.GetStatusField(node, "mod-beta-status"); + if (betaStatus.HasValue) + { + betaCompatibility = new WikiCompatibilityInfo + { + Status = betaStatus.Value, + UnofficialVersion = this.GetSemanticVersionField(node, "mod-beta-unofficial-version"), + UnofficialUrl = this.GetMetadataField(node, "mod-beta-unofficial-url"), + Summary = this.GetMetadataField(node, "mod-beta-summary") + }; + } + } // yield model - yield return new WikiCompatibilityEntry + yield return new WikiModEntry { - // mod info ID = ids, Name = name, + AlternateNames = alternateNames, + Author = author, + AlternateAuthors = alternateAuthors, NexusID = nexusID, ChucklefishID = chucklefishID, GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, CustomUrl = customUrl, - - // stable compatibility - Status = status, - Summary = summary, - UnofficialVersion = unofficialVersion, - - // beta compatibility - BetaStatus = betaStatus, - BetaSummary = betaSummary, - BetaUnofficialVersion = betaUnofficialVersion + BrokeIn = brokeIn, + Compatibility = compatibility, + BetaCompatibility = betaCompatibility, + Anchor = anchor }; } } - /// Get a compatibility status attribute value. - /// The HTML node. - /// The attribute name. - private WikiCompatibilityStatus? GetStatusAttribute(HtmlNode node, string attributeName) + /// Get the value of a metadata field. + /// The metadata container. + /// The field name. + private string GetMetadataField(HtmlNode container, string name) { - string raw = node.GetAttributeValue(attributeName, null); + return container.Descendants().FirstOrDefault(p => p.HasClass(name))?.InnerHtml; + } + + /// Get the value of a metadata field as a compatibility status. + /// The metadata container. + /// The field name. + private WikiCompatibilityStatus? GetStatusField(HtmlNode container, string name) + { + string raw = this.GetMetadataField(container, name); if (raw == null) - return null; // not a mod node? + return null; if (!Enum.TryParse(raw, true, out WikiCompatibilityStatus status)) throw new InvalidOperationException($"Unknown status '{raw}' when parsing compatibility list."); return status; } - /// Get a semantic version attribute value. - /// The HTML node. - /// The attribute name. - private ISemanticVersion GetSemanticVersionAttribute(HtmlNode node, string attributeName) + /// Get the value of a metadata field as a semantic version. + /// The metadata container. + /// The field name. + private ISemanticVersion GetSemanticVersionField(HtmlNode container, string name) { - string raw = node.GetAttributeValue(attributeName, null); + string raw = this.GetMetadataField(container, name); return SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version : null; } - /// Get a nullable integer attribute value. - /// The HTML node. - /// The attribute name. - private int? GetNullableIntAttribute(HtmlNode node, string attributeName) + /// Get the value of a metadata field as a nullable integer. + /// The metadata container. + /// The field name. + private int? GetNullableIntField(HtmlNode container, string name) { - string raw = this.GetAttribute(node, attributeName); + string raw = this.GetMetadataField(container, name); if (raw != null && int.TryParse(raw, out int value)) return value; return null; } - /// Get a strings attribute value. - /// The HTML node. - /// The attribute name. - private string GetAttribute(HtmlNode node, string attributeName) - { - string raw = node.GetAttributeValue(attributeName, null); - if (raw != null) - raw = HtmlEntity.DeEntitize(raw); - return raw; - } - /// The response model for the MediaWiki parse API. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs deleted file mode 100644 index 3cb9c97c..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// An entry in the mod compatibility list. - public class WikiCompatibilityEntry - { - /********* - ** Accessors - *********/ - /**** - ** Mod info - ****/ - /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). - public string[] ID { get; set; } - - /// The mod's display name. - public string Name { get; set; } - - /// The mod ID on Nexus. - public int? NexusID { get; set; } - - /// The mod ID in the Chucklefish mod repo. - public int? ChucklefishID { get; set; } - - /// The GitHub repository in the form 'owner/repo'. - public string GitHubRepo { get; set; } - - /// The URL to a non-GitHub source repo. - public string CustomSourceUrl { get; set; } - - /// The custom mod page URL (if applicable). - public string CustomUrl { get; set; } - - /**** - ** Stable compatibility - ****/ - /// The compatibility status. - public WikiCompatibilityStatus Status { get; set; } - - /// The human-readable summary of the compatibility status or workaround, without HTML formatting. - public string Summary { get; set; } - - /// The version of the latest unofficial update, if applicable. - public ISemanticVersion UnofficialVersion { get; set; } - - /**** - ** Beta compatibility - ****/ - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - public bool HasBetaInfo => this.BetaStatus != null; - - /// The compatibility status for the Stardew Valley beta, if a beta is in progress. - public WikiCompatibilityStatus? BetaStatus { get; set; } - - /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting. - public string BetaSummary { get; set; } - - /// The version of the latest unofficial update for the Stardew Valley beta (if any), if applicable. - public ISemanticVersion BetaUnofficialVersion { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs new file mode 100644 index 00000000..2725df1a --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// Compatibility info for a mod. + public class WikiCompatibilityInfo + { + /********* + ** Accessors + *********/ + /// The compatibility status. + public WikiCompatibilityStatus Status { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + public string Summary { get; set; } + + /// The version of the latest unofficial update, if applicable. + public ISemanticVersion UnofficialVersion { get; set; } + + /// The URL to the latest unofficial update, if applicable. + public string UnofficialUrl { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs new file mode 100644 index 00000000..752b526c --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -0,0 +1,54 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// A mod entry in the wiki list. + public class WikiModEntry + { + /********* + ** Accessors + *********/ + /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + public string[] ID { get; set; } + + /// The mod's display name. + public string Name { get; set; } + + /// The mod's alternative names, if any. + public string AlternateNames { get; set; } + + /// The mod's author name. + public string Author { get; set; } + + /// The mod's alternative author names, if any. + public string AlternateAuthors { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string BrokeIn { get; set; } + + /// The mod's compatibility with the latest stable version of the game. + public WikiCompatibilityInfo Compatibility { get; set; } + + /// The mod's compatibility with the latest beta version of the game (if any). + public WikiCompatibilityInfo BetaCompatibility { get; set; } + + /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . + public bool HasBetaInfo => this.BetaCompatibility != null; + + /// The link anchor for the mod entry in the wiki compatibility list. + public string Anchor { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 8c78b2f3..44503b20 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Toolkit } /// Extract mod metadata from the wiki compatibility list. - public async Task GetWikiCompatibilityListAsync() + public async Task GetWikiCompatibilityListAsync() { var client = new WikiCompatibilityClient(this.UserAgent); return await client.FetchAsync(); -- cgit From 4272669d89860846ba2dd5ef2896c1923057606c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Oct 2018 17:43:42 -0400 Subject: fix Chucklefish pages not being linked (#597) --- .../Framework/Clients/Wiki/WikiCompatibilityClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index 569e820b..20436e66 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string alternateAuthors = this.GetMetadataField(node, "mod-author2"); string[] ids = this.GetMetadataField(node, "mod-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntField(node, "mod-nexus-id"); - int? chucklefishID = this.GetNullableIntField(node, "mod-chucklefish-id"); + int? chucklefishID = this.GetNullableIntField(node, "mod-cf-id"); string githubRepo = this.GetMetadataField(node, "mod-github"); string customSourceUrl = this.GetMetadataField(node, "mod-custom-source"); string customUrl = this.GetMetadataField(node, "mod-url"); -- cgit From de561e52d7785597f1af2c6fd0d712d19ac5f928 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Oct 2018 20:19:12 -0400 Subject: fetch game versions from the wiki (#597) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- src/SMAPI.Web/Controllers/ModsController.cs | 9 +- src/SMAPI.Web/Views/Mods/Index.cshtml | 2 +- .../Framework/Clients/Wiki/WikiClient.cs | 208 +++++++++++++++++++++ .../Clients/Wiki/WikiCompatibilityClient.cs | 198 -------------------- .../Framework/Clients/Wiki/WikiModList.cs | 18 ++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 6 +- 7 files changed, 236 insertions(+), 207 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 5caa5758..6e517a97 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -223,7 +223,7 @@ namespace StardewModdingAPI.Web.Controllers { try { - WikiModEntry[] entries = await toolkit.GetWikiCompatibilityListAsync(); + WikiModEntry[] entries = (await toolkit.GetWikiCompatibilityListAsync()).Mods; entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes); return entries; } diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index f258c745..57aa9da9 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -56,11 +56,12 @@ namespace StardewModdingAPI.Web.Controllers { return await this.Cache.GetOrCreateAsync($"{nameof(ModsController)}_mod_list", async entry => { - WikiModEntry[] entries = await new ModToolkit().GetWikiCompatibilityListAsync(); + WikiModList data = await new ModToolkit().GetWikiCompatibilityListAsync(); ModListModel model = new ModListModel( - stableVersion: "1.3.28", - betaVersion: "1.3.31-beta", - mods: entries + stableVersion: data.StableVersion, + betaVersion: data.BetaVersion, + mods: data + .Mods .Select(mod => new ModModel(mod)) .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting ); diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 3626c4d8..66ec0c72 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -22,7 +22,7 @@ @if (Model.BetaVersion != null) {
-

Note: "SDV beta only" means Stardew Valley @Model.BetaVersion; if you didn't opt in to the beta, you have the stable version and can ignore that line. If a mod doesn't have a "SDV beta only" line, the compatibility applies to both versions of the game.

+

Note: "SDV beta only" means Stardew Valley @Model.BetaVersion-beta; if you didn't opt in to the beta, you have the stable version and can ignore that line. If a mod doesn't have a "SDV beta only" line, the compatibility applies to both versions of the game.

} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs new file mode 100644 index 00000000..9be760f8 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// An HTTP client for fetching mod metadata from the wiki. + public class WikiClient : IDisposable + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the wiki API. + /// The base URL for the wiki API. + public WikiClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Fetch mods from the compatibility list. + public async Task FetchModsAsync() + { + // fetch HTML + ResponseModel response = await this.Client + .GetAsync("") + .WithArguments(new + { + action = "parse", + page = "Modding:SMAPI_compatibility", + format = "json" + }) + .As(); + string html = response.Parse.Text["*"]; + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // fetch game versions + string stableVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-stable-version']")?.InnerText; + string betaVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-beta-version']")?.InnerText; + + // find mod entries + HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); + if (modNodes == null) + throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); + + // parse + WikiModEntry[] mods = this.ParseEntries(modNodes).ToArray(); + return new WikiModList + { + StableVersion = stableVersion, + BetaVersion = betaVersion, + Mods = mods + }; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Parse valid mod compatibility entries. + /// The HTML compatibility entries. + private IEnumerable ParseEntries(IEnumerable nodes) + { + foreach (HtmlNode node in nodes) + { + // extract fields + string name = this.GetMetadataField(node, "mod-name"); + string alternateNames = this.GetMetadataField(node, "mod-name2"); + string author = this.GetMetadataField(node, "mod-author"); + string alternateAuthors = this.GetMetadataField(node, "mod-author2"); + string[] ids = this.GetMetadataField(node, "mod-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; + int? nexusID = this.GetNullableIntField(node, "mod-nexus-id"); + int? chucklefishID = this.GetNullableIntField(node, "mod-cf-id"); + string githubRepo = this.GetMetadataField(node, "mod-github"); + string customSourceUrl = this.GetMetadataField(node, "mod-custom-source"); + string customUrl = this.GetMetadataField(node, "mod-url"); + string brokeIn = this.GetMetadataField(node, "mod-broke-in"); + string anchor = this.GetMetadataField(node, "mod-anchor"); + + // parse stable compatibility + WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo + { + Status = this.GetStatusField(node, "mod-status") ?? WikiCompatibilityStatus.Ok, + UnofficialVersion = this.GetSemanticVersionField(node, "mod-unofficial-version"), + UnofficialUrl = this.GetMetadataField(node, "mod-unofficial-url"), + Summary = this.GetMetadataField(node, "mod-summary")?.Trim() + }; + + // parse beta compatibility + WikiCompatibilityInfo betaCompatibility = null; + { + WikiCompatibilityStatus? betaStatus = this.GetStatusField(node, "mod-beta-status"); + if (betaStatus.HasValue) + { + betaCompatibility = new WikiCompatibilityInfo + { + Status = betaStatus.Value, + UnofficialVersion = this.GetSemanticVersionField(node, "mod-beta-unofficial-version"), + UnofficialUrl = this.GetMetadataField(node, "mod-beta-unofficial-url"), + Summary = this.GetMetadataField(node, "mod-beta-summary") + }; + } + } + + // yield model + yield return new WikiModEntry + { + ID = ids, + Name = name, + AlternateNames = alternateNames, + Author = author, + AlternateAuthors = alternateAuthors, + NexusID = nexusID, + ChucklefishID = chucklefishID, + GitHubRepo = githubRepo, + CustomSourceUrl = customSourceUrl, + CustomUrl = customUrl, + BrokeIn = brokeIn, + Compatibility = compatibility, + BetaCompatibility = betaCompatibility, + Anchor = anchor + }; + } + } + + /// Get the value of a metadata field. + /// The metadata container. + /// The field name. + private string GetMetadataField(HtmlNode container, string name) + { + return container.Descendants().FirstOrDefault(p => p.HasClass(name))?.InnerHtml; + } + + /// Get the value of a metadata field as a compatibility status. + /// The metadata container. + /// The field name. + private WikiCompatibilityStatus? GetStatusField(HtmlNode container, string name) + { + string raw = this.GetMetadataField(container, name); + if (raw == null) + return null; + if (!Enum.TryParse(raw, true, out WikiCompatibilityStatus status)) + throw new InvalidOperationException($"Unknown status '{raw}' when parsing compatibility list."); + return status; + } + + /// Get the value of a metadata field as a semantic version. + /// The metadata container. + /// The field name. + private ISemanticVersion GetSemanticVersionField(HtmlNode container, string name) + { + string raw = this.GetMetadataField(container, name); + return SemanticVersion.TryParse(raw, out ISemanticVersion version) + ? version + : null; + } + + /// Get the value of a metadata field as a nullable integer. + /// The metadata container. + /// The field name. + private int? GetNullableIntField(HtmlNode container, string name) + { + string raw = this.GetMetadataField(container, name); + if (raw != null && int.TryParse(raw, out int value)) + return value; + return null; + } + + /// The response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseModel + { + /// The parse API results. + public ResponseParseModel Parse { get; set; } + } + + /// The inner response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseParseModel + { + /// The parsed text. + public IDictionary Text { get; set; } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs deleted file mode 100644 index 20436e66..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using HtmlAgilityPack; -using Pathoschild.Http.Client; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// An HTTP client for fetching mod metadata from the wiki compatibility list. - public class WikiCompatibilityClient : IDisposable - { - /********* - ** Properties - *********/ - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The user agent for the wiki API. - /// The base URL for the wiki API. - public WikiCompatibilityClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") - { - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); - } - - /// Fetch mod compatibility entries. - public async Task FetchAsync() - { - // fetch HTML - ResponseModel response = await this.Client - .GetAsync("") - .WithArguments(new - { - action = "parse", - page = "Modding:SMAPI_compatibility", - format = "json" - }) - .As(); - string html = response.Parse.Text["*"]; - - // parse HTML - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - // find mod entries - HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); - if (modNodes == null) - throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); - - // parse - return this.ParseEntries(modNodes).ToArray(); - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - this.Client?.Dispose(); - } - - - /********* - ** Private methods - *********/ - /// Parse valid mod compatibility entries. - /// The HTML compatibility entries. - private IEnumerable ParseEntries(IEnumerable nodes) - { - foreach (HtmlNode node in nodes) - { - // extract fields - string name = this.GetMetadataField(node, "mod-name"); - string alternateNames = this.GetMetadataField(node, "mod-name2"); - string author = this.GetMetadataField(node, "mod-author"); - string alternateAuthors = this.GetMetadataField(node, "mod-author2"); - string[] ids = this.GetMetadataField(node, "mod-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; - int? nexusID = this.GetNullableIntField(node, "mod-nexus-id"); - int? chucklefishID = this.GetNullableIntField(node, "mod-cf-id"); - string githubRepo = this.GetMetadataField(node, "mod-github"); - string customSourceUrl = this.GetMetadataField(node, "mod-custom-source"); - string customUrl = this.GetMetadataField(node, "mod-url"); - string brokeIn = this.GetMetadataField(node, "mod-broke-in"); - string anchor = this.GetMetadataField(node, "mod-anchor"); - - // parse stable compatibility - WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo - { - Status = this.GetStatusField(node, "mod-status") ?? WikiCompatibilityStatus.Ok, - UnofficialVersion = this.GetSemanticVersionField(node, "mod-unofficial-version"), - UnofficialUrl = this.GetMetadataField(node, "mod-unofficial-url"), - Summary = this.GetMetadataField(node, "mod-summary")?.Trim() - }; - - // parse beta compatibility - WikiCompatibilityInfo betaCompatibility = null; - { - WikiCompatibilityStatus? betaStatus = this.GetStatusField(node, "mod-beta-status"); - if (betaStatus.HasValue) - { - betaCompatibility = new WikiCompatibilityInfo - { - Status = betaStatus.Value, - UnofficialVersion = this.GetSemanticVersionField(node, "mod-beta-unofficial-version"), - UnofficialUrl = this.GetMetadataField(node, "mod-beta-unofficial-url"), - Summary = this.GetMetadataField(node, "mod-beta-summary") - }; - } - } - - // yield model - yield return new WikiModEntry - { - ID = ids, - Name = name, - AlternateNames = alternateNames, - Author = author, - AlternateAuthors = alternateAuthors, - NexusID = nexusID, - ChucklefishID = chucklefishID, - GitHubRepo = githubRepo, - CustomSourceUrl = customSourceUrl, - CustomUrl = customUrl, - BrokeIn = brokeIn, - Compatibility = compatibility, - BetaCompatibility = betaCompatibility, - Anchor = anchor - }; - } - } - - /// Get the value of a metadata field. - /// The metadata container. - /// The field name. - private string GetMetadataField(HtmlNode container, string name) - { - return container.Descendants().FirstOrDefault(p => p.HasClass(name))?.InnerHtml; - } - - /// Get the value of a metadata field as a compatibility status. - /// The metadata container. - /// The field name. - private WikiCompatibilityStatus? GetStatusField(HtmlNode container, string name) - { - string raw = this.GetMetadataField(container, name); - if (raw == null) - return null; - if (!Enum.TryParse(raw, true, out WikiCompatibilityStatus status)) - throw new InvalidOperationException($"Unknown status '{raw}' when parsing compatibility list."); - return status; - } - - /// Get the value of a metadata field as a semantic version. - /// The metadata container. - /// The field name. - private ISemanticVersion GetSemanticVersionField(HtmlNode container, string name) - { - string raw = this.GetMetadataField(container, name); - return SemanticVersion.TryParse(raw, out ISemanticVersion version) - ? version - : null; - } - - /// Get the value of a metadata field as a nullable integer. - /// The metadata container. - /// The field name. - private int? GetNullableIntField(HtmlNode container, string name) - { - string raw = this.GetMetadataField(container, name); - if (raw != null && int.TryParse(raw, out int value)) - return value; - return null; - } - - /// The response model for the MediaWiki parse API. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] - private class ResponseModel - { - /// The parse API results. - public ResponseParseModel Parse { get; set; } - } - - /// The inner response model for the MediaWiki parse API. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] - private class ResponseParseModel - { - /// The parsed text. - public IDictionary Text { get; set; } - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs new file mode 100644 index 00000000..0d614f28 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// Metadata from the wiki's mod compatibility list. + public class WikiModList + { + /********* + ** Accessors + *********/ + /// The stable game version. + public string StableVersion { get; set; } + + /// The beta game version (if any). + public string BetaVersion { get; set; } + + /// The mods on the wiki. + public WikiModEntry[] Mods { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 44503b20..c55f6c70 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -47,10 +47,10 @@ namespace StardewModdingAPI.Toolkit } /// Extract mod metadata from the wiki compatibility list. - public async Task GetWikiCompatibilityListAsync() + public async Task GetWikiCompatibilityListAsync() { - var client = new WikiCompatibilityClient(this.UserAgent); - return await client.FetchAsync(); + var client = new WikiClient(this.UserAgent); + return await client.FetchModsAsync(); } /// Get SMAPI's internal mod database. -- cgit From e94aaaf7c8dccd0910d6275860821b16c90ef6c6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Oct 2018 20:37:42 -0400 Subject: update for changes to wiki compatibility list (#597) --- src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs | 12 ++- src/SMAPI.Web/ViewModels/ModModel.cs | 12 +-- src/SMAPI.Web/Views/Mods/Index.cshtml | 2 +- .../Clients/WebApi/ModExtendedMetadataModel.cs | 2 +- .../Framework/Clients/Wiki/WikiClient.cs | 112 ++++++++++++--------- .../Clients/Wiki/WikiCompatibilityInfo.cs | 3 + .../Framework/Clients/Wiki/WikiModEntry.cs | 22 ++-- 7 files changed, 92 insertions(+), 73 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs index d331c093..61756176 100644 --- a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs +++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs @@ -11,12 +11,15 @@ namespace StardewModdingAPI.Web.ViewModels /// The compatibility status, as a string like "Broken". public string Status { get; set; } - /// A link to the unofficial version which fixes compatibility, if any. - public ModLinkModel UnofficialVersion { get; set; } - /// The human-readable summary, as an HTML block. public string Summary { get; set; } + /// The game or SMAPI version which broke this mod (if applicable). + public string BrokeIn { get; set; } + + /// A link to the unofficial version which fixes compatibility, if any. + public ModLinkModel UnofficialVersion { get; set; } + /********* ** Public methods @@ -26,9 +29,10 @@ namespace StardewModdingAPI.Web.ViewModels public ModCompatibilityModel(WikiCompatibilityInfo info) { this.Status = info.Status.ToString(); + this.Summary = info.Summary; + this.BrokeIn = info.BrokeIn; if (info.UnofficialVersion != null) this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl, info.UnofficialVersion.ToString()); - this.Summary = info.Summary; } } } diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 4fb9d5b5..1199fe20 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -34,9 +34,6 @@ namespace StardewModdingAPI.Web.ViewModels /// Links to the available mod pages. public ModLinkModel[] ModPages { get; set; } - /// The game or SMAPI version which broke this mod (if applicable). - public string BrokeIn { get; set; } - /// A unique identifier for the mod that can be used in an anchor URL. public string Slug { get; set; } @@ -49,15 +46,14 @@ namespace StardewModdingAPI.Web.ViewModels public ModModel(WikiModEntry entry) { // basic info - this.Name = entry.Name; - this.AlternateNames = entry.AlternateNames; - this.Author = entry.Author; - this.AlternateAuthors = entry.AlternateAuthors; + this.Name = entry.Name.FirstOrDefault(); + this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray()); + this.Author = entry.Author.FirstOrDefault(); + this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray()); this.SourceUrl = this.GetSourceUrl(entry); this.Compatibility = new ModCompatibilityModel(entry.Compatibility); this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null; this.ModPages = this.GetModPageUrls(entry).ToArray(); - this.BrokeIn = entry.BrokeIn; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index b2ab61d7..b2e20c7a 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -71,7 +71,7 @@ - + source no source diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index e6f2e4b4..247730d7 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -74,7 +74,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi if (wiki != null) { this.ID = wiki.ID; - this.Name = wiki.Name; + this.Name = wiki.Name.FirstOrDefault(); this.NexusID = wiki.NexusID; this.ChucklefishID = wiki.ChucklefishID; this.GitHubRepo = wiki.GitHubRepo; diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 9be760f8..7197bf2c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; using Pathoschild.Http.Client; @@ -84,40 +85,40 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki foreach (HtmlNode node in nodes) { // extract fields - string name = this.GetMetadataField(node, "mod-name"); - string alternateNames = this.GetMetadataField(node, "mod-name2"); - string author = this.GetMetadataField(node, "mod-author"); - string alternateAuthors = this.GetMetadataField(node, "mod-author2"); - string[] ids = this.GetMetadataField(node, "mod-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; - int? nexusID = this.GetNullableIntField(node, "mod-nexus-id"); - int? chucklefishID = this.GetNullableIntField(node, "mod-cf-id"); - string githubRepo = this.GetMetadataField(node, "mod-github"); - string customSourceUrl = this.GetMetadataField(node, "mod-custom-source"); - string customUrl = this.GetMetadataField(node, "mod-url"); - string brokeIn = this.GetMetadataField(node, "mod-broke-in"); - string anchor = this.GetMetadataField(node, "mod-anchor"); + string[] names = this.GetAttributeAsCsv(node, "data-name"); + string[] authors = this.GetAttributeAsCsv(node, "data-author"); + string[] ids = this.GetAttributeAsCsv(node, "data-id"); + string[] warnings = this.GetAttributeAsCsv(node, "data-warnings"); + int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); + int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); + string githubRepo = this.GetAttribute(node, "data-github"); + string customSourceUrl = this.GetAttribute(node, "data-custom-source"); + string customUrl = this.GetAttribute(node, "data-url"); + string anchor = this.GetAttribute(node, "id"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo { - Status = this.GetStatusField(node, "mod-status") ?? WikiCompatibilityStatus.Ok, - UnofficialVersion = this.GetSemanticVersionField(node, "mod-unofficial-version"), - UnofficialUrl = this.GetMetadataField(node, "mod-unofficial-url"), - Summary = this.GetMetadataField(node, "mod-summary")?.Trim() + Status = this.GetAttributeAsStatus(node, "data-status") ?? WikiCompatibilityStatus.Ok, + BrokeIn = this.GetAttribute(node, "data-broke-in"), + UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"), + UnofficialUrl = this.GetAttribute(node, "data-unofficial-url"), + Summary = this.GetInnerHtml(node, "mod-summary")?.Trim() }; // parse beta compatibility WikiCompatibilityInfo betaCompatibility = null; { - WikiCompatibilityStatus? betaStatus = this.GetStatusField(node, "mod-beta-status"); + WikiCompatibilityStatus? betaStatus = this.GetAttributeAsStatus(node, "data-beta-status"); if (betaStatus.HasValue) { betaCompatibility = new WikiCompatibilityInfo { Status = betaStatus.Value, - UnofficialVersion = this.GetSemanticVersionField(node, "mod-beta-unofficial-version"), - UnofficialUrl = this.GetMetadataField(node, "mod-beta-unofficial-url"), - Summary = this.GetMetadataField(node, "mod-beta-summary") + BrokeIn = this.GetAttribute(node, "data-beta-broke-in"), + UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"), + UnofficialUrl = this.GetAttribute(node, "data-beta-unofficial-url"), + Summary = this.GetInnerHtml(node, "mod-beta-summary") }; } } @@ -126,37 +127,50 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki yield return new WikiModEntry { ID = ids, - Name = name, - AlternateNames = alternateNames, - Author = author, - AlternateAuthors = alternateAuthors, + Name = names, + Author = authors, NexusID = nexusID, ChucklefishID = chucklefishID, GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, CustomUrl = customUrl, - BrokeIn = brokeIn, Compatibility = compatibility, BetaCompatibility = betaCompatibility, + Warnings = warnings, Anchor = anchor }; } } - /// Get the value of a metadata field. - /// The metadata container. - /// The field name. - private string GetMetadataField(HtmlNode container, string name) + /// Get an attribute value. + /// The element whose attributes to read. + /// The attribute name. + private string GetAttribute(HtmlNode element, string name) { - return container.Descendants().FirstOrDefault(p => p.HasClass(name))?.InnerHtml; + string value = element.GetAttributeValue(name, null); + if (string.IsNullOrWhiteSpace(value)) + return null; + + return WebUtility.HtmlDecode(value); } - /// Get the value of a metadata field as a compatibility status. - /// The metadata container. - /// The field name. - private WikiCompatibilityStatus? GetStatusField(HtmlNode container, string name) + /// Get an attribute value and parse it as a comma-delimited list of strings. + /// The element whose attributes to read. + /// The attribute name. + private string[] GetAttributeAsCsv(HtmlNode element, string name) + { + string raw = this.GetAttribute(element, name); + return !string.IsNullOrWhiteSpace(raw) + ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() + : new string[0]; + } + + /// Get an attribute value and parse it as a compatibility status. + /// The element whose attributes to read. + /// The attribute name. + private WikiCompatibilityStatus? GetAttributeAsStatus(HtmlNode element, string name) { - string raw = this.GetMetadataField(container, name); + string raw = this.GetAttribute(element, name); if (raw == null) return null; if (!Enum.TryParse(raw, true, out WikiCompatibilityStatus status)) @@ -164,28 +178,36 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki return status; } - /// Get the value of a metadata field as a semantic version. - /// The metadata container. - /// The field name. - private ISemanticVersion GetSemanticVersionField(HtmlNode container, string name) + /// Get an attribute value and parse it as a semantic version. + /// The element whose attributes to read. + /// The attribute name. + private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name) { - string raw = this.GetMetadataField(container, name); + string raw = this.GetAttribute(element, name); return SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version : null; } - /// Get the value of a metadata field as a nullable integer. - /// The metadata container. - /// The field name. - private int? GetNullableIntField(HtmlNode container, string name) + /// Get an attribute value and parse it as a nullable int. + /// The element whose attributes to read. + /// The attribute name. + private int? GetAttributeAsNullableInt(HtmlNode element, string name) { - string raw = this.GetMetadataField(container, name); + string raw = this.GetAttribute(element, name); if (raw != null && int.TryParse(raw, out int value)) return value; return null; } + /// Get the text of an element with the given class name. + /// The metadata container. + /// The field name. + private string GetInnerHtml(HtmlNode container, string className) + { + return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml; + } + /// The response model for the MediaWiki parse API. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs index 2725df1a..204acd2b 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The human-readable summary of the compatibility status or workaround, without HTML formatting. public string Summary { get; set; } + /// The game or SMAPI version which broke this mod (if applicable). + public string BrokeIn { get; set; } + /// The version of the latest unofficial update, if applicable. public ISemanticVersion UnofficialVersion { get; set; } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 752b526c..ce8d6c5f 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -6,20 +6,14 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /********* ** Accessors *********/ - /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. public string[] ID { get; set; } - /// The mod's display name. - public string Name { get; set; } + /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. + public string[] Name { get; set; } - /// The mod's alternative names, if any. - public string AlternateNames { get; set; } - - /// The mod's author name. - public string Author { get; set; } - - /// The mod's alternative author names, if any. - public string AlternateAuthors { get; set; } + /// The mod's author name. If the author has multiple names, the first one is the most canonical name. + public string[] Author { get; set; } /// The mod ID on Nexus. public int? NexusID { get; set; } @@ -36,9 +30,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The custom mod page URL (if applicable). public string CustomUrl { get; set; } - /// The game or SMAPI version which broke this mod (if applicable). - public string BrokeIn { get; set; } - /// The mod's compatibility with the latest stable version of the game. public WikiCompatibilityInfo Compatibility { get; set; } @@ -48,6 +39,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . public bool HasBetaInfo => this.BetaCompatibility != null; + /// The human-readable warnings for players about this mod. + public string[] Warnings { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } -- 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/StardewModdingAPI.Toolkit') 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 f621a44982532b7488a7820d6cb56b216c77b5f2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:43:45 -0500 Subject: update web packages --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 8 ++++---- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 4814d169..942a4b20 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 91aae3e6..3fa28d19 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -12,7 +12,7 @@ - + -- cgit From 49e944d06fa7040889b569b62cb0025c057834df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:52:59 -0500 Subject: update HTTP URLs to HTTPS --- .editorconfig | 2 +- docs/README.md | 2 +- docs/release-notes.md | 6 +++--- docs/technical-docs.md | 4 ++-- src/SMAPI.Installer/unix-install.sh | 2 +- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/SemanticVersion.cs | 2 +- src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/.editorconfig b/.editorconfig index 126fdbd4..5bfc44bd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root: true ########## ## General formatting -## documentation: http://editorconfig.org +## documentation: https://editorconfig.org ########## [*] indent_style = space diff --git a/docs/README.md b/docs/README.md index e7d6d682..b8e3b50b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -**SMAPI** is an open-source modding API for [Stardew Valley](http://stardewvalley.net/) that lets +**SMAPI** is an open-source modding API for [Stardew Valley](https://stardewvalley.net/) that lets you play the game with mods. It's safely installed alongside the game's executable, and doesn't change any of your game files. It serves eight main purposes: diff --git a/docs/release-notes.md b/docs/release-notes.md index 8a15a4d2..6c2879df 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -666,7 +666,7 @@ For mod developers: * The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing. * Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised. * Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly. -* Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), +* Several obsolete APIs have been removed (see [migration guides](https://stardewvalleywiki.com/Modding:Index#Migration_guides)), and all _notice_-level deprecations have been increased to _info_. * Removed the experimental `IConfigFile`. @@ -749,7 +749,7 @@ For players: For developers: * Deprecated `Version` in favour of `SemanticVersion`. - _This new implementation is [semver 2.0](http://semver.org/)-compliant, introduces `NewerThan(version)` and `OlderThan(version)` convenience methods, adds support for parsing a version string into a `SemanticVersion`, and fixes various bugs with the former implementation. This also replaces `Manifest` with `IManifest`._ + _This new implementation is [semver 2.0](https://semver.org/)-compliant, introduces `NewerThan(version)` and `OlderThan(version)` convenience methods, adds support for parsing a version string into a `SemanticVersion`, and fixes various bugs with the former implementation. This also replaces `Manifest` with `IManifest`._ * Increased deprecation levels for `SObject`, `Extensions`, `LogWriter` (not `Log`), `SPlayer`, and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`). ## 1.4 @@ -828,7 +828,7 @@ For mod developers: * Added OS version to log. * Added zoom-adjusted mouse position to mouse-changed event arguments. * Added SMAPI code documentation. - * Switched to [semantic versioning](http://semver.org). + * Switched to [semantic versioning](https://semver.org). * Fixed mod versions not shown correctly in the log. * Fixed misspelled field in `manifest.json` schema. * Fixed some events getting wrong data. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index be809c3f..08590cba 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -29,7 +29,7 @@ Using an official SMAPI release is recommended for most users. SMAPI uses some C# 7 code, so you'll need at least [Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, -[MonoDevelop 7.0](http://www.monodevelop.com/) on Linux, +[MonoDevelop 7.0](https://www.monodevelop.com/) on Linux, [Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent IDE to compile it. It uses build configuration derived from the [crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect @@ -48,7 +48,7 @@ To prepare a crossplatform SMAPI release, you'll need to compile it on two platf 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: + [semantic version](https://semver.org). Recommended format: build type | format | example :--------- | :----------------------- | :------ diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh index df02bb37..8379ed87 100644 --- a/src/SMAPI.Installer/unix-install.sh +++ b/src/SMAPI.Installer/unix-install.sh @@ -16,6 +16,6 @@ fi if $COMMAND mono >/dev/null 2>&1; then mono internal/Mono/install.exe else - echo "Oops! Looks like Mono isn't installed. Please install Mono from http://mono-project.com, reboot, and run this installer again." + echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again." read fi diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 51b839df..4a1f04c6 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields { Status = ModStatus.AssumeBroken, - AlternativeUrl = "http://example.org" + AlternativeUrl = "https://example.org" }); // act diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5f2e90aa..e76e7a1d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -266,7 +266,7 @@ namespace StardewModdingAPI.Framework // show details if game crashed during last session if (File.Exists(Constants.FatalCrashMarker)) { - this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: http://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error); this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error); this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); Console.ReadKey(); diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 587ff286..a65afeb8 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -70,7 +70,7 @@ namespace StardewModdingAPI /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. /// The value is null. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + /// The implementation is defined by Semantic Version 2.0 (https://semver.org/). public int CompareTo(ISemanticVersion other) { return this.Version.CompareTo(other); diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index 2a78d2f0..32fea3d7 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Toolkit { /// A semantic version with an optional release tag. /// - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/), with a few deviations: + /// The implementation is defined by Semantic Version 2.0 (https://semver.org/), with a few deviations: /// - short-form "x.y" versions are supported (equivalent to "x.y.0"); /// - hyphens are synonymous with dots in prerelease tags (like "-unofficial.3-pathoschild"); /// - +build suffixes are not supported; -- cgit From 0b03b4f16a446e4b139efd33f6378598ae66efee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 12:04:20 -0500 Subject: deprecate old manifest version format --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 6 ++++++ src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 7 ++++++- .../Serialisation/Converters/SemanticVersionConverter.cs | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 57fb33a4..4e2d7cab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -48,6 +48,7 @@ * Fixed `Context.IsPlayerFree` being true before the player finishes transitioning to a new location in multiplayer. * Suppressed the game's 'added crickets' debug output. * Updated dependencies (Harmony 1.0.9.1 → 1.2.0.1, Mono.Cecil 0.10 → 0.10.1). + * **Deprecation:** non-string manifest versions are now deprecated and will no longer work in SMAPI 3.0. * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods. * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mod releases, but you'll need to update the build config NuGet package. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 3e8766c2..6cb564ce 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -895,6 +895,12 @@ namespace StardewModdingAPI.Framework return false; } + // add deprecation warning for old version format + { + if (mod.Manifest?.Version is Toolkit.SemanticVersion version && version.IsLegacyFormat) + this.DeprecationManager.Warn(mod.DisplayName, "non-string manifest version", "2.8", DeprecationLevel.Notice); + } + // validate dependencies // Although dependences are validated before mods are loaded, a dependency may have failed to load. if (mod.Manifest.Dependencies?.Any() == true) diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index 32fea3d7..de33df2f 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -42,6 +42,9 @@ namespace StardewModdingAPI.Toolkit /// An optional prerelease tag. public string Build { get; } + /// Whether the version was parsed from the legacy object format. + public bool IsLegacyFormat { get; } + /********* ** Public methods @@ -51,12 +54,14 @@ namespace StardewModdingAPI.Toolkit /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible fixes. /// An optional prerelease tag. - public SemanticVersion(int major, int minor, int patch, string tag = null) + /// Whether the version was parsed from the legacy object format. + public SemanticVersion(int major, int minor, int patch, string tag = null, bool isLegacyFormat = false) { this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; this.Build = this.GetNormalisedTag(tag); + this.IsLegacyFormat = isLegacyFormat; this.AssertValid(); } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 070f2c97..e0e185c9 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters if (build == "0") build = null; // '0' from incorrect examples in old SMAPI documentation - return new SemanticVersion(major, minor, patch, build); + return new SemanticVersion(major, minor, patch, build, isLegacyFormat: true); } /// Read a JSON string. -- cgit From c0738296273304e71183ddc537aaf9ffc6d3da46 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 12:17:53 -0500 Subject: deprecate version build field --- docs/release-notes.md | 9 ++++++--- src/SMAPI/Framework/SCore.cs | 9 +++++---- src/SMAPI/SemanticVersion.cs | 17 ++++++++++++++++- .../ISemanticVersion.cs | 4 ++++ src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 10 +++++++--- 5 files changed, 38 insertions(+), 11 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4e2d7cab..2783ebc2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -48,9 +48,12 @@ * Fixed `Context.IsPlayerFree` being true before the player finishes transitioning to a new location in multiplayer. * Suppressed the game's 'added crickets' debug output. * Updated dependencies (Harmony 1.0.9.1 → 1.2.0.1, Mono.Cecil 0.10 → 0.10.1). - * **Deprecation:** non-string manifest versions are now deprecated and will no longer work in SMAPI 3.0. - * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods. - * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mod releases, but you'll need to update the build config NuGet package. + * **Deprecations:** + * Non-string manifest versions are now deprecated and will no longer work in SMAPI 3.0. Affected mods should be updated to use a string version, like `"Version": "1.0.0"`. + * `ISemanticVersion.Build` is now deprecated and will be removed in SMAPI 3.0. Affected mods should use `ISemanticVersion.PrereleaseTag` instead. + * **Breaking changes:** + * `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods. + * Most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mod releases, but you'll need to update the build config NuGet package. * For SMAPI developers: * Added support for parallel stable/beta unofficial updates in update checks. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6cb564ce..7567c6db 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. /// This is initialised after the game starts. - private DeprecationManager DeprecationManager; + private readonly DeprecationManager DeprecationManager; /// Manages SMAPI events for mods. private readonly EventManager EventManager; @@ -134,6 +134,10 @@ namespace StardewModdingAPI.Framework }; this.MonitorForGame = this.GetSecondaryMonitor("game"); this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + + // inject deprecation managers + SemanticVersion.DeprecationManager = this.DeprecationManager; // init logging this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); @@ -340,9 +344,6 @@ namespace StardewModdingAPI.Framework /// Initialise SMAPI and mods after the game starts. private void InitialiseAfterGameStart() { - // load core components - this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); - // redirect direct console output if (this.MonitorForGame.WriteToConsole) this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index a65afeb8..401f62c2 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; +using StardewModdingAPI.Framework; namespace StardewModdingAPI { @@ -12,6 +13,9 @@ namespace StardewModdingAPI /// The underlying semantic version implementation. private readonly ISemanticVersion Version; + /// Manages deprecation warnings. + internal static DeprecationManager DeprecationManager { get; set; } + /********* ** Accessors @@ -26,7 +30,18 @@ namespace StardewModdingAPI public int PatchVersion => this.Version.PatchVersion; /// An optional build tag. - public string Build => this.Version.Build; + [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] + public string Build + { + get + { + SemanticVersion.DeprecationManager?.Warn($"{nameof(ISemanticVersion)}.{nameof(ISemanticVersion.Build)}", "2.8", DeprecationLevel.Notice); + return this.Version.PrereleaseTag; + } + } + + /// An optional prerelease tag. + public string PrereleaseTag => this.Version.PrereleaseTag; /********* diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index 961ef777..6631b01d 100644 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -18,8 +18,12 @@ namespace StardewModdingAPI int PatchVersion { get; } /// An optional build tag. + [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] string Build { get; } + /// An optional prerelease tag. + string PrereleaseTag { get; } + /********* ** Accessors diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index de33df2f..a7990d13 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -40,7 +40,11 @@ namespace StardewModdingAPI.Toolkit public int PatchVersion { get; } /// An optional prerelease tag. - public string Build { get; } + [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] + public string Build => this.PrereleaseTag; + + /// An optional prerelease tag. + public string PrereleaseTag { get; } /// Whether the version was parsed from the legacy object format. public bool IsLegacyFormat { get; } @@ -60,7 +64,7 @@ namespace StardewModdingAPI.Toolkit this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; - this.Build = this.GetNormalisedTag(tag); + this.PrereleaseTag = this.GetNormalisedTag(tag); this.IsLegacyFormat = isLegacyFormat; this.AssertValid(); @@ -98,7 +102,7 @@ namespace StardewModdingAPI.Toolkit this.MajorVersion = int.Parse(match.Groups["major"].Value); this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; this.AssertValid(); } -- cgit