From c916cc5a1023ab81df89458ecb679405081f54c6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Aug 2018 13:25:08 -0400 Subject: mark old SpaceCore versions incompatible --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 133006e8..0b3259f5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,7 @@ # Release notes +## 2.8 (upcoming) +* Updated compatibility list. + ## 2.7 * For players: * Updated for Stardew Valley 1.3.28. -- cgit From d918ceb224bd6ed8b428219ab28a436896a30b45 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Aug 2018 23:00:01 -0400 Subject: add IContentPack.WriteJsonFile method (#468) --- docs/release-notes.md | 8 ++++++-- src/SMAPI/Framework/ContentPack.cs | 10 ++++++++++ src/SMAPI/IContentPack.cs | 8 +++++++- 3 files changed, 23 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0b3259f5..fc6ea97f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,10 @@ # Release notes ## 2.8 (upcoming) -* Updated compatibility list. +* For players: + * Updated compatibility list. + +* For modders: + * Added `IContentPack.WriteJsonFile` method. ## 2.7 * For players: @@ -14,6 +18,7 @@ * Fixed `player_add` command not recognising return scepter. * Fixed `player_add` command showing fish twice. * Fixed some SMAPI logs not deleted when starting a new session. + * Updated compatibility list. * For modders: * Added support for `.json` data files in the content API (including Content Patcher). @@ -25,7 +30,6 @@ * All enums are now JSON-serialised by name instead of numeric value. (Previously only a few enums were serialised that way. JSON files which already have numeric enum values will still be parsed fine.) * Fixed false compatibility error when constructing multidimensional arrays. * Fixed `.ToSButton()` methods not being public. - * Updated compatibility list. * For SMAPI developers: * Dropped support for pre-SMAPI-2.6 update checks in the web API. diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 62d8b80d..ccb2b9a0 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -59,6 +59,16 @@ namespace StardewModdingAPI.Framework : null; } + /// 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. + public void WriteJsonFile(string path, TModel data) where TModel : class + { + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + this.JsonHelper.WriteJsonFile(path, data); + } + /// 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. /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. /// The local path to a content file relative to the content pack folder. diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 15a2b7dd..fa793b13 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -22,11 +22,17 @@ namespace StardewModdingAPI ** Public methods *********/ /// Read a JSON file from the content pack folder. - /// The model type. + /// 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. 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. + 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. /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. /// The local path to a content file relative to the content pack folder. -- 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 'docs') 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 'docs') 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 826dd53ab550e5b92796c510569118beee6bd044 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 18:28:16 -0400 Subject: move most SMAPI files into subfolder (#582) --- build/common.targets | 22 +++-- build/prepare-install-package.targets | 57 +++++------ docs/release-notes.md | 3 + docs/technical-docs.md | 36 +++---- src/SMAPI.Installer/InteractiveInstaller.cs | 115 ++++++++++++++--------- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- src/SMAPI.ModBuildConfig/package.nuspec | 5 +- src/SMAPI/Constants.cs | 7 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 1 + src/SMAPI/Program.cs | 85 +++++++++++------ 10 files changed, 197 insertions(+), 136 deletions(-) (limited to 'docs') diff --git a/build/common.targets b/build/common.targets index 5b6511f8..90c477b6 100644 --- a/build/common.targets +++ b/build/common.targets @@ -99,14 +99,14 @@ - - - - - - + + + + + + @@ -114,12 +114,14 @@ - - + + + - - + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 79185896..35ff78a5 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -23,47 +23,50 @@ - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/docs/release-notes.md b/docs/release-notes.md index c7097f97..0ec842ef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,12 +1,15 @@ # Release notes ## 2.8 (upcoming) * For players: + * Moved most SMAPI files into a `smapi-internal` subfolder. * Updated compatibility list. * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added `IContentPack.WriteJsonFile` method. + * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. + * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mods, but you'll need to update the mod build config NuGet package when compiling mods. ## 2.7 * For players: diff --git a/docs/technical-docs.md b/docs/technical-docs.md index ed45871a..be809c3f 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -50,15 +50,15 @@ on the wiki for the first-time setup. 1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a [semantic version](http://semver.org). Recommended format: - build type | format | example - :--------- | :-------------------------------- | :------ - dev build | `-alpha.` | `2.0-alpha.20171230` - prerelease | `-prerelease.` | `2.0-prerelease.2` - release | `` | `2.0` + build type | format | example + :--------- | :----------------------- | :------ + dev build | `-alpha.` | `3.0-alpha.20171230` + prerelease | `-beta.` | `3.0-beta.2` + release | `` | `3.0` 2. In Windows: 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 3.0`). 2. Transfer the `SMAPI ` folder to Linux or Mac. _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, since we need to set Unix file permissions that Windows won't save._ @@ -69,36 +69,26 @@ on the wiki for the first-time setup. 3. If you did everything right so far, you should have a folder like this: ``` - SMAPI-2.x/ - install.exe - readme.txt + SMAPI 3.0 installer/ + install on Linux.sh + install on Mac.command + install on Windows.exe + README.txt internal/ Mono/ Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll + smapi-internal/* StardewModdingAPI - StardewModdingAPI.config.json - StardewModdingAPI.Internal.dll - StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml steam_appid.txt - System.Numerics.dll - System.Runtime.Caching.dll - System.ValueTuple.dll Windows/ Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI.config.json - StardewModdingAPI.Internal.dll - StardewModdingAPI.metadata.json + smapi-internal/* StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml - System.ValueTuple.dll steam_appid.txt ``` 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 0aac1da2..f9e1ff94 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -93,40 +93,39 @@ namespace StardewModdingApi.Installer { string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); - // common - yield return GetInstallPath("0Harmony.dll"); - yield return GetInstallPath("0Harmony.pdb"); - yield return GetInstallPath("Mono.Cecil.dll"); - yield return GetInstallPath("Newtonsoft.Json.dll"); + // current files + yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI.exe"); - yield return GetInstallPath("StardewModdingAPI.config.json"); - yield return GetInstallPath("StardewModdingAPI.metadata.json"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); - yield return GetInstallPath("System.ValueTuple.dll"); - yield return GetInstallPath("steam_appid.txt"); - - // Linux/Mac only - yield return GetInstallPath("libgdiplus.dylib"); - yield return GetInstallPath("StardewModdingAPI"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); - yield return GetInstallPath("System.Numerics.dll"); - yield return GetInstallPath("System.Runtime.Caching.dll"); - - // Windows only - yield return GetInstallPath("StardewModdingAPI.pdb"); + yield return GetInstallPath("smapi-internal"); // obsolete - yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 + yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) - yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 + yield return GetInstallPath("0Harmony.dll"); // moved in 2.8 + yield return GetInstallPath("0Harmony.pdb"); // moved in 2.8 + yield return GetInstallPath("Mono.Cecil.dll"); // moved in 2.8 + yield return GetInstallPath("Newtonsoft.Json.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.config.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.metadata.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.xml"); // moved in 2.8 + yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 + yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8 + yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8 + yield return GetInstallPath("steam_appid.txt"); // moved in 2.8 + if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) @@ -438,14 +437,13 @@ namespace StardewModdingApi.Installer { // copy SMAPI files to game dir this.PrintDebug("Adding SMAPI files..."); - foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo sourceEntry in paths.PackageDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { - if (sourceFile.Name == this.InstallerFileName) + if (sourceEntry.Name == this.InstallerFileName) continue; - string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name); - this.InteractivelyDelete(targetPath); - sourceFile.CopyTo(targetPath); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name)); + this.RecursiveCopy(sourceEntry, paths.GameDir); } // replace mod launcher (if possible) @@ -508,7 +506,7 @@ namespace StardewModdingApi.Installer targetDir.Create(); // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopy)) sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); } @@ -690,6 +688,31 @@ namespace StardewModdingApi.Installer } } + /// Recursively copy a directory or file. + /// The file or folder to copy. + /// The folder to copy into. + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder) + { + if (!targetFolder.Exists) + targetFolder.Create(); + + switch (source) + { + case FileInfo sourceFile: + sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); + break; + + case DirectoryInfo sourceDir: + DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); + foreach (var entry in sourceDir.EnumerateFileSystemInfos()) + this.RecursiveCopy(entry, targetSubfolder); + break; + + default: + throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); + } + } + /// Delete a file or folder regardless of file permissions, and block until deletion completes. /// The file or folder to reset. /// This method is mirred from FileUtilities.ForceDelete in the toolkit. @@ -871,7 +894,7 @@ namespace StardewModdingApi.Installer this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); // move mods if no conflicts (else warn) - foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { // get type bool isDir = entry is DirectoryInfo; @@ -928,22 +951,26 @@ namespace StardewModdingApi.Installer Directory.CreateDirectory(newPath); DirectoryInfo directory = (DirectoryInfo)entry; - foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopy)) this.Move(child, Path.Combine(newPath, child.Name)); directory.Delete(recursive: true); } } - /// Get whether a file should be copied when moving a folder. - /// The file info. - private bool ShouldCopyFile(FileSystemInfo file) + /// Get whether a file or folder should be copied from the installer files. + /// The file or folder info. + private bool ShouldCopy(FileSystemInfo entry) { - // ignore Mac symlink - if (file is FileInfo && file.Name == "mcs") - return false; - - return true; + switch (entry.Name) + { + case "mcs": + return false; // ignore Mac symlink + case "Mods": + return false; // Mods folder handled separately + default: + return true; + } } } } diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d1c8a4eb..db9fe8bd 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -136,7 +136,7 @@ true - $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll false true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 3d6f2598..04880101 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0 + 2.1.1 Build package for SMAPI mods Pathoschild Pathoschild @@ -19,6 +19,9 @@ - Added option to ignore files by regex pattern. - Added reference to new SMAPI DLL. - Fixed some game paths not detected by NuGet package. + + 2.1.1: + - Update for SMAPI 2.8. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bd512fb1..0e0ae239 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -64,11 +64,14 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; + /// The absolute path to the folder containing SMAPI's internal files. + internal static readonly string InternalFilesPath = Program.DllSearchPath; + /// The file path for the SMAPI configuration file. - internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json"); /// The file path for the SMAPI metadata file. - internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); + internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.metadata.json"); /// The filename prefix used for all SMAPI logs. internal static string LogNamePrefix { get; } = "SMAPI-"; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 37b1a378..e750c659 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -45,6 +45,7 @@ namespace StardewModdingAPI.Framework.ModLoading this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary(); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c40d2ff6..64eeb45a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -45,6 +45,11 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// The absolute path to search for SMAPI's internal DLLs. + /// We can't use directly, since depends on DLLs loaded from this folder. + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + internal static readonly string DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal"); + /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -111,6 +116,8 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { + // initial setup + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; Program.AssertMinimumCompatibility(); // get flags from arguments @@ -135,10 +142,48 @@ namespace StardewModdingAPI program.RunInteractively(); } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // skip if already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; + this.Monitor.Log("Disposing...", LogLevel.Trace); + + // dispose mod data + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { + try + { + (mod.Mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } + + // dispose core components + this.IsGameRunning = false; + this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); + this.CancellationTokenSource?.Dispose(); + this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); + + // end game (moved from Game1.OnExiting to let us clean up first) + Process.GetCurrentProcess().Kill(); + } + + + /********* + ** Private methods + *********/ /// Construct an instance. /// The path to search for mods. /// Whether to output log messages to the console. - public Program(string modsPath, bool writeToConsole) + private Program(string modsPath, bool writeToConsole) { // init paths this.VerifyPath(modsPath); @@ -189,7 +234,7 @@ namespace StardewModdingAPI /// Launch SMAPI. [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - public void RunInteractively() + private void RunInteractively() { // initialise SMAPI try @@ -320,44 +365,28 @@ namespace StardewModdingAPI } } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + /// Method called when assembly resolution fails, which may return a manually resolved assembly. + /// The event sender. + /// The event arguments. + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { - // skip if already disposed - if (this.IsDisposed) - return; - this.IsDisposed = true; - this.Monitor.Log("Disposing...", LogLevel.Trace); - - // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { try { - (mod.Mod as IDisposable)?.Dispose(); + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + return Assembly.LoadFrom(dll.FullName); } catch (Exception ex) { - mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting the smapi-lib folder and reinstalling SMAPI.", ex); } } - // dispose core components - this.IsGameRunning = false; - this.ConsoleManager?.Dispose(); - this.ContentCore?.Dispose(); - this.CancellationTokenSource?.Dispose(); - this.GameInstance?.Dispose(); - this.LogFile?.Dispose(); - - // end game (moved from Game1.OnExiting to let us clean up first) - Process.GetCurrentProcess().Kill(); + return null; } - - /********* - ** Private methods - *********/ /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. private static void AssertMinimumCompatibility() { -- cgit From 100e303b488a36e8410ff67e32c35bff80f21ba2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 20:27:28 -0400 Subject: add recursive mod search (#583) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +- .../Framework/ModScanning/ModFolder.cs | 15 ++--- .../Framework/ModScanning/ModScanner.cs | 64 ++++++++++++++++++++-- 4 files changed, 66 insertions(+), 18 deletions(-) (limited to 'docs') 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 307bf6ce55e8880f0e0382cb678d7fcc6c74c11e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 21:08:58 -0400 Subject: adjust SaveBackup mod to simplify installer logic (#583) --- docs/release-notes.md | 4 +++ src/SMAPI.Installer/InteractiveInstaller.cs | 30 ++-------------------- src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs | 9 ------- src/SMAPI.Mods.SaveBackup/ModEntry.cs | 13 ++++++---- .../StardewModdingAPI.Mods.SaveBackup.csproj | 3 +-- 5 files changed, 15 insertions(+), 44 deletions(-) delete mode 100644 src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index c742204e..7a30b0b6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * 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. + * Moved save backups into a `save-backups` subfolder (instead of `Mods/SaveBackup/backups`). * Updated compatibility list. * For modders: @@ -12,6 +13,9 @@ * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mods, but you'll need to update the mod build config NuGet package when compiling mods. +* For SMAPI developers: + * Adjusted `SaveBackup` mod to make it easier to account for custom mod subfolders in the installer. + ## 2.7 * For players: * Updated for Stardew Valley 1.3.28. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index f9e1ff94..a92edadf 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -22,12 +22,6 @@ namespace StardewModdingApi.Installer /// The name of the installer file in the package. private readonly string InstallerFileName = "install.exe"; - /// Mod files which shouldn't be deleted when deploying bundled mods (mod folder name => file names). - private readonly IDictionary> ProtectBundledFiles = new Dictionary>(StringComparer.InvariantCultureIgnoreCase) - { - ["SaveBackup"] = new HashSet(new[] { "backups", "config.json" }, StringComparer.InvariantCultureIgnoreCase) - }; - /// The value that represents Windows 7. private readonly Version Windows7Version = new Version(6, 1); @@ -474,18 +468,6 @@ namespace StardewModdingApi.Installer { this.PrintDebug("Adding bundled mods..."); - // special case: rename Omegasis' SaveBackup mod - { - DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "SaveBackup")); - DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "AdvancedSaveBackup")); - FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json")); - if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1)) - { - this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}..."); - this.Move(oldFolder, newFolder.FullName); - } - } - // add bundled mods foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) { @@ -494,16 +476,8 @@ namespace StardewModdingApi.Installer // init/clear target dir DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, sourceDir.Name)); if (targetDir.Exists) - { - this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet protectedFiles); - foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos()) - { - if (protectedFiles == null || !protectedFiles.Contains(entry.Name)) - this.InteractivelyDelete(entry.FullName); - } - } - else - targetDir.Create(); + this.InteractivelyDelete(targetDir.FullName); + targetDir.Create(); // copy files foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopy)) diff --git a/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs b/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs deleted file mode 100644 index c9dcb216..00000000 --- a/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StardewModdingAPI.Mods.SaveBackup.Framework -{ - /// The mod configuration. - internal class ModConfig - { - /// The number of backups to keep. - public int BackupsToKeep { get; set; } = 10; - } -} diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index 78578c3c..4d56789a 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -4,7 +4,6 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; -using StardewModdingAPI.Mods.SaveBackup.Framework; using StardewValley; namespace StardewModdingAPI.Mods.SaveBackup @@ -15,6 +14,12 @@ namespace StardewModdingAPI.Mods.SaveBackup /********* ** Properties *********/ + /// The number of backups to keep. + private readonly int BackupsToKeep = 10; + + /// The absolute path to the folder in which to store save backups. + private readonly string BackupFolder = Path.Combine(Constants.ExecutionPath, "save-backups"); + /// The name of the save archive to create. private readonly string FileName = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}.zip"; @@ -28,15 +33,13 @@ namespace StardewModdingAPI.Mods.SaveBackup { try { - ModConfig config = this.Helper.ReadConfig(); - // init backup folder - DirectoryInfo backupFolder = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "backups")); + DirectoryInfo backupFolder = new DirectoryInfo(this.BackupFolder); backupFolder.Create(); // back up saves this.CreateBackup(backupFolder); - this.PruneBackups(backupFolder, config.BackupsToKeep); + this.PruneBackups(backupFolder, this.BackupsToKeep); } catch (Exception ex) { diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index 0ccbcc6c..fafa4d25 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -1,4 +1,4 @@ - + @@ -36,7 +36,6 @@ Properties\GlobalAssemblyInfo.cs - -- cgit From 9f64dd2abb182b588cf5ae11201ca4dfbe26c45f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 21:50:42 -0400 Subject: add installer logic to detect if player moved the bundled mods (#583) --- docs/release-notes.md | 4 +- src/SMAPI.Installer/InteractiveInstaller.cs | 57 ++++++++++++++-------- src/SMAPI.Installer/Program.cs | 47 +++++++++++++++++- .../StardewModdingAPI.Installer.csproj | 6 +++ 4 files changed, 92 insertions(+), 22 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7a30b0b6..3ce7d6bb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,7 +3,8 @@ * 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. - * Moved save backups into a `save-backups` subfolder (instead of `Mods/SaveBackup/backups`). + * 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. * Updated compatibility list. * For modders: @@ -15,6 +16,7 @@ * For SMAPI developers: * 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`). ## 2.7 * For players: diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index a92edadf..565ad732 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -10,6 +10,9 @@ using StardewModdingApi.Installer.Enums; using StardewModdingAPI.Installer.Framework; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.ConsoleWriting; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingApi.Installer { @@ -468,31 +471,45 @@ namespace StardewModdingApi.Installer { this.PrintDebug("Adding bundled mods..."); - // add bundled mods - foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) + ModToolkit toolkit = new ModToolkit(); + ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray(); + foreach (ModFolder sourceMod in toolkit.GetModFolders(packagedModsDir.FullName)) { - this.PrintDebug($" adding {sourceDir.Name}..."); - - // init/clear target dir - DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, sourceDir.Name)); - if (targetDir.Exists) - this.InteractivelyDelete(targetDir.FullName); - targetDir.Create(); + // validate source mod + if (sourceMod.Manifest == null) + { + this.PrintWarning($" ignored invalid bundled mod {sourceMod.DisplayName}: {sourceMod.ManifestParseError}"); + continue; + } + + // find target folder + ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase) == true); + DirectoryInfo defaultTargetFolder = new DirectoryInfo(Path.Combine(paths.ModsPath, sourceMod.Directory.Name)); + DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder; + this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName + ? $" adding {sourceMod.Manifest.Name}..." + : $" adding {sourceMod.Manifest.Name} to {Path.Combine(paths.ModsDir.Name, PathUtilities.GetRelativePath(paths.ModsPath, targetFolder.FullName))}..." + ); + + // (re)create target folder + if (targetFolder.Exists) + this.InteractivelyDelete(targetFolder.FullName); + targetFolder.Create(); // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopy)) - sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); + foreach (FileInfo sourceFile in sourceMod.Directory.EnumerateFiles().Where(this.ShouldCopy)) + sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); } + } - // set SMAPI's color scheme if defined - if (scheme != MonitorColorScheme.AutoDetect) - { - string configPath = Path.Combine(paths.GamePath, "StardewModdingAPI.config.json"); - string text = File - .ReadAllText(configPath) - .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}"""); - File.WriteAllText(configPath, text); - } + // set SMAPI's color scheme if defined + if (scheme != MonitorColorScheme.AutoDetect) + { + string configPath = Path.Combine(paths.GamePath, "StardewModdingAPI.config.json"); + string text = File + .ReadAllText(configPath) + .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}"""); + File.WriteAllText(configPath, text); } // remove obsolete appdata mods diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index 8f328ecf..4d259fd3 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -1,8 +1,22 @@ -namespace StardewModdingApi.Installer +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using StardewModdingAPI.Internal; + +namespace StardewModdingApi.Installer { /// The entry point for SMAPI's install and uninstall console app. internal class Program { + /********* + ** Properties + *********/ + /// The absolute path to search for referenced assemblies. + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + private static string DllSearchPath; + + /********* ** Public methods *********/ @@ -10,8 +24,39 @@ /// The command line arguments. public static void Main(string[] args) { + // set up assembly resolution + string installerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + Program.DllSearchPath = Path.Combine(installerPath, "internal", EnvironmentUtility.DetectPlatform() == Platform.Windows ? "Windows" : "Mono", "smapi-internal"); + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; + + // launch installer var installer = new InteractiveInstaller(); installer.Run(args); } + + /********* + ** Private methods + *********/ + /// Method called when assembly resolution fails, which may return a manually resolved assembly. + /// The event sender. + /// The event arguments. + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) + { + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) + { + try + { + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + return Assembly.LoadFrom(dll.FullName); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting and redownloading the SMAPI installer.", ex); + } + } + + return null; + } } } diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index e82c6093..e9af16c5 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -58,6 +58,12 @@ PreserveNewest + + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + + -- 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 'docs') 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 ceac1de6ec7ed5f7ecc32cb99a665af891863657 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 23:03:09 -0400 Subject: change mod registry to return a container interface (#534) --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 5 +---- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 8 ++++---- src/SMAPI/IModInfo.cs | 9 +++++++++ src/SMAPI/IModRegistry.cs | 4 ++-- src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 src/SMAPI/IModInfo.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 34f7404e..f9bcc6ab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. + * **Breaking change:** `helper.ModRegistry` returns a new `IModInfo` interface instead of `IManifest` directly. This lets SMAPI return more metadata about mods in future versions. * **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: diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 2145105b..edd7eed6 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework { /// Metadata for a mod. - internal interface IModMetadata + internal interface IModMetadata : IModInfo { /********* ** Accessors @@ -16,9 +16,6 @@ namespace StardewModdingAPI.Framework /// The mod's full directory path. string DirectoryPath { get; } - /// The mod manifest. - IManifest Manifest { get; } - /// Metadata about the mod from SMAPI's internal data (if any). ModDataRecordVersionedFields DataRecord { get; } diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 008a80f5..5cc2a20f 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -40,17 +40,17 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// Get metadata for all loaded mods. - public IEnumerable GetAll() + public IEnumerable GetAll() { - return this.Registry.GetAll().Select(p => p.Manifest); + return this.Registry.GetAll(); } /// Get metadata for a loaded mod. /// The mod's unique ID. /// Returns the matching mod's metadata, or null if not found. - public IManifest Get(string uniqueID) + public IModInfo Get(string uniqueID) { - return this.Registry.Get(uniqueID)?.Manifest; + return this.Registry.Get(uniqueID); } /// Get whether a mod has been loaded. diff --git a/src/SMAPI/IModInfo.cs b/src/SMAPI/IModInfo.cs new file mode 100644 index 00000000..a16c2d7b --- /dev/null +++ b/src/SMAPI/IModInfo.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI +{ + /// Metadata for a loaded mod. + public interface IModInfo + { + /// The mod manifest. + IManifest Manifest { get; } + } +} diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index a06e099e..10b3121e 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -6,12 +6,12 @@ namespace StardewModdingAPI public interface IModRegistry : IModLinked { /// Get metadata for all loaded mods. - IEnumerable GetAll(); + IEnumerable GetAll(); /// Get metadata for a loaded mod. /// The mod's unique ID. /// Returns the matching mod's metadata, or null if not found. - IManifest Get(string uniqueID); + IModInfo Get(string uniqueID); /// Get whether a mod has been loaded. /// The mod's unique ID. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 713b1b31..740af15f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -186,6 +186,7 @@ + -- cgit From aabd76f38cd22065a51269b049c9b166759307f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Aug 2018 20:10:57 -0400 Subject: fix path init error-handling using monitor before it's initialised --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f9bcc6ab..ff2a7105 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * 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. + * Fixed error-handling when initialising paths. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a9ec5ee4..8368152d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -675,7 +675,8 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); + // note: this happens before this.Monitor is initialised + Console.WriteLine($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}"); } } -- cgit From 04778dcb26f888b56a8d9a70586f8fb6146d3971 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Aug 2018 20:16:03 -0400 Subject: suppress the game's 'added cricket' debug output --- docs/release-notes.md | 3 ++- src/SMAPI/Framework/SCore.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index ff2a7105..6af726a1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,7 +14,8 @@ * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. - * **Breaking change:** `helper.ModRegistry` returns a new `IModInfo` interface instead of `IManifest` directly. This lets SMAPI return more metadata about mods in future versions. + * Suppressed the game's 'added crickets' debug output. + * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods in future versions. * **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: diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 8368152d..3e93760b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -91,7 +91,7 @@ namespace StardewModdingAPI.Framework new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^DebugOutput:\s+(?:added CLOUD|added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), }; -- 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 'docs') 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 b5adfd8bce12afb885d6bb6a347d25e33be602af Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Aug 2018 20:42:31 -0400 Subject: add more default game install paths --- build/common.targets | 7 +++++++ docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 8 ++++++-- src/SMAPI.ModBuildConfig/build/smapi.targets | 4 ++++ 4 files changed, 18 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/build/common.targets b/build/common.targets index 90c477b6..b5cbbe67 100644 --- a/build/common.targets +++ b/build/common.targets @@ -9,13 +9,20 @@ $(HOME)/GOG Games/Stardew Valley/game $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley $(HOME)/.steam/steam/steamapps/common/Stardew Valley + /Applications/Stardew Valley.app/Contents/MacOS $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files\GalaxyClient\Games\Stardew Valley + C:\Program Files\GOG Galaxy\Games\Stardew Valley + C:\Program Files\Steam\steamapps\common\Stardew Valley + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) diff --git a/docs/release-notes.md b/docs/release-notes.md index 12feb68a..0ff90aa3 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. + * Fixed some game install paths not detected on Windows. * 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.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 7f7acb0b..e6e71cf4 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -55,8 +55,12 @@ namespace StardewModdingApi.Installer case Platform.Windows: { // Windows - yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley"; - yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"; + foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) + { + yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; + yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley"; + yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; + } // Windows registry IDictionary registryKeys = new Dictionary diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 7bdd8f8f..99011629 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -42,6 +42,10 @@ + C:\Program Files\GalaxyClient\Games\Stardew Valley + C:\Program Files\GOG Galaxy\Games\Stardew Valley + C:\Program Files\Steam\steamapps\common\Stardew Valley + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley -- cgit From ff8ffbdef0306254094258be326d75fcdc2bcbe1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Aug 2018 00:48:34 -0400 Subject: add paranoid warnings mode (#590) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 ++++++++ .../ModLoading/InstructionHandleResult.cs | 8 +++++- src/SMAPI/Framework/ModLoading/ModWarning.cs | 8 +++++- src/SMAPI/Framework/Models/SConfig.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 11 ++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 19 ++++++++++++-- src/SMAPI/StardewModdingAPI.config.json | 30 ++++++++++++++-------- 8 files changed, 75 insertions(+), 15 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ff90aa3..5d0d8a4e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mods, but you'll need to update the mod build config NuGet package when compiling mods. * For SMAPI developers: + * Added a 'paranoid warnings' option which reports mods using potentially sensitive .NET APIs (like file or shell access) in the mod issues list. * Adjusted `SaveBackup` mod to make it easier to account for custom mod subfolders in the installer. * Installer no longer special-cases Omegasis' older `SaveBackup` mod (now named `AdvancedSaveBackup`). diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index e750c659..fdbfdd8d 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -350,6 +350,16 @@ namespace StardewModdingAPI.Framework.ModLoading mod.SetWarning(ModWarning.UsesDynamic); break; + case InstructionHandleResult.DetectedFilesystemAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesFilesystem); + break; + + case InstructionHandleResult.DetectedShellAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected shell or process access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesShell); + break; + case InstructionHandleResult.None: break; diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index cfa23d08..f3555c2d 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -24,6 +24,12 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedDynamic, /// The instruction is compatible, but references which may impact stability. - DetectedUnvalidatedUpdateTick + DetectedUnvalidatedUpdateTick, + + /// The instruction accesses the filesystem directly. + DetectedFilesystemAccess, + + /// The instruction accesses the OS shell or processes directly. + DetectedShellAccess } } diff --git a/src/SMAPI/Framework/ModLoading/ModWarning.cs b/src/SMAPI/Framework/ModLoading/ModWarning.cs index 0e4b2570..c62199b2 100644 --- a/src/SMAPI/Framework/ModLoading/ModWarning.cs +++ b/src/SMAPI/Framework/ModLoading/ModWarning.cs @@ -26,6 +26,12 @@ namespace StardewModdingAPI.Framework.ModLoading UsesUnvalidatedUpdateTick = 16, /// The mod has no update keys set. - NoUpdateKeys = 32 + NoUpdateKeys = 32, + + /// Uses .NET APIs for filesystem access. + AccessesFilesystem = 64, + + /// Uses .NET APIs for shell or process access. + AccessesShell = 128 } } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 15671af4..bdfaa670 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } + /// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access. + public bool ParanoidWarnings { get; set; } + /// Whether to show beta versions as valid updates. public bool UseBetaChannel { get; set; } = Constants.ApiVersion.IsPrerelease(); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 3e93760b..af8df8a0 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1026,6 +1026,17 @@ namespace StardewModdingAPI.Framework "These mods change the save serialiser. They may corrupt your save files, or make them unusable if", "you uninstall these mods." ); + if (this.Settings.ParanoidWarnings) + { + LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly", + "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be", + "legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); + LogWarningGroup(ModWarning.AccessesShell, LogLevel.Warn, "Accesses shell/process directly", + "These mods directly access the OS shell or processes, and you enabled paranoid warnings. (Note that", + "this may be legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); + } LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code", "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", "your game has issues, try removing these first. Otherwise you can ignore this warning." diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 2f0c1b15..5a47fb1a 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Metadata new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), /**** - ** detect incompatible code + ** detect mod issues ****/ // detect broken code new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies), @@ -61,7 +61,22 @@ namespace StardewModdingAPI.Metadata new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), - new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick) + new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick), + + /**** + ** detect paranoid issues + ****/ + // filesystem access + new TypeFinder(typeof(System.IO.File).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.FileStream).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.FileInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.Directory).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.DirectoryInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.DriveInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess), + new TypeFinder(typeof(System.IO.FileSystemWatcher).FullName, InstructionHandleResult.DetectedFilesystemAccess), + + // shell access + new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess) }; } } diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 115997ba..d81292c8 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -9,10 +9,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ { /** - * Whether to enable features intended for mod developers. Currently this only makes TRACE-level - * messages appear in the console. + * The console color theme to use. The possible values are: + * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. + * - LightBackground: use darker text colors that look better on a white or light background. + * - DarkBackground: use lighter text colors that look better on a black or dark background. */ - "DeveloperMode": true, + "ColorScheme": "AutoDetect", /** * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new @@ -21,6 +23,20 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "CheckForUpdates": true, + /** + * Whether to enable features intended for mod developers. Currently this only makes TRACE-level + * messages appear in the console. + */ + "DeveloperMode": true, + + /** + * Whether to add a section to the 'mod issues' list for mods which directly use potentially + * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as + * part of their normal functionality, so these warnings are meaningless without further + * investigation. + */ + "ParanoidWarnings": false, + /** * Whether SMAPI should show newer beta versions as an available update. If not specified, SMAPI * will only show beta updates if the current version is beta. @@ -50,14 +66,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "DumpMetadata": false, - /** - * The console color theme to use. The possible values are: - * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. - * - LightBackground: use darker text colors that look better on a white or light background. - * - DarkBackground: use lighter text colors that look better on a black or dark background. - */ - "ColorScheme": "AutoDetect", - /** * The mod IDs SMAPI should ignore when performing update checks or validating update keys. */ -- cgit From c94f3e7c63a2f1aec89c68417db348d4e684fb79 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Sep 2018 19:19:13 -0400 Subject: only use valid update keys in update-check logic (#592) --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 10 ++++++++-- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 23 ++++++++++++++++------ src/SMAPI/Framework/SCore.cs | 13 +++++++----- .../Framework/UpdateData/UpdateKey.cs | 8 ++++++++ 5 files changed, 42 insertions(+), 13 deletions(-) (limited to 'docs') 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 047091a1a400ec426eab36cfa94d11a8e917800c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Sep 2018 19:32:36 -0400 Subject: rewrite PNG premultiplication so mods can load PNGs during a draw cycle --- docs/release-notes.md | 1 + .../Framework/ContentManagers/ModContentManager.cs | 60 +++------------------- 2 files changed, 8 insertions(+), 53 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4fbb2e07..69943248 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. + * Mods are no longer prevented from loading a PNG while the game is drawing. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. * Suppressed the game's 'added crickets' debug output. * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods in future versions. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 24ce69ea..7048dcf3 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -165,63 +165,17 @@ namespace StardewModdingAPI.Framework.ContentManagers return file; } - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// Premultiply a texture's alpha values to avoid transparency issues in the game. /// The texture to premultiply. /// Returns a premultiplied texture. - /// Based on code by Layoric. + /// Based on code by David Gouveia. private Texture2D PremultiplyTransparency(Texture2D texture) { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - + Color[] data = new Color[texture.Width * texture.Height]; + texture.GetData(data); + for (int i = 0; i < data.Length; i++) + data[i] = Color.FromNonPremultiplied(data[i].ToVector4()); + texture.SetData(data); return texture; } } -- cgit From af6c273d8aaf4199741aaecab6bd4e95754dce49 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Sep 2018 19:40:05 -0400 Subject: fix transparency issues on Linux/Mac due to MonoGame bug (#279) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 8 ++++++++ 2 files changed, 9 insertions(+) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 69943248..2d6cbebf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * 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. + * Fixed transparency issues on Linux/Mac for many mods. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 7048dcf3..ed76a925 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -171,6 +171,14 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Based on code by David Gouveia. private Texture2D PremultiplyTransparency(Texture2D texture) { + // Textures loaded by Texture2D.FromStream are already premultiplied on Linux/Mac, even + // though the XNA documentation explicitly says otherwise. That's a glitch in MonoGame + // fixed in newer versions, but the game uses a bundled version that will always be + // affected. See https://github.com/MonoGame/MonoGame/issues/4820 for more info. + if (Constants.TargetPlatform != GamePlatform.Windows) + return texture; + + // premultiply pixels Color[] data = new Color[texture.Width * texture.Height]; texture.GetData(data); for (int i = 0; i < data.Length; i++) -- cgit From 47bc21109c183d48b80458380ad988c7d1045cf6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 4 Sep 2018 01:49:12 -0400 Subject: update compatibility list --- docs/release-notes.md | 2 +- src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2d6cbebf..bf0abae3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,7 +10,7 @@ * 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. - * Fixed transparency issues on Linux/Mac for many mods. + * Fixed transparency issues on Linux/Mac for some mod images. * Updated compatibility list. * For modders: diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json index 47e72d9f..4a292684 100644 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -194,7 +194,7 @@ "Fix Scythe Exp": { "ID": "bcmpinc.FixScytheExp", - "~0.2 | Status": "AssumeBroken" // Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator. + "~0.3 | Status": "AssumeBroken" // broke in 1.3: Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator. }, "Gate Opener": { -- 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 'docs') 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 43a3af1a671c46b48b4c4eb5ab1552d1177cb9fb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 17:49:36 -0400 Subject: prefer xterm when launching SMAPI --- docs/release-notes.md | 1 + src/SMAPI.Installer/unix-launcher.sh | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index b9cee4c6..cb9e8788 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. + * 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. * Fixed installer duplicating bundled mods if you moved them after the last install. * Fixed crash when a mod manifest is corrupted. diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index 1e969c20..b64c021c 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -62,7 +62,9 @@ else fi # open SMAPI in terminal - if $COMMAND x-terminal-emulator 2>/dev/null; then + if $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" + elif $COMMAND x-terminal-emulator 2>/dev/null; then # Terminator converts -e to -x when used through x-terminal-emulator for some reason (per # `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator # is mapped to Terminator, invoke it directly instead. @@ -71,8 +73,6 @@ else else x-terminal-emulator -e "$LAUNCHER" fi - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" elif $COMMAND xfce4-terminal 2>/dev/null; then xfce4-terminal -e "env TERM=xterm; $LAUNCHER" elif $COMMAND gnome-terminal 2>/dev/null; then -- cgit From 8b2fd0bab715622208df256b8066361b32a3b209 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 17:52:48 -0400 Subject: fix translation issues not shown as warnings --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index cb9e8788..22730e28 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * Fixed error-handling when initialising paths. * Fixed 'no update keys' warning not shown for mods with only invalid update keys. * Fixed transparency issues on Linux/Mac for some mod images. + * Fixed translation issues not shown as warnings. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 00e608c1..33edc698 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1114,11 +1114,11 @@ namespace StardewModdingAPI.Framework if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary data)) translations[locale] = data; else - metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed."); + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed.", LogLevel.Warn); } catch (Exception ex) { - metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}"); + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}", LogLevel.Warn); } } } -- cgit From 239173558c5a6b9d1340ed9d77edb7fce2cb19f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 18:49:07 -0400 Subject: fix dependencies not enforced if the dependency failed in the load phase --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 22730e28..84fb88b0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * Fixed 'no update keys' warning not shown for mods with only invalid update keys. * Fixed transparency issues on Linux/Mac for some mod images. * Fixed translation issues not shown as warnings. + * Fixed dependencies not correctly enforced if the dependency is installed but failed to load. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 28994060..14380f7b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -701,7 +701,7 @@ namespace StardewModdingAPI.Framework InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); foreach (IModMetadata mod in mods) { - if (!this.TryLoadMod(mod, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) { skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails); if (mod.Status != ModMetadataStatus.Failed) @@ -840,6 +840,7 @@ namespace StardewModdingAPI.Framework /// Load a given mod. /// The mod to load. + /// The mods being loaded. /// Preprocesses and loads mod assemblies /// Generates proxy classes to access mod APIs through an arbitrary interface. /// The JSON helper with which to read mods' JSON files. @@ -849,7 +850,7 @@ namespace StardewModdingAPI.Framework /// The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable). /// More detailed details about the error intended for developers (if any). /// Returns whether the mod was successfully loaded. - private bool TryLoadMod(IModMetadata mod, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails) + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails) { errorDetails = null; @@ -875,6 +876,23 @@ namespace StardewModdingAPI.Framework return false; } + // validate dependencies + // Although dependences are validated before mods are loaded, a dependency may have failed. + if (mod.Manifest.Dependencies?.Any() == true) + { + foreach (IManifestDependency dependency in mod.Manifest.Dependencies) + { + if (this.ModRegistry.Get(dependency.UniqueID) == null) + { + string dependencyName = mods + .FirstOrDefault(p => dependency.UniqueID.Equals(p.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + ?.DisplayName ?? dependency.UniqueID; + errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; + return false; + } + } + } + // load as content pack if (mod.IsContentPack) { -- cgit From f56636d2c8c78d06b150c4f942f2b791228c560b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 21:03:05 -0400 Subject: fix trace logs not showing path for invalid mods --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 84fb88b0..f6119d23 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Mods are no longer prevented from loading a PNG while the game is drawing. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. + * Fixed trace logs not showing path for invalid mods. * Suppressed the game's 'added crickets' debug output. * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods in future versions. * **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. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 14380f7b..d05f5bb9 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -837,7 +837,7 @@ namespace StardewModdingAPI.Framework // unlock mod integrations this.ModRegistry.AreAllModsInitialised = true; } - + /// Load a given mod. /// The mod to load. /// The mods being loaded. @@ -855,13 +855,14 @@ namespace StardewModdingAPI.Framework errorDetails = null; // log entry - if (mod.IsContentPack) - this.Monitor.Log($" {mod.DisplayName} (content pack, {PathUtilities.GetRelativePath(this.ModsPath, mod.DirectoryPath)})...", LogLevel.Trace); - else { - this.Monitor.Log(mod.Manifest?.EntryDll != null - ? $" {mod.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, mod.DirectoryPath)}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid - : $" {mod.DisplayName}...", LogLevel.Trace); + string relativePath = PathUtilities.GetRelativePath(this.ModsPath, mod.DirectoryPath); + if (mod.IsContentPack) + this.Monitor.Log($" {mod.DisplayName} ({relativePath}) [content pack]...", LogLevel.Trace); + else if (mod.Manifest.EntryDll != null) + this.Monitor.Log($" {mod.DisplayName} ({relativePath}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})...", LogLevel.Trace); // don't use Path.Combine here, since EntryDLL might not be valid + else + this.Monitor.Log($" {mod.DisplayName} ({relativePath})...", LogLevel.Trace); } // add warning for missing update key -- cgit From 99e4a4a1ccf5dcbe0e4b1af7591e229a0eeae447 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Sep 2018 18:25:33 -0400 Subject: fix crash when mods add an invalid location with no name --- docs/release-notes.md | 1 + src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f6119d23..6360f3e7 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. + * 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. * Fixed installer duplicating bundled mods if you moved them after the last install. diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs index d9090c08..5a259663 100644 --- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs +++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Buildings; @@ -18,10 +20,10 @@ namespace StardewModdingAPI.Framework.StateTracking private readonly ICollectionWatcher LocationListWatcher; /// A lookup of the tracked locations. - private IDictionary LocationDict { get; } = new Dictionary(); + private IDictionary LocationDict { get; } = new Dictionary(new ObjectReferenceComparer()); /// A lookup of registered buildings and their indoor location. - private readonly IDictionary BuildingIndoors = new Dictionary(); + private readonly IDictionary BuildingIndoors = new Dictionary(new ObjectReferenceComparer()); /********* @@ -37,10 +39,10 @@ namespace StardewModdingAPI.Framework.StateTracking public IEnumerable Locations => this.LocationDict.Values; /// The locations removed since the last update. - public ICollection Added { get; } = new HashSet(); + public ICollection Added { get; } = new HashSet(new ObjectReferenceComparer()); /// The locations added since the last update. - public ICollection Removed { get; } = new HashSet(); + public ICollection Removed { get; } = new HashSet(new ObjectReferenceComparer()); /********* -- cgit From b9844c4acd93411c2a7d21bd115cb4fee1791d76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Sep 2018 00:58:46 -0400 Subject: add support for semi-transparency when overlaying images --- docs/release-notes.md | 1 + src/SMAPI/Framework/Content/AssetDataForImage.cs | 39 ++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6360f3e7..3fea347e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). + * Added support for overlaying images with semi-transparency using `asset.AsImage().PatchImage`. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Mods are no longer prevented from loading a PNG while the game is drawing. diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 5c7b87de..cd372948 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -7,6 +7,14 @@ namespace StardewModdingAPI.Framework.Content /// Encapsulates access and changes to image content being read from a data file. internal class AssetDataForImage : AssetData, IAssetDataForImage { + /********* + ** Properties + *********/ + /// The minimum value to consider non-transparent. + /// On Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason. + private const byte MinOpacity = 5; + + /********* ** Public methods *********/ @@ -53,13 +61,38 @@ namespace StardewModdingAPI.Framework.Content // merge data in overlay mode if (patchMode == PatchMode.Overlay) { + // get target data + Color[] targetData = new Color[pixelCount]; + target.GetData(0, targetArea, targetData, 0, pixelCount); + + // merge pixels Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; target.GetData(0, targetArea, newData, 0, newData.Length); for (int i = 0; i < sourceData.Length; i++) { - Color pixel = sourceData[i]; - if (pixel.A > 4) // not transparent (note: on Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason) - newData[i] = pixel; + Color above = sourceData[i]; + Color below = targetData[i]; + + // shortcut transparency + if (above.A < AssetDataForImage.MinOpacity) + continue; + if (below.A < AssetDataForImage.MinOpacity) + { + newData[i] = above; + continue; + } + + // merge pixels + // This performs a conventional alpha blend for the pixels, which are already + // premultiplied by the content pipeline. + float alphaAbove = above.A / 255f; + float alphaBelow = (255 - above.A) / 255f; + newData[i] = new Color( + r: (int)((above.R * alphaAbove) + (below.R * alphaBelow)), + g: (int)((above.G * alphaAbove) + (below.G * alphaBelow)), + b: (int)((above.B * alphaAbove) + (below.B * alphaBelow)), + a: Math.Max(above.A, below.A) + ); } sourceData = newData; } -- 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 'docs') 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 'docs') 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 c531acb6599b4e115e8b6f6d12e9194b3f83ff9d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Sep 2018 18:30:14 -0400 Subject: fix command errors logged as SMAPI instead of the affected mod --- docs/release-notes.md | 1 + src/SMAPI/Framework/Command.cs | 12 +++++----- src/SMAPI/Framework/CommandManager.cs | 31 +++++++++++++++++-------- src/SMAPI/Framework/ModHelpers/CommandHelper.cs | 15 ++++++------ src/SMAPI/Framework/SCore.cs | 12 +++++----- src/SMAPI/Framework/SGame.cs | 22 ++++++++++++++++-- 6 files changed, 61 insertions(+), 32 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7c25eba6..f4bce8c8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ * Fixed transparency issues on Linux/Mac for some mod images. * Fixed translation issues not shown as warnings. * Fixed dependencies not correctly enforced if the dependency is installed but failed to load. + * Fixed some errors logged as SMAPI instead of the affected mod. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/Command.cs b/src/SMAPI/Framework/Command.cs index 943e018d..8c9df47d 100644 --- a/src/SMAPI/Framework/Command.cs +++ b/src/SMAPI/Framework/Command.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace StardewModdingAPI.Framework { @@ -8,8 +8,8 @@ namespace StardewModdingAPI.Framework /********* ** Accessor *********/ - /// The friendly name for the mod that registered the command. - public string ModName { get; } + /// The mod that registered the command (or null if registered by SMAPI). + public IModMetadata Mod { get; } /// The command name, which the user must type to trigger it. public string Name { get; } @@ -25,13 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. - /// The friendly name for the mod that registered the command. + /// The mod that registered the command (or null if registered by SMAPI). /// The command name, which the user must type to trigger it. /// The human-readable documentation shown when the player runs the built-in 'help' command. /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - public Command(string modName, string name, string documentation, Action callback) + public Command(IModMetadata mod, string name, string documentation, Action callback) { - this.ModName = modName; + this.Mod = mod; this.Name = name; this.Documentation = documentation; this.Callback = callback; diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index f9651ed9..aabe99c3 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Add a console command. - /// The friendly mod name for this instance. + /// The mod adding the command (or null for a SMAPI command). /// The command name, which the user must type to trigger it. /// The human-readable documentation shown when the player runs the built-in 'help' command. /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework /// The or is null or empty. /// The is not a valid format. /// There's already a command with that name. - public void Add(string modName, string name, string documentation, Action callback, bool allowNullCallback = false) + public void Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) { name = this.GetNormalisedName(name); @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name."); // add command - this.Commands.Add(name, new Command(modName, name, documentation, callback)); + this.Commands.Add(name, new Command(mod, name, documentation, callback)); } /// Get a command by its unique name. @@ -65,19 +65,30 @@ namespace StardewModdingAPI.Framework .OrderBy(p => p.Name); } - /// Trigger a command. - /// The raw command input. - /// Returns whether a matching command was triggered. - public bool Trigger(string input) + /// Try to parse a raw line of user input into an executable command. + /// The raw user input. + /// The parsed command name. + /// The parsed command arguments. + /// The command which can handle the input. + /// Returns true if the input was successfully parsed and matched to a command; else false. + public bool TryParse(string input, out string name, out string[] args, out Command command) { + // ignore if blank if (string.IsNullOrWhiteSpace(input)) + { + name = null; + args = null; + command = null; return false; + } - string[] args = this.ParseArgs(input); - string name = args[0]; + // parse input + args = this.ParseArgs(input); + name = this.GetNormalisedName(args[0]); args = args.Skip(1).ToArray(); - return this.Trigger(name, args); + // get command + return this.Commands.TryGetValue(name, out command); } /// Trigger a command. diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs index bdedb07c..5a3304f3 100644 --- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs @@ -8,8 +8,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ - /// The friendly mod name for this instance. - private readonly string ModName; + /// The mod using this instance. + private readonly IModMetadata Mod; /// Manages console commands. private readonly CommandManager CommandManager; @@ -19,13 +19,12 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// Construct an instance. - /// The unique ID of the relevant mod. - /// The friendly mod name for this instance. + /// The mod using this instance. /// Manages console commands. - public CommandHelper(string modID, string modName, CommandManager commandManager) - : base(modID) + public CommandHelper(IModMetadata mod, CommandManager commandManager) + : base(mod?.Manifest?.UniqueID ?? "SMAPI") { - this.ModName = modName; + this.Mod = mod; this.CommandManager = commandManager; } @@ -38,7 +37,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// There's already a command with that name. public ICommandHelper Add(string name, string documentation, Action callback) { - this.CommandManager.Add(this.ModName, name, documentation, callback); + this.CommandManager.Add(this.Mod, name, documentation, callback); return this; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5f30c9ef..0594c793 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -427,8 +427,8 @@ namespace StardewModdingAPI.Framework { // prepare console this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.GameInstance.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); - this.GameInstance.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); + this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); // start handling command line input Thread inputThread = new Thread(() => @@ -973,7 +973,7 @@ namespace StardewModdingAPI.Framework IModHelper modHelper; { IModEvents events = new ModEvents(mod, this.EventManager); - ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, mod.DisplayName, this.GameInstance.CommandManager); + ICommandHelper commandHelper = new CommandHelper(mod, this.GameInstance.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection, this.DeprecationManager); @@ -1216,15 +1216,15 @@ namespace StardewModdingAPI.Framework if (result == null) this.Monitor.Log("There's no command with that name.", LogLevel.Error); else - this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); + this.Monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); } else { string message = "The following commands are registered:\n"; - IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); + IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); foreach (var group in groups) { - string modName = group.Key; + string modName = group.Key ?? "SMAPI"; string[] commandNames = group.ToArray(); message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 1bd583bf..a9b80bc7 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -268,14 +268,32 @@ namespace StardewModdingAPI.Framework *********/ while (this.CommandQueue.TryDequeue(out string rawInput)) { + // parse command + string name; + string[] args; + Command command; try { - if (!this.CommandManager.Trigger(rawInput)) + if (!this.CommandManager.TryParse(rawInput, out name, out args, out command)) this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); } catch (Exception ex) { - this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // execute command + try + { + command.Callback.Invoke(name, args); + } + catch (Exception ex) + { + if (command.Mod != null) + command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); } } -- cgit From f5386fcf6028cb6d6f9e93bf282c4c99a62debf7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 1 Oct 2018 18:43:14 -0400 Subject: add error if player has wrong version of SMAPI installed for their OS --- docs/release-notes.md | 33 ++++++++++++++++++--------------- src/SMAPI/Framework/SCore.cs | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f4bce8c8..8573c175 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,19 +1,21 @@ # 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. - * 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. - * 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. + * 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. + * Improved error messages when... + * an XNB mod is added to `Mods`; + * you install the wrong version of SMAPI for your OS; + * SMAPI can't prepare its folders. * Fixed transparency issues on Linux/Mac for some mod images. + * Fixed error when a mod manifest is corrupted. + * Fixed error when a mod adds an unnamed location. + * Fixed some Windows install paths not detected. + * Fixed Linux compatibility issues for some players. SMAPI will now always use xterm if it's available. + * Fixed installer duplicating bundled mods if you moved them after the last install. * Fixed translation issues not shown as warnings. * Fixed dependencies not correctly enforced if the dependency is installed but failed to load. * Fixed some errors logged as SMAPI instead of the affected mod. @@ -21,15 +23,16 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). - * Added support for overlaying images with semi-transparency using `asset.AsImage().PatchImage`. + * Added support for overlaying image assets with semi-transparency using the content API. * Added `IContentPack.WriteJsonFile` method. - * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. + * Added IntelliSense documentation for the non-developers version of SMAPI. * Mods are no longer prevented from loading a PNG while the game is drawing. * 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. * Suppressed the game's 'added crickets' debug output. - * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods in future versions. - * **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. + * **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. * 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 0594c793..4c33f348 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -141,6 +141,23 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); + // validate platform +#if SMAPI_FOR_WINDOWS + if (Constants.Platform != Platform.Windows) + { + this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or Mac. Please reinstall SMAPI to fix this.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } +#else + if (Constants.Platform == Platform.Windows) + { + this.Monitor.Log("Oops! You're running {Constants.Platform}, but this version of SMAPI is for Windows. Please reinstall SMAPI to fix this.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } +#endif + // validate game version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { -- 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 'docs') 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 ec6025aad35addab8121a31d1c4abf667fd5210a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 18:57:09 -0400 Subject: add more events (#310) --- docs/release-notes.md | 7 +- src/SMAPI/Enums/SkillType.cs | 26 +++++ src/SMAPI/Events/EventArgsInventoryChanged.cs | 2 +- src/SMAPI/Events/EventArgsLevelUp.cs | 13 ++- src/SMAPI/Events/IDisplayEvents.cs | 39 +++++++ src/SMAPI/Events/IGameLoopEvents.cs | 6 + src/SMAPI/Events/IModEvents.cs | 9 ++ src/SMAPI/Events/IPlayerEvents.cs | 17 +++ src/SMAPI/Events/ISpecialisedEvents.cs | 14 +++ src/SMAPI/Events/InventoryChangedEventArgs.cs | 56 +++++++++ src/SMAPI/Events/ItemStackSizeChange.cs | 35 ++++++ src/SMAPI/Events/LevelChangedEventArgs.cs | 42 +++++++ src/SMAPI/Events/MenuChangedEventArgs.cs | 31 +++++ src/SMAPI/Events/RenderedActiveMenuEventArgs.cs | 16 +++ src/SMAPI/Events/RenderedEventArgs.cs | 16 +++ src/SMAPI/Events/RenderedHudEventArgs.cs | 16 +++ src/SMAPI/Events/RenderedWorldEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingActiveMenuEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingHudEventArgs.cs | 16 +++ src/SMAPI/Events/RenderingWorldEventArgs.cs | 16 +++ src/SMAPI/Events/ReturnedToTitleEventArgs.cs | 7 ++ src/SMAPI/Events/SpecialisedEvents.cs | 2 +- src/SMAPI/Events/TimeChangedEventArgs.cs | 30 +++++ .../Events/UnvalidatedUpdateTickedEventArgs.cs | 36 ++++++ .../Events/UnvalidatedUpdateTickingEventArgs.cs | 36 ++++++ src/SMAPI/Events/WarpedEventArgs.cs | 37 ++++++ src/SMAPI/Events/WindowResizedEventArgs.cs | 31 +++++ src/SMAPI/Framework/Events/EventManager.cs | 80 +++++++++++++ src/SMAPI/Framework/Events/ModDisplayEvents.cs | 93 +++++++++++++++ src/SMAPI/Framework/Events/ModEvents.cs | 12 ++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 15 +++ src/SMAPI/Framework/Events/ModPlayerEvents.cs | 43 +++++++ src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 36 ++++++ src/SMAPI/Framework/InternalExtensions.cs | 4 +- src/SMAPI/Framework/SGame.cs | 81 ++++++++++--- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 19 +-- src/SMAPI/StardewModdingAPI.csproj | 128 +++++++++++++-------- 38 files changed, 1029 insertions(+), 86 deletions(-) create mode 100644 src/SMAPI/Enums/SkillType.cs create mode 100644 src/SMAPI/Events/IDisplayEvents.cs create mode 100644 src/SMAPI/Events/IPlayerEvents.cs create mode 100644 src/SMAPI/Events/ISpecialisedEvents.cs create mode 100644 src/SMAPI/Events/InventoryChangedEventArgs.cs create mode 100644 src/SMAPI/Events/ItemStackSizeChange.cs create mode 100644 src/SMAPI/Events/LevelChangedEventArgs.cs create mode 100644 src/SMAPI/Events/MenuChangedEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedActiveMenuEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedHudEventArgs.cs create mode 100644 src/SMAPI/Events/RenderedWorldEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingActiveMenuEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingHudEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingWorldEventArgs.cs create mode 100644 src/SMAPI/Events/ReturnedToTitleEventArgs.cs create mode 100644 src/SMAPI/Events/TimeChangedEventArgs.cs create mode 100644 src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs create mode 100644 src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs create mode 100644 src/SMAPI/Events/WarpedEventArgs.cs create mode 100644 src/SMAPI/Events/WindowResizedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModDisplayEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModPlayerEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModSpecialisedEvents.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index beba403a..63db5dc3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,10 +23,13 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). - * Added support for overlaying image assets with semi-transparency using the content API. * 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. + * Added more events to the prototype `helper.Events` for SMAPI 3.0. + * Added `SkillType` enum constant. + * Improved content API: + * added support for overlaying image assets with semi-transparency; + * mods can now load PNGs even if the game is currently 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. diff --git a/src/SMAPI/Enums/SkillType.cs b/src/SMAPI/Enums/SkillType.cs new file mode 100644 index 00000000..10518ec9 --- /dev/null +++ b/src/SMAPI/Enums/SkillType.cs @@ -0,0 +1,26 @@ +using StardewValley; + +namespace StardewModdingAPI.Enums +{ + /// The player skill types. + public enum SkillType + { + /// The combat skill. + Combat = Farmer.combatSkill, + + /// The farming skill. + Farming = Farmer.farmingSkill, + + /// The fishing skill. + Fishing = Farmer.fishingSkill, + + /// The foraging skill. + Foraging = Farmer.foragingSkill, + + /// The mining skill. + Mining = Farmer.miningSkill, + + /// The luck skill. + Luck = Farmer.luckSkill + } +} diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs index 1fdca834..3a2354b6 100644 --- a/src/SMAPI/Events/EventArgsInventoryChanged.cs +++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The player's inventory. /// The inventory changes. - public EventArgsInventoryChanged(IList inventory, List changedItems) + public EventArgsInventoryChanged(IList inventory, ItemStackChange[] changedItems) { this.Inventory = inventory; this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); diff --git a/src/SMAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs index fe6696d4..e9a697e7 100644 --- a/src/SMAPI/Events/EventArgsLevelUp.cs +++ b/src/SMAPI/Events/EventArgsLevelUp.cs @@ -1,4 +1,5 @@ using System; +using StardewModdingAPI.Enums; namespace StardewModdingAPI.Events { @@ -18,22 +19,22 @@ namespace StardewModdingAPI.Events public enum LevelType { /// The combat skill. - Combat, + Combat = SkillType.Combat, /// The farming skill. - Farming, + Farming = SkillType.Farming, /// The fishing skill. - Fishing, + Fishing = SkillType.Fishing, /// The foraging skill. - Foraging, + Foraging = SkillType.Foraging, /// The mining skill. - Mining, + Mining = SkillType.Mining, /// The luck skill. - Luck + Luck = SkillType.Luck } diff --git a/src/SMAPI/Events/IDisplayEvents.cs b/src/SMAPI/Events/IDisplayEvents.cs new file mode 100644 index 00000000..dbf8d90f --- /dev/null +++ b/src/SMAPI/Events/IDisplayEvents.cs @@ -0,0 +1,39 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Events related to UI and drawing to the screen. + public interface IDisplayEvents + { + /// Raised after a game menu is opened, closed, or replaced. + event EventHandler MenuChanged; + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + event EventHandler Rendering; + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + event EventHandler Rendered; + + /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. + event EventHandler RenderingWorld; + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. + event EventHandler RenderedWorld; + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. + event EventHandler RenderingActiveMenu; + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. + event EventHandler RenderedActiveMenu; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. + event EventHandler RenderingHud; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. + event EventHandler RenderedHud; + + /// Raised after the game window is resized. + event EventHandler WindowResized; + } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 5f531ba7..e1900f79 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -34,5 +34,11 @@ namespace StardewModdingAPI.Events /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . event EventHandler DayEnding; + + /// Raised after the in-game clock time changes. + event EventHandler TimeChanged; + + /// Raised after the game returns to the title screen. + event EventHandler ReturnedToTitle; } } diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index cf2f8cb8..76da7751 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,13 +3,22 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events related to UI and drawing to the screen. + IDisplayEvents Display { get; } + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. IGameLoopEvents GameLoop { get; } /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } + /// Events raised when the player data changes. + IPlayerEvents Player { get; } + /// Events raised when something changes in the world. IWorldEvents World { get; } + + /// Events serving specialised edge cases that shouldn't be used by most mods. + ISpecialisedEvents Specialised { get; } } } diff --git a/src/SMAPI/Events/IPlayerEvents.cs b/src/SMAPI/Events/IPlayerEvents.cs new file mode 100644 index 00000000..81e17b1a --- /dev/null +++ b/src/SMAPI/Events/IPlayerEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player data changes. + public interface IPlayerEvents + { + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player. + event EventHandler InventoryChanged; + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the current player. + event EventHandler LevelChanged; + + /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the current player. + event EventHandler Warped; + } +} diff --git a/src/SMAPI/Events/ISpecialisedEvents.cs b/src/SMAPI/Events/ISpecialisedEvents.cs new file mode 100644 index 00000000..928cd05d --- /dev/null +++ b/src/SMAPI/Events/ISpecialisedEvents.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + public interface ISpecialisedEvents + { + /// Raised before the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + event EventHandler UnvalidatedUpdateTicking; + + /// Raised after the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + event EventHandler UnvalidatedUpdateTicked; + } +} diff --git a/src/SMAPI/Events/InventoryChangedEventArgs.cs b/src/SMAPI/Events/InventoryChangedEventArgs.cs new file mode 100644 index 00000000..a081611b --- /dev/null +++ b/src/SMAPI/Events/InventoryChangedEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class InventoryChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose inventory changed. + public Farmer Player { get; } + + /// The added items. + public IEnumerable Added { get; } + + /// The removed items. + public IEnumerable Removed { get; } + + /// The items whose stack sizes changed, with the relative change. + public IEnumerable QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player whose inventory changed. + /// The inventory changes. + public InventoryChangedEventArgs(Farmer player, ItemStackChange[] changedItems) + { + this.Player = player; + this.Added = changedItems + .Where(n => n.ChangeType == ChangeType.Added) + .Select(p => p.Item) + .ToArray(); + + this.Removed = changedItems + .Where(n => n.ChangeType == ChangeType.Removed) + .Select(p => p.Item) + .ToArray(); + + this.QuantityChanged = changedItems + .Where(n => n.ChangeType == ChangeType.StackChange) + .Select(change => new ItemStackSizeChange( + item: change.Item, + oldSize: change.Item.Stack - change.StackChange, + newSize: change.Item.Stack + )) + .ToArray(); + } + } +} diff --git a/src/SMAPI/Events/ItemStackSizeChange.cs b/src/SMAPI/Events/ItemStackSizeChange.cs new file mode 100644 index 00000000..35369be2 --- /dev/null +++ b/src/SMAPI/Events/ItemStackSizeChange.cs @@ -0,0 +1,35 @@ +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// An inventory item stack size change. + public class ItemStackSizeChange + { + /********* + ** Accessors + *********/ + /// The item whose stack size changed. + public Item Item { get; } + + /// The previous stack size. + public int OldSize { get; } + + /// The new stack size. + public int NewSize { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The item whose stack size changed. + /// The previous stack size. + /// The new stack size. + public ItemStackSizeChange(Item item, int oldSize, int newSize) + { + this.Item = item; + this.OldSize = oldSize; + this.NewSize = newSize; + } + } +} diff --git a/src/SMAPI/Events/LevelChangedEventArgs.cs b/src/SMAPI/Events/LevelChangedEventArgs.cs new file mode 100644 index 00000000..174094c7 --- /dev/null +++ b/src/SMAPI/Events/LevelChangedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using StardewModdingAPI.Enums; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LevelChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose skill level changed. + public Farmer Player { get; } + + /// The skill whose level changed. + public SkillType Skill { get; } + + /// The previous skill level. + public int OldLevel { get; } + + /// The new skill level. + public int NewLevel { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player whose skill level changed. + /// The skill whose level changed. + /// The previous skill level. + /// The new skill level. + public LevelChangedEventArgs(Farmer player, SkillType skill, int oldLevel, int newLevel) + { + this.Player = player; + this.Skill = skill; + this.OldLevel = oldLevel; + this.NewLevel = newLevel; + } + } +} diff --git a/src/SMAPI/Events/MenuChangedEventArgs.cs b/src/SMAPI/Events/MenuChangedEventArgs.cs new file mode 100644 index 00000000..e1c049a2 --- /dev/null +++ b/src/SMAPI/Events/MenuChangedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class MenuChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous menu. + public IClickableMenu OldMenu { get; } + + /// The current menu. + public IClickableMenu NewMenu { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous menu. + /// The current menu. + public MenuChangedEventArgs(IClickableMenu oldMenu, IClickableMenu newMenu) + { + this.OldMenu = oldMenu; + this.NewMenu = newMenu; + } + } +} diff --git a/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs b/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs new file mode 100644 index 00000000..efd4163b --- /dev/null +++ b/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedActiveMenuEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderedEventArgs.cs b/src/SMAPI/Events/RenderedEventArgs.cs new file mode 100644 index 00000000..d6341b19 --- /dev/null +++ b/src/SMAPI/Events/RenderedEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderedHudEventArgs.cs b/src/SMAPI/Events/RenderedHudEventArgs.cs new file mode 100644 index 00000000..46e89013 --- /dev/null +++ b/src/SMAPI/Events/RenderedHudEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedHudEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderedWorldEventArgs.cs b/src/SMAPI/Events/RenderedWorldEventArgs.cs new file mode 100644 index 00000000..56145381 --- /dev/null +++ b/src/SMAPI/Events/RenderedWorldEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedWorldEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs b/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs new file mode 100644 index 00000000..103f56df --- /dev/null +++ b/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingActiveMenuEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingEventArgs.cs b/src/SMAPI/Events/RenderingEventArgs.cs new file mode 100644 index 00000000..5acbef09 --- /dev/null +++ b/src/SMAPI/Events/RenderingEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingHudEventArgs.cs b/src/SMAPI/Events/RenderingHudEventArgs.cs new file mode 100644 index 00000000..84c96ecd --- /dev/null +++ b/src/SMAPI/Events/RenderingHudEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingHudEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/RenderingWorldEventArgs.cs b/src/SMAPI/Events/RenderingWorldEventArgs.cs new file mode 100644 index 00000000..d0d44789 --- /dev/null +++ b/src/SMAPI/Events/RenderingWorldEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingWorldEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/SMAPI/Events/ReturnedToTitleEventArgs.cs b/src/SMAPI/Events/ReturnedToTitleEventArgs.cs new file mode 100644 index 00000000..96309cde --- /dev/null +++ b/src/SMAPI/Events/ReturnedToTitleEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ReturnedToTitleEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs index af84b397..bdf25ccb 100644 --- a/src/SMAPI/Events/SpecialisedEvents.cs +++ b/src/SMAPI/Events/SpecialisedEvents.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { - /// Events serving specialised edge cases that shouldn't be used by most mod. + /// Events serving specialised edge cases that shouldn't be used by most mods. public static class SpecialisedEvents { /********* diff --git a/src/SMAPI/Events/TimeChangedEventArgs.cs b/src/SMAPI/Events/TimeChangedEventArgs.cs new file mode 100644 index 00000000..fd472092 --- /dev/null +++ b/src/SMAPI/Events/TimeChangedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class TimeChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous time of day in 24-hour notation (like 1600 for 4pm). The clock time resets when the player sleeps, so 2am (before sleeping) is 2600. + public int OldTime { get; } + + /// The current time of day in 24-hour notation (like 1600 for 4pm). The clock time resets when the player sleeps, so 2am (before sleeping) is 2600. + public int NewTime { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous time of day in 24-hour notation (like 1600 for 4pm). + /// The current time of day in 24-hour notation (like 1600 for 4pm). + public TimeChangedEventArgs(int oldTime, int newTime) + { + this.OldTime = oldTime; + this.NewTime = newTime; + } + } +} diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs new file mode 100644 index 00000000..5638bdb7 --- /dev/null +++ b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UnvalidatedUpdateTickedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public UnvalidatedUpdateTickedEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs new file mode 100644 index 00000000..ebadbb99 --- /dev/null +++ b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UnvalidatedUpdateTickingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public UnvalidatedUpdateTickingEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/WarpedEventArgs.cs b/src/SMAPI/Events/WarpedEventArgs.cs new file mode 100644 index 00000000..1b1c7381 --- /dev/null +++ b/src/SMAPI/Events/WarpedEventArgs.cs @@ -0,0 +1,37 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class WarpedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who warped to a new location. + public Farmer Player { get; } + + /// The player's previous location. + public GameLocation OldLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who warped to a new location. + /// The player's previous location. + /// The player's current location. + public WarpedEventArgs(Farmer player, GameLocation oldLocation, GameLocation newLocation) + { + this.Player = player; + this.NewLocation = newLocation; + this.OldLocation = oldLocation; + } + } +} diff --git a/src/SMAPI/Events/WindowResizedEventArgs.cs b/src/SMAPI/Events/WindowResizedEventArgs.cs new file mode 100644 index 00000000..a990ba9d --- /dev/null +++ b/src/SMAPI/Events/WindowResizedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class WindowResizedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous window size. + public Point OldSize { get; } + + /// The current window size. + public Point NewSize { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous window size. + /// The current window size. + public WindowResizedEventArgs(Point oldSize, Point newSize) + { + this.OldSize = oldSize; + this.NewSize = newSize; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 616db7d9..31b0346a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -11,6 +11,39 @@ namespace StardewModdingAPI.Framework.Events /********* ** Events (new) *********/ + /**** + ** Display + ****/ + /// Raised after a game menu is opened, closed, or replaced. + public readonly ManagedEvent MenuChanged; + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + public readonly ManagedEvent Rendering; + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + public readonly ManagedEvent Rendered; + + /// Raised before the game world is drawn to the screen. + public readonly ManagedEvent RenderingWorld; + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. + public readonly ManagedEvent RenderedWorld; + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. + public readonly ManagedEvent RenderingActiveMenu; + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. + public readonly ManagedEvent RenderedActiveMenu; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. + public readonly ManagedEvent RenderingHud; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. + public readonly ManagedEvent RenderedHud; + + /// Raised after the game window is resized. + public readonly ManagedEvent WindowResized; + /**** ** Game loop ****/ @@ -44,6 +77,12 @@ namespace StardewModdingAPI.Framework.Events /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . public readonly ManagedEvent DayEnding; + /// Raised after the in-game clock time changes. + public readonly ManagedEvent TimeChanged; + + /// Raised after the game returns to the title screen. + public readonly ManagedEvent ReturnedToTitle; + /**** ** Input ****/ @@ -59,6 +98,18 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player scrolls the mouse wheel. public readonly ManagedEvent MouseWheelScrolled; + /**** + ** Player + ****/ + /// Raised after items are added or removed to a player's inventory. + public readonly ManagedEvent InventoryChanged; + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public readonly ManagedEvent LevelChanged; + + /// Raised after a player warps to a new location. + public readonly ManagedEvent Warped; + /**** ** World ****/ @@ -83,6 +134,15 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent TerrainFeatureListChanged; + /**** + ** Specialised + ****/ + /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . + public readonly ManagedEvent UnvalidatedUpdateTicking; + + /// Raised after the game performs its overall update tick (≈60 times per second). See notes on . + public readonly ManagedEvent UnvalidatedUpdateTicked; + /********* ** Events (old) @@ -285,6 +345,17 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); + this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); + this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); + this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); + this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); + this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); + this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); + this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); + this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); + this.WindowResized = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); + this.GameLaunched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); @@ -295,12 +366,18 @@ namespace StardewModdingAPI.Framework.Events this.SaveLoaded = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveLoaded)); this.DayStarted = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayStarted)); this.DayEnding = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayEnding)); + this.TimeChanged = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged)); + this.ReturnedToTitle = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); + this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); + this.Warped = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); + this.BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); this.LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); @@ -309,6 +386,9 @@ namespace StardewModdingAPI.Framework.Events this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); + // init events (old) this.Legacy_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); diff --git a/src/SMAPI/Framework/Events/ModDisplayEvents.cs b/src/SMAPI/Framework/Events/ModDisplayEvents.cs new file mode 100644 index 00000000..e383eec6 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModDisplayEvents.cs @@ -0,0 +1,93 @@ +using System; +using StardewModdingAPI.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events related to UI and drawing to the screen. + internal class ModDisplayEvents : ModEventsBase, IDisplayEvents + { + /********* + ** Accessors + *********/ + /// Raised after a game menu is opened, closed, or replaced. + public event EventHandler MenuChanged + { + add => this.EventManager.MenuChanged.Add(value); + remove => this.EventManager.MenuChanged.Remove(value); + } + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + public event EventHandler Rendering + { + add => this.EventManager.Rendering.Add(value); + remove => this.EventManager.Rendering.Remove(value); + } + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + public event EventHandler Rendered + { + add => this.EventManager.Rendered.Add(value); + remove => this.EventManager.Rendered.Remove(value); + } + + /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. + public event EventHandler RenderingWorld + { + add => this.EventManager.RenderingWorld.Add(value); + remove => this.EventManager.RenderingWorld.Remove(value); + } + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. + public event EventHandler RenderedWorld + { + add => this.EventManager.RenderedWorld.Add(value); + remove => this.EventManager.RenderedWorld.Remove(value); + } + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. + public event EventHandler RenderingActiveMenu + { + add => this.EventManager.RenderingActiveMenu.Add(value); + remove => this.EventManager.RenderingActiveMenu.Remove(value); + } + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. + public event EventHandler RenderedActiveMenu + { + add => this.EventManager.RenderedActiveMenu.Add(value); + remove => this.EventManager.RenderedActiveMenu.Remove(value); + } + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. + public event EventHandler RenderingHud + { + add => this.EventManager.RenderingHud.Add(value); + remove => this.EventManager.RenderingHud.Remove(value); + } + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. + public event EventHandler RenderedHud + { + add => this.EventManager.RenderedHud.Add(value); + remove => this.EventManager.RenderedHud.Remove(value); + } + + /// Raised after the game window is resized. + public event EventHandler WindowResized + { + add => this.EventManager.WindowResized.Add(value); + remove => this.EventManager.WindowResized.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModDisplayEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 9e474457..7a318e8b 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,15 +8,24 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events related to UI and drawing to the screen. + public IDisplayEvents Display { get; } + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. public IGameLoopEvents GameLoop { get; } /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } + /// Events raised when the player data changes. + public IPlayerEvents Player { get; } + /// Events raised when something changes in the world. public IWorldEvents World { get; } + /// Events serving specialised edge cases that shouldn't be used by most mods. + public ISpecialisedEvents Specialised { get; } + /********* ** Public methods @@ -26,9 +35,12 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.Display = new ModDisplayEvents(mod, eventManager); this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); + this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); + this.Specialised = new ModSpecialisedEvents(mod, eventManager); } } } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index d1def91b..a5beac99 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -79,6 +79,21 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.DayEnding.Remove(value); } + /// Raised after the in-game clock time changes. + public event EventHandler TimeChanged + { + + add => this.EventManager.TimeChanged.Add(value); + remove => this.EventManager.TimeChanged.Remove(value); + } + + /// Raised after the game returns to the title screen. + public event EventHandler ReturnedToTitle + { + add => this.EventManager.ReturnedToTitle.Add(value); + remove => this.EventManager.ReturnedToTitle.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/Events/ModPlayerEvents.cs b/src/SMAPI/Framework/Events/ModPlayerEvents.cs new file mode 100644 index 00000000..ca7cfd96 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModPlayerEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player data changes. + internal class ModPlayerEvents : ModEventsBase, IPlayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player. + public event EventHandler InventoryChanged + { + add => this.EventManager.InventoryChanged.Add(value); + remove => this.EventManager.InventoryChanged.Remove(value); + } + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the local player. + public event EventHandler LevelChanged + { + add => this.EventManager.LevelChanged.Add(value); + remove => this.EventManager.LevelChanged.Remove(value); + } + + /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the local player. + public event EventHandler Warped + { + add => this.EventManager.Warped.Add(value); + remove => this.EventManager.Warped.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModPlayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs new file mode 100644 index 00000000..17c32bb8 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -0,0 +1,36 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + internal class ModSpecialisedEvents : ModEventsBase, ISpecialisedEvents + { + /********* + ** Accessors + *********/ + /// Raised before the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + public event EventHandler UnvalidatedUpdateTicking + { + add => this.EventManager.UnvalidatedUpdateTicking.Add(value); + remove => this.EventManager.UnvalidatedUpdateTicking.Remove(value); + } + + /// Raised after the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + public event EventHandler UnvalidatedUpdateTicked + { + add => this.EventManager.UnvalidatedUpdateTicked.Add(value); + remove => this.EventManager.UnvalidatedUpdateTicked.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModSpecialisedEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index b51ff6a8..f52bfe2b 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -43,8 +43,8 @@ namespace StardewModdingAPI.Framework /**** ** ManagedEvent ****/ - /// Raise the event and notify all handlers. - /// The empty event arguments type to construct. + /// Raise the event using the default event args and notify all handlers. + /// The event args type to construct. /// The event to raise. public static void RaiseEmpty(this ManagedEvent @event) where TEventArgs : new() { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index bd266ce1..57f48d11 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Netcode; +using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; @@ -265,7 +266,9 @@ namespace StardewModdingAPI.Framework // update tick are neglible and not worth the complications of bypassing Game1.Update. if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) { + this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); base.Update(gameTime); + this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); this.Events.Legacy_UnvalidatedUpdateTick.Raise(); return; } @@ -344,7 +347,9 @@ namespace StardewModdingAPI.Framework } // suppress non-save events + this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); base.Update(gameTime); + this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); this.Events.Legacy_UnvalidatedUpdateTick.Raise(); return; } @@ -406,6 +411,7 @@ namespace StardewModdingAPI.Framework if (wasWorldReady && !Context.IsWorldReady) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); + this.Events.ReturnedToTitle.RaiseEmpty(); this.Events.Legacy_AfterReturnToTitle.Raise(); } else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) @@ -441,6 +447,11 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + + Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; + Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; + + this.Events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); this.Events.Legacy_Resize.Raise(); this.Watchers.WindowSizeWatcher.Reset(); } @@ -551,6 +562,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events + this.Events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); if (now != null) this.Events.Legacy_MenuChanged.Raise(new EventArgsClickableMenuChanged(was, now)); else @@ -673,6 +685,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); + this.Events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); this.Events.Legacy_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } else @@ -681,39 +694,45 @@ namespace StardewModdingAPI.Framework // raise player events if (raiseWorldEvents) { - PlayerTracker curPlayer = this.Watchers.CurrentPlayerTracker; + PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker; // raise current location changed - if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) + if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) { if (this.VerboseLogging) this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); - this.Events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation)); + + GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; + this.Events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); + this.Events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(oldLocation, newLocation)); } // raise player leveled up a skill - foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) + foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) { if (this.VerboseLogging) this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); - this.Events.Legacy_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); + + this.Events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); + this.Events.Legacy_LeveledUp.Raise(new EventArgsLevelUp((EventArgsLevelUp.LevelType)pair.Key, pair.Value.CurrentValue)); } // raise player inventory changed - ItemStackChange[] changedItems = curPlayer.GetInventoryChanges().ToArray(); + ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); if (changedItems.Any()) { if (this.VerboseLogging) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); + this.Events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); + this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems)); } // raise mine level changed - if (curPlayer.TryGetNewMineLevel(out int mineLevel)) + if (playerTracker.TryGetNewMineLevel(out int mineLevel)) { if (this.VerboseLogging) this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); - this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); + this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel)); } } this.Watchers.CurrentPlayerTracker?.Reset(); @@ -728,6 +747,7 @@ namespace StardewModdingAPI.Framework this.TicksElapsed++; if (this.TicksElapsed == 1) this.Events.GameLaunched.Raise(new GameLaunchedEventArgs()); + this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); this.Events.UpdateTicking.Raise(new UpdateTickingEventArgs(this.TicksElapsed)); try { @@ -738,6 +758,7 @@ namespace StardewModdingAPI.Framework { this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } + this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); this.Events.UpdateTicked.Raise(new UpdateTickedEventArgs(this.TicksElapsed)); /********* @@ -844,10 +865,13 @@ namespace StardewModdingAPI.Framework if (activeClickableMenu != null) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); try { + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -855,7 +879,9 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); activeClickableMenu.exitThisMenu(); } - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); + Game1.spriteBatch.End(); } if (Game1.overlayMenu != null) @@ -872,11 +898,15 @@ namespace StardewModdingAPI.Framework if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + + this.Events.Rendering.RaiseEmpty(); try { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -884,7 +914,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); Game1.activeClickableMenu.exitThisMenu(); } - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) @@ -904,10 +935,12 @@ namespace StardewModdingAPI.Framework else if (Game1.gameMode == (byte)11) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); Game1.spriteBatch.End(); } else if (Game1.currentMinigame != null) @@ -932,12 +965,15 @@ namespace StardewModdingAPI.Framework else if (Game1.showingEndOfNightStuff) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); if (Game1.activeClickableMenu != null) { try { + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -946,7 +982,7 @@ namespace StardewModdingAPI.Framework Game1.activeClickableMenu.exitThisMenu(); } } - this.RaisePostRender(); + this.Events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel == 1.0) @@ -960,6 +996,7 @@ namespace StardewModdingAPI.Framework else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Rendering.RaiseEmpty(); string str1 = ""; for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) str1 += "."; @@ -971,6 +1008,7 @@ namespace StardewModdingAPI.Framework int x = 64; int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); + this.Events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) @@ -991,11 +1029,15 @@ namespace StardewModdingAPI.Framework } else { + byte batchOpens = 0; // used for rendering event + Microsoft.Xna.Framework.Rectangle rectangle; Viewport viewport; if (Game1.gameMode == (byte)0) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (++batchOpens == 1) + this.Events.Rendering.RaiseEmpty(); } else { @@ -1004,6 +1046,8 @@ namespace StardewModdingAPI.Framework this.GraphicsDevice.SetRenderTarget(Game1.lightmap); this.GraphicsDevice.Clear(Color.White * 0.0f); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (++batchOpens == 1) + this.Events.Rendering.RaiseEmpty(); Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight)); for (int index = 0; index < Game1.currentLightSources.Count; ++index) { @@ -1017,6 +1061,9 @@ namespace StardewModdingAPI.Framework Game1.bloom.BeginDraw(); this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (++batchOpens == 1) + this.Events.Rendering.RaiseEmpty(); + this.Events.RenderingWorld.RaiseEmpty(); this.Events.Legacy_OnPreRenderEvent.Raise(); if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); @@ -1328,8 +1375,10 @@ namespace StardewModdingAPI.Framework this.drawBillboard(); if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) { + this.Events.RenderingHud.RaiseEmpty(); this.Events.Legacy_OnPreRenderHudEvent.Raise(); this.drawHUD(); + this.Events.RenderedHud.RaiseEmpty(); this.Events.Legacy_OnPostRenderHudEvent.Raise(); } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) @@ -1438,8 +1487,10 @@ namespace StardewModdingAPI.Framework { try { + this.Events.RenderingActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.RenderedActiveMenu.RaiseEmpty(); this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) @@ -1455,7 +1506,9 @@ namespace StardewModdingAPI.Framework string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1); } - this.RaisePostRender(); + this.Events.RenderedWorld.RaiseEmpty(); + this.Events.Rendered.RaiseEmpty(); + this.Events.Legacy_OnPostRenderEvent.Raise(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); this.renderScreenBuffer(); diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 59e80d8a..6a705e50 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; @@ -41,7 +42,7 @@ namespace StardewModdingAPI.Framework.StateTracking public IValueWatcher MineLevelWatcher { get; } /// Tracks changes to the player's skill levels. - public IDictionary> SkillWatchers { get; } + public IDictionary> SkillWatchers { get; } /********* @@ -58,14 +59,14 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); - this.SkillWatchers = new Dictionary> + this.SkillWatchers = new Dictionary> { - [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), - [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), - [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), - [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), - [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), - [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) + [SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), + [SkillType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), + [SkillType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), + [SkillType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), + [SkillType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), + [SkillType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) }; // track watchers for convenience @@ -124,7 +125,7 @@ namespace StardewModdingAPI.Framework.StateTracking } /// Get the player skill levels which changed. - public IEnumerable>> GetChangedSkills() + public IEnumerable>> GetChangedSkills() { return this.SkillWatchers.Where(p => p.Value.IsChanged); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index efb85c52..d58aee56 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,32 +79,90 @@ Properties\GlobalAssemblyInfo.cs + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -121,9 +179,7 @@ - - @@ -132,6 +188,7 @@ + @@ -191,33 +248,10 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -256,12 +290,6 @@ - - - - - - @@ -278,8 +306,6 @@ - - -- cgit From ea3d3a6648f0a1c679a7593cfed0dacaed1ec045 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 19:13:03 -0400 Subject: make duplicate-mod more intuitive The error now shows the relative folder paths (instead of manifest IDs), and only one error will be logged instead of one per duplicate. --- docs/release-notes.md | 5 +---- src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 ++++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 5 +++-- src/SMAPI/Framework/SCore.cs | 10 +++++++--- 5 files changed, 20 insertions(+), 10 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 63db5dc3..8b644a43 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,10 +6,7 @@ * You can now group mods into subfolders to organise them. * Most SMAPI files are now tucked into a `smapi-internal` subfolder. * Save backups are now in a `save-backups` subfolder, so they're easier to access. Note that previous backups will be deleted when you update. - * Improved error messages when... - * an XNB mod is added to `Mods`; - * you install the wrong version of SMAPI for your OS; - * SMAPI can't prepare its folders. + * Improved various error messages to be more clear and intuitive. * Fixed transparency issues on Linux/Mac for some mod images. * Fixed error when a mod manifest is corrupted. * Fixed error when a mod adds an unnamed location. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 85d1b619..a62c9950 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -18,6 +18,9 @@ namespace StardewModdingAPI.Framework /// The mod's full directory path. string DirectoryPath { get; } + /// The relative to the game's Mods folder. + string RelativeDirectoryPath { get; } + /// Metadata about the mod from SMAPI's internal data (if any). ModDataRecordVersionedFields DataRecord { get; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index c02f0830..0a5f5d3f 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's full directory path. public string DirectoryPath { get; } + /// The relative to the game's Mods folder. + public string RelativeDirectoryPath { get; } + /// The mod manifest. public IManifest Manifest { get; } @@ -59,12 +62,14 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The mod's display name. /// The mod's full directory path. + /// The relative to the game's Mods folder. /// The mod manifest. /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) + public ModMetadata(string displayName, string directoryPath, string relativeDirectoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; + this.RelativeDirectoryPath = relativeDirectoryPath; this.Manifest = manifest; this.DataRecord = dataRecord; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 65a311dc..26ec82d7 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -41,7 +41,8 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } @@ -188,7 +189,7 @@ namespace StardewModdingAPI.Framework.ModLoading { if (mod.Status == ModMetadataStatus.Failed) continue; // don't replace metadata error - mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); + mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed ({string.Join(", ", group.Select(p => p.RelativeDirectoryPath).OrderBy(p => p))})."); } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2c5cd2bd..af517379 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -908,7 +908,7 @@ namespace StardewModdingAPI.Framework } // validate dependencies - // Although dependences are validated before mods are loaded, a dependency may have failed. + // Although dependences are validated before mods are loaded, a dependency may have failed to load. if (mod.Manifest.Dependencies?.Any() == true) { foreach (IManifestDependency dependency in mod.Manifest.Dependencies) @@ -955,7 +955,6 @@ namespace StardewModdingAPI.Framework catch (IncompatibleInstructionException) // details already in trace logs { string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."; return false; } @@ -1050,13 +1049,18 @@ namespace StardewModdingAPI.Framework this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); this.Monitor.Newline(); + HashSet logged = new HashSet(); foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; string errorReason = pair.Value.Item1; string errorDetails = pair.Value.Item2; + string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}"; + + if (!logged.Add($"{message}|{errorDetails}")) + continue; // skip duplicate messages (e.g. if multiple copies of the mod are installed) - this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}", LogLevel.Error); + this.Monitor.Log(message, LogLevel.Error); if (errorDetails != null) this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); } -- cgit From 6053b8c01f4bf12bb6addb48c31e1032dc2e2b30 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 19:50:09 -0400 Subject: prevent game crash caused by invalid dialogue --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Patches/CatchDialogueErrorPatch.cs | 100 +++++++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Patches/CatchDialogueErrorPatch.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8b644a43..4e4c5dd9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * Most SMAPI files are now tucked into a `smapi-internal` subfolder. * Save backups are now in a `save-backups` subfolder, so they're easier to access. Note that previous backups will be deleted when you update. * Improved various error messages to be more clear and intuitive. + * SMAPI now prevents a crash caused by mods adding dialogue the game can't parse. * Fixed transparency issues on Linux/Mac for some mod images. * Fixed error when a mod manifest is corrupted. * Fixed error when a mod adds an unnamed location. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index af517379..9f642649 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -26,6 +26,7 @@ using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; +using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; @@ -174,7 +175,7 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - // new GameLocationPatch() + new DialoguePatch(this.MonitorForGame, this.Reflection) ); } diff --git a/src/SMAPI/Patches/CatchDialogueErrorPatch.cs b/src/SMAPI/Patches/CatchDialogueErrorPatch.cs new file mode 100644 index 00000000..6f5143fd --- /dev/null +++ b/src/SMAPI/Patches/CatchDialogueErrorPatch.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for method which intercepts invalid dialogue lines and logs an error instead of crashing. + internal class DialoguePatch : IHarmonyPatch + { + /********* + ** Private methods + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + /// Simplifies access to private code. + private static Reflector Reflection; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(GameLocation)}.{nameof(GameLocation.updateSeasonalTileSheets)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + /// Simplifies access to private code. + public DialoguePatch(IMonitor monitorForGame, Reflector reflector) + { + DialoguePatch.MonitorForGame = monitorForGame; + DialoguePatch.Reflection = reflector; + } + + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + ConstructorInfo constructor = AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(DialoguePatch.Prefix)); + + harmony.Patch(constructor, new HarmonyMethod(prefix), null); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the Dialogue constructor. + /// The instance being patched. + /// The dialogue being parsed. + /// The NPC for which the dialogue is being parsed. + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Prefix(Dialogue __instance, string masterDialogue, NPC speaker) + { + // get private members + bool nameArraysTranslated = DialoguePatch.Reflection.GetField(typeof(Dialogue), "nameArraysTranslated").GetValue(); + IReflectedMethod translateArraysOfStrings = DialoguePatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); + IReflectedMethod parseDialogueString = DialoguePatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialoguePatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + IReflectedField> dialogues = DialoguePatch.Reflection.GetField>(__instance, "dialogues"); + + // replicate base constructor + if (dialogues.GetValue() == null) + dialogues.SetValue(new List()); + + // duplicate code with try..catch + try + { + if (!nameArraysTranslated) + translateArraysOfStrings.Invoke(); + __instance.speaker = speaker; + parseDialogueString.Invoke(masterDialogue); + checkForSpecialDialogueAttributes.Invoke(); + } + catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) + { + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialoguePatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); + + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return false; + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index d58aee56..7a4faa59 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -309,6 +309,7 @@ + -- cgit From 7829df45cbe563ecbf95b84ffbc8cc86e06b1c92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 20:10:12 -0400 Subject: track game logs separately in log parser --- docs/release-notes.md | 5 ++++- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4e4c5dd9..6ce0169a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,7 @@ # Release notes ## 2.8 (upcoming) * For players: - * Update checks now work even when the mod has no update keys in most cases. + * 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. @@ -19,6 +19,9 @@ * Fixed some errors logged as SMAPI instead of the affected mod. * Updated compatibility list. +* For the web UI: + * The log parser now has a separate filter for game messages. + * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added `IContentPack.WriteJsonFile` method. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 013c6c47..f9b5ba76 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -70,6 +70,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing // parse log messages LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "" }; + LogModInfo gameMod = new LogModInfo { Name = "game", Author = "", Description = "" }; IDictionary mods = new Dictionary(); bool inModList = false; bool inContentPackList = false; @@ -78,10 +79,23 @@ namespace StardewModdingAPI.Web.Framework.LogParsing // collect stats if (message.Level == LogLevel.Error) { - if (message.Mod == "SMAPI") - smapiMod.Errors++; - else if (mods.ContainsKey(message.Mod)) - mods[message.Mod].Errors++; + switch (message.Mod) + { + case "SMAPI": + smapiMod.Errors++; + break; + + case "game": + gameMod.Errors++; + break; + + default: + { + if (mods.ContainsKey(message.Mod)) + mods[message.Mod].Errors++; + break; + } + } } // collect SMAPI metadata @@ -151,7 +165,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing } // finalise log - log.Mods = new[] { smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray(); + gameMod.Version = log.GameVersion; + log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray(); return log; } catch (LogParseException ex) -- cgit From 40f0a53f8fbab603b9b5349d9a9707cf1373c063 Mon Sep 17 00:00:00 2001 From: danvolchek Date: Mon, 8 Oct 2018 19:58:53 -0500 Subject: display author of content packs --- docs/release-notes.md | 1 + src/SMAPI.Web/Views/LogParser/Index.cshtml | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6ce0169a..0abef381 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ * For the web UI: * The log parser now has a separate filter for game messages. + * The log parser now displays the author of each content pack. * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index e735e8f3..1bd5558a 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -165,7 +165,18 @@ else if (Model.ParsedLog?.IsValid == true) } - @mod.Author + + @mod.Author + @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList)) + { +
+ @foreach (var contentPack in contentPackList) + { + + @contentPack.Author
+ } +
+ } + @if (mod.Errors == 0) { no errors -- cgit From 4814d11488132ba0b938abe3854afb314c9194e7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Oct 2018 22:19:16 -0400 Subject: tweak release note (#595) --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0abef381..f1d470df 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,7 +21,7 @@ * For the web UI: * The log parser now has a separate filter for game messages. - * The log parser now displays the author of each content pack. + * The log parser now shows content pack authors (thanks to danvolchek!). * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). -- cgit From e09499f628e6fb019ea856b197111f4a5bf3adf8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Oct 2018 14:43:09 -0400 Subject: recommend compatible SMAPI version in game version check error --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 20 ++++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 14 -------------- src/SMAPI/Program.cs | 18 +++++++++++++----- 4 files changed, 34 insertions(+), 19 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f1d470df..9601c946 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Save backups are now in a `save-backups` subfolder, so they're easier to access. Note that previous backups will be deleted when you update. * Improved 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. * Fixed transparency issues on Linux/Mac for some mod images. * Fixed error when a mod manifest is corrupted. * Fixed error when a mod adds an unnamed location. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 1a20ac68..83b17401 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -107,6 +107,26 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ + /// Get the SMAPI version to recommend for an older game version, if any. + /// The game version to search. + /// Returns the compatible SMAPI version, or null if none was found. + internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion version) + { + switch (version.ToString()) + { + case "1.3.28": + return new SemanticVersion(2, 7, 0); + + case "1.2.30": + case "1.2.31": + case "1.2.32": + case "1.2.33": + return new SemanticVersion(2, 5, 5); + } + + return null; + } + /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 47370afd..6c897382 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -159,20 +159,6 @@ namespace StardewModdingAPI.Framework } #endif - // validate game version - if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) - { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) - { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - // apply game patches new GamePatcher(this.Monitor).Apply( new DialoguePatch(this.MonitorForGame, this.Reflection) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c6f39e3f..b46ecff4 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -83,15 +83,23 @@ namespace StardewModdingAPI ) + "See the readme.txt file for details." ); + return; } - // Stardew Valley 1.2 types not present - if (Type.GetType($"StardewValley.LocalizedContentManager+LanguageCode, {gameAssemblyName}", throwOnError: false) == null) + // validate game version + if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - PrintErrorAndExit(Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion) - ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." - : "Oops! SMAPI doesn't seem to be compatible with your game. Make sure you're running the latest version of Stardew Valley and SMAPI." + ISemanticVersion suggestedApiVersion = Constants.GetCompatibleApiVersion(Constants.GameVersion); + PrintErrorAndExit(suggestedApiVersion != null + ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. You can install SMAPI {suggestedApiVersion} instead to fix this error, or update your game to the latest version." + : $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." ); + return; + } + if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) + { + PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io."); + return; } } -- cgit From 2430b9c4ef755f5fcfb062e6c11570ad05211b73 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 Oct 2018 12:24:18 -0400 Subject: add troubleshooting guide link to intro, update release notes (#597) --- docs/release-notes.md | 2 ++ src/SMAPI.Web/Views/Mods/Index.cshtml | 20 +++++++++++--------- src/SMAPI.Web/wwwroot/Content/css/mods.css | 4 +--- 3 files changed, 14 insertions(+), 12 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9601c946..f946a7e8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,8 +21,10 @@ * Updated compatibility list. * For the web UI: + * Added a [mod compatibility page](https://mods.smapi.io). * The log parser now has a separate filter for game messages. * The log parser now shows content pack authors (thanks to danvolchek!). + * Corrected log parser instructions for Mac. * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 66ec0c72..b5b38c79 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -4,11 +4,11 @@ ViewData["Title"] = "SMAPI mod compatibility"; } @section Head { - + - + } -

This page lists all known SMAPI mods, whether they're compatible with the latest versions of Stardew Valley and SMAPI, and how to fix broken mods if possible. The list is updated every few days. (You can help edit this list!)

+
+

This page lists all known SMAPI mods, whether they're compatible with the latest versions of Stardew Valley and SMAPI, and how to fix broken mods if possible. The list is updated every few days. (You can help edit this list!)

-@if (Model.BetaVersion != null) -{ -
-

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.

-
-} +

If a mod doesn't work after following the instructions below, check the troubleshooting guide or ask for help.

+ + @if (Model.BetaVersion != null) + { +

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/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css index b11c48d1..9f82e3e6 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/mods.css +++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css @@ -5,13 +5,11 @@ max-width: calc(100% - 2em); /* allow for wider table if room available */ } -#blurb { - margin-top: 0; +#intro { width: 50em; } #beta-blurb { - width: 50em; margin-bottom: 2em; padding: 1em; border: 3px solid darkgreen; -- cgit From db88738666ee28b11468ae986af5c0418bb4ee5a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Oct 2018 13:08:15 -0400 Subject: fix friendly error no longer shown when SMAPI isn't run from the game folder --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 53 +++++++++++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 23 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f946a7e8..22c483c4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * Fixed transparency issues on Linux/Mac for some mod images. * Fixed error when a mod manifest is corrupted. * Fixed error when a mod adds an unnamed location. + * Fixed friendly error no longer shown when SMAPI isn't run from the game folder. * Fixed some Windows install paths not detected. * Fixed Linux compatibility issues for some players. SMAPI will now always use xterm if it's available. * Fixed installer duplicating bundled mods if you moved them after the last install. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b46ecff4..2efcfecb 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -7,6 +7,7 @@ using System.Threading; #if SMAPI_FOR_WINDOWS #endif using StardewModdingAPI.Framework; +using StardewModdingAPI.Internal; namespace StardewModdingAPI { @@ -30,7 +31,8 @@ namespace StardewModdingAPI public static void Main(string[] args) { AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; - Program.AssertMinimumCompatibility(); + Program.AssertGamePresent(); + Program.AssertGameVersion(); Program.Start(args); } @@ -60,22 +62,15 @@ namespace StardewModdingAPI } } - /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. - private static void AssertMinimumCompatibility() + /// Assert that the game is available. + /// This must be checked *before* any references to , and this method should not reference itself to avoid errors in Mono. + private static void AssertGamePresent() { - void PrintErrorAndExit(string message) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(message); - Console.ResetColor(); - Program.PressAnyKeyToExit(showMessage: true); - } - string gameAssemblyName = Constants.GameAssemblyName; - - // game not present + Platform platform = EnvironmentUtility.DetectPlatform(); + string gameAssemblyName = platform == Platform.Windows ? "Stardew Valley" : "StardewValley"; if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null) { - PrintErrorAndExit( + Program.PrintErrorAndExit( "Oops! SMAPI can't find the game. " + (Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Windows")) || Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Mono")) ? "It looks like you're running SMAPI from the download package, but you need to run the installed version instead. " @@ -83,24 +78,26 @@ namespace StardewModdingAPI ) + "See the readme.txt file for details." ); - return; } + } - // validate game version + /// Assert that the game version is within and . + private static void AssertGameVersion() + { + // min version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { ISemanticVersion suggestedApiVersion = Constants.GetCompatibleApiVersion(Constants.GameVersion); - PrintErrorAndExit(suggestedApiVersion != null + Program.PrintErrorAndExit(suggestedApiVersion != null ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. You can install SMAPI {suggestedApiVersion} instead to fix this error, or update your game to the latest version." : $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." ); - return; - } - if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) - { - PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io."); - return; } + + // max version + else if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) + Program.PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io."); + } /// Initialise SMAPI and launch the game. @@ -130,6 +127,16 @@ namespace StardewModdingAPI core.RunInteractively(); } + /// Write an error directly to the console and exit. + /// The error message to display. + private static void PrintErrorAndExit(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + Program.PressAnyKeyToExit(showMessage: true); + } + /// Show a 'press any key to exit' message, and exit when they press a key. /// Whether to print a 'press any key to exit' message to the console. private static void PressAnyKeyToExit(bool showMessage) -- 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 'docs') 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 2872cad9fea48a5435bcac2f4803af96133eb6b9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Oct 2018 19:31:12 -0400 Subject: fix Context.IsPlayerFree being true before player finishes transitioning to a new location in multiplayer --- docs/release-notes.md | 1 + src/SMAPI/Context.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index acbaef99..cb83e7ec 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -42,6 +42,7 @@ * 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. + * Fixed `Context.IsPlayerFree` being true before the player finishes transitioning to a new location in multiplayer. * Suppressed the game's 'added crickets' debug output. * **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/Context.cs b/src/SMAPI/Context.cs index 3905699e..c7aed81d 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI public static bool IsWorldReady { get; internal set; } /// Whether is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc). - public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && (!Game1.eventUp || Game1.isFestival()); + public static bool IsPlayerFree => Context.IsWorldReady && Game1.currentLocation != null && Game1.activeClickableMenu == null && !Game1.dialogueUp && (!Game1.eventUp || Game1.isFestival()); /// Whether is true and the player is free to move (e.g. not using a tool). public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; -- cgit From 02a46bf13f29ce0dd8ac2f422113083c59dae42d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 3 Nov 2018 01:29:01 -0400 Subject: add APIs to send/receive messages in multiplayer (#480) --- docs/release-notes.md | 1 + src/SMAPI/Events/IModEvents.cs | 3 + src/SMAPI/Events/IMultiplayerEvents.cs | 11 + src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 46 +++ src/SMAPI/Framework/Events/EventManager.cs | 8 + src/SMAPI/Framework/Events/ManagedEvent.cs | 24 ++ src/SMAPI/Framework/Events/ManagedEventBase.cs | 12 +- src/SMAPI/Framework/Events/ModEvents.cs | 4 + src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 29 ++ .../Framework/ModHelpers/MultiplayerHelper.cs | 31 +- src/SMAPI/Framework/Networking/ModMessageModel.cs | 72 +++++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 14 +- src/SMAPI/Framework/SCore.cs | 5 +- src/SMAPI/Framework/SGame.cs | 25 +- src/SMAPI/Framework/SMultiplayer.cs | 339 ++++++++++++++++----- src/SMAPI/IMultiplayerHelper.cs | 13 +- src/SMAPI/IMultiplayerPeer.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 22 +- 18 files changed, 545 insertions(+), 116 deletions(-) create mode 100644 src/SMAPI/Events/IMultiplayerEvents.cs create mode 100644 src/SMAPI/Events/ModMessageReceivedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModMultiplayerEvents.cs create mode 100644 src/SMAPI/Framework/Networking/ModMessageModel.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index cb83e7ec..2111e35e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -31,6 +31,7 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). + * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. * Added more events to the prototype `helper.Events` for SMAPI 3.0. diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 76da7751..bd7ab880 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } + /// Events raised for multiplayer messages and connections. + IMultiplayerEvents Multiplayer { get; } + /// Events raised when the player data changes. IPlayerEvents Player { get; } diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs new file mode 100644 index 00000000..a6ac6fd3 --- /dev/null +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -0,0 +1,11 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised for multiplayer messages and connections. + public interface IMultiplayerEvents + { + /// Raised after a mod message is received over the network. + event EventHandler ModMessageReceived; + } +} diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs new file mode 100644 index 00000000..b1960a22 --- /dev/null +++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs @@ -0,0 +1,46 @@ +using System; +using StardewModdingAPI.Framework.Networking; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a mod receives a message over the network. + public class ModMessageReceivedEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The underlying message model. + private readonly ModMessageModel Message; + + + /********* + ** Accessors + *********/ + /// The unique ID of the player from whose computer the message was sent. + public long FromPlayerID => this.Message.FromPlayerID; + + /// The unique ID of the mod which sent the message. + public string FromModID => this.Message.FromModID; + + /// A message type which can be used to decide whether it's the one you want to handle, like SetPlayerLocation. This doesn't need to be globally unique, so mods should check the . + public string Type => this.Message.Type; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The received message. + internal ModMessageReceivedEventArgs(ModMessageModel message) + { + this.Message = message; + } + + /// Read the message data into the given model type. + /// The message model type. + public TModel ReadAs() + { + return this.Message.Data.ToObject(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 31b0346a..519cf48a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -98,6 +98,12 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player scrolls the mouse wheel. public readonly ManagedEvent MouseWheelScrolled; + /**** + ** Multiplayer + ****/ + /// Raised after a mod message is received over the network. + public readonly ManagedEvent ModMessageReceived; + /**** ** Player ****/ @@ -374,6 +380,8 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); this.Warped = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index c1ebf6c7..65f6e38e 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -67,6 +67,30 @@ namespace StardewModdingAPI.Framework.Events } } } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + /// A lambda which returns true if the event should be raised for the given mod. + public void RaiseForMods(TEventArgs args, Func match) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + if (match(this.GetSourceMod(handler))) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } } /// An event wrapper which intercepts and logs errors in handler code. diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs index f3a278dc..defd903a 100644 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -69,12 +69,22 @@ namespace StardewModdingAPI.Framework.Events this.SourceMods.Remove(handler); } + /// Get the mod which registered the given event handler, if available. + /// The event handler. + protected IModMetadata GetSourceMod(TEventHandler handler) + { + return this.SourceMods.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; + } + /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. protected void LogError(TEventHandler handler, Exception ex) { - if (this.SourceMods.TryGetValue(handler, out IModMetadata mod)) + IModMetadata mod = this.GetSourceMod(handler); + if (mod != null) mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); else this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 7a318e8b..8ad3936c 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } + /// Events raised for multiplayer messages and connections. + public IMultiplayerEvents Multiplayer { get; } + /// Events raised when the player data changes. public IPlayerEvents Player { get; } @@ -38,6 +41,7 @@ namespace StardewModdingAPI.Framework.Events this.Display = new ModDisplayEvents(mod, eventManager); this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); + this.Multiplayer = new ModMultiplayerEvents(mod, eventManager); this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); this.Specialised = new ModSpecialisedEvents(mod, eventManager); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs new file mode 100644 index 00000000..a830a54a --- /dev/null +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised for multiplayer messages and connections. + internal class ModMultiplayerEvents : ModEventsBase, IMultiplayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after a mod message is received over the network. + public event EventHandler ModMessageReceived + { + add => this.EventManager.ModMessageReceived.Add(value); + remove => this.EventManager.ModMessageReceived.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModMultiplayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index 86f8e012..eedad0bc 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewModdingAPI.Framework.Networking; using StardewValley; @@ -26,18 +27,18 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Multiplayer = multiplayer; } - /// Get the locations which are being actively synced from the host. - public IEnumerable GetActiveLocations() - { - return this.Multiplayer.activeLocations(); - } - /// Get a new multiplayer ID. public long GetNewID() { return this.Multiplayer.getNewID(); } + /// Get the locations which are being actively synced from the host. + public IEnumerable GetActiveLocations() + { + return this.Multiplayer.activeLocations(); + } + /// Get a connected player. /// The player's unique ID. /// Returns the connected player, or null if no such player is connected. @@ -53,5 +54,23 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.Multiplayer.Peers.Values; } + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + public void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null) + { + this.Multiplayer.BroadcastModMessage( + message: message, + messageType: messageType, + fromModID: this.ModID, + toModIDs: modIDs, + toPlayerIDs: playerIDs + ); + } } } diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs new file mode 100644 index 00000000..7ee39863 --- /dev/null +++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Framework.Networking +{ + /// The metadata for a mod message. + internal class ModMessageModel + { + /********* + ** Accessors + *********/ + /**** + ** Origin + ****/ + /// The unique ID of the player who broadcast the message. + public long FromPlayerID { get; set; } + + /// The unique ID of the mod which broadcast the message. + public string FromModID { get; set; } + + /**** + ** Destination + ****/ + /// The players who should receive the message, or null for all players. + public long[] ToPlayerIDs { get; set; } + + /// The mods which should receive the message, or null for all mods. + public string[] ToModIDs { get; set; } + + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + public string Type { get; set; } + + /// The custom mod data being broadcast. + public JToken Data { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModMessageModel() { } + + /// Construct an instance. + /// The unique ID of the player who broadcast the message. + /// The unique ID of the mod which broadcast the message. + /// The players who should receive the message, or null for all players. + /// The mods which should receive the message, or null for all mods. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The custom mod data being broadcast. + public ModMessageModel(long fromPlayerID, string fromModID, long[] toPlayerIDs, string[] toModIDs, string type, JToken data) + { + this.FromPlayerID = fromPlayerID; + this.FromModID = fromModID; + this.ToPlayerIDs = toPlayerIDs; + this.ToModIDs = toModIDs; + this.Type = type; + this.Data = data; + } + + /// Construct an instance. + /// The message to clone. + public ModMessageModel(ModMessageModel message) + { + this.FromPlayerID = message.FromPlayerID; + this.FromModID = message.FromModID; + this.ToPlayerIDs = message.ToPlayerIDs?.ToArray(); + this.ToModIDs = message.ToModIDs?.ToArray(); + this.Type = message.Type; + this.Data = message.Data; + } + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index e97e36bc..c7f8ffad 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.Networking public long PlayerID { get; } /// Whether this is a connection to the host player. - public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID; + public bool IsHost { get; } /// Whether the player has SMAPI installed. public bool HasSmapi => this.ApiVersion != null; @@ -57,9 +57,11 @@ namespace StardewModdingAPI.Framework.Networking /// The server through which to send messages. /// The server connection through which to send messages. /// The client through which to send messages. - public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client) + /// Whether this is a connection to the host player. + public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client, bool isHost) { this.PlayerID = playerID; + this.IsHost = isHost; if (model != null) { this.Platform = model.Platform; @@ -84,7 +86,8 @@ namespace StardewModdingAPI.Framework.Networking model: model, server: server, serverConnection: serverConnection, - client: null + client: null, + isHost: false ); } @@ -99,7 +102,8 @@ namespace StardewModdingAPI.Framework.Networking model: model, server: null, serverConnection: null, - client: client + client: client, + isHost: true ); } @@ -119,7 +123,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to send. public void SendMessage(OutgoingMessage message) { - if (this.IsHostPlayer) + if (this.IsHost) this.Client.sendMessage(message); else this.Server.SendMessage(this.ServerConnection, message); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 69b33699..ca343389 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -209,7 +209,7 @@ namespace StardewModdingAPI.Framework // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose, this.Settings.VerboseLogging); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -340,9 +340,6 @@ namespace StardewModdingAPI.Framework /// Initialise SMAPI and mods after the game starts. private void InitialiseAfterGameStart() { - // load settings - this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; - // load core components this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6b19f538..c7f5962f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -51,6 +51,12 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Whether SMAPI should log more information about the game context. + private readonly bool VerboseLogging; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -116,9 +122,6 @@ namespace StardewModdingAPI.Framework /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; - /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } - /// A list of queued commands to execute. /// This property must be threadsafe, since it's accessed from a separate console input thread. public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); @@ -136,7 +139,8 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting) + /// Whether SMAPI should log more information about the game context. + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting, bool verboseLogging) { SGame.ConstructorHack = null; @@ -151,11 +155,13 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.MonitorForGame = monitorForGame; this.Events = eventManager; + this.ModRegistry = modRegistry; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; + this.VerboseLogging = verboseLogging; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging, this.OnModMessageReceived); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -191,6 +197,15 @@ namespace StardewModdingAPI.Framework this.Events.DayEnding.RaiseEmpty(); } + /// A callback invoked when a mod message is received. + /// The message to deliver to applicable mods. + private void OnModMessageReceived(ModMessageModel message) + { + // raise events for applicable mods + HashSet modIDs = new HashSet(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.InvariantCultureIgnoreCase); + this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); + } + /// Constructor a content manager to read XNB files. /// The service provider to use to locate services. /// The root directory to search for content. diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index a151272e..e4f912d2 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using Lidgren.Network; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; @@ -38,6 +40,9 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI should log more detailed information. private readonly bool VerboseLogging; + /// A callback to invoke when a mod message is received. + private readonly Action OnModMessageReceived; + /********* ** Accessors @@ -45,9 +50,15 @@ namespace StardewModdingAPI.Framework /// The message ID for a SMAPI message containing context about a player. public const byte ContextSyncMessageID = 255; + /// The message ID for a mod message. + public const byte ModMessageID = 254; + /// The metadata for each connected peer. public IDictionary Peers { get; } = new Dictionary(); + /// The metadata for the host player, if the current player is a farmhand. + public MultiplayerPeer HostPeer; + /********* ** Public methods @@ -59,7 +70,8 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. /// Simplifies access to private code. /// Whether SMAPI should log more detailed information. - public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging) + /// A callback to invoke when a mod message is received. + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging, Action onModMessageReceived) { this.Monitor = monitor; this.EventManager = eventManager; @@ -67,6 +79,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.VerboseLogging = verboseLogging; + this.OnModMessageReceived = onModMessageReceived; this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); } @@ -113,7 +126,7 @@ namespace StardewModdingAPI.Framework return server; } - /// Process an incoming network message from an unknown farmhand. + /// Process an incoming network message from an unknown farmhand, usually a player whose connection hasn't been approved yet. /// The server instance that received the connection. /// The raw network message that was received. /// The message to process. @@ -123,69 +136,99 @@ namespace StardewModdingAPI.Framework if (!Game1.IsMasterGame) return; - // sync SMAPI context with connected instances - if (message.MessageType == SMultiplayer.ContextSyncMessageID) + switch (message.MessageType) { - // get server - if (!(server is SLidgrenServer customServer)) - { - this.Monitor.Log($"Received context from farmhand {message.FarmerID} via unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); - return; - } - - // parse message - string data = message.Reader.ReadString(); - RemoteContextModel model = this.JsonHelper.Deserialise(data); - if (model.ApiVersion == null) - model = null; // no data available for unmodded players - - // log info - if (model != null) - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - else - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - - // store peer - MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection); - - // reply with known contexts - if (this.VerboseLogging) - this.Monitor.Log(" Replying with context for current player...", LogLevel.Trace); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) - { - if (this.VerboseLogging) - this.Monitor.Log($" Replying with context for player {otherPeer.PlayerID}...", LogLevel.Trace); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); - } + // sync SMAPI context with connected instances + case SMultiplayer.ContextSyncMessageID: + { + // get server + if (!(server is SLidgrenServer customServer)) + { + this.Monitor.Log($"Received context from farmhand {message.FarmerID} via unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); + return; + } + + // parse message + string data = message.Reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + if (model.ApiVersion == null) + model = null; // no data available for unmodded players + + // log info + if (model != null) + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + else + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection); + + // reply with known contexts + this.VerboseLog(" Replying with context for current player..."); + newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); + newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); + } + + // forward to other peers + if (this.Peers.Count > 1) + { + object[] fields = this.GetContextSyncMessageFields(newPeer); + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); + otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + } + } + } + break; - // forward to other peers - if (this.Peers.Count > 1) - { - object[] fields = this.GetContextSyncMessageFields(newPeer); - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + // handle intro from unmodded player + case Multiplayer.playerIntroduction: + if (!this.Peers.ContainsKey(message.FarmerID)) { - if (this.VerboseLogging) - this.Monitor.Log($" Forwarding context to player {otherPeer.PlayerID}...", LogLevel.Trace); - otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + // get server + if (!(server is SLidgrenServer customServer)) + { + this.Monitor.Log($"Received connection from farmhand {message.FarmerID} with unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); + return; + } + + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + var peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; } - } + break; + + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + break; } + } - // handle intro from unmodded player - else if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) + /// Process an incoming message from an approved connection. + /// The message to process. + public override void processIncomingMessage(IncomingMessage message) + { + switch (message.MessageType) { - // get server - if (!(server is SLidgrenServer customServer)) - { - this.Monitor.Log($"Received connection from farmhand {message.FarmerID} with unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); - return; - } - - // store peer - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + break; + + // let game process message + default: + base.processIncomingMessage(message); + break; } + } /// Process an incoming network message from the server. @@ -194,33 +237,50 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public bool TryProcessMessageFromServer(SLidgrenClient client, IncomingMessage message) { - // receive SMAPI context from a connected player - if (message.MessageType == SMultiplayer.ContextSyncMessageID) + switch (message.MessageType) { - // parse message - string data = message.Reader.ReadString(); - RemoteContextModel model = this.JsonHelper.Deserialise(data); - - // log info - if (model != null) - this.Monitor.Log($"Received context for {(model.IsHost ? "host" : "farmhand")} {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - else - this.Monitor.Log($"Received context for player {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - - // store peer - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); - return true; - } + // receive SMAPI context from a connected player + case SMultiplayer.ContextSyncMessageID: + { + // parse message + string data = message.Reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + + // log info + if (model != null) + this.Monitor.Log($"Received context for {(model.IsHost ? "host" : "farmhand")} {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + else + this.Monitor.Log($"Received context for player {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } + return true; - // handle intro from unmodded player - if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) - { - // store peer - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); - } + // handle intro from unmodded player + case Multiplayer.playerIntroduction: + if (!this.Peers.ContainsKey(message.FarmerID)) + { + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + var peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } + return false; + + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + return true; - return false; + default: + return false; + } } /// Remove players who are disconnecting. @@ -235,10 +295,117 @@ namespace StardewModdingAPI.Framework base.removeDisconnectedFarmers(); } + /// Broadcast a mod message to matching players. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The unique ID of the mod sending the message. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + public void BroadcastModMessage(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs) + { + // validate + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (string.IsNullOrWhiteSpace(messageType)) + throw new ArgumentNullException(nameof(messageType)); + if (string.IsNullOrWhiteSpace(fromModID)) + throw new ArgumentNullException(nameof(fromModID)); + if (!this.Peers.Any()) + { + this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); + return; + } + + // filter player IDs + HashSet playerIDs = null; + if (toPlayerIDs != null && toPlayerIDs.Any()) + { + playerIDs = new HashSet(toPlayerIDs); + playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); + if (!playerIDs.Any()) + { + this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); + return; + } + } + + // get data to send + ModMessageModel model = new ModMessageModel( + fromPlayerID: Game1.player.UniqueMultiplayerID, + fromModID: fromModID, + toModIDs: toModIDs, + toPlayerIDs: playerIDs?.ToArray(), + type: messageType, + data: JToken.FromObject(message) + ); + string data = JsonConvert.SerializeObject(model, Formatting.None); + + // log message + if (this.VerboseLogging) + this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace); + + // send message + if (Context.IsMainPlayer) + { + foreach (MultiplayerPeer peer in this.Peers.Values) + { + if (playerIDs == null || playerIDs.Contains(peer.PlayerID)) + { + model.ToPlayerIDs = new[] { peer.PlayerID }; + peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, data)); + } + } + } + else if (this.HostPeer != null && this.HostPeer.HasSmapi) + this.HostPeer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, this.HostPeer.PlayerID, data)); + else + this.VerboseLog(" Can't send message because no valid connections were found."); + + } + /********* ** Private methods *********/ + /// Receive a mod message sent from another player's mods. + /// The raw message to parse. + private void ReceiveModMessage(IncomingMessage message) + { + // parse message + string json = message.Reader.ReadString(); + ModMessageModel model = this.JsonHelper.Deserialise(json); + HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); + if (this.VerboseLogging) + this.Monitor.Log($"Received message: {json}."); + + // notify local mods + if (playerIDs.Contains(Game1.player.UniqueMultiplayerID)) + this.OnModMessageReceived(model); + + // forward to other players + if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID)) + { + ModMessageModel newModel = new ModMessageModel(model); + foreach (long playerID in playerIDs) + { + if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) + { + newModel.ToPlayerIDs = new[] { peer.PlayerID }; + this.VerboseLog($" Forwarding message to player {peer.PlayerID}."); + peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); + } + } + } + } + + /// Get all connected player IDs, including the current player. + private IEnumerable GetKnownPlayerIDs() + { + yield return Game1.player.UniqueMultiplayerID; + foreach (long peerID in this.Peers.Keys) + yield return peerID; + } + /// Get the fields to include in a context sync message sent to other players. private object[] GetContextSyncMessageFields() { @@ -271,7 +438,7 @@ namespace StardewModdingAPI.Framework RemoteContextModel model = new RemoteContextModel { - IsHost = peer.IsHostPlayer, + IsHost = peer.IsHost, Platform = peer.Platform.Value, ApiVersion = peer.ApiVersion, GameVersion = peer.GameVersion, @@ -287,5 +454,13 @@ namespace StardewModdingAPI.Framework return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; } + + /// Log a trace message if is enabled. + /// The message to log. + private void VerboseLog(string message) + { + if (this.VerboseLogging) + this.Monitor.Log(message, LogLevel.Trace); + } } } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs index b01a7bed..4067a676 100644 --- a/src/SMAPI/IMultiplayerHelper.cs +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewValley; @@ -12,8 +13,6 @@ namespace StardewModdingAPI /// Get the locations which are being actively synced from the host. IEnumerable GetActiveLocations(); - /* disable until ready for release: - /// Get a connected player. /// The player's unique ID. /// Returns the connected player, or null if no such player is connected. @@ -21,6 +20,14 @@ namespace StardewModdingAPI /// Get all connected players. IEnumerable GetConnectedPlayers(); - */ + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null); } } diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs index e314eba5..0d4d3261 100644 --- a/src/SMAPI/IMultiplayerPeer.cs +++ b/src/SMAPI/IMultiplayerPeer.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI long PlayerID { get; } /// Whether this is a connection to the host player. - bool IsHostPlayer { get; } + bool IsHost { get; } /// Whether the player has SMAPI installed. bool HasSmapi { get; } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2fdf4d97..11fa5d35 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -116,6 +116,7 @@ + @@ -130,6 +131,7 @@ + @@ -160,10 +162,20 @@ - + + + + + + + + + + + @@ -183,15 +195,8 @@ - - - - - - - @@ -224,7 +229,6 @@ - -- cgit From 8f32c87aa1c7ad3afc8426904e992f8a6ff5d231 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:22:33 -0500 Subject: migrate to new Harmony NuGet package --- docs/release-notes.md | 1 + src/SMAPI/StardewModdingAPI.csproj | 7 +++---- src/lib/0Harmony.dll | Bin 99840 -> 0 bytes src/lib/0Harmony.pdb | Bin 323072 -> 0 bytes 4 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 src/lib/0Harmony.dll delete mode 100644 src/lib/0Harmony.pdb (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2111e35e..db45c12b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -40,6 +40,7 @@ * added support for overlaying image assets with semi-transparency; * mods can now load PNGs even if the game is currently 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). + * Updated Harmony from 1.0.9.1 to 1.2.0.1. * 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/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 8c12c1f9..bf230baf 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -55,14 +55,13 @@ + + 1.2.0.1 + - - False - ..\lib\0Harmony.dll - diff --git a/src/lib/0Harmony.dll b/src/lib/0Harmony.dll deleted file mode 100644 index 63619429..00000000 Binary files a/src/lib/0Harmony.dll and /dev/null differ diff --git a/src/lib/0Harmony.pdb b/src/lib/0Harmony.pdb deleted file mode 100644 index d7a4c67c..00000000 Binary files a/src/lib/0Harmony.pdb and /dev/null differ -- cgit From a4a1777ca43a0deac76acbc8891dc0f61e2a00ae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:47:17 -0500 Subject: update Mono.Cecil --- docs/release-notes.md | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index db45c12b..8a15a4d2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -40,12 +40,12 @@ * added support for overlaying image assets with semi-transparency; * mods can now load PNGs even if the game is currently 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). - * Updated Harmony from 1.0.9.1 to 1.2.0.1. * 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. * 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). * **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/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index bf230baf..f606ef44 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -58,7 +58,7 @@ 1.2.0.1 - + -- 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 'docs') 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 e1e02357af6e2c5851aad799f4b5e78a990a2790 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Nov 2018 02:13:44 -0500 Subject: add privacy page --- docs/release-notes.md | 3 +- src/SMAPI.Web/Controllers/IndexController.cs | 7 +++++ src/SMAPI.Web/StardewModdingAPI.Web.csproj | 3 ++ src/SMAPI.Web/Views/Index/Index.cshtml | 24 +++++++++------ src/SMAPI.Web/Views/Index/Privacy.cshtml | 43 +++++++++++++++++++++++++++ src/SMAPI.Web/wwwroot/Content/css/index.css | 5 ++++ src/SMAPI.Web/wwwroot/Content/css/privacy.css | 3 ++ 7 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 src/SMAPI.Web/Views/Index/Privacy.cshtml create mode 100644 src/SMAPI.Web/wwwroot/Content/css/privacy.css (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6c2879df..e7434c3c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,9 +25,10 @@ * For the web UI: * Added a [mod compatibility page](https://mods.smapi.io). + * Added [privacy page](https://smapi.io/privacy). * The log parser now has a separate filter for game messages. * The log parser now shows content pack authors (thanks to danvolchek!). - * Corrected log parser instructions for Mac. + * Fixed log parser instructions for Mac. * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index dadbedd1..d7be664d 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -76,6 +76,13 @@ namespace StardewModdingAPI.Web.Controllers return this.View(model); } + /// Display the index page. + [HttpGet("/privacy")] + public ViewResult Privacy() + { + return this.View(); + } + /********* ** Private methods diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 942a4b20..9d1990d9 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -27,6 +27,9 @@ + + $(IncludeRazorContentInPack) + $(IncludeRazorContentInPack) diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index a5a82121..01874f50 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -1,7 +1,10 @@ +@using Microsoft.Extensions.Options +@using StardewModdingAPI.Web.Framework.ConfigModels +@inject IOptions SiteConfig +@model StardewModdingAPI.Web.ViewModels.IndexModel @{ ViewData["Title"] = "SMAPI"; } -@model StardewModdingAPI.Web.ViewModels.IndexModel @section Head { @@ -16,7 +19,7 @@
- Download SMAPI @Model.StableVersion.Version
+ Download SMAPI @Model.StableVersion.Version

} - Player guide
+ +

Get help

@@ -55,7 +61,7 @@
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
-

See the release notes and mod compatibility list for more info.

+

See the release notes and mod compatibility list for more info.

} else { @@ -64,13 +70,13 @@ else
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
-

See the release notes and mod compatibility list for more info.

+

See the release notes and mod compatibility list for more info.

SMAPI @Model.BetaVersion.Version?

@Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description))
-

See the release notes and mod compatibility list for more info.

+

See the release notes and mod compatibility list for more info.

} diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml new file mode 100644 index 00000000..ca99eef6 --- /dev/null +++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml @@ -0,0 +1,43 @@ +@using Microsoft.Extensions.Options +@using StardewModdingAPI.Web.Framework.ConfigModels +@inject IOptions SiteConfig +@{ + ViewData["Title"] = "SMAPI privacy notes"; +} +@section Head { + +} + +← back to SMAPI page + +

SMAPI is an open-source and non-profit project. Your privacy is important, so this page explains what information SMAPI uses and transmits. This page is informational only, it's not a legal document.

+ +

Principles

+
    +
  1. SMAPI collects the minimum information needed to enable its features (see below).
  2. +
  3. SMAPI does not collect telemetry, analytics, etc.
  4. +
  5. SMAPI will never sell your information.
  6. +
+ +

Data collected and transmitted

+

Web logging

+

This website and SMAPI's web API are hosted by Amazon Web Services. Their servers may automatically collect diagnostics like your IP address, but this information is not visible to SMAPI's web application or developers. For more information, see the Amazon Privacy Notice.

+ +

Update checks

+

SMAPI notifies you when there's a new version of SMAPI or your mods available. To do so, it sends your SMAPI and mod versions to its web API. No personal information is stored by the web application, but see web logging.

+ +

You can disable update checks, and no information will be transmitted to the web API. To do so:

+
    +
  1. find your game folder;
  2. +
  3. open the smapi-internal/StardewModdingAPI.config.json file in a text editor;
  4. +
  5. change "CheckForUpdates": true to "CheckForUpdates": false.
  6. +
+ +

Log parser

+

The log parser page lets you store a log file for analysis and sharing. The log data is stored indefinitely in an obfuscated form as unlisted pastes in Pastebin. No personal information is stored by the log parser beyond what you choose to upload, but see web logging and the Pastebin Privacy Statement.

+ +

Multiplayer sync

+

As part of its multiplayer API, SMAPI transmits basic context to players you connect to (mainly your OS, SMAPI version, game version, and installed mods). This is used to enable multiplayer features like inter-mod messages, compatibility checks, etc. Although this information is normally hidden from players, it may be visible due to mods or configuration changes.

+ +

Custom mods

+

Mods may collect and transmit any information. Mods (except those provided as part of the SMAPI download) are not covered by this page. Install third-party mods at your own risk.

diff --git a/src/SMAPI.Web/wwwroot/Content/css/index.css b/src/SMAPI.Web/wwwroot/Content/css/index.css index 514e1a5c..979af4af 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/index.css +++ b/src/SMAPI.Web/wwwroot/Content/css/index.css @@ -93,6 +93,11 @@ h1 { display: block; } +.sublinks { + font-size: 0.9em; + margin-bottom: 1em; +} + /********* ** Subsections *********/ diff --git a/src/SMAPI.Web/wwwroot/Content/css/privacy.css b/src/SMAPI.Web/wwwroot/Content/css/privacy.css new file mode 100644 index 00000000..94bc68a9 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/css/privacy.css @@ -0,0 +1,3 @@ +h3 { + border: 0; +} -- cgit From 6a1994b850b30889459e650629ed9e1357e8abac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Nov 2018 21:24:46 -0500 Subject: fix crash log deleted immediately on game relaunch --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/SCore.cs | 30 ++++++++++++++++++------------ 3 files changed, 20 insertions(+), 13 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index e7434c3c..57fb33a4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ * Fixed translation issues not shown as warnings. * Fixed dependencies not correctly enforced if the dependency is installed but failed to load. * Fixed some errors logged as SMAPI instead of the affected mod. + * Fixed crash log deleted immediately when you relaunch the game. * Updated compatibility list. * For the web UI: diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 83b17401..67f03fab 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -82,7 +82,7 @@ namespace StardewModdingAPI /// The filename extension for SMAPI log files. internal static string LogExtension { get; } = "txt"; - /// A copy of the log leading up to the previous fatal crash, if any. + /// The file path for the log containing the previous fatal crash, if any. internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); /// The file path which stores a fatal crash message for the next run. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index e76e7a1d..3e8766c2 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Framework this.ModsPath = modsPath; // init log file - this.PurgeLogFiles(); + this.PurgeNormalLogs(); string logPath = this.GetLogPath(); // init basics @@ -1329,8 +1329,8 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException("Could not find an available log path."); } - /// Delete all log files created by SMAPI. - private void PurgeLogFiles() + /// Delete normal (non-crash) log files created by SMAPI. + private void PurgeNormalLogs() { DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); if (!logsDir.Exists) @@ -1338,16 +1338,22 @@ namespace StardewModdingAPI.Framework foreach (FileInfo logFile in logsDir.EnumerateFiles()) { - if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + // skip non-SMAPI file + if (!logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + continue; + + // skip crash log + if (logFile.FullName == Constants.FatalCrashLog) + continue; + + // delete file + try { - try - { - FileUtilities.ForceDelete(logFile); - } - catch (IOException) - { - // ignore file if it's in use - } + FileUtilities.ForceDelete(logFile); + } + catch (IOException) + { + // ignore file if it's in use } } } -- 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 'docs') 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 'docs') 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 From 6f569c579422641d6a56b491795504abd11893e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 13:33:56 -0500 Subject: add verbose logging as a monitor feature --- docs/release-notes.md | 1 + src/SMAPI/Framework/Monitor.cs | 15 +++++++++++++- src/SMAPI/Framework/SCore.cs | 16 ++++----------- src/SMAPI/Framework/SGame.cs | 31 ++++++++++++---------------- src/SMAPI/Framework/SMultiplayer.cs | 41 +++++++++++++------------------------ src/SMAPI/IMonitor.cs | 9 +++++++- 6 files changed, 54 insertions(+), 59 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2783ebc2..b9a73f99 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -34,6 +34,7 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. + * Added [verbose logging](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Logging#Verbose_logging) feature. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. * Added more events to the prototype `helper.Events` for SMAPI 3.0. diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 2812a9cc..a4d92e4b 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + public bool IsVerbose { get; } + /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } @@ -56,7 +59,8 @@ namespace StardewModdingAPI.Framework /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. /// The console color scheme to use. - public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme, bool isVerbose) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -68,6 +72,7 @@ namespace StardewModdingAPI.Framework this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme); this.ConsoleInterceptor = consoleInterceptor; this.ExitTokenSource = exitTokenSource; + this.IsVerbose = isVerbose; } /// Log a message for the player or developer. @@ -78,6 +83,14 @@ namespace StardewModdingAPI.Framework this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } + /// Log a message that only appears when is enabled. + /// The message to log. + public void VerboseLog(string message) + { + if (this.IsVerbose) + this.Log(message, LogLevel.Trace); + } + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// The reason for the shutdown. public void ExitGameImmediately(string reason) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ee175a1b..7e5b6e02 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) { WriteToConsole = writeToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, @@ -213,7 +213,7 @@ namespace StardewModdingAPI.Framework // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.DeprecationManager, this.InitialiseAfterGameStart, this.Dispose, this.Settings.VerboseLogging); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.DeprecationManager, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -355,7 +355,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - this.VerboseLog("Verbose logging enabled."); + this.Monitor.VerboseLog("Verbose logging enabled."); // validate XNB integrity if (!this.ValidateContentIntegrity()) @@ -1298,7 +1298,7 @@ namespace StardewModdingAPI.Framework /// The name of the module which will log messages with this instance. private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, @@ -1306,14 +1306,6 @@ namespace StardewModdingAPI.Framework }; } - /// Log a message if verbose mode is enabled. - /// The message to log. - private void VerboseLog(string message) - { - if (this.Settings.VerboseLogging) - this.Monitor.Log(message, LogLevel.Trace); - } - /// Get the absolute path to the next available log file. private string GetLogPath() { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 04db9632..3de97aea 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -57,9 +57,6 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; - /// Whether SMAPI should log more information about the game context. - private readonly bool VerboseLogging; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -143,8 +140,7 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - /// Whether SMAPI should log more information about the game context. - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting, bool verboseLogging) + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting) { SGame.ConstructorHack = null; @@ -164,9 +160,8 @@ namespace StardewModdingAPI.Framework this.DeprecationManager = deprecationManager; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; - this.VerboseLogging = verboseLogging; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging, this.OnModMessageReceived); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -468,7 +463,7 @@ namespace StardewModdingAPI.Framework // since the game adds & removes its own handler on the fly. if (this.Watchers.WindowSizeWatcher.IsChanged) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; @@ -507,7 +502,7 @@ namespace StardewModdingAPI.Framework int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; this.Watchers.MouseWheelScrollWatcher.Reset(); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); this.Events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); } @@ -520,7 +515,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); this.Events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); @@ -542,7 +537,7 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); this.Events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); @@ -581,7 +576,7 @@ namespace StardewModdingAPI.Framework IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events @@ -609,7 +604,7 @@ namespace StardewModdingAPI.Framework GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); this.Watchers.LocationsWatcher.ResetLocationList(); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) { string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; @@ -705,7 +700,7 @@ namespace StardewModdingAPI.Framework int now = this.Watchers.TimeWatcher.CurrentValue; this.Watchers.TimeWatcher.Reset(); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); this.Events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); @@ -722,7 +717,7 @@ namespace StardewModdingAPI.Framework // raise current location changed if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; @@ -733,7 +728,7 @@ namespace StardewModdingAPI.Framework // raise player leveled up a skill foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); this.Events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); @@ -744,7 +739,7 @@ namespace StardewModdingAPI.Framework ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); if (changedItems.Any()) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); this.Events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems)); @@ -753,7 +748,7 @@ namespace StardewModdingAPI.Framework // raise mine level changed if (playerTracker.TryGetNewMineLevel(out int mineLevel)) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel)); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 2d0f8b9b..dc9b8b68 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -50,9 +50,6 @@ namespace StardewModdingAPI.Framework /// The players who are currently disconnecting. private readonly IList DisconnectingFarmers; - /// Whether SMAPI should log more detailed information. - private readonly bool VerboseLogging; - /// A callback to invoke when a mod message is received. private readonly Action OnModMessageReceived; @@ -76,16 +73,14 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Simplifies access to private code. - /// Whether SMAPI should log more detailed information. /// A callback to invoke when a mod message is received. - public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging, Action onModMessageReceived) + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action onModMessageReceived) { this.Monitor = monitor; this.EventManager = eventManager; this.JsonHelper = jsonHelper; this.ModRegistry = modRegistry; this.Reflection = reflection; - this.VerboseLogging = verboseLogging; this.OnModMessageReceived = onModMessageReceived; this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); @@ -140,7 +135,7 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); resume(); @@ -152,7 +147,7 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) @@ -177,7 +172,7 @@ namespace StardewModdingAPI.Framework /// Process the message using the game's default logic. public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) @@ -199,13 +194,13 @@ namespace StardewModdingAPI.Framework this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); // reply with own context - this.VerboseLog(" Replying with host context..."); + this.Monitor.VerboseLog(" Replying with host context..."); newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); // reply with other players' context foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) { - this.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); + this.Monitor.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); } @@ -215,7 +210,7 @@ namespace StardewModdingAPI.Framework object[] fields = this.GetContextSyncMessageFields(newPeer); foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) { - this.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); + this.Monitor.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } @@ -256,7 +251,7 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) @@ -351,7 +346,7 @@ namespace StardewModdingAPI.Framework throw new ArgumentNullException(nameof(fromModID)); if (!this.Peers.Any()) { - this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); + this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); return; } @@ -363,7 +358,7 @@ namespace StardewModdingAPI.Framework playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); if (!playerIDs.Any()) { - this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); + this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); return; } } @@ -380,7 +375,7 @@ namespace StardewModdingAPI.Framework string data = JsonConvert.SerializeObject(model, Formatting.None); // log message - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace); // send message @@ -398,7 +393,7 @@ namespace StardewModdingAPI.Framework else if (this.HostPeer != null && this.HostPeer.HasSmapi) this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data)); else - this.VerboseLog(" Can't send message because no valid connections were found."); + this.Monitor.VerboseLog(" Can't send message because no valid connections were found."); } @@ -441,7 +436,7 @@ namespace StardewModdingAPI.Framework string json = message.Reader.ReadString(); ModMessageModel model = this.JsonHelper.Deserialise(json); HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Received message: {json}."); // notify local mods @@ -457,7 +452,7 @@ namespace StardewModdingAPI.Framework if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) { newModel.ToPlayerIDs = new[] { peer.PlayerID }; - this.VerboseLog($" Forwarding message to player {peer.PlayerID}."); + this.Monitor.VerboseLog($" Forwarding message to player {peer.PlayerID}."); peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); } } @@ -520,13 +515,5 @@ namespace StardewModdingAPI.Framework return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; } - - /// Log a trace message if is enabled. - /// The message to log. - private void VerboseLog(string message) - { - if (this.VerboseLogging) - this.Monitor.Log(message, LogLevel.Trace); - } } } diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs index 62c479bc..0f153e10 100644 --- a/src/SMAPI/IMonitor.cs +++ b/src/SMAPI/IMonitor.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI +namespace StardewModdingAPI { /// Encapsulates monitoring and logging for a given module. public interface IMonitor @@ -9,6 +9,9 @@ /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. bool IsExiting { get; } + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + bool IsVerbose { get; } + /********* ** Methods @@ -18,6 +21,10 @@ /// The log severity level. void Log(string message, LogLevel level = LogLevel.Debug); + /// Log a message that only appears when is enabled. + /// The message to log. + void VerboseLog(string message); + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// The reason for the shutdown. void ExitGameImmediately(string reason); -- cgit From 9560baeb71ca17c8b86b3674daddd13765ba0f24 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Nov 2018 17:35:56 -0500 Subject: add filters to mod compatibility list (#597) --- docs/release-notes.md | 4 +- src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs | 2 + src/SMAPI.Web/ViewModels/ModModel.cs | 3 + src/SMAPI.Web/Views/Mods/Index.cshtml | 34 +++-- src/SMAPI.Web/wwwroot/Content/css/mods.css | 47 +++++-- src/SMAPI.Web/wwwroot/Content/js/mods.js | 144 ++++++++++++++++++++-- 6 files changed, 201 insertions(+), 33 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index b9a73f99..9d587ab7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,7 +10,7 @@ * 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. + * SMAPI now recommends a compatible SMAPI version if you have an older game version. * Fixed transparency issues on Linux/Mac for some mod images. * Fixed error when a mod manifest is corrupted. * Fixed error when a mod adds an unnamed location. @@ -50,7 +50,7 @@ * 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). * **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"`. + * Non-string manifest versions are now deprecated and will stop working in SMAPI 3.0. Affected mods should 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. diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs index 61756176..85bf1e46 100644 --- a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs +++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs @@ -29,6 +29,8 @@ namespace StardewModdingAPI.Web.ViewModels public ModCompatibilityModel(WikiCompatibilityInfo info) { this.Status = info.Status.ToString(); + this.Status = this.Status.Substring(0, 1).ToLower() + this.Status.Substring(1); + this.Summary = info.Summary; this.BrokeIn = info.BrokeIn; if (info.UnofficialVersion != null) diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 309ed828..0e7d2076 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -40,6 +40,9 @@ namespace StardewModdingAPI.Web.ViewModels /// A unique identifier for the mod that can be used in an anchor URL. public string Slug { get; set; } + /// The sites where the mod can be downloaded. + public string[] ModPageSites => this.ModPages.Select(p => p.Text).ToArray(); + /********* ** Public methods diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index b326fd36..372d6706 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -4,11 +4,11 @@ ViewData["Title"] = "SMAPI mod compatibility"; } @section Head { - + - +