From f9eb16489fcf3f4c486df5f96a94edf16cf19a09 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Aug 2018 14:44:18 -0400 Subject: refactor some methods for reuse (#468) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9ac95fd4..09880d03 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; @@ -181,7 +180,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate ID format - if (Regex.IsMatch(mod.Manifest.UniqueID, "[^a-z0-9_.-]", RegexOptions.IgnoreCase)) + if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } -- cgit From 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index fc6ea97f..09d444a3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For modders: * Added `IContentPack.WriteJsonFile` method. + * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. ## 2.7 * For players: diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index ccb2b9a0..49285388 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -51,8 +51,12 @@ namespace StardewModdingAPI.Framework /// The model type. /// The file path relative to the contnet directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). public TModel ReadJsonFile(string path) where TModel : class { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.ReadJsonFile)} with a relative path."); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model) ? model @@ -63,8 +67,12 @@ namespace StardewModdingAPI.Framework /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the mod folder. /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). public void WriteJsonFile(string path, TModel data) where TModel : class { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.WriteJsonFile)} with a relative path."); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, data); } diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index fa793b13..9ba32394 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -25,12 +25,14 @@ namespace StardewModdingAPI /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the content pack directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). TModel ReadJsonFile(string path) where TModel : class; /// Save data to a JSON file in the content pack's folder. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the mod folder. /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). void WriteJsonFile(string path, TModel data) where TModel : class; /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs index b959f9b5..79748c25 100644 --- a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs @@ -63,6 +63,18 @@ namespace StardewModdingAPI.Toolkit.Utilities return relative; } + /// Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain ../). + /// The path to check. + public static bool IsSafeRelativePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return true; + + return + !Path.IsPathRooted(path) + && PathUtilities.GetSegments(path).All(segment => segment.Trim() != ".."); + } + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). /// The string to check. public static bool IsSlug(string str) -- cgit From 417c04076634ea87d7b3030a1acf46825da6e3e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 01:53:35 -0400 Subject: add data API (#468) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/DataHelper.cs | 155 +++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 28 ++-- src/SMAPI/IDataHelper.cs | 61 ++++++++ src/SMAPI/IModHelper.cs | 11 +- src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 2 + .../Serialisation/JsonHelper.cs | 5 +- 8 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/DataHelper.cs create mode 100644 src/SMAPI/IDataHelper.cs (limited to 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') diff --git a/build/common.targets b/build/common.targets index 5b6511f8..90c477b6 100644 --- a/build/common.targets +++ b/build/common.targets @@ -99,14 +99,14 @@ - - - - - - + + + + + + @@ -114,12 +114,14 @@ - - + + + - - + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 79185896..35ff78a5 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -23,47 +23,50 @@ - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/docs/release-notes.md b/docs/release-notes.md index c7097f97..0ec842ef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,12 +1,15 @@ # Release notes ## 2.8 (upcoming) * For players: + * Moved most SMAPI files into a `smapi-internal` subfolder. * Updated compatibility list. * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added `IContentPack.WriteJsonFile` method. + * Added IntelliSense documentation when not using the 'for developers' version of SMAPI. * Fixed `IContentPack.ReadJsonFile` allowing non-relative paths. + * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mods, but you'll need to update the mod build config NuGet package when compiling mods. ## 2.7 * For players: diff --git a/docs/technical-docs.md b/docs/technical-docs.md index ed45871a..be809c3f 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -50,15 +50,15 @@ on the wiki for the first-time setup. 1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a [semantic version](http://semver.org). Recommended format: - build type | format | example - :--------- | :-------------------------------- | :------ - dev build | `-alpha.` | `2.0-alpha.20171230` - prerelease | `-prerelease.` | `2.0-prerelease.2` - release | `` | `2.0` + build type | format | example + :--------- | :----------------------- | :------ + dev build | `-alpha.` | `3.0-alpha.20171230` + prerelease | `-beta.` | `3.0-beta.2` + release | `` | `3.0` 2. In Windows: 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 3.0`). 2. Transfer the `SMAPI ` folder to Linux or Mac. _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, since we need to set Unix file permissions that Windows won't save._ @@ -69,36 +69,26 @@ on the wiki for the first-time setup. 3. If you did everything right so far, you should have a folder like this: ``` - SMAPI-2.x/ - install.exe - readme.txt + SMAPI 3.0 installer/ + install on Linux.sh + install on Mac.command + install on Windows.exe + README.txt internal/ Mono/ Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll + smapi-internal/* StardewModdingAPI - StardewModdingAPI.config.json - StardewModdingAPI.Internal.dll - StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml steam_appid.txt - System.Numerics.dll - System.Runtime.Caching.dll - System.ValueTuple.dll Windows/ Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI.config.json - StardewModdingAPI.Internal.dll - StardewModdingAPI.metadata.json + smapi-internal/* StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml - System.ValueTuple.dll steam_appid.txt ``` 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 0aac1da2..f9e1ff94 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -93,40 +93,39 @@ namespace StardewModdingApi.Installer { string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); - // common - yield return GetInstallPath("0Harmony.dll"); - yield return GetInstallPath("0Harmony.pdb"); - yield return GetInstallPath("Mono.Cecil.dll"); - yield return GetInstallPath("Newtonsoft.Json.dll"); + // current files + yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI.exe"); - yield return GetInstallPath("StardewModdingAPI.config.json"); - yield return GetInstallPath("StardewModdingAPI.metadata.json"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); - yield return GetInstallPath("System.ValueTuple.dll"); - yield return GetInstallPath("steam_appid.txt"); - - // Linux/Mac only - yield return GetInstallPath("libgdiplus.dylib"); - yield return GetInstallPath("StardewModdingAPI"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); - yield return GetInstallPath("System.Numerics.dll"); - yield return GetInstallPath("System.Runtime.Caching.dll"); - - // Windows only - yield return GetInstallPath("StardewModdingAPI.pdb"); + yield return GetInstallPath("smapi-internal"); // obsolete - yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 + yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) - yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 + yield return GetInstallPath("0Harmony.dll"); // moved in 2.8 + yield return GetInstallPath("0Harmony.pdb"); // moved in 2.8 + yield return GetInstallPath("Mono.Cecil.dll"); // moved in 2.8 + yield return GetInstallPath("Newtonsoft.Json.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.config.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.metadata.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.xml"); // moved in 2.8 + yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 + yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8 + yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8 + yield return GetInstallPath("steam_appid.txt"); // moved in 2.8 + if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) @@ -438,14 +437,13 @@ namespace StardewModdingApi.Installer { // copy SMAPI files to game dir this.PrintDebug("Adding SMAPI files..."); - foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo sourceEntry in paths.PackageDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { - if (sourceFile.Name == this.InstallerFileName) + if (sourceEntry.Name == this.InstallerFileName) continue; - string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name); - this.InteractivelyDelete(targetPath); - sourceFile.CopyTo(targetPath); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name)); + this.RecursiveCopy(sourceEntry, paths.GameDir); } // replace mod launcher (if possible) @@ -508,7 +506,7 @@ namespace StardewModdingApi.Installer targetDir.Create(); // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopy)) sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); } @@ -690,6 +688,31 @@ namespace StardewModdingApi.Installer } } + /// Recursively copy a directory or file. + /// The file or folder to copy. + /// The folder to copy into. + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder) + { + if (!targetFolder.Exists) + targetFolder.Create(); + + switch (source) + { + case FileInfo sourceFile: + sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); + break; + + case DirectoryInfo sourceDir: + DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); + foreach (var entry in sourceDir.EnumerateFileSystemInfos()) + this.RecursiveCopy(entry, targetSubfolder); + break; + + default: + throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); + } + } + /// Delete a file or folder regardless of file permissions, and block until deletion completes. /// The file or folder to reset. /// This method is mirred from FileUtilities.ForceDelete in the toolkit. @@ -871,7 +894,7 @@ namespace StardewModdingApi.Installer this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); // move mods if no conflicts (else warn) - foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { // get type bool isDir = entry is DirectoryInfo; @@ -928,22 +951,26 @@ namespace StardewModdingApi.Installer Directory.CreateDirectory(newPath); DirectoryInfo directory = (DirectoryInfo)entry; - foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopy)) this.Move(child, Path.Combine(newPath, child.Name)); directory.Delete(recursive: true); } } - /// Get whether a file should be copied when moving a folder. - /// The file info. - private bool ShouldCopyFile(FileSystemInfo file) + /// Get whether a file or folder should be copied from the installer files. + /// The file or folder info. + private bool ShouldCopy(FileSystemInfo entry) { - // ignore Mac symlink - if (file is FileInfo && file.Name == "mcs") - return false; - - return true; + switch (entry.Name) + { + case "mcs": + return false; // ignore Mac symlink + case "Mods": + return false; // Mods folder handled separately + default: + return true; + } } } } diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d1c8a4eb..db9fe8bd 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -136,7 +136,7 @@ true - $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll false true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 3d6f2598..04880101 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0 + 2.1.1 Build package for SMAPI mods Pathoschild Pathoschild @@ -19,6 +19,9 @@ - Added option to ignore files by regex pattern. - Added reference to new SMAPI DLL. - Fixed some game paths not detected by NuGet package. + + 2.1.1: + - Update for SMAPI 2.8. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bd512fb1..0e0ae239 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -64,11 +64,14 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; + /// The absolute path to the folder containing SMAPI's internal files. + internal static readonly string InternalFilesPath = Program.DllSearchPath; + /// The file path for the SMAPI configuration file. - internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json"); /// The file path for the SMAPI metadata file. - internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); + internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.metadata.json"); /// The filename prefix used for all SMAPI logs. internal static string LogNamePrefix { get; } = "SMAPI-"; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 37b1a378..e750c659 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -45,6 +45,7 @@ namespace StardewModdingAPI.Framework.ModLoading this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary(); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c40d2ff6..64eeb45a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -45,6 +45,11 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// The absolute path to search for SMAPI's internal DLLs. + /// We can't use directly, since depends on DLLs loaded from this folder. + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + internal static readonly string DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal"); + /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -111,6 +116,8 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { + // initial setup + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; Program.AssertMinimumCompatibility(); // get flags from arguments @@ -135,10 +142,48 @@ namespace StardewModdingAPI program.RunInteractively(); } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // skip if already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; + this.Monitor.Log("Disposing...", LogLevel.Trace); + + // dispose mod data + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { + try + { + (mod.Mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } + + // dispose core components + this.IsGameRunning = false; + this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); + this.CancellationTokenSource?.Dispose(); + this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); + + // end game (moved from Game1.OnExiting to let us clean up first) + Process.GetCurrentProcess().Kill(); + } + + + /********* + ** Private methods + *********/ /// Construct an instance. /// The path to search for mods. /// Whether to output log messages to the console. - public Program(string modsPath, bool writeToConsole) + private Program(string modsPath, bool writeToConsole) { // init paths this.VerifyPath(modsPath); @@ -189,7 +234,7 @@ namespace StardewModdingAPI /// Launch SMAPI. [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - public void RunInteractively() + private void RunInteractively() { // initialise SMAPI try @@ -320,44 +365,28 @@ namespace StardewModdingAPI } } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + /// Method called when assembly resolution fails, which may return a manually resolved assembly. + /// The event sender. + /// The event arguments. + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { - // skip if already disposed - if (this.IsDisposed) - return; - this.IsDisposed = true; - this.Monitor.Log("Disposing...", LogLevel.Trace); - - // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { try { - (mod.Mod as IDisposable)?.Dispose(); + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + return Assembly.LoadFrom(dll.FullName); } catch (Exception ex) { - mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting the smapi-lib folder and reinstalling SMAPI.", ex); } } - // dispose core components - this.IsGameRunning = false; - this.ConsoleManager?.Dispose(); - this.ContentCore?.Dispose(); - this.CancellationTokenSource?.Dispose(); - this.GameInstance?.Dispose(); - this.LogFile?.Dispose(); - - // end game (moved from Game1.OnExiting to let us clean up first) - Process.GetCurrentProcess().Kill(); + return null; } - - /********* - ** Private methods - *********/ /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. private static void AssertMinimumCompatibility() { -- cgit From 100e303b488a36e8410ff67e32c35bff80f21ba2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 20:27:28 -0400 Subject: add recursive mod search (#583) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +- .../Framework/ModScanning/ModFolder.cs | 15 ++--- .../Framework/ModScanning/ModScanner.cs | 64 ++++++++++++++++++++-- 4 files changed, 66 insertions(+), 18 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ec842ef..c742204e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ # Release notes ## 2.8 (upcoming) * For players: + * Added support for subfolders under `Mods`, for players who want to organise their mods. * Moved most SMAPI files into a `smapi-internal` subfolder. * Updated compatibility list. diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 09880d03..11518444 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName); + displayName = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); // apply defaults if (manifest != null && dataRecord != null) @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + yield return new ModMetadata(displayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 4aaa3f83..83c9c44d 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -11,11 +11,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /********* ** Accessors *********/ - /// The Mods subfolder containing this mod. - public DirectoryInfo SearchDirectory { get; } - - /// The folder containing manifest.json. - public DirectoryInfo ActualDirectory { get; } + /// The folder containing the mod's manifest.json. + public DirectoryInfo Directory { get; } /// The mod manifest. public Manifest Manifest { get; } @@ -28,14 +25,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ** Public methods *********/ /// Construct an instance. - /// The Mods subfolder containing this mod. - /// The folder containing manifest.json. + /// The folder containing the mod's manifest.json. /// The mod manifest. /// The error which occurred parsing the manifest, if any. - public ModFolder(DirectoryInfo searchDirectory, DirectoryInfo actualDirectory, Manifest manifest, string manifestParseError = null) + public ModFolder(DirectoryInfo directory, Manifest manifest, string manifestParseError = null) { - this.SearchDirectory = searchDirectory; - this.ActualDirectory = actualDirectory; + this.Directory = directory; this.Manifest = manifest; this.ManifestParseError = manifestParseError; } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index f1cce4a4..063ec2f4 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -16,6 +16,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The JSON helper with which to read manifests. private readonly JsonHelper JsonHelper; + /// A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod. + private readonly HashSet IgnoreFilesystemEntries = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ".DS_Store", + "mcs", + "Thumbs.db" + }; + /********* ** Public methods @@ -31,19 +39,23 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The root folder containing mods. public IEnumerable GetModFolders(string rootPath) { - foreach (DirectoryInfo folder in new DirectoryInfo(rootPath).EnumerateDirectories()) - yield return this.ReadFolder(rootPath, folder); + DirectoryInfo root = new DirectoryInfo(rootPath); + return this.GetModFolders(root, root); } /// Extract information from a mod folder. - /// The root folder containing mods. /// The folder to search for a mod. - public ModFolder ReadFolder(string rootPath, DirectoryInfo searchFolder) + public ModFolder ReadFolder(DirectoryInfo searchFolder) { // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); if (manifestFile == null) - return new ModFolder(searchFolder, null, null, "it doesn't have a manifest."); + { + bool isEmpty = !searchFolder.GetFileSystemInfos().Where(this.IsRelevant).Any(); + if (isEmpty) + return new ModFolder(searchFolder, null, "it's an empty folder."); + return new ModFolder(searchFolder, null, "it contains files, but none of them are manifest.json."); + } // read mod info Manifest manifest = null; @@ -64,13 +76,33 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - return new ModFolder(searchFolder, manifestFile.Directory, manifest, manifestError); + return new ModFolder(manifestFile.Directory, manifest, manifestError); } /********* ** Private methods *********/ + /// Recursively extract information about all mods in the given folder. + /// The root mod folder. + /// The folder to search for mods. + public IEnumerable GetModFolders(DirectoryInfo root, DirectoryInfo folder) + { + // recurse into subfolders + if (this.IsModSearchFolder(root, folder)) + { + foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) + { + foreach (ModFolder match in this.GetModFolders(root, subfolder)) + yield return match; + } + } + + // treat as mod folder + else + yield return this.ReadFolder(folder); + } + /// Find the manifest for a mod folder. /// The folder to search. private FileInfo FindManifest(DirectoryInfo folder) @@ -94,5 +126,25 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return null; } } + + /// Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods). + /// The root mod folder. + /// The folder to search for mods. + private bool IsModSearchFolder(DirectoryInfo root, DirectoryInfo folder) + { + if (root.FullName == folder.FullName) + return true; + + DirectoryInfo[] subfolders = folder.GetDirectories().Where(this.IsRelevant).ToArray(); + FileInfo[] files = folder.GetFiles().Where(this.IsRelevant).ToArray(); + return subfolders.Any() && !files.Any(); + } + + /// Get whether a file or folder is relevant when deciding how to process a mod folder. + /// The file or folder. + private bool IsRelevant(FileSystemInfo entry) + { + return !this.IgnoreFilesystemEntries.Contains(entry.Name); + } } } -- cgit From ca8699c68f238f3092966a550643859bce357a86 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 21:22:48 -0400 Subject: add display name field to ModFolder (#583) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 9 +-------- .../Framework/ModScanning/ModFolder.cs | 13 ++++++++++++- .../Framework/ModScanning/ModScanner.cs | 11 ++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 11518444..65a311dc 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -30,13 +30,6 @@ namespace StardewModdingAPI.Framework.ModLoading // parse internal data record (if any) ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); - // get display name - string displayName = manifest?.Name; - if (string.IsNullOrWhiteSpace(displayName)) - displayName = dataRecord?.DisplayName; - if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); - // apply defaults if (manifest != null && dataRecord != null) { @@ -48,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 83c9c44d..d2fea9e2 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.ModScanning { @@ -11,6 +12,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /********* ** Accessors *********/ + /// A suggested display name for the mod folder. + public string DisplayName { get; } + /// The folder containing the mod's manifest.json. public DirectoryInfo Directory { get; } @@ -25,14 +29,21 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ** Public methods *********/ /// Construct an instance. + /// The root folder containing mods. /// The folder containing the mod's manifest.json. /// The mod manifest. /// The error which occurred parsing the manifest, if any. - public ModFolder(DirectoryInfo directory, Manifest manifest, string manifestParseError = null) + public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null) { + // save info this.Directory = directory; this.Manifest = manifest; this.ManifestParseError = manifestParseError; + + // set display name + this.DisplayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(this.DisplayName)) + this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); } /// Get the update keys for a mod. diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 063ec2f4..71dc0cb3 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -44,8 +44,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } /// Extract information from a mod folder. + /// The root folder containing mods. /// The folder to search for a mod. - public ModFolder ReadFolder(DirectoryInfo searchFolder) + public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) { // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); @@ -53,8 +54,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { bool isEmpty = !searchFolder.GetFileSystemInfos().Where(this.IsRelevant).Any(); if (isEmpty) - return new ModFolder(searchFolder, null, "it's an empty folder."); - return new ModFolder(searchFolder, null, "it contains files, but none of them are manifest.json."); + return new ModFolder(root, searchFolder, null, "it's an empty folder."); + return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json."); } // read mod info @@ -76,7 +77,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - return new ModFolder(manifestFile.Directory, manifest, manifestError); + return new ModFolder(root, manifestFile.Directory, manifest, manifestError); } @@ -100,7 +101,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // treat as mod folder else - yield return this.ReadFolder(folder); + yield return this.ReadFolder(root, folder); } /// Find the manifest for a mod folder. -- cgit From 5dfbae2010960b854bd470316b27423dd05fbed2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 22:51:30 -0400 Subject: add error when using Read/WriteSaveData when not main player (#468) --- src/SMAPI/Framework/ModHelpers/DataHelper.cs | 14 +++++++++----- src/SMAPI/IDataHelper.cs | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 6ba099b4..cdb3718f 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Framework.ModHelpers 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."); + throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing)."); path = Path.Combine(this.ModFolderPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, data); @@ -74,11 +74,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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. + /// The player hasn't loaded a save file yet or isn't the main player. 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."); + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded."); + if (!Context.IsMainPlayer) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); return Game1.CustomData.TryGetValue(this.GetSaveFileKey(key), out string value) ? this.JsonHelper.Deserialise(value) @@ -89,11 +91,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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. + /// The player hasn't loaded a save file yet or isn't the main player. 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."); + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded."); + if (!Context.IsMainPlayer) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); Game1.CustomData[this.GetSaveFileKey(key)] = this.JsonHelper.Serialise(data, Formatting.None); } diff --git a/src/SMAPI/IDataHelper.cs b/src/SMAPI/IDataHelper.cs index 722d5062..6afdc529 100644 --- a/src/SMAPI/IDataHelper.cs +++ b/src/SMAPI/IDataHelper.cs @@ -32,14 +32,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 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. + /// The player hasn't loaded a save file yet or isn't the main player. 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. + /// The player hasn't loaded a save file yet or isn't the main player. void WriteSaveData(string key, TModel data) where TModel : class; -- cgit From a4ecb49a44559f2ca9c3f00049a1c8a7862cc0c9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 01:44:38 -0400 Subject: update for Stardew Valley 1.3.29 beta (#585) --- src/SMAPI/Framework/SGame.cs | 240 +++++++++++++-------- .../FieldWatchers/NetCollectionWatcher.cs | 2 +- .../StateTracking/FieldWatchers/WatcherFactory.cs | 2 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 1 + src/SMAPI/Program.cs | 6 +- 5 files changed, 161 insertions(+), 90 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 83e8c9a7..3af4beb1 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -17,7 +17,6 @@ using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking; using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; @@ -74,9 +73,6 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper; - /**** ** Game state ****/ @@ -95,6 +91,10 @@ namespace StardewModdingAPI.Framework /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; + /// A reference to the private _farmerShadows field on . + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Deliberately named to match original game code as closely as possible.")] + private readonly List __farmerShadows; + /********* ** Accessors @@ -129,10 +129,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. /// Simplifies access to private game code. /// Manages SMAPI events for mods. - /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, Action onGameInitialised, Action onGameExiting) + internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) { SGame.ConstructorHack = null; @@ -147,12 +146,14 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.Events = eventManager; this.Reflection = reflection; - this.JsonHelper = jsonHelper; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); + // init reflection + this.__farmerShadows = reflection.GetField>(this, "_farmerShadows").GetValue(); + // init observables Game1.locations = new ObservableCollection(); } @@ -796,32 +797,9 @@ namespace StardewModdingAPI.Framework [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] private void DrawImpl(GameTime gameTime) { - if (Game1.debugMode) - { - if (Game1._fpsStopwatch.IsRunning) - { - float totalSeconds = (float)Game1._fpsStopwatch.Elapsed.TotalSeconds; - Game1._fpsList.Add(totalSeconds); - while (Game1._fpsList.Count >= 120) - Game1._fpsList.RemoveAt(0); - float num = 0.0f; - foreach (float fps in Game1._fpsList) - num += fps; - Game1._fps = (float)(1.0 / ((double)num / (double)Game1._fpsList.Count)); - } - Game1._fpsStopwatch.Restart(); - } - else - { - if (Game1._fpsStopwatch.IsRunning) - Game1._fpsStopwatch.Reset(); - Game1._fps = 0.0f; - Game1._fpsList.Clear(); - } if (Game1._newDayTask != null) { this.GraphicsDevice.Clear(this.bgColor); - //base.Draw(gameTime); } else { @@ -854,7 +832,6 @@ namespace StardewModdingAPI.Framework Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } - //base.Draw(gameTime); this.renderScreenBuffer(); } else @@ -882,14 +859,19 @@ namespace StardewModdingAPI.Framework { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); } else if (Game1.gameMode == (byte)11) { - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); 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); @@ -907,14 +889,13 @@ namespace StardewModdingAPI.Framework } this.drawOverlays(Game1.spriteBatch); this.RaisePostRender(needsNewBatch: true); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } + if ((double)Game1.options.zoomLevel == 1.0) + return; + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); } else if (Game1.showingEndOfNightStuff) { @@ -936,14 +917,13 @@ namespace StardewModdingAPI.Framework this.RaisePostRender(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } + if ((double)Game1.options.zoomLevel == 1.0) + return; + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); } else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null) { @@ -965,15 +945,22 @@ namespace StardewModdingAPI.Framework { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } //base.Draw(gameTime); } else { 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); @@ -1004,15 +991,23 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); Game1.currentLocation.drawWater(Game1.spriteBatch); - IEnumerable source = Game1.currentLocation.farmers; + this.__farmerShadows.Clear(); if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) - source = (IEnumerable)Game1.currentLocation.currentEvent.farmerActors; - IEnumerable farmers = source.Where((Func)(farmer => { - if (!farmer.IsLocalPlayer) - return !(bool)((NetFieldBase)farmer.hidden); - return true; - })); + foreach (Farmer farmerActor in Game1.currentLocation.currentEvent.farmerActors) + { + if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase)farmerActor.hidden)) + this.__farmerShadows.Add(farmerActor); + } + } + else + { + foreach (Farmer farmer in Game1.currentLocation.farmers) + { + if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase)farmer.hidden)) + this.__farmerShadows.Add(farmer); + } + } if (!Game1.currentLocation.shouldHideCharacters()) { if (Game1.CurrentEvent == null) @@ -1031,13 +1026,13 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (Farmer farmer in farmers) + foreach (Farmer farmerShadow in this.__farmerShadows) { - if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) + if (!(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f)); + Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); Color white = Color.White; double num1 = 0.0; @@ -1046,7 +1041,7 @@ namespace StardewModdingAPI.Framework bounds = Game1.shadowTexture.Bounds; double y = (double)bounds.Center.Y; Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); + double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); int num3 = 0; double num4 = 0.0; spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); @@ -1075,13 +1070,13 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (Farmer farmer in farmers) + foreach (Farmer farmerShadow in this.__farmerShadows) { - if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) + if (!(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f)); + Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); Color white = Color.White; double num1 = 0.0; @@ -1090,7 +1085,7 @@ namespace StardewModdingAPI.Framework bounds = Game1.shadowTexture.Bounds; double y = (double)bounds.Center.Y; Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); + double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); int num3 = 0; double num4 = 0.0; spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); @@ -1141,14 +1136,14 @@ namespace StardewModdingAPI.Framework Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); Size size2 = Game1.viewport.Size; if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_139; + goto label_129; } else - goto label_139; + goto label_129; } Game1.drawPlayerHeldObject(Game1.player); } - label_139: + label_129: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1186,9 +1181,23 @@ namespace StardewModdingAPI.Framework if (Game1.farmEvent != null) Game1.farmEvent.draw(Game1.spriteBatch); if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Black * Game1.currentLocation.LightLevel; + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Game1.screenGlowColor * Game1.screenGlowAlpha; + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) Game1.player.CurrentTool.draw(Game1.spriteBatch); @@ -1221,25 +1230,66 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); if (Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.OrangeRed * 0.45f; + spriteBatch.Draw(staminaRect, bounds, color); + } Game1.spriteBatch.End(); } Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); if (Game1.drawGrid) { - int x1 = -Game1.viewport.X % 64; - float num1 = (float)(-Game1.viewport.Y % 64); - int x2 = x1; - while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) + int num1 = -Game1.viewport.X % 64; + float num2 = (float)(-Game1.viewport.Y % 64); + int num3 = num1; + while (true) { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); - x2 += 64; + int num4 = num3; + viewport = Game1.graphics.GraphicsDevice.Viewport; + int width1 = viewport.Width; + if (num4 < width1) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + int x = num3; + int y = (int)num2; + int width2 = 1; + viewport = Game1.graphics.GraphicsDevice.Viewport; + int height = viewport.Height; + Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width2, height); + Color color = Color.Red * 0.5f; + spriteBatch.Draw(staminaRect, destinationRectangle, color); + num3 += 64; + } + else + break; } - float num2 = num1; - while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) + float num5 = num2; + while (true) { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); - num2 += 64f; + double num4 = (double)num5; + viewport = Game1.graphics.GraphicsDevice.Viewport; + double height1 = (double)viewport.Height; + if (num4 < height1) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + int x = num1; + int y = (int)num5; + viewport = Game1.graphics.GraphicsDevice.Viewport; + int width = viewport.Width; + int height2 = 1; + Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, height2); + Color color = Color.Red * 0.5f; + spriteBatch.Draw(staminaRect, destinationRectangle, color); + num5 += 64f; + } + else + break; } } if (Game1.currentBillboard != 0) @@ -1288,13 +1338,34 @@ namespace StardewModdingAPI.Framework if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); if (Game1.isRaining && Game1.currentLocation != null && ((bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Blue * 0.2f; + spriteBatch.Draw(staminaRect, bounds, color); + } if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } else if ((double)Game1.flashAlpha > 0.0) { if (Game1.options.screenFlash) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha)); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.White * Math.Min(1f, Game1.flashAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } Game1.flashAlpha -= 0.1f; } if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) @@ -1356,7 +1427,6 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); this.renderScreenBuffer(); - //base.Draw(gameTime); } } } diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs index f92edb90..8a841a79 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { /// A watcher which detects changes to a Netcode collection. internal class NetCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher - where TValue : INetObject + where TValue : class, INetObject { /********* ** Properties diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index d7a02668..ab4ab0d5 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -56,7 +56,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// Get a watcher for a net collection. /// The value type. /// The net collection. - public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : INetObject + public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : class, INetObject { return new NetCollectionWatcher(collection); } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 3814e534..59e80d8a 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -5,6 +5,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; +using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f5a32b4f..b39077c1 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -98,8 +98,8 @@ namespace StardewModdingAPI 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(@"^Multiplayer auth success$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) + new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), }; /// The mod toolkit used for generic mod interactions. @@ -275,7 +275,7 @@ namespace StardewModdingAPI // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler -- cgit From 6443fb12317932c749730fa42457b7c3295cebf4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 02:24:30 -0400 Subject: fix deprecated Read/WriteJsonFiles method enforcing newer restrictions (#468) --- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 17 ++++++++++++++--- src/SMAPI/Program.cs | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 0ed3df12..ae0368f0 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,7 +4,9 @@ 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 { @@ -30,6 +32,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The full path to the mod's folder. public string DirectoryPath { get; } + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. public IModEvents Events { get; } @@ -64,6 +69,7 @@ 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. @@ -78,7 +84,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, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, 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 @@ -89,6 +95,7 @@ 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); @@ -136,7 +143,10 @@ namespace StardewModdingAPI.Framework.ModHelpers public TModel ReadJsonFile(string path) where TModel : class { - return this.Data.ReadJsonFile(path); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + ? data + : null; } /// Save to a JSON file. @@ -147,7 +157,8 @@ namespace StardewModdingAPI.Framework.ModHelpers public void WriteJsonFile(string path, TModel model) where TModel : class { - this.Data.WriteJsonFile(path, model); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + this.JsonHelper.WriteJsonFile(path, model); } /**** diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b39077c1..d34cbab5 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -907,7 +907,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod -- 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 'src/SMAPI/Framework') 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 fd925e9a8c13d31980d934195ea9cc81acaf970a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 23:07:48 -0400 Subject: let mods access mod.IsContentPack property (#534) --- src/SMAPI/Framework/IModMetadata.cs | 7 ++----- src/SMAPI/IModInfo.cs | 3 +++ 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index edd7eed6..1a007297 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -28,10 +28,10 @@ namespace StardewModdingAPI.Framework /// The reason the metadata is invalid, if any. string Error { get; } - /// The mod instance (if loaded and is false). + /// The mod instance (if loaded and is false). IMod Mod { get; } - /// The content pack instance (if loaded and is true). + /// The content pack instance (if loaded and is true). IContentPack ContentPack { get; } /// Writes messages to the console and log file as this mod. @@ -40,9 +40,6 @@ namespace StardewModdingAPI.Framework /// The mod-provided API (if any). object Api { get; } - /// Whether the mod is a content pack. - bool IsContentPack { get; } - /// The update-check metadata for this mod (if any). ModEntryModel UpdateCheckData { get; } diff --git a/src/SMAPI/IModInfo.cs b/src/SMAPI/IModInfo.cs index a16c2d7b..3c85d454 100644 --- a/src/SMAPI/IModInfo.cs +++ b/src/SMAPI/IModInfo.cs @@ -5,5 +5,8 @@ namespace StardewModdingAPI { /// The mod manifest. IManifest Manifest { get; } + + /// Whether the mod is a content pack. + bool IsContentPack { get; } } } -- cgit From 788f7ae3b7dc37d3323d83830cfeb92bea958e66 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Aug 2018 01:59:31 -0400 Subject: split core logic out of Program (#582) This is needed because Mono validates Program's instance fields before the static Main runs, so the custom assembly resolution isn't set up until the app has already crashed due to invalid property types. --- src/SMAPI/Framework/SCore.cs | 1283 ++++++++++++++++++++++++++++++++++++ src/SMAPI/Program.cs | 1281 +---------------------------------- src/SMAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 1304 insertions(+), 1261 deletions(-) create mode 100644 src/SMAPI/Framework/SCore.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs new file mode 100644 index 00000000..a9ec5ee4 --- /dev/null +++ b/src/SMAPI/Framework/SCore.cs @@ -0,0 +1,1283 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +#if SMAPI_FOR_WINDOWS +using System.Windows.Forms; +#endif +using Newtonsoft.Json; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModHelpers; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; +using Object = StardewValley.Object; +using ThreadState = System.Threading.ThreadState; + +namespace StardewModdingAPI.Framework +{ + /// The core class which initialises and manages SMAPI. + internal class SCore : IDisposable + { + /********* + ** Properties + *********/ + /// The log file to which to write messages. + private readonly LogFileManager LogFile; + + /// Manages console output interception. + private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + + /// The core logger and monitor for SMAPI. + private readonly Monitor Monitor; + + /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + + /// Simplifies access to private game code. + private readonly Reflector Reflection = new Reflector(); + + /// The SMAPI configuration settings. + private readonly SConfig Settings; + + /// The underlying game instance. + private SGame GameInstance; + + /// The underlying content manager. + private ContentCoordinator ContentCore => this.GameInstance.ContentCore; + + /// Tracks the installed mods. + /// This is initialised after the game starts. + private readonly ModRegistry ModRegistry = new ModRegistry(); + + /// Manages deprecation warnings. + /// This is initialised after the game starts. + private DeprecationManager DeprecationManager; + + /// Manages SMAPI events for mods. + private readonly EventManager EventManager; + + /// Whether the game is currently running. + private bool IsGameRunning; + + /// Whether the program has been disposed. + private bool IsDisposed; + + /// Regex patterns which match console messages to suppress from the console and log. + private readonly Regex[] SuppressConsolePatterns = + { + new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + 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(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + }; + + /// The mod toolkit used for generic mod interactions. + private readonly ModToolkit Toolkit = new ModToolkit(); + + /// The path to search for mods. + private readonly string ModsPath; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The path to search for mods. + /// Whether to output log messages to the console. + public SCore(string modsPath, bool writeToConsole) + { + // init paths + this.VerifyPath(modsPath); + this.VerifyPath(Constants.LogDir); + this.ModsPath = modsPath; + + // init log file + this.PurgeLogFiles(); + string logPath = this.GetLogPath(); + + // 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) + { + WriteToConsole = writeToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; + this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + + // init logging + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + this.Monitor.Log($"Mods go here: {modsPath}"); + if (modsPath != Constants.DefaultModsPath) + this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); + this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); + + // 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 GameLocationPatch() + ); + } + + /// Launch SMAPI. + [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions + public void RunInteractively() + { + // initialise SMAPI + try + { + // hook up events + ContentEvents.Init(this.EventManager); + ControlEvents.Init(this.EventManager); + GameEvents.Init(this.EventManager); + GraphicsEvents.Init(this.EventManager); + InputEvents.Init(this.EventManager); + LocationEvents.Init(this.EventManager); + MenuEvents.Init(this.EventManager); + MineEvents.Init(this.EventManager); + MultiplayerEvents.Init(this.EventManager); + PlayerEvents.Init(this.EventManager); + SaveEvents.Init(this.EventManager); + SpecialisedEvents.Init(this.EventManager); + TimeEvents.Init(this.EventManager); + + // init JSON parser + JsonConverter[] converters = { + new ColorConverter(), + new PointConverter(), + new RectangleConverter() + }; + foreach (JsonConverter converter in converters) + this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); + + // add error handlers +#if SMAPI_FOR_WINDOWS + Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); +#endif + AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + + // add more leniant assembly resolvers + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); + + // override game + SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); + this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); + StardewValley.Program.gamePtr = this.GameInstance; + + // add exit handler + new Thread(() => + { + this.CancellationTokenSource.Token.WaitHandle.WaitOne(); + if (this.IsGameRunning) + { + try + { + File.WriteAllText(Constants.FatalCrashMarker, string.Empty); + File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); + } + + this.GameInstance.Exit(); + } + }).Start(); + + // hook into game events + ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); + + // set window titles + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + + // check update marker + if (File.Exists(Constants.UpdateMarker)) + { + string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); + if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) + { + if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) + { + this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); + this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); + this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); + Console.ReadKey(); + } + } + File.Delete(Constants.UpdateMarker); + } + + // 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("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(); + File.Delete(Constants.FatalCrashLog); + File.Delete(Constants.FatalCrashMarker); + } + + // start game + this.Monitor.Log("Starting game...", LogLevel.Debug); + try + { + this.IsGameRunning = true; + StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window + this.GameInstance.Run(); + } + catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) + { + this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); + this.PressAnyKeyToExit(); + } + catch (Exception ex) + { + this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + } + finally + { + this.Dispose(); + } + } + + /// 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 + *********/ + /// 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); + + // redirect direct console output + { + Monitor monitor = this.GetSecondaryMonitor("game"); + if (monitor.WriteToConsole) + this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); + } + + // add headers + if (this.Settings.DeveloperMode) + this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); + if (!this.Settings.CheckForUpdates) + this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + if (!this.Monitor.WriteToConsole) + this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + this.VerboseLog("Verbose logging enabled."); + + // validate XNB integrity + if (!this.ValidateContentIntegrity()) + this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); + + // load mod data + ModToolkit toolkit = new ModToolkit(); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); + + // load mods + { + this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); + ModResolver resolver = new ModResolver(); + + // 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(); + + // load mods + this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); + + // write metadata file + if (this.Settings.DumpMetadata) + { + ModFolderExport export = new ModFolderExport + { + Exported = DateTime.UtcNow.ToString("O"), + ApiVersion = Constants.ApiVersion.ToString(), + GameVersion = Constants.GameVersion.ToString(), + ModFolderPath = this.ModsPath, + Mods = mods + }; + this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export); + } + + // check for updates + this.CheckForUpdatesAsync(mods); + } + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); + return; + } + + // update window titles + int modsLoaded = this.ModRegistry.GetAll().Count(); + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; + + // start SMAPI console + new Thread(this.RunConsoleLoop).Start(); + } + + /// Handle the game changing locale. + private void OnLocaleChanged() + { + // get locale + string locale = this.ContentCore.GetLocale(); + LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; + + // update mod translation helpers + foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) + (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); + } + + /// Run a loop handling console input. + [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] + private void RunConsoleLoop() + { + // 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); + + // start handling command line input + Thread inputThread = new Thread(() => + { + while (true) + { + // get input + string input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + // handle command + this.Monitor.LogUserInput(input); + this.GameInstance.CommandQueue.Enqueue(input); + } + }); + inputThread.Start(); + + // keep console thread alive while the game is running + while (this.IsGameRunning && !this.Monitor.IsExiting) + Thread.Sleep(1000 / 10); + if (inputThread.ThreadState == ThreadState.Running) + inputThread.Abort(); + } + + /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. + /// Returns whether all integrity checks passed. + private bool ValidateContentIntegrity() + { + this.Monitor.Log("Detecting common issues...", LogLevel.Trace); + bool issuesFound = false; + + // object format (commonly broken by outdated files) + { + // detect issues + bool hasObjectIssues = false; + void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace); + foreach (KeyValuePair entry in Game1.objectInformation) + { + // must not be empty + if (string.IsNullOrWhiteSpace(entry.Value)) + { + LogIssue(entry.Key, "entry is empty"); + hasObjectIssues = true; + continue; + } + + // require core fields + string[] fields = entry.Value.Split('/'); + if (fields.Length < Object.objectInfoDescriptionIndex + 1) + { + LogIssue(entry.Key, "too few fields for an object"); + hasObjectIssues = true; + continue; + } + + // check min length for specific types + switch (fields[Object.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) + { + case "Cooking": + if (fields.Length < Object.objectInfoBuffDurationIndex + 1) + { + LogIssue(entry.Key, "too few fields for a cooking item"); + hasObjectIssues = true; + } + break; + } + } + + // log error + if (hasObjectIssues) + { + issuesFound = true; + this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); + } + } + + return !issuesFound; + } + + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. + /// The mods to include in the update check (if eligible). + private void CheckForUpdatesAsync(IModMetadata[] mods) + { + if (!this.Settings.CheckForUpdates) + return; + + new Thread(() => + { + // create client + string url = this.Settings.WebApiBaseUrl; +#if !SMAPI_FOR_WINDOWS + url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + WebApiClient client = new WebApiClient(url, Constants.ApiVersion); + this.Monitor.Log("Checking for updates...", LogLevel.Trace); + + // check SMAPI version + ISemanticVersion updateFound = null; + try + { + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; + ISemanticVersion latestStable = response.Main?.Version; + ISemanticVersion latestBeta = response.Optional?.Version; + + if (latestStable == null && response.Errors.Any()) + { + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); + } + else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) + { + updateFound = latestBeta; + this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); + } + else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) + { + updateFound = latestStable; + this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); + } + else + this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? $"Error: {ex.Message}" + : $"Error: {ex.GetLogSummary()}" + ); + } + + // show update message on next launch + if (updateFound != null) + File.WriteAllText(Constants.UpdateMarker, updateFound.ToString()); + + // check mod versions + if (mods.Any()) + { + try + { + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); + + // prepare search model + List searchMods = new List(); + foreach (IModMetadata mod in mods) + { + if (!mod.HasID()) + continue; + + string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + } + + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary results = client.GetModInfo(searchMods.ToArray()); + + // extract update alerts & errors + var updates = new List>(); + var errors = new StringBuilder(); + foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) + { + // link to update-check data + if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) + continue; + mod.SetUpdateData(result); + + // handle errors + if (result.Errors != null && result.Errors.Any()) + { + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName}: {result.Errors[0]}" + : $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}" + ); + } + + // parse versions + 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; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); + 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)); + } + + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) + { + this.Monitor.Newline(); + this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updates) + { + IModMetadata mod = entry.Item1; + ISemanticVersion newVersion = entry.Item2; + string newUrl = entry.Item3; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } + } + else + this.Monitor.Log(" All mods up to date.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? ex.Message + : ex.ToString() + ); + } + } + }).Start(); + } + + /// Get whether a given version should be offered to the user as an update. + /// The current semantic version. + /// The target semantic version. + /// Whether the user enabled the beta channel and should be offered pre-release updates. + private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) + { + return + newVersion != null + && newVersion.IsNewerThan(currentVersion) + && (useBetaChannel || !newVersion.IsPrerelease()); + } + + /// Create a directory path if it doesn't exist. + /// The directory path. + private void VerifyPath(string path) + { + try + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + /// Load and hook up the given mods. + /// The mods to load. + /// The JSON helper with which to read mods' JSON files. + /// The content manager to use for mod content. + /// Handles access to SMAPI's internal mod metadata list. + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) + { + this.Monitor.Log("Loading mods...", LogLevel.Trace); + + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); + IDictionary skippedMods = new Dictionary(); + void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase }; + + // load content packs + foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) + { + 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()) + metadata.SetWarning(ModWarning.NoUpdateKeys); + + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } + + // load mod as content pack + IManifest manifest = metadata.Manifest; + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); + metadata.SetMod(contentPack, monitor); + this.ModRegistry.Add(metadata); + } + IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); + + // load mods + { + // get content packs by mod ID + IDictionary contentPacksByModID = + loadedContentPacks + .GroupBy(p => p.Manifest.ContentPackFor.UniqueID, StringComparer.InvariantCultureIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.Select(metadata => metadata.ContentPack).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + + // load mods from metadata + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) + { + InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); + foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) + { + // get basic info + IManifest manifest = metadata.Manifest; + this.Monitor.Log(metadata.Manifest?.EntryDll != null + ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid + : $" {metadata.DisplayName}...", LogLevel.Trace); + + // show warnings + if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) + metadata.SetWarning(ModWarning.NoUpdateKeys); + + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } + + // load mod + string assemblyPath = metadata.Manifest?.EntryDll != null + ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) + : null; + Assembly modAssembly; + try + { + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); + } + catch (IncompatibleInstructionException) // details already in trace logs + { + string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); + + TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); + continue; + } + catch (SAssemblyLoadFailedException ex) + { + TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); + continue; + } + catch (Exception ex) + { + TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); + continue; + } + + // initialise mod + try + { + // get mod instance + if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + continue; + + // get content packs + if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) + contentPacks = new IContentPack[0]; + + // init mod helpers + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IModHelper modHelper; + { + 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); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); + + IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); + } + + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + } + + // init mod + mod.ModManifest = manifest; + mod.Helper = modHelper; + mod.Monitor = monitor; + + // track mod + metadata.SetMod(mod); + this.ModRegistry.Add(metadata); + } + catch (Exception ex) + { + TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); + } + } + } + } + IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); + + // log loaded mods + this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + + // log loaded content packs + if (loadedContentPacks.Any()) + { + string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + + this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); + foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + } + + // log mod warnings + this.LogModWarnings(this.ModRegistry.GetAll().ToArray(), skippedMods); + + // initialise translations + this.ReloadTranslations(loadedMods); + + // initialise loaded non-content-pack mods + foreach (IModMetadata metadata in loadedMods) + { + // add interceptors + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + // ReSharper disable SuspiciousTypeConversion.Global + if (metadata.Mod is IAssetEditor editor) + helper.ObservableAssetEditors.Add(editor); + if (metadata.Mod is IAssetLoader loader) + helper.ObservableAssetLoaders.Add(loader); + // ReSharper restore SuspiciousTypeConversion.Global + + this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; + this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; + } + + // call entry method + try + { + IMod mod = metadata.Mod; + mod.Entry(mod.Helper); + } + catch (Exception ex) + { + metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // get mod API + try + { + object api = metadata.Mod.GetApi(); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } + + if (api != null) + this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); + metadata.SetApi(api); + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + } + } + + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) + foreach (IModMetadata metadata in loadedMods) + { + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => + { + if (e.NewItems?.Count > 0) + { + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentCore.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); + } + }; + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => + { + if (e.NewItems?.Count > 0) + { + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); + } + }; + } + } + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentCore.InvalidateCacheFor(editors, loaders); + } + + // unlock mod integrations + this.ModRegistry.AreAllModsInitialised = true; + } + + /// Write a summary of mod warnings to the console and log. + /// The loaded mods. + /// The mods which were skipped, along with the friendly and developer reasons. + private void LogModWarnings(IModMetadata[] mods, IDictionary skippedMods) + { + // get mods with warnings + IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); + if (!modsWithWarnings.Any() && !skippedMods.Any()) + return; + + // log intro + { + int count = modsWithWarnings.Union(skippedMods.Keys).Count(); + this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info); + } + + // log skipped mods + if (skippedMods.Any()) + { + this.Monitor.Log(" Skipped mods", LogLevel.Error); + this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); + this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); + this.Monitor.Newline(); + + foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) + { + IModMetadata mod = pair.Key; + string[] reason = pair.Value; + + this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error); + if (reason[1] != null) + this.Monitor.Log($" ({reason[1]})", LogLevel.Trace); + } + this.Monitor.Newline(); + } + + // log warnings + if (modsWithWarnings.Any()) + { + // issue block format logic + void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb) + { + IModMetadata[] matches = modsWithWarnings.Where(p => p.Warnings.HasFlag(warning)).ToArray(); + if (!matches.Any()) + return; + + this.Monitor.Log(" " + heading, logLevel); + this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel); + foreach (string line in blurb) + this.Monitor.Log(" " + line, logLevel); + this.Monitor.Newline(); + foreach (IModMetadata match in matches) + this.Monitor.Log($" - {match.DisplayName}", logLevel); + this.Monitor.Newline(); + } + + // supported issues + LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", + "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", + "errors, or crashes in-game." + ); + LogWarningGroup(ModWarning.ChangesSaveSerialiser, LogLevel.Warn, "Changed save serialiser", + "These mods change the save serialiser. They may corrupt your save files, or make them unusable if", + "you uninstall these mods." + ); + 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." + ); + LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", + "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", + "corruption. If your game has issues, try removing these first." + ); + LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", + "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", + "mods. Consider notifying the mod authors about this problem." + ); + LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", + "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." + ); + } + } + + /// Load a mod's entry class. + /// The mod assembly. + /// A callback invoked when loading fails. + /// The loaded instance. + private bool TryLoadModEntry(Assembly modAssembly, Action onError, out Mod mod) + { + mod = null; + + // find type + TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); + if (modEntries.Length == 0) + { + onError($"its DLL has no '{nameof(Mod)}' subclass."); + return false; + } + if (modEntries.Length > 1) + { + onError($"its DLL contains multiple '{nameof(Mod)}' subclasses."); + return false; + } + + // get implementation + mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString()); + if (mod == null) + { + onError("its entry class couldn't be instantiated."); + return false; + } + + return true; + } + + /// Reload translations for all mods. + /// The mods for which to reload translations. + private void ReloadTranslations(IEnumerable mods) + { + JsonHelper jsonHelper = this.Toolkit.JsonHelper; + foreach (IModMetadata metadata in mods) + { + if (metadata.IsContentPack) + throw new InvalidOperationException("Can't reload translations for a content pack."); + + // read translation files + IDictionary> translations = new Dictionary>(); + DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); + if (translationsDir.Exists) + { + foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + { + string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); + try + { + if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary data)) + translations[locale] = data; + else + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed."); + } + catch (Exception ex) + { + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}"); + } + } + } + + // validate translations + foreach (string locale in translations.Keys.ToArray()) + { + // skip empty files + if (translations[locale] == null || !translations[locale].Keys.Any()) + { + metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn); + translations.Remove(locale); + continue; + } + + // handle duplicates + HashSet keys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet duplicateKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in translations[locale].Keys.ToArray()) + { + if (!keys.Add(key)) + { + duplicateKeys.Add(key); + translations[locale].Remove(key); + } + } + if (duplicateKeys.Any()) + metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); + } + + // update translation + TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; + translationHelper.SetTranslations(translations); + } + } + + /// The method called when the user submits a core SMAPI command in the console. + /// The command name. + /// The command arguments. + private void HandleCommand(string name, string[] arguments) + { + switch (name) + { + case "help": + if (arguments.Any()) + { + Command result = this.GameInstance.CommandManager.Get(arguments[0]); + 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); + } + 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(); + foreach (var group in groups) + { + string modName = group.Key; + string[] commandNames = group.ToArray(); + message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; + } + message += "For more information about a command, type 'help command_name'."; + + this.Monitor.Log(message, LogLevel.Info); + } + break; + + case "reload_i18n": + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); + this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); + break; + + default: + throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); + } + } + + /// Redirect messages logged directly to the console to the given monitor. + /// The monitor with which to log messages. + /// The message to log. + private void HandleConsoleMessage(IMonitor monitor, string message) + { + // detect exception + LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; + + // ignore suppressed message + if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) + return; + + // forward to monitor + monitor.Log(message, level); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + private void PressAnyKeyToExit() + { + this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + this.PressAnyKeyToExit(showMessage: false); + } + + /// 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 void PressAnyKeyToExit(bool showMessage) + { + if (showMessage) + Console.WriteLine("Game has ended. Press any key to exit."); + Thread.Sleep(100); + Console.ReadKey(); + Environment.Exit(0); + } + + /// Get a monitor instance derived from SMAPI's current settings. + /// 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) + { + WriteToConsole = this.Monitor.WriteToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; + } + + /// 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() + { + // default path + { + FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}")); + if (!defaultFile.Exists) + return defaultFile.FullName; + } + + // get first disambiguated path + for (int i = 2; i < int.MaxValue; i++) + { + FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}")); + if (!file.Exists) + return file.FullName; + } + + // should never happen + throw new InvalidOperationException("Could not find an available log path."); + } + + /// Delete all log files created by SMAPI. + private void PurgeLogFiles() + { + DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); + if (!logsDir.Exists) + return; + + foreach (FileInfo logFile in logsDir.EnumerateFiles()) + { + if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + FileUtilities.ForceDelete(logFile); + } + catch (IOException) + { + // ignore file if it's in use + } + } + } + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d34cbab5..c6f39e3f 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -1,46 +1,17 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net; using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Security; -using System.Text; -using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS -using System.Windows.Forms; #endif -using Newtonsoft.Json; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.ModHelpers; -using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Toolkit; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; -using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Utilities; -using StardewValley; -using Monitor = StardewModdingAPI.Framework.Monitor; -using SObject = StardewValley.Object; -using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI { /// The main entry point for SMAPI, responsible for hooking into and launching the game. - internal class Program : IDisposable + internal class Program { /********* ** Properties @@ -50,64 +21,6 @@ namespace StardewModdingAPI [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; - - /// Manages console output interception. - private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); - - /// The core logger and monitor for SMAPI. - private readonly Monitor Monitor; - - /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. - private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); - - /// Simplifies access to private game code. - private readonly Reflector Reflection = new Reflector(); - - /// The SMAPI configuration settings. - private readonly SConfig Settings; - - /// The underlying game instance. - private SGame GameInstance; - - /// The underlying content manager. - private ContentCoordinator ContentCore => this.GameInstance.ContentCore; - - /// Tracks the installed mods. - /// This is initialised after the game starts. - private readonly ModRegistry ModRegistry = new ModRegistry(); - - /// Manages deprecation warnings. - /// This is initialised after the game starts. - private DeprecationManager DeprecationManager; - - /// Manages SMAPI events for mods. - private readonly EventManager EventManager; - - /// Whether the game is currently running. - private bool IsGameRunning; - - /// Whether the program has been disposed. - private bool IsDisposed; - - /// Regex patterns which match console messages to suppress from the console and log. - private readonly Regex[] SuppressConsolePatterns = - { - new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - 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(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - }; - - /// The mod toolkit used for generic mod interactions. - private readonly ModToolkit Toolkit = new ModToolkit(); - - /// The path to search for mods. - private readonly string ModsPath; - /********* ** Public methods @@ -116,255 +29,15 @@ 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 - bool writeToConsole = !args.Contains("--no-terminal"); - - // get mods path from arguments - string modsPath = null; - { - int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - { - modsPath = args[pathIndex]; - if (!string.IsNullOrWhiteSpace(modsPath) && !Path.IsPathRooted(modsPath)) - modsPath = Path.Combine(Constants.ExecutionPath, modsPath); - } - if (string.IsNullOrWhiteSpace(modsPath)) - modsPath = Constants.DefaultModsPath; - } - - // load SMAPI - using (Program program = new Program(modsPath, writeToConsole)) - 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(); + Program.Start(args); } /********* ** Private methods *********/ - /// Construct an instance. - /// The path to search for mods. - /// Whether to output log messages to the console. - private Program(string modsPath, bool writeToConsole) - { - // init paths - this.VerifyPath(modsPath); - this.VerifyPath(Constants.LogDir); - this.ModsPath = modsPath; - - // init log file - this.PurgeLogFiles(); - string logPath = this.GetLogPath(); - - // 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) - { - WriteToConsole = writeToConsole, - ShowTraceInConsole = this.Settings.DeveloperMode, - ShowFullStampInConsole = this.Settings.DeveloperMode - }; - this.EventManager = new EventManager(this.Monitor, this.ModRegistry); - - // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); - this.Monitor.Log($"Mods go here: {modsPath}"); - if (modsPath != Constants.DefaultModsPath) - this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); - this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); - - // 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 GameLocationPatch() - ); - } - - /// Launch SMAPI. - [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - private void RunInteractively() - { - // initialise SMAPI - try - { - // hook up events - ContentEvents.Init(this.EventManager); - ControlEvents.Init(this.EventManager); - GameEvents.Init(this.EventManager); - GraphicsEvents.Init(this.EventManager); - InputEvents.Init(this.EventManager); - LocationEvents.Init(this.EventManager); - MenuEvents.Init(this.EventManager); - MineEvents.Init(this.EventManager); - MultiplayerEvents.Init(this.EventManager); - PlayerEvents.Init(this.EventManager); - SaveEvents.Init(this.EventManager); - SpecialisedEvents.Init(this.EventManager); - TimeEvents.Init(this.EventManager); - - // init JSON parser - JsonConverter[] converters = { - new ColorConverter(), - new PointConverter(), - new RectangleConverter() - }; - foreach (JsonConverter converter in converters) - this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); - - // add error handlers -#if SMAPI_FOR_WINDOWS - Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); -#endif - AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - - // add more leniant assembly resolvers - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); - - // override game - SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); - StardewValley.Program.gamePtr = this.GameInstance; - - // add exit handler - new Thread(() => - { - this.CancellationTokenSource.Token.WaitHandle.WaitOne(); - if (this.IsGameRunning) - { - try - { - File.WriteAllText(Constants.FatalCrashMarker, string.Empty); - File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); - } - - this.GameInstance.Exit(); - } - }).Start(); - - // hook into game events - ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); - - // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - - // check update marker - if (File.Exists(Constants.UpdateMarker)) - { - string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); - if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) - { - if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) - { - this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); - this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); - this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); - Console.ReadKey(); - } - } - File.Delete(Constants.UpdateMarker); - } - - // 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("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(); - File.Delete(Constants.FatalCrashLog); - File.Delete(Constants.FatalCrashMarker); - } - - // start game - this.Monitor.Log("Starting game...", LogLevel.Debug); - try - { - this.IsGameRunning = true; - StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window - this.GameInstance.Run(); - } - catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) - { - this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); - this.PressAnyKeyToExit(); - } - catch (Exception ex) - { - this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); - } - finally - { - this.Dispose(); - } - } - /// Method called when assembly resolution fails, which may return a manually resolved assembly. /// The event sender. /// The event arguments. @@ -422,880 +95,31 @@ namespace StardewModdingAPI } } - /// 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); - - // redirect direct console output - { - Monitor monitor = this.GetSecondaryMonitor("game"); - if (monitor.WriteToConsole) - this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); - } - - // add headers - if (this.Settings.DeveloperMode) - this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - if (!this.Settings.CheckForUpdates) - this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); - if (!this.Monitor.WriteToConsole) - this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - this.VerboseLog("Verbose logging enabled."); - - // validate XNB integrity - if (!this.ValidateContentIntegrity()) - this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); - - // load mod data - ModToolkit toolkit = new ModToolkit(); - ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); - - // load mods - { - this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); - ModResolver resolver = new ModResolver(); - - // 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(); - - // load mods - this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); - - // write metadata file - if (this.Settings.DumpMetadata) - { - ModFolderExport export = new ModFolderExport - { - Exported = DateTime.UtcNow.ToString("O"), - ApiVersion = Constants.ApiVersion.ToString(), - GameVersion = Constants.GameVersion.ToString(), - ModFolderPath = this.ModsPath, - Mods = mods - }; - this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export); - } - - // check for updates - this.CheckForUpdatesAsync(mods); - } - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); - return; - } - - // update window titles - int modsLoaded = this.ModRegistry.GetAll().Count(); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; - - // start SMAPI console - new Thread(this.RunConsoleLoop).Start(); - } - - /// Handle the game changing locale. - private void OnLocaleChanged() - { - // get locale - string locale = this.ContentCore.GetLocale(); - LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; - - // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) - (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); - } - - /// Run a loop handling console input. - [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] - private void RunConsoleLoop() - { - // 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); - - // start handling command line input - Thread inputThread = new Thread(() => - { - while (true) - { - // get input - string input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) - continue; - - // handle command - this.Monitor.LogUserInput(input); - this.GameInstance.CommandQueue.Enqueue(input); - } - }); - inputThread.Start(); - - // keep console thread alive while the game is running - while (this.IsGameRunning && !this.Monitor.IsExiting) - Thread.Sleep(1000 / 10); - if (inputThread.ThreadState == ThreadState.Running) - inputThread.Abort(); - } - - /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. - /// Returns whether all integrity checks passed. - private bool ValidateContentIntegrity() - { - this.Monitor.Log("Detecting common issues...", LogLevel.Trace); - bool issuesFound = false; - - // object format (commonly broken by outdated files) - { - // detect issues - bool hasObjectIssues = false; - void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace); - foreach (KeyValuePair entry in Game1.objectInformation) - { - // must not be empty - if (string.IsNullOrWhiteSpace(entry.Value)) - { - LogIssue(entry.Key, "entry is empty"); - hasObjectIssues = true; - continue; - } - - // require core fields - string[] fields = entry.Value.Split('/'); - if (fields.Length < SObject.objectInfoDescriptionIndex + 1) - { - LogIssue(entry.Key, "too few fields for an object"); - hasObjectIssues = true; - continue; - } - - // check min length for specific types - switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) - { - case "Cooking": - if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) - { - LogIssue(entry.Key, "too few fields for a cooking item"); - hasObjectIssues = true; - } - break; - } - } - - // log error - if (hasObjectIssues) - { - issuesFound = true; - this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); - } - } - - return !issuesFound; - } - - /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. - /// The mods to include in the update check (if eligible). - private void CheckForUpdatesAsync(IModMetadata[] mods) - { - if (!this.Settings.CheckForUpdates) - return; - - new Thread(() => - { - // create client - string url = this.Settings.WebApiBaseUrl; -#if !SMAPI_FOR_WINDOWS - url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif - WebApiClient client = new WebApiClient(url, Constants.ApiVersion); - this.Monitor.Log("Checking for updates...", LogLevel.Trace); - - // check SMAPI version - ISemanticVersion updateFound = null; - try - { - ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; - ISemanticVersion latestStable = response.Main?.Version; - ISemanticVersion latestBeta = response.Optional?.Version; - - if (latestStable == null && response.Errors.Any()) - { - this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); - } - else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) - { - updateFound = latestBeta; - this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) - { - updateFound = latestStable; - this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else - this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); - } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log(ex is WebException && ex.InnerException == null - ? $"Error: {ex.Message}" - : $"Error: {ex.GetLogSummary()}" - ); - } - - // show update message on next launch - if (updateFound != null) - File.WriteAllText(Constants.UpdateMarker, updateFound.ToString()); - - // check mod versions - if (mods.Any()) - { - try - { - HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - - // prepare search model - List searchMods = new List(); - foreach (IModMetadata mod in mods) - { - if (!mod.HasID()) - continue; - - string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; - searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); - } - - // fetch results - this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); - IDictionary results = client.GetModInfo(searchMods.ToArray()); - - // extract update alerts & errors - var updates = new List>(); - var errors = new StringBuilder(); - foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) - { - // link to update-check data - if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) - continue; - mod.SetUpdateData(result); - - // handle errors - if (result.Errors != null && result.Errors.Any()) - { - errors.AppendLine(result.Errors.Length == 1 - ? $" {mod.DisplayName}: {result.Errors[0]}" - : $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}" - ); - } - - // parse versions - 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; - - // show update alerts - if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) - updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); - 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)); - } - - // show update errors - if (errors.Length != 0) - this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace); - - // show update alerts - if (updates.Any()) - { - this.Monitor.Newline(); - this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updates) - { - IModMetadata mod = entry.Item1; - ISemanticVersion newVersion = entry.Item2; - string newUrl = entry.Item3; - this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); - } - } - else - this.Monitor.Log(" All mods up to date.", LogLevel.Trace); - } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); - this.Monitor.Log(ex is WebException && ex.InnerException == null - ? ex.Message - : ex.ToString() - ); - } - } - }).Start(); - } - - /// Get whether a given version should be offered to the user as an update. - /// The current semantic version. - /// The target semantic version. - /// Whether the user enabled the beta channel and should be offered pre-release updates. - private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) - { - return - newVersion != null - && newVersion.IsNewerThan(currentVersion) - && (useBetaChannel || !newVersion.IsPrerelease()); - } - - /// Create a directory path if it doesn't exist. - /// The directory path. - private void VerifyPath(string path) - { - try - { - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - /// Load and hook up the given mods. - /// The mods to load. - /// The JSON helper with which to read mods' JSON files. - /// The content manager to use for mod content. - /// Handles access to SMAPI's internal mod metadata list. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) - { - this.Monitor.Log("Loading mods...", LogLevel.Trace); - - HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - IDictionary skippedMods = new Dictionary(); - void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase }; - - // load content packs - foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) - { - 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()) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod as content pack - IManifest manifest = metadata.Manifest; - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); - metadata.SetMod(contentPack, monitor); - this.ModRegistry.Add(metadata); - } - IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); - - // load mods - { - // get content packs by mod ID - IDictionary contentPacksByModID = - loadedContentPacks - .GroupBy(p => p.Manifest.ContentPackFor.UniqueID, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(metadata => metadata.ContentPack).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // load mods from metadata - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) - { - InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) - { - // get basic info - IManifest manifest = metadata.Manifest; - this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid - : $" {metadata.DisplayName}...", LogLevel.Trace); - - // show warnings - if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod - string assemblyPath = metadata.Manifest?.EntryDll != null - ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) - : null; - Assembly modAssembly; - try - { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); - } - catch (IncompatibleInstructionException) // details already in trace logs - { - string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - - TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); - continue; - } - catch (SAssemblyLoadFailedException ex) - { - TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); - continue; - } - catch (Exception ex) - { - TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); - continue; - } - - // initialise mod - try - { - // get mod instance - if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) - continue; - - // get content packs - if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) - contentPacks = new IContentPack[0]; - - // init mod helpers - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IModHelper modHelper; - { - 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); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); - - IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) - { - IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); - } - - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); - } - - // init mod - mod.ModManifest = manifest; - mod.Helper = modHelper; - mod.Monitor = monitor; - - // track mod - metadata.SetMod(mod); - this.ModRegistry.Add(metadata); - } - catch (Exception ex) - { - TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); - } - } - } - } - IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); - - // log loaded mods - this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); - foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - - // log loaded content packs - if (loadedContentPacks.Any()) - { - string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; - - this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); - foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - } - - // log mod warnings - this.LogModWarnings(this.ModRegistry.GetAll().ToArray(), skippedMods); - - // initialise translations - this.ReloadTranslations(loadedMods); - - // initialise loaded non-content-pack mods - foreach (IModMetadata metadata in loadedMods) - { - // add interceptors - if (metadata.Mod.Helper.Content is ContentHelper helper) - { - // ReSharper disable SuspiciousTypeConversion.Global - if (metadata.Mod is IAssetEditor editor) - helper.ObservableAssetEditors.Add(editor); - if (metadata.Mod is IAssetLoader loader) - helper.ObservableAssetLoaders.Add(loader); - // ReSharper restore SuspiciousTypeConversion.Global - - this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; - } - - // call entry method - try - { - IMod mod = metadata.Mod; - mod.Entry(mod.Helper); - } - catch (Exception ex) - { - metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // get mod API - try - { - object api = metadata.Mod.GetApi(); - if (api != null && !api.GetType().IsPublic) - { - api = null; - this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); - } - - if (api != null) - this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); - metadata.SetApi(api); - } - catch (Exception ex) - { - this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); - } - } - - // invalidate cache entries when needed - // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) - foreach (IModMetadata metadata in loadedMods) - { - if (metadata.Mod.Helper.Content is ContentHelper helper) - { - helper.ObservableAssetEditors.CollectionChanged += (sender, e) => - { - if (e.NewItems?.Count > 0) - { - this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); - this.ContentCore.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); - } - }; - helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => - { - if (e.NewItems?.Count > 0) - { - this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); - this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); - } - }; - } - } - - // reset cache now if any editors or loaders were added during entry - IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); - IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); - if (editors.Any() || loaders.Any()) - { - this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); - this.ContentCore.InvalidateCacheFor(editors, loaders); - } - - // unlock mod integrations - this.ModRegistry.AreAllModsInitialised = true; - } - - /// Write a summary of mod warnings to the console and log. - /// The loaded mods. - /// The mods which were skipped, along with the friendly and developer reasons. - private void LogModWarnings(IModMetadata[] mods, IDictionary skippedMods) - { - // get mods with warnings - IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); - if (!modsWithWarnings.Any() && !skippedMods.Any()) - return; - - // log intro - { - int count = modsWithWarnings.Union(skippedMods.Keys).Count(); - this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info); - } - - // log skipped mods - if (skippedMods.Any()) - { - this.Monitor.Log(" Skipped mods", LogLevel.Error); - this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); - this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); - this.Monitor.Newline(); - - foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) - { - IModMetadata mod = pair.Key; - string[] reason = pair.Value; - - this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error); - if (reason[1] != null) - this.Monitor.Log($" ({reason[1]})", LogLevel.Trace); - } - this.Monitor.Newline(); - } - - // log warnings - if (modsWithWarnings.Any()) - { - // issue block format logic - void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb) - { - IModMetadata[] matches = modsWithWarnings.Where(p => p.Warnings.HasFlag(warning)).ToArray(); - if (!matches.Any()) - return; - - this.Monitor.Log(" " + heading, logLevel); - this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel); - foreach (string line in blurb) - this.Monitor.Log(" " + line, logLevel); - this.Monitor.Newline(); - foreach (IModMetadata match in matches) - this.Monitor.Log($" - {match.DisplayName}", logLevel); - this.Monitor.Newline(); - } - - // supported issues - LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", - "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", - "errors, or crashes in-game." - ); - LogWarningGroup(ModWarning.ChangesSaveSerialiser, LogLevel.Warn, "Changed save serialiser", - "These mods change the save serialiser. They may corrupt your save files, or make them unusable if", - "you uninstall these mods." - ); - 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." - ); - LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", - "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", - "corruption. If your game has issues, try removing these first." - ); - LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", - "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", - "mods. Consider notifying the mod authors about this problem." - ); - LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", - "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." - ); - } - } - - /// Load a mod's entry class. - /// The mod assembly. - /// A callback invoked when loading fails. - /// The loaded instance. - private bool TryLoadModEntry(Assembly modAssembly, Action onError, out Mod mod) + /// Initialise SMAPI and launch the game. + /// The command-line arguments. + /// This method is separate from because that can't contain any references to assemblies loaded by (e.g. via ), or Mono will incorrectly show an assembly resolution error before assembly resolution is set up. + private static void Start(string[] args) { - mod = null; - - // find type - TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); - if (modEntries.Length == 0) - { - onError($"its DLL has no '{nameof(Mod)}' subclass."); - return false; - } - if (modEntries.Length > 1) - { - onError($"its DLL contains multiple '{nameof(Mod)}' subclasses."); - return false; - } - - // get implementation - mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString()); - if (mod == null) - { - onError("its entry class couldn't be instantiated."); - return false; - } - - return true; - } + // get flags from arguments + bool writeToConsole = !args.Contains("--no-terminal"); - /// Reload translations for all mods. - /// The mods for which to reload translations. - private void ReloadTranslations(IEnumerable mods) - { - JsonHelper jsonHelper = this.Toolkit.JsonHelper; - foreach (IModMetadata metadata in mods) + // get mods path from arguments + string modsPath = null; { - if (metadata.IsContentPack) - throw new InvalidOperationException("Can't reload translations for a content pack."); - - // read translation files - IDictionary> translations = new Dictionary>(); - DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); - if (translationsDir.Exists) - { - foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) - { - string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); - try - { - if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary data)) - translations[locale] = data; - else - metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed."); - } - catch (Exception ex) - { - metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}"); - } - } - } - - // validate translations - foreach (string locale in translations.Keys.ToArray()) + int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1; + if (pathIndex >= 1 && args.Length >= pathIndex) { - // skip empty files - if (translations[locale] == null || !translations[locale].Keys.Any()) - { - metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn); - translations.Remove(locale); - continue; - } - - // handle duplicates - HashSet keys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet duplicateKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string key in translations[locale].Keys.ToArray()) - { - if (!keys.Add(key)) - { - duplicateKeys.Add(key); - translations[locale].Remove(key); - } - } - if (duplicateKeys.Any()) - metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); + modsPath = args[pathIndex]; + if (!string.IsNullOrWhiteSpace(modsPath) && !Path.IsPathRooted(modsPath)) + modsPath = Path.Combine(Constants.ExecutionPath, modsPath); } - - // update translation - TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; - translationHelper.SetTranslations(translations); - } - } - - /// The method called when the user submits a core SMAPI command in the console. - /// The command name. - /// The command arguments. - private void HandleCommand(string name, string[] arguments) - { - switch (name) - { - case "help": - if (arguments.Any()) - { - Command result = this.GameInstance.CommandManager.Get(arguments[0]); - 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); - } - 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(); - foreach (var group in groups) - { - string modName = group.Key; - string[] commandNames = group.ToArray(); - message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; - } - message += "For more information about a command, type 'help command_name'."; - - this.Monitor.Log(message, LogLevel.Info); - } - break; - - case "reload_i18n": - this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); - this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); - break; - - default: - throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); + if (string.IsNullOrWhiteSpace(modsPath)) + modsPath = Constants.DefaultModsPath; } - } - /// Redirect messages logged directly to the console to the given monitor. - /// The monitor with which to log messages. - /// The message to log. - private void HandleConsoleMessage(IMonitor monitor, string message) - { - // detect exception - LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; - - // ignore suppressed message - if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) - return; - - // forward to monitor - monitor.Log(message, level); - } - - /// Show a 'press any key to exit' message, and exit when they press a key. - private void PressAnyKeyToExit() - { - this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); - Program.PressAnyKeyToExit(showMessage: false); + // load SMAPI + using (SCore core = new SCore(modsPath, writeToConsole)) + core.RunInteractively(); } /// Show a 'press any key to exit' message, and exit when they press a key. @@ -1308,70 +132,5 @@ namespace StardewModdingAPI Console.ReadKey(); Environment.Exit(0); } - - /// Get a monitor instance derived from SMAPI's current settings. - /// 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) - { - WriteToConsole = this.Monitor.WriteToConsole, - ShowTraceInConsole = this.Settings.DeveloperMode, - ShowFullStampInConsole = this.Settings.DeveloperMode - }; - } - - /// 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() - { - // default path - { - FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}")); - if (!defaultFile.Exists) - return defaultFile.FullName; - } - - // get first disambiguated path - for (int i = 2; i < int.MaxValue; i++) - { - FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}")); - if (!file.Exists) - return file.FullName; - } - - // should never happen - throw new InvalidOperationException("Could not find an available log path."); - } - - /// Delete all log files created by SMAPI. - private void PurgeLogFiles() - { - DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); - if (!logsDir.Exists) - return; - - foreach (FileInfo logFile in logsDir.EnumerateFiles()) - { - if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) - { - try - { - FileUtilities.ForceDelete(logFile); - } - catch (IOException) - { - // ignore file if it's in use - } - } - } - } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 740af15f..2602da27 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,6 +110,7 @@ + -- cgit From 6ef7de33e8251f5d836a8ed2936939d9ce2fadb4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Aug 2018 23:01:46 -0400 Subject: tweak data API keys (#468) --- src/SMAPI/Framework/ModHelpers/DataHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index cdb3718f..506c9c54 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -136,7 +136,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private string GetSaveFileKey(string key) { this.AssertSlug(key, nameof(key)); - return $"smapi-mod-data/{this.ModID}/{key}".ToLower(); + return $"smapi/mod-data/{this.ModID}/{key}".ToLower(); } /// Get the absolute path for a global data file. @@ -144,7 +144,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private string GetGlobalDataPath(string key) { this.AssertSlug(key, nameof(key)); - return Path.Combine(Constants.SavesPath, ".smapi-mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); + 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. -- 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 73c389df74a8df796b3c1cacf2c035ac7e34a063 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Aug 2018 19:08:38 -0400 Subject: delete data API entries when they're set to null (#468) --- src/SMAPI/Framework/ModHelpers/DataHelper.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 506c9c54..e5100aed 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -99,7 +99,11 @@ namespace StardewModdingAPI.Framework.ModHelpers if (!Context.IsMainPlayer) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); - Game1.CustomData[this.GetSaveFileKey(key)] = this.JsonHelper.Serialise(data, Formatting.None); + string internalKey = this.GetSaveFileKey(key); + if (data != null) + Game1.CustomData[internalKey] = this.JsonHelper.Serialise(data, Formatting.None); + else + Game1.CustomData.Remove(internalKey); } /**** @@ -124,7 +128,10 @@ namespace StardewModdingAPI.Framework.ModHelpers public void WriteGlobalData(string key, TModel data) where TModel : class { string path = this.GetGlobalDataPath(key); - this.JsonHelper.WriteJsonFile(path, data); + if (data != null) + this.JsonHelper.WriteJsonFile(path, data); + else + File.Delete(path); } -- cgit From ff8ffbdef0306254094258be326d75fcdc2bcbe1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Aug 2018 00:48:34 -0400 Subject: add paranoid warnings mode (#590) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 ++++++++ .../ModLoading/InstructionHandleResult.cs | 8 +++++- src/SMAPI/Framework/ModLoading/ModWarning.cs | 8 +++++- src/SMAPI/Framework/Models/SConfig.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 11 ++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 19 ++++++++++++-- src/SMAPI/StardewModdingAPI.config.json | 30 ++++++++++++++-------- 8 files changed, 75 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Framework') 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 f4a0632e0ea7c9d0c0f4d4886a2143016ab62cdf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Aug 2018 17:43:17 -0400 Subject: enable paranoid warnings automatically in debug mode (#590) --- src/SMAPI/Framework/Models/SConfig.cs | 7 ++++++- src/SMAPI/StardewModdingAPI.config.json | 9 +++++---- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index bdfaa670..e2b33160 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -15,7 +15,12 @@ namespace StardewModdingAPI.Framework.Models 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; } + public bool ParanoidWarnings { get; set; } = +#if DEBUG + true; +#else + false; +#endif /// Whether to show beta versions as valid updates. public bool UseBetaChannel { get; set; } = Constants.ApiVersion.IsPrerelease(); diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index d81292c8..ad908fc0 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -33,13 +33,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * 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. + * investigation. When this is commented out, it'll be true for local debug builds and false + * otherwise. */ - "ParanoidWarnings": false, + //"ParanoidWarnings": true, /** - * 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. + * Whether SMAPI should show newer beta versions as an available update. When this is commented + * out, it'll be true if the current SMAPI version is beta, and false otherwise. */ //"UseBetaChannel": true, -- cgit From c94f3e7c63a2f1aec89c68417db348d4e684fb79 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Sep 2018 19:19:13 -0400 Subject: only use valid update keys in update-check logic (#592) --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 10 ++++++++-- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 23 ++++++++++++++++------ src/SMAPI/Framework/SCore.cs | 13 +++++++----- .../Framework/UpdateData/UpdateKey.cs | 8 ++++++++ 5 files changed, 42 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 f2cb952dd1b3752bd172161afadf956b195ec73f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Sep 2018 21:41:02 -0400 Subject: add support for parallel stable/beta unofficial versions (#594) --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 17 ++++++ src/SMAPI/Framework/SCore.cs | 5 +- .../Framework/Clients/WebApi/ModEntryModel.cs | 6 ++ .../Clients/WebApi/ModExtendedMetadataModel.cs | 21 +++++++ .../Clients/Wiki/WikiCompatibilityClient.cs | 66 +++++++++++++++------- .../Clients/Wiki/WikiCompatibilityEntry.cs | 32 +++++++++-- 7 files changed, 121 insertions(+), 27 deletions(-) (limited to 'src/SMAPI/Framework') 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 fa40aa020502f01998110fd74f6f5994492b8e0f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 17:50:22 -0400 Subject: update for Stardew Valley 1.3.30 --- src/SMAPI/Framework/SGame.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 3af4beb1..166f958c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -91,10 +91,6 @@ namespace StardewModdingAPI.Framework /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; - /// A reference to the private _farmerShadows field on . - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Deliberately named to match original game code as closely as possible.")] - private readonly List __farmerShadows; - /********* ** Accessors @@ -151,9 +147,6 @@ namespace StardewModdingAPI.Framework Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); - // init reflection - this.__farmerShadows = reflection.GetField>(this, "_farmerShadows").GetValue(); - // init observables Game1.locations = new ObservableCollection(); } @@ -991,13 +984,13 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); Game1.currentLocation.drawWater(Game1.spriteBatch); - this.__farmerShadows.Clear(); + this._farmerShadows.Clear(); if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) { foreach (Farmer farmerActor in Game1.currentLocation.currentEvent.farmerActors) { if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase)farmerActor.hidden)) - this.__farmerShadows.Add(farmerActor); + this._farmerShadows.Add(farmerActor); } } else @@ -1005,7 +998,7 @@ namespace StardewModdingAPI.Framework foreach (Farmer farmer in Game1.currentLocation.farmers) { if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase)farmer.hidden)) - this.__farmerShadows.Add(farmer); + this._farmerShadows.Add(farmer); } } if (!Game1.currentLocation.shouldHideCharacters()) @@ -1026,7 +1019,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (Farmer farmerShadow in this.__farmerShadows) + foreach (Farmer farmerShadow in this._farmerShadows) { if (!(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) { @@ -1070,7 +1063,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (Farmer farmerShadow in this.__farmerShadows) + foreach (Farmer farmerShadow in this._farmerShadows) { if (!(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) { -- 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 'src/SMAPI/Framework') 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 2cd0c8130c5f9688db493d8b0acd07505c86a2aa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 18:47:32 -0400 Subject: unify logic for loading content packs & mods to simplify common validation --- src/SMAPI/Framework/SCore.cs | 309 +++++++++++++++++++++++-------------------- 1 file changed, 162 insertions(+), 147 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 33edc698..28994060 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -693,150 +693,23 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log("Loading mods...", LogLevel.Trace); - HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - IDictionary skippedMods = new Dictionary(); - void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase }; - - // load content packs - foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) - { - 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.HasValidUpdateKeys()) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod as content pack - IManifest manifest = metadata.Manifest; - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); - metadata.SetMod(contentPack, monitor); - this.ModRegistry.Add(metadata); - } - IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); - // load mods + IDictionary> skippedMods = new Dictionary>(); + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) { - // get content packs by mod ID - IDictionary contentPacksByModID = - loadedContentPacks - .GroupBy(p => p.Manifest.ContentPackFor.UniqueID, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(metadata => metadata.ContentPack).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // load mods from metadata - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); + InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); + foreach (IModMetadata mod in mods) { - InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) + if (!this.TryLoadMod(mod, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) { - // get basic info - IManifest manifest = metadata.Manifest; - this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid - : $" {metadata.DisplayName}...", LogLevel.Trace); - - // show warnings - if (metadata.HasManifest() && !metadata.HasValidUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod - string assemblyPath = metadata.Manifest?.EntryDll != null - ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) - : null; - Assembly modAssembly; - try - { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); - } - catch (IncompatibleInstructionException) // details already in trace logs - { - string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - - TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); - continue; - } - catch (SAssemblyLoadFailedException ex) - { - TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); - continue; - } - catch (Exception ex) - { - TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); - continue; - } - - // initialise mod - try - { - // get mod instance - if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) - continue; - - // get content packs - if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) - contentPacks = new IContentPack[0]; - - // init mod helpers - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IModHelper modHelper; - { - 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); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); - - IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) - { - IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); - } - - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); - } - - // init mod - mod.ModManifest = manifest; - mod.Helper = modHelper; - mod.Monitor = monitor; - - // track mod - metadata.SetMod(mod); - this.ModRegistry.Add(metadata); - } - catch (Exception ex) - { - TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); - } + skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails); + if (mod.Status != ModMetadataStatus.Failed) + mod.SetStatus(ModMetadataStatus.Failed, errorPhrase); } } } + IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); // log loaded mods @@ -964,11 +837,150 @@ namespace StardewModdingAPI.Framework // unlock mod integrations this.ModRegistry.AreAllModsInitialised = true; } + + /// Load a given mod. + /// The mod to load. + /// 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. + /// The content manager to use for mod content. + /// Handles access to SMAPI's internal mod metadata list. + /// The mod IDs to ignore when validating update keys. + /// 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) + { + 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); + } + + // add warning for missing update key + if (mod.HasID() && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) && !mod.HasValidUpdateKeys()) + mod.SetWarning(ModWarning.NoUpdateKeys); + + // validate status + if (mod.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {mod.Error}", LogLevel.Trace); + errorReasonPhrase = mod.Error; + return false; + } + + // load as content pack + if (mod.IsContentPack) + { + IManifest manifest = mod.Manifest; + IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); + IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, jsonHelper); + mod.SetMod(contentPack, monitor); + this.ModRegistry.Add(mod); + + errorReasonPhrase = null; + return true; + } + + // load as mod + else + { + IManifest manifest = mod.Manifest; + + // load mod + string assemblyPath = manifest?.EntryDll != null + ? Path.Combine(mod.DirectoryPath, manifest.EntryDll) + : null; + Assembly modAssembly; + try + { + modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); + } + 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; + } + catch (SAssemblyLoadFailedException ex) + { + errorReasonPhrase = $"it DLL couldn't be loaded: {ex.Message}"; + return false; + } + catch (Exception ex) + { + errorReasonPhrase = "its DLL couldn't be loaded."; + errorDetails = $"Error: {ex.GetLogSummary()}"; + return false; + } + + // initialise mod + try + { + // get mod instance + if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase)) + return false; + + // get content packs + IContentPack[] contentPacks = this.ModRegistry + .GetAll(assemblyMods: false) + .Where(p => p.IsContentPack && mod.Manifest.UniqueID.Equals(p.Manifest.ContentPackFor.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .Select(p => p.ContentPack) + .ToArray(); + + // init mod helpers + IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + IModHelper modHelper; + { + IModEvents events = new ModEvents(mod, this.EventManager); + ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, mod.DisplayName, 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); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); + IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); + + IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); + } + + modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + } + + // init mod + modEntry.ModManifest = manifest; + modEntry.Helper = modHelper; + modEntry.Monitor = monitor; + + // track mod + mod.SetMod(modEntry); + this.ModRegistry.Add(mod); + return true; + } + catch (Exception ex) + { + errorReasonPhrase = $"initialisation failed:\n{ex.GetLogSummary()}"; + return false; + } + } + } /// Write a summary of mod warnings to the console and log. /// The loaded mods. /// The mods which were skipped, along with the friendly and developer reasons. - private void LogModWarnings(IModMetadata[] mods, IDictionary skippedMods) + private void LogModWarnings(IModMetadata[] mods, IDictionary> skippedMods) { // get mods with warnings IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); @@ -992,11 +1004,12 @@ namespace StardewModdingAPI.Framework foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; - string[] reason = pair.Value; + string errorReason = pair.Value.Item1; + string errorDetails = pair.Value.Item2; - this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error); - if (reason[1] != null) - this.Monitor.Log($" ({reason[1]})", LogLevel.Trace); + this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}", LogLevel.Error); + if (errorDetails != null) + this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); } this.Monitor.Newline(); } @@ -1061,9 +1074,10 @@ namespace StardewModdingAPI.Framework /// Load a mod's entry class. /// The mod assembly. - /// A callback invoked when loading fails. /// The loaded instance. - private bool TryLoadModEntry(Assembly modAssembly, Action onError, out Mod mod) + /// The error indicating why loading failed (if applicable). + /// Returns whether the mod entry class was successfully loaded. + private bool TryLoadModEntry(Assembly modAssembly, out Mod mod, out string error) { mod = null; @@ -1071,12 +1085,12 @@ namespace StardewModdingAPI.Framework TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); if (modEntries.Length == 0) { - onError($"its DLL has no '{nameof(Mod)}' subclass."); + error = $"its DLL has no '{nameof(Mod)}' subclass."; return false; } if (modEntries.Length > 1) { - onError($"its DLL contains multiple '{nameof(Mod)}' subclasses."); + error = $"its DLL contains multiple '{nameof(Mod)}' subclasses."; return false; } @@ -1084,10 +1098,11 @@ namespace StardewModdingAPI.Framework mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString()); if (mod == null) { - onError("its entry class couldn't be instantiated."); + error = "its entry class couldn't be instantiated."; return false; } + error = null; return true; } -- 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 09f83a28f5e02f516dbc3ec84ce3251c1ee4d790 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 21:10:19 -0400 Subject: fix error if mod doesn't have a loaded manifest --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d05f5bb9..6441e889 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -859,7 +859,7 @@ namespace StardewModdingAPI.Framework 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) + 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); -- cgit From 7f47271be42a8017d48b8378600f2f17ccd38cae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Sep 2018 21:51:50 -0400 Subject: fix content packs not loaded before the mods that read them --- src/SMAPI/Framework/SCore.cs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6441e889..24a15fc6 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -697,16 +697,28 @@ namespace StardewModdingAPI.Framework IDictionary> skippedMods = new Dictionary>(); using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) { + // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - foreach (IModMetadata mod in mods) + void LogSkip(IModMetadata mod, string errorPhrase, 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) - mod.SetStatus(ModMetadataStatus.Failed, errorPhrase); - } + skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails); + if (mod.Status != ModMetadataStatus.Failed) + mod.SetStatus(ModMetadataStatus.Failed, errorPhrase); + } + + // load content packs first (so they're available to mods) + foreach (IModMetadata contentPack in mods.Where(p => p.IsContentPack)) + { + if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + LogSkip(contentPack, errorPhrase, errorDetails); + } + + // load SMAPI mods + foreach (IModMetadata contentPack in mods.Where(p => !p.IsContentPack)) + { + if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + LogSkip(contentPack, errorPhrase, errorDetails); } } IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); -- 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 43e935b01761b657ed361d3ae67c7ca001cde623 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Sep 2018 01:00:47 -0400 Subject: simplify alpha blending --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index cd372948..a8ec79a8 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -84,13 +84,13 @@ namespace StardewModdingAPI.Framework.Content // 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; + // premultiplied by the content pipeline. The formula is derived from + // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + float alphaBelow = 1 - (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)), + r: (int)(above.R + (below.R * alphaBelow)), + g: (int)(above.G + (below.G * alphaBelow)), + b: (int)(above.B + (below.B * alphaBelow)), a: Math.Max(above.A, below.A) ); } -- cgit From b73234b56d816ba68c0f92a9d356ea85941dca94 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 27 Sep 2018 01:23:17 -0400 Subject: log base update loop errors as 'game' instead of SMAPI --- src/SMAPI/Framework/SCore.cs | 13 +++++++------ src/SMAPI/Framework/SGame.cs | 13 +++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 24a15fc6..5f30c9ef 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -52,6 +52,9 @@ namespace StardewModdingAPI.Framework /// The core logger and monitor for SMAPI. private readonly Monitor Monitor; + /// The core logger and monitor on behalf of the game. + private readonly Monitor MonitorForGame; + /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); @@ -128,6 +131,7 @@ namespace StardewModdingAPI.Framework ShowTraceInConsole = this.Settings.DeveloperMode, ShowFullStampInConsole = this.Settings.DeveloperMode }; + this.MonitorForGame = this.GetSecondaryMonitor("game"); this.EventManager = new EventManager(this.Monitor, this.ModRegistry); // init logging @@ -200,7 +204,7 @@ namespace StardewModdingAPI.Framework // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -338,11 +342,8 @@ namespace StardewModdingAPI.Framework this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); // redirect direct console output - { - Monitor monitor = this.GetSecondaryMonitor("game"); - if (monitor.WriteToConsole) - this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); - } + if (this.MonitorForGame.WriteToConsole) + this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); // add headers if (this.Settings.DeveloperMode) diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 166f958c..1bd583bf 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -39,9 +39,12 @@ namespace StardewModdingAPI.Framework /**** ** SMAPI state ****/ - /// Encapsulates monitoring and logging. + /// Encapsulates monitoring and logging for SMAPI. private readonly IMonitor Monitor; + /// Encapsulates monitoring and logging on the game's behalf. + private readonly IMonitor MonitorForGame; + /// Manages SMAPI events for mods. private readonly EventManager Events; @@ -122,12 +125,13 @@ namespace StardewModdingAPI.Framework ** Protected methods *********/ /// Construct an instance. - /// Encapsulates monitoring and logging. + /// Encapsulates monitoring and logging for SMAPI. + /// Encapsulates monitoring and logging on the game's behalf. /// Simplifies access to private game code. /// Manages SMAPI events for mods. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) { SGame.ConstructorHack = null; @@ -140,6 +144,7 @@ namespace StardewModdingAPI.Framework // init SMAPI this.Monitor = monitor; + this.MonitorForGame = monitorForGame; this.Events = eventManager; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; @@ -697,7 +702,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } this.Events.GameLoop_Updated.Raise(new GameLoopUpdatedEventArgs(this.TicksElapsed)); -- 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 0530824cc2c573cae0a80f7def2b46bf8aeb9af9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 1 Oct 2018 19:33:46 -0400 Subject: fix error in second-pass dependency checks if a mod is invalid --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 4c33f348..2c5cd2bd 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -916,7 +916,7 @@ namespace StardewModdingAPI.Framework if (this.ModRegistry.Get(dependency.UniqueID) == null) { string dependencyName = mods - .FirstOrDefault(p => dependency.UniqueID.Equals(p.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(otherMod => otherMod.HasID() && dependency.UniqueID.Equals(otherMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) ?.DisplayName ?? dependency.UniqueID; errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; return false; -- cgit From 63fb4dbe8ae4d611c4854f863b9b29265e02fdee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 5 Oct 2018 21:59:57 -0400 Subject: tweak new event naming convention (#310) --- src/SMAPI/Events/BuildingListChangedEventArgs.cs | 39 +++ src/SMAPI/Events/ButtonPressedEventArgs.cs | 60 +++++ src/SMAPI/Events/ButtonReleasedEventArgs.cs | 60 +++++ src/SMAPI/Events/ContentEvents.cs | 4 +- src/SMAPI/Events/ControlEvents.cs | 32 +-- src/SMAPI/Events/CursorMovedEventArgs.cs | 30 +++ src/SMAPI/Events/DebrisListChangedEventArgs.cs | 38 +++ src/SMAPI/Events/GameEvents.cs | 32 +-- src/SMAPI/Events/GameLaunchedEventArgs.cs | 7 + src/SMAPI/Events/GameLoopLaunchedEventArgs.cs | 7 - src/SMAPI/Events/GameLoopUpdatedEventArgs.cs | 36 --- src/SMAPI/Events/GameLoopUpdatingEventArgs.cs | 36 --- src/SMAPI/Events/GraphicsEvents.cs | 28 +-- src/SMAPI/Events/IGameLoopEvents.cs | 10 +- src/SMAPI/Events/IInputEvents.cs | 8 +- src/SMAPI/Events/IWorldEvents.cs | 14 +- src/SMAPI/Events/InputButtonPressedEventArgs.cs | 60 ----- src/SMAPI/Events/InputButtonReleasedEventArgs.cs | 60 ----- src/SMAPI/Events/InputCursorMovedEventArgs.cs | 30 --- src/SMAPI/Events/InputEvents.cs | 8 +- .../Events/InputMouseWheelScrolledEventArgs.cs | 38 --- .../LargeTerrainFeatureListChangedEventArgs.cs | 39 +++ src/SMAPI/Events/LocationEvents.cs | 12 +- src/SMAPI/Events/LocationListChangedEventArgs.cs | 33 +++ src/SMAPI/Events/MenuEvents.cs | 8 +- src/SMAPI/Events/MineEvents.cs | 4 +- src/SMAPI/Events/MouseWheelScrolledEventArgs.cs | 38 +++ src/SMAPI/Events/MultiplayerEvents.cs | 16 +- src/SMAPI/Events/NpcListChangedEventArgs.cs | 38 +++ src/SMAPI/Events/ObjectListChangedEventArgs.cs | 40 +++ src/SMAPI/Events/PlayerEvents.cs | 12 +- src/SMAPI/Events/SaveEvents.cs | 24 +- src/SMAPI/Events/SpecialisedEvents.cs | 4 +- .../Events/TerrainFeatureListChangedEventArgs.cs | 40 +++ src/SMAPI/Events/TimeEvents.cs | 8 +- src/SMAPI/Events/UpdateTickedEventArgs.cs | 36 +++ src/SMAPI/Events/UpdateTickingEventArgs.cs | 36 +++ .../Events/WorldBuildingListChangedEventArgs.cs | 39 --- .../Events/WorldDebrisListChangedEventArgs.cs | 38 --- ...WorldLargeTerrainFeatureListChangedEventArgs.cs | 39 --- .../Events/WorldLocationListChangedEventArgs.cs | 33 --- src/SMAPI/Events/WorldNpcListChangedEventArgs.cs | 38 --- .../Events/WorldObjectListChangedEventArgs.cs | 40 --- .../WorldTerrainFeatureListChangedEventArgs.cs | 40 --- src/SMAPI/Framework/Events/EventManager.cs | 276 ++++++++++----------- src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 18 +- src/SMAPI/Framework/Events/ModInputEvents.cs | 24 +- src/SMAPI/Framework/Events/ModWorldEvents.cs | 42 ++-- src/SMAPI/Framework/SGame.cs | 136 +++++----- src/SMAPI/Framework/SMultiplayer.cs | 8 +- src/SMAPI/StardewModdingAPI.csproj | 28 +-- 51 files changed, 912 insertions(+), 912 deletions(-) create mode 100644 src/SMAPI/Events/BuildingListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/ButtonPressedEventArgs.cs create mode 100644 src/SMAPI/Events/ButtonReleasedEventArgs.cs create mode 100644 src/SMAPI/Events/CursorMovedEventArgs.cs create mode 100644 src/SMAPI/Events/DebrisListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLaunchedEventArgs.cs delete mode 100644 src/SMAPI/Events/GameLoopLaunchedEventArgs.cs delete mode 100644 src/SMAPI/Events/GameLoopUpdatedEventArgs.cs delete mode 100644 src/SMAPI/Events/GameLoopUpdatingEventArgs.cs delete mode 100644 src/SMAPI/Events/InputButtonPressedEventArgs.cs delete mode 100644 src/SMAPI/Events/InputButtonReleasedEventArgs.cs delete mode 100644 src/SMAPI/Events/InputCursorMovedEventArgs.cs delete mode 100644 src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs create mode 100644 src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/LocationListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/MouseWheelScrolledEventArgs.cs create mode 100644 src/SMAPI/Events/NpcListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/ObjectListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs create mode 100644 src/SMAPI/Events/UpdateTickedEventArgs.cs create mode 100644 src/SMAPI/Events/UpdateTickingEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldLocationListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldNpcListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldObjectListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/BuildingListChangedEventArgs.cs b/src/SMAPI/Events/BuildingListChangedEventArgs.cs new file mode 100644 index 00000000..9bc691fc --- /dev/null +++ b/src/SMAPI/Events/BuildingListChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class BuildingListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public BuildingListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/ButtonPressedEventArgs.cs b/src/SMAPI/Events/ButtonPressedEventArgs.cs new file mode 100644 index 00000000..9e6c187f --- /dev/null +++ b/src/SMAPI/Events/ButtonPressedEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed. + public class ButtonPressedEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The game's current input state. + private readonly SInputState InputState; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The game's current input state. + internal ButtonPressedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) + { + this.Button = button; + this.Cursor = cursor; + this.InputState = inputState; + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + public bool IsSuppressed() + { + return this.IsSuppressed(this.Button); + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + } +} diff --git a/src/SMAPI/Events/ButtonReleasedEventArgs.cs b/src/SMAPI/Events/ButtonReleasedEventArgs.cs new file mode 100644 index 00000000..2a289bc7 --- /dev/null +++ b/src/SMAPI/Events/ButtonReleasedEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is released. + public class ButtonReleasedEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The game's current input state. + private readonly SInputState InputState; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The game's current input state. + internal ButtonReleasedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) + { + this.Button = button; + this.Cursor = cursor; + this.InputState = inputState; + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + public bool IsSuppressed() + { + return this.IsSuppressed(this.Button); + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + } +} diff --git a/src/SMAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs index 63645258..3ee0560b 100644 --- a/src/SMAPI/Events/ContentEvents.cs +++ b/src/SMAPI/Events/ContentEvents.cs @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Events /// Raised after the content language changes. public static event EventHandler> AfterLocaleChanged { - add => ContentEvents.EventManager.Content_LocaleChanged.Add(value); - remove => ContentEvents.EventManager.Content_LocaleChanged.Remove(value); + add => ContentEvents.EventManager.Legacy_LocaleChanged.Add(value); + remove => ContentEvents.EventManager.Legacy_LocaleChanged.Remove(value); } diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index a3994d1d..56a4fa3f 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -20,57 +20,57 @@ namespace StardewModdingAPI.Events /// Raised when the changes. That happens when the player presses or releases a key. public static event EventHandler KeyboardChanged { - add => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Remove(value); + add => ControlEvents.EventManager.Legacy_KeyboardChanged.Add(value); + remove => ControlEvents.EventManager.Legacy_KeyboardChanged.Remove(value); } /// Raised after the player presses a keyboard key. public static event EventHandler KeyPressed { - add => ControlEvents.EventManager.Legacy_Control_KeyPressed.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_KeyPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_KeyPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_KeyPressed.Remove(value); } /// Raised after the player releases a keyboard key. public static event EventHandler KeyReleased { - add => ControlEvents.EventManager.Legacy_Control_KeyReleased.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_KeyReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_KeyReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_KeyReleased.Remove(value); } /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. public static event EventHandler MouseChanged { - add => ControlEvents.EventManager.Legacy_Control_MouseChanged.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_MouseChanged.Remove(value); + add => ControlEvents.EventManager.Legacy_MouseChanged.Add(value); + remove => ControlEvents.EventManager.Legacy_MouseChanged.Remove(value); } /// The player pressed a controller button. This event isn't raised for trigger buttons. public static event EventHandler ControllerButtonPressed { - add => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_ControllerButtonPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_ControllerButtonPressed.Remove(value); } /// The player released a controller button. This event isn't raised for trigger buttons. public static event EventHandler ControllerButtonReleased { - add => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_ControllerButtonReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_ControllerButtonReleased.Remove(value); } /// The player pressed a controller trigger button. public static event EventHandler ControllerTriggerPressed { - add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_ControllerTriggerPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_ControllerTriggerPressed.Remove(value); } /// The player released a controller trigger button. public static event EventHandler ControllerTriggerReleased { - add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Add(value); - remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_ControllerTriggerReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_ControllerTriggerReleased.Remove(value); } diff --git a/src/SMAPI/Events/CursorMovedEventArgs.cs b/src/SMAPI/Events/CursorMovedEventArgs.cs new file mode 100644 index 00000000..453743b9 --- /dev/null +++ b/src/SMAPI/Events/CursorMovedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the in-game cursor is moved. + public class CursorMovedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous cursor position. + public ICursorPosition OldPosition { get; } + + /// The current cursor position. + public ICursorPosition NewPosition { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous cursor position. + /// The new cursor position. + public CursorMovedEventArgs(ICursorPosition oldPosition, ICursorPosition newPosition) + { + this.OldPosition = oldPosition; + this.NewPosition = newPosition; + } + } +} diff --git a/src/SMAPI/Events/DebrisListChangedEventArgs.cs b/src/SMAPI/Events/DebrisListChangedEventArgs.cs new file mode 100644 index 00000000..1337bd3b --- /dev/null +++ b/src/SMAPI/Events/DebrisListChangedEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class DebrisListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The debris added to the location. + public IEnumerable Added { get; } + + /// The debris removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The debris added to the location. + /// The debris removed from the location. + public DebrisListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs index 92879280..952b3570 100644 --- a/src/SMAPI/Events/GameEvents.cs +++ b/src/SMAPI/Events/GameEvents.cs @@ -19,57 +19,57 @@ namespace StardewModdingAPI.Events /// Raised when the game updates its state (≈60 times per second). public static event EventHandler UpdateTick { - add => GameEvents.EventManager.Game_UpdateTick.Add(value); - remove => GameEvents.EventManager.Game_UpdateTick.Remove(value); + add => GameEvents.EventManager.Legacy_UpdateTick.Add(value); + remove => GameEvents.EventManager.Legacy_UpdateTick.Remove(value); } /// Raised every other tick (≈30 times per second). public static event EventHandler SecondUpdateTick { - add => GameEvents.EventManager.Game_SecondUpdateTick.Add(value); - remove => GameEvents.EventManager.Game_SecondUpdateTick.Remove(value); + add => GameEvents.EventManager.Legacy_SecondUpdateTick.Add(value); + remove => GameEvents.EventManager.Legacy_SecondUpdateTick.Remove(value); } /// Raised every fourth tick (≈15 times per second). public static event EventHandler FourthUpdateTick { - add => GameEvents.EventManager.Game_FourthUpdateTick.Add(value); - remove => GameEvents.EventManager.Game_FourthUpdateTick.Remove(value); + add => GameEvents.EventManager.Legacy_FourthUpdateTick.Add(value); + remove => GameEvents.EventManager.Legacy_FourthUpdateTick.Remove(value); } /// Raised every eighth tick (≈8 times per second). public static event EventHandler EighthUpdateTick { - add => GameEvents.EventManager.Game_EighthUpdateTick.Add(value); - remove => GameEvents.EventManager.Game_EighthUpdateTick.Remove(value); + add => GameEvents.EventManager.Legacy_EighthUpdateTick.Add(value); + remove => GameEvents.EventManager.Legacy_EighthUpdateTick.Remove(value); } /// Raised every 15th tick (≈4 times per second). public static event EventHandler QuarterSecondTick { - add => GameEvents.EventManager.Game_QuarterSecondTick.Add(value); - remove => GameEvents.EventManager.Game_QuarterSecondTick.Remove(value); + add => GameEvents.EventManager.Legacy_QuarterSecondTick.Add(value); + remove => GameEvents.EventManager.Legacy_QuarterSecondTick.Remove(value); } /// Raised every 30th tick (≈twice per second). public static event EventHandler HalfSecondTick { - add => GameEvents.EventManager.Game_HalfSecondTick.Add(value); - remove => GameEvents.EventManager.Game_HalfSecondTick.Remove(value); + add => GameEvents.EventManager.Legacy_HalfSecondTick.Add(value); + remove => GameEvents.EventManager.Legacy_HalfSecondTick.Remove(value); } /// Raised every 60th tick (≈once per second). public static event EventHandler OneSecondTick { - add => GameEvents.EventManager.Game_OneSecondTick.Add(value); - remove => GameEvents.EventManager.Game_OneSecondTick.Remove(value); + add => GameEvents.EventManager.Legacy_OneSecondTick.Add(value); + remove => GameEvents.EventManager.Legacy_OneSecondTick.Remove(value); } /// Raised once after the game initialises and all methods have been called. public static event EventHandler FirstUpdateTick { - add => GameEvents.EventManager.Game_FirstUpdateTick.Add(value); - remove => GameEvents.EventManager.Game_FirstUpdateTick.Remove(value); + add => GameEvents.EventManager.Legacy_FirstUpdateTick.Add(value); + remove => GameEvents.EventManager.Legacy_FirstUpdateTick.Remove(value); } diff --git a/src/SMAPI/Events/GameLaunchedEventArgs.cs b/src/SMAPI/Events/GameLaunchedEventArgs.cs new file mode 100644 index 00000000..a4c78754 --- /dev/null +++ b/src/SMAPI/Events/GameLaunchedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLaunchedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs deleted file mode 100644 index 6a42e4f9..00000000 --- a/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class GameLoopLaunchedEventArgs : EventArgs { } -} diff --git a/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs deleted file mode 100644 index 3ad34b69..00000000 --- a/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class GameLoopUpdatedEventArgs : 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 GameLoopUpdatedEventArgs(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/GameLoopUpdatingEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs deleted file mode 100644 index d6a8b5c2..00000000 --- a/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class GameLoopUpdatingEventArgs : 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 GameLoopUpdatingEventArgs(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/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs index e1ff4ee7..53f04822 100644 --- a/src/SMAPI/Events/GraphicsEvents.cs +++ b/src/SMAPI/Events/GraphicsEvents.cs @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Events /// Raised after the game window is resized. public static event EventHandler Resize { - add => GraphicsEvents.EventManager.Graphics_Resize.Add(value); - remove => GraphicsEvents.EventManager.Graphics_Resize.Remove(value); + add => GraphicsEvents.EventManager.Legacy_Resize.Add(value); + remove => GraphicsEvents.EventManager.Legacy_Resize.Remove(value); } /**** @@ -29,15 +29,15 @@ namespace StardewModdingAPI.Events /// Raised before drawing the world to the screen. public static event EventHandler OnPreRenderEvent { - add => GraphicsEvents.EventManager.Graphics_OnPreRenderEvent.Add(value); - remove => GraphicsEvents.EventManager.Graphics_OnPreRenderEvent.Remove(value); + add => GraphicsEvents.EventManager.Legacy_OnPreRenderEvent.Add(value); + remove => GraphicsEvents.EventManager.Legacy_OnPreRenderEvent.Remove(value); } /// Raised after drawing the world to the screen. public static event EventHandler OnPostRenderEvent { - add => GraphicsEvents.EventManager.Graphics_OnPostRenderEvent.Add(value); - remove => GraphicsEvents.EventManager.Graphics_OnPostRenderEvent.Remove(value); + add => GraphicsEvents.EventManager.Legacy_OnPostRenderEvent.Add(value); + remove => GraphicsEvents.EventManager.Legacy_OnPostRenderEvent.Remove(value); } /**** @@ -46,15 +46,15 @@ namespace StardewModdingAPI.Events /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) public static event EventHandler OnPreRenderHudEvent { - add => GraphicsEvents.EventManager.Graphics_OnPreRenderHudEvent.Add(value); - remove => GraphicsEvents.EventManager.Graphics_OnPreRenderHudEvent.Remove(value); + add => GraphicsEvents.EventManager.Legacy_OnPreRenderHudEvent.Add(value); + remove => GraphicsEvents.EventManager.Legacy_OnPreRenderHudEvent.Remove(value); } /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) public static event EventHandler OnPostRenderHudEvent { - add => GraphicsEvents.EventManager.Graphics_OnPostRenderHudEvent.Add(value); - remove => GraphicsEvents.EventManager.Graphics_OnPostRenderHudEvent.Remove(value); + add => GraphicsEvents.EventManager.Legacy_OnPostRenderHudEvent.Add(value); + remove => GraphicsEvents.EventManager.Legacy_OnPostRenderHudEvent.Remove(value); } /**** @@ -63,15 +63,15 @@ namespace StardewModdingAPI.Events /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. public static event EventHandler OnPreRenderGuiEvent { - add => GraphicsEvents.EventManager.Graphics_OnPreRenderGuiEvent.Add(value); - remove => GraphicsEvents.EventManager.Graphics_OnPreRenderGuiEvent.Remove(value); + add => GraphicsEvents.EventManager.Legacy_OnPreRenderGuiEvent.Add(value); + remove => GraphicsEvents.EventManager.Legacy_OnPreRenderGuiEvent.Remove(value); } /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. public static event EventHandler OnPostRenderGuiEvent { - add => GraphicsEvents.EventManager.Graphics_OnPostRenderGuiEvent.Add(value); - remove => GraphicsEvents.EventManager.Graphics_OnPostRenderGuiEvent.Remove(value); + add => GraphicsEvents.EventManager.Legacy_OnPostRenderGuiEvent.Add(value); + remove => GraphicsEvents.EventManager.Legacy_OnPostRenderGuiEvent.Remove(value); } diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index a56b3de3..8ab86c9e 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -6,12 +6,12 @@ namespace StardewModdingAPI.Events public interface IGameLoopEvents { /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations. - event EventHandler Launched; + event EventHandler GameLaunched; - /// Raised before the game performs its overall update tick (≈60 times per second). - event EventHandler Updating; + /// Raised before the game state is updated (≈60 times per second). + event EventHandler UpdateTicking; - /// Raised after the game performs its overall update tick (≈60 times per second). - event EventHandler Updated; + /// Raised after the game state is updated (≈60 times per second). + event EventHandler UpdateTicked; } } diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 8e2ef406..5c40a438 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -6,15 +6,15 @@ namespace StardewModdingAPI.Events public interface IInputEvents { /// Raised after the player presses a button on the keyboard, controller, or mouse. - event EventHandler ButtonPressed; + event EventHandler ButtonPressed; /// Raised after the player releases a button on the keyboard, controller, or mouse. - event EventHandler ButtonReleased; + event EventHandler ButtonReleased; /// Raised after the player moves the in-game cursor. - event EventHandler CursorMoved; + event EventHandler CursorMoved; /// Raised after the player scrolls the mouse wheel. - event EventHandler MouseWheelScrolled; + event EventHandler MouseWheelScrolled; } } diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index d4efb53b..0ceffcc1 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -6,24 +6,24 @@ namespace StardewModdingAPI.Events public interface IWorldEvents { /// Raised after a game location is added or removed. - event EventHandler LocationListChanged; + event EventHandler LocationListChanged; /// Raised after buildings are added or removed in a location. - event EventHandler BuildingListChanged; + event EventHandler BuildingListChanged; /// Raised after debris are added or removed in a location. - event EventHandler DebrisListChanged; + event EventHandler DebrisListChanged; /// Raised after large terrain features (like bushes) are added or removed in a location. - event EventHandler LargeTerrainFeatureListChanged; + event EventHandler LargeTerrainFeatureListChanged; /// Raised after NPCs are added or removed in a location. - event EventHandler NpcListChanged; + event EventHandler NpcListChanged; /// Raised after objects are added or removed in a location. - event EventHandler ObjectListChanged; + event EventHandler ObjectListChanged; /// Raised after terrain features (like floors and trees) are added or removed in a location. - event EventHandler TerrainFeatureListChanged; + event EventHandler TerrainFeatureListChanged; } } diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs deleted file mode 100644 index 8c6844dd..00000000 --- a/src/SMAPI/Events/InputButtonPressedEventArgs.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using StardewModdingAPI.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments when a button is pressed. - public class InputButtonPressedEventArgs : EventArgs - { - /********* - ** Properties - *********/ - /// The game's current input state. - private readonly SInputState InputState; - - - /********* - ** Accessors - *********/ - /// The button on the controller, keyboard, or mouse. - public SButton Button { get; } - - /// The current cursor position. - public ICursorPosition Cursor { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// The game's current input state. - internal InputButtonPressedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) - { - this.Button = button; - this.Cursor = cursor; - this.InputState = inputState; - } - - /// Whether a mod has indicated the key was already handled, so the game should handle it. - public bool IsSuppressed() - { - return this.IsSuppressed(this.Button); - } - - /// Whether a mod has indicated the key was already handled, so the game should handle it. - /// The button to check. - public bool IsSuppressed(SButton button) - { - return this.InputState.SuppressButtons.Contains(button); - } - - /// Get whether a given button was pressed or held. - /// The button to check. - public bool IsDown(SButton button) - { - return this.InputState.IsDown(button); - } - } -} diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs deleted file mode 100644 index 4b0bc326..00000000 --- a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using StardewModdingAPI.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments when a button is released. - public class InputButtonReleasedEventArgs : EventArgs - { - /********* - ** Properties - *********/ - /// The game's current input state. - private readonly SInputState InputState; - - - /********* - ** Accessors - *********/ - /// The button on the controller, keyboard, or mouse. - public SButton Button { get; } - - /// The current cursor position. - public ICursorPosition Cursor { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// The game's current input state. - internal InputButtonReleasedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) - { - this.Button = button; - this.Cursor = cursor; - this.InputState = inputState; - } - - /// Whether a mod has indicated the key was already handled, so the game should handle it. - public bool IsSuppressed() - { - return this.IsSuppressed(this.Button); - } - - /// Whether a mod has indicated the key was already handled, so the game should handle it. - /// The button to check. - public bool IsSuppressed(SButton button) - { - return this.InputState.SuppressButtons.Contains(button); - } - - /// Get whether a given button was pressed or held. - /// The button to check. - public bool IsDown(SButton button) - { - return this.InputState.IsDown(button); - } - } -} diff --git a/src/SMAPI/Events/InputCursorMovedEventArgs.cs b/src/SMAPI/Events/InputCursorMovedEventArgs.cs deleted file mode 100644 index 53aac5b3..00000000 --- a/src/SMAPI/Events/InputCursorMovedEventArgs.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments when the in-game cursor is moved. - public class InputCursorMovedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous cursor position. - public ICursorPosition OldPosition { get; } - - /// The current cursor position. - public ICursorPosition NewPosition { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous cursor position. - /// The new cursor position. - public InputCursorMovedEventArgs(ICursorPosition oldPosition, ICursorPosition newPosition) - { - this.OldPosition = oldPosition; - this.NewPosition = newPosition; - } - } -} diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index e62d6ee6..4c1781a5 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -19,15 +19,15 @@ namespace StardewModdingAPI.Events /// Raised when the player presses a button on the keyboard, controller, or mouse. public static event EventHandler ButtonPressed { - add => InputEvents.EventManager.Legacy_Input_ButtonPressed.Add(value); - remove => InputEvents.EventManager.Legacy_Input_ButtonPressed.Remove(value); + add => InputEvents.EventManager.Legacy_ButtonPressed.Add(value); + remove => InputEvents.EventManager.Legacy_ButtonPressed.Remove(value); } /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. public static event EventHandler ButtonReleased { - add => InputEvents.EventManager.Legacy_Input_ButtonReleased.Add(value); - remove => InputEvents.EventManager.Legacy_Input_ButtonReleased.Remove(value); + add => InputEvents.EventManager.Legacy_ButtonReleased.Add(value); + remove => InputEvents.EventManager.Legacy_ButtonReleased.Remove(value); } diff --git a/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs deleted file mode 100644 index 9afab9cc..00000000 --- a/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments when the player scrolls the mouse wheel. - public class InputMouseWheelScrolledEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The cursor position. - public ICursorPosition Position { get; } - - /// The old scroll value. - public int OldValue { get; } - - /// The new scroll value. - public int NewValue { get; } - - /// The amount by which the scroll value changed. - public int Delta => this.NewValue - this.OldValue; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The cursor position. - /// The old scroll value. - /// The new scroll value. - public InputMouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue) - { - this.Position = position; - this.OldValue = oldValue; - this.NewValue = newValue; - } - } -} diff --git a/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..63b12687 --- /dev/null +++ b/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LargeTerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The large terrain features added to the location. + public IEnumerable Added { get; } + + /// The large terrain features removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The large terrain features added to the location. + /// The large terrain features removed from the location. + public LargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index e2108de0..81f547ae 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -19,22 +19,22 @@ namespace StardewModdingAPI.Events /// Raised after a game location is added or removed. public static event EventHandler LocationsChanged { - add => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Add(value); - remove => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Remove(value); + add => LocationEvents.EventManager.Legacy_LocationsChanged.Add(value); + remove => LocationEvents.EventManager.Legacy_LocationsChanged.Remove(value); } /// Raised after buildings are added or removed in a location. public static event EventHandler BuildingsChanged { - add => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Add(value); - remove => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Remove(value); + add => LocationEvents.EventManager.Legacy_BuildingsChanged.Add(value); + remove => LocationEvents.EventManager.Legacy_BuildingsChanged.Remove(value); } /// Raised after objects are added or removed in a location. public static event EventHandler ObjectsChanged { - add => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Add(value); - remove => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Remove(value); + add => LocationEvents.EventManager.Legacy_ObjectsChanged.Add(value); + remove => LocationEvents.EventManager.Legacy_ObjectsChanged.Remove(value); } diff --git a/src/SMAPI/Events/LocationListChangedEventArgs.cs b/src/SMAPI/Events/LocationListChangedEventArgs.cs new file mode 100644 index 00000000..e93f0a80 --- /dev/null +++ b/src/SMAPI/Events/LocationListChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LocationListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public LocationListChangedEventArgs(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs index 7fcc3844..362b5070 100644 --- a/src/SMAPI/Events/MenuEvents.cs +++ b/src/SMAPI/Events/MenuEvents.cs @@ -19,15 +19,15 @@ namespace StardewModdingAPI.Events /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. public static event EventHandler MenuChanged { - add => MenuEvents.EventManager.Menu_Changed.Add(value); - remove => MenuEvents.EventManager.Menu_Changed.Remove(value); + add => MenuEvents.EventManager.Legacy_MenuChanged.Add(value); + remove => MenuEvents.EventManager.Legacy_MenuChanged.Remove(value); } /// Raised after a game menu is closed. public static event EventHandler MenuClosed { - add => MenuEvents.EventManager.Menu_Closed.Add(value); - remove => MenuEvents.EventManager.Menu_Closed.Remove(value); + add => MenuEvents.EventManager.Legacy_MenuClosed.Add(value); + remove => MenuEvents.EventManager.Legacy_MenuClosed.Remove(value); } diff --git a/src/SMAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs index 5ee4001b..f5565a76 100644 --- a/src/SMAPI/Events/MineEvents.cs +++ b/src/SMAPI/Events/MineEvents.cs @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Events /// Raised after the player warps to a new level of the mine. public static event EventHandler MineLevelChanged { - add => MineEvents.EventManager.Mine_LevelChanged.Add(value); - remove => MineEvents.EventManager.Mine_LevelChanged.Remove(value); + add => MineEvents.EventManager.Legacy_MineLevelChanged.Add(value); + remove => MineEvents.EventManager.Legacy_MineLevelChanged.Remove(value); } diff --git a/src/SMAPI/Events/MouseWheelScrolledEventArgs.cs b/src/SMAPI/Events/MouseWheelScrolledEventArgs.cs new file mode 100644 index 00000000..3ab9d412 --- /dev/null +++ b/src/SMAPI/Events/MouseWheelScrolledEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the player scrolls the mouse wheel. + public class MouseWheelScrolledEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The cursor position. + public ICursorPosition Position { get; } + + /// The old scroll value. + public int OldValue { get; } + + /// The new scroll value. + public int NewValue { get; } + + /// The amount by which the scroll value changed. + public int Delta => this.NewValue - this.OldValue; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cursor position. + /// The old scroll value. + /// The new scroll value. + public MouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue) + { + this.Position = position; + this.OldValue = oldValue; + this.NewValue = newValue; + } + } +} diff --git a/src/SMAPI/Events/MultiplayerEvents.cs b/src/SMAPI/Events/MultiplayerEvents.cs index f96ecba5..49de380e 100644 --- a/src/SMAPI/Events/MultiplayerEvents.cs +++ b/src/SMAPI/Events/MultiplayerEvents.cs @@ -19,29 +19,29 @@ namespace StardewModdingAPI.Events /// Raised before the game syncs changes from other players. public static event EventHandler BeforeMainSync { - add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Add(value); - remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Remove(value); + add => MultiplayerEvents.EventManager.Legacy_BeforeMainSync.Add(value); + remove => MultiplayerEvents.EventManager.Legacy_BeforeMainSync.Remove(value); } /// Raised after the game syncs changes from other players. public static event EventHandler AfterMainSync { - add => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Add(value); - remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Remove(value); + add => MultiplayerEvents.EventManager.Legacy_AfterMainSync.Add(value); + remove => MultiplayerEvents.EventManager.Legacy_AfterMainSync.Remove(value); } /// Raised before the game broadcasts changes to other players. public static event EventHandler BeforeMainBroadcast { - add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Add(value); - remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Remove(value); + add => MultiplayerEvents.EventManager.Legacy_BeforeMainBroadcast.Add(value); + remove => MultiplayerEvents.EventManager.Legacy_BeforeMainBroadcast.Remove(value); } /// Raised after the game broadcasts changes to other players. public static event EventHandler AfterMainBroadcast { - add => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Add(value); - remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Remove(value); + add => MultiplayerEvents.EventManager.Legacy_AfterMainBroadcast.Add(value); + remove => MultiplayerEvents.EventManager.Legacy_AfterMainBroadcast.Remove(value); } diff --git a/src/SMAPI/Events/NpcListChangedEventArgs.cs b/src/SMAPI/Events/NpcListChangedEventArgs.cs new file mode 100644 index 00000000..eca28244 --- /dev/null +++ b/src/SMAPI/Events/NpcListChangedEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class NpcListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The NPCs added to the location. + public IEnumerable Added { get; } + + /// The NPCs removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The NPCs added to the location. + /// The NPCs removed from the location. + public NpcListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/ObjectListChangedEventArgs.cs b/src/SMAPI/Events/ObjectListChangedEventArgs.cs new file mode 100644 index 00000000..55a4034f --- /dev/null +++ b/src/SMAPI/Events/ObjectListChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class ObjectListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + public ObjectListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs index 6e7050e3..bfc1b569 100644 --- a/src/SMAPI/Events/PlayerEvents.cs +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -19,22 +19,22 @@ namespace StardewModdingAPI.Events /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). public static event EventHandler InventoryChanged { - add => PlayerEvents.EventManager.Player_InventoryChanged.Add(value); - remove => PlayerEvents.EventManager.Player_InventoryChanged.Remove(value); + add => PlayerEvents.EventManager.Legacy_InventoryChanged.Add(value); + remove => PlayerEvents.EventManager.Legacy_InventoryChanged.Remove(value); } /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. public static event EventHandler LeveledUp { - add => PlayerEvents.EventManager.Player_LeveledUp.Add(value); - remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value); + add => PlayerEvents.EventManager.Legacy_LeveledUp.Add(value); + remove => PlayerEvents.EventManager.Legacy_LeveledUp.Remove(value); } /// Raised after the player warps to a new location. public static event EventHandler Warped { - add => PlayerEvents.EventManager.Player_Warped.Add(value); - remove => PlayerEvents.EventManager.Player_Warped.Remove(value); + add => PlayerEvents.EventManager.Legacy_PlayerWarped.Add(value); + remove => PlayerEvents.EventManager.Legacy_PlayerWarped.Remove(value); } diff --git a/src/SMAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs index 62184282..731bf9d1 100644 --- a/src/SMAPI/Events/SaveEvents.cs +++ b/src/SMAPI/Events/SaveEvents.cs @@ -19,43 +19,43 @@ namespace StardewModdingAPI.Events /// Raised before the game creates the save file. public static event EventHandler BeforeCreate { - add => SaveEvents.EventManager.Save_BeforeCreate.Add(value); - remove => SaveEvents.EventManager.Save_BeforeCreate.Remove(value); + add => SaveEvents.EventManager.Legacy_BeforeCreateSave.Add(value); + remove => SaveEvents.EventManager.Legacy_BeforeCreateSave.Remove(value); } /// Raised after the game finishes creating the save file. public static event EventHandler AfterCreate { - add => SaveEvents.EventManager.Save_AfterCreate.Add(value); - remove => SaveEvents.EventManager.Save_AfterCreate.Remove(value); + add => SaveEvents.EventManager.Legacy_AfterCreateSave.Add(value); + remove => SaveEvents.EventManager.Legacy_AfterCreateSave.Remove(value); } /// Raised before the game begins writes data to the save file. public static event EventHandler BeforeSave { - add => SaveEvents.EventManager.Save_BeforeSave.Add(value); - remove => SaveEvents.EventManager.Save_BeforeSave.Remove(value); + add => SaveEvents.EventManager.Legacy_BeforeSave.Add(value); + remove => SaveEvents.EventManager.Legacy_BeforeSave.Remove(value); } /// Raised after the game finishes writing data to the save file. public static event EventHandler AfterSave { - add => SaveEvents.EventManager.Save_AfterSave.Add(value); - remove => SaveEvents.EventManager.Save_AfterSave.Remove(value); + add => SaveEvents.EventManager.Legacy_AfterSave.Add(value); + remove => SaveEvents.EventManager.Legacy_AfterSave.Remove(value); } /// Raised after the player loads a save slot. public static event EventHandler AfterLoad { - add => SaveEvents.EventManager.Save_AfterLoad.Add(value); - remove => SaveEvents.EventManager.Save_AfterLoad.Remove(value); + add => SaveEvents.EventManager.Legacy_AfterLoad.Add(value); + remove => SaveEvents.EventManager.Legacy_AfterLoad.Remove(value); } /// Raised after the game returns to the title screen. public static event EventHandler AfterReturnToTitle { - add => SaveEvents.EventManager.Save_AfterReturnToTitle.Add(value); - remove => SaveEvents.EventManager.Save_AfterReturnToTitle.Remove(value); + add => SaveEvents.EventManager.Legacy_AfterReturnToTitle.Add(value); + remove => SaveEvents.EventManager.Legacy_AfterReturnToTitle.Remove(value); } diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs index 33ebf3b2..af84b397 100644 --- a/src/SMAPI/Events/SpecialisedEvents.cs +++ b/src/SMAPI/Events/SpecialisedEvents.cs @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Events /// Raised when the game updates its state (≈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 method will trigger a stability warning in the SMAPI console. public static event EventHandler UnvalidatedUpdateTick { - add => SpecialisedEvents.EventManager.Specialised_UnvalidatedUpdateTick.Add(value); - remove => SpecialisedEvents.EventManager.Specialised_UnvalidatedUpdateTick.Remove(value); + add => SpecialisedEvents.EventManager.Legacy_UnvalidatedUpdateTick.Add(value); + remove => SpecialisedEvents.EventManager.Legacy_UnvalidatedUpdateTick.Remove(value); } diff --git a/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..562b1d3c --- /dev/null +++ b/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class TerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The terrain features added to the location. + public IEnumerable> Added { get; } + + /// The terrain features removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The terrain features added to the location. + /// The terrain features removed from the location. + public TerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/TimeEvents.cs b/src/SMAPI/Events/TimeEvents.cs index f769fd08..311ffe9e 100644 --- a/src/SMAPI/Events/TimeEvents.cs +++ b/src/SMAPI/Events/TimeEvents.cs @@ -19,15 +19,15 @@ namespace StardewModdingAPI.Events /// Raised after the game begins a new day, including when loading a save. public static event EventHandler AfterDayStarted { - add => TimeEvents.EventManager.Time_AfterDayStarted.Add(value); - remove => TimeEvents.EventManager.Time_AfterDayStarted.Remove(value); + add => TimeEvents.EventManager.Legacy_AfterDayStarted.Add(value); + remove => TimeEvents.EventManager.Legacy_AfterDayStarted.Remove(value); } /// Raised after the in-game clock changes. public static event EventHandler TimeOfDayChanged { - add => TimeEvents.EventManager.Time_TimeOfDayChanged.Add(value); - remove => TimeEvents.EventManager.Time_TimeOfDayChanged.Remove(value); + add => TimeEvents.EventManager.Legacy_TimeOfDayChanged.Add(value); + remove => TimeEvents.EventManager.Legacy_TimeOfDayChanged.Remove(value); } diff --git a/src/SMAPI/Events/UpdateTickedEventArgs.cs b/src/SMAPI/Events/UpdateTickedEventArgs.cs new file mode 100644 index 00000000..56912643 --- /dev/null +++ b/src/SMAPI/Events/UpdateTickedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UpdateTickedEventArgs : 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 UpdateTickedEventArgs(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/UpdateTickingEventArgs.cs b/src/SMAPI/Events/UpdateTickingEventArgs.cs new file mode 100644 index 00000000..5998fd9b --- /dev/null +++ b/src/SMAPI/Events/UpdateTickingEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UpdateTickingEventArgs : 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 UpdateTickingEventArgs(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/WorldBuildingListChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs deleted file mode 100644 index e73b9396..00000000 --- a/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; -using StardewValley.Buildings; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldBuildingListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The buildings added to the location. - public IEnumerable Added { get; } - - /// The buildings removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The buildings added to the location. - /// The buildings removed from the location. - public WorldBuildingListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs b/src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs deleted file mode 100644 index aad9c24d..00000000 --- a/src/SMAPI/Events/WorldDebrisListChangedEventArgs.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldDebrisListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The debris added to the location. - public IEnumerable Added { get; } - - /// The debris removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The debris added to the location. - /// The debris removed from the location. - public WorldDebrisListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs deleted file mode 100644 index 053a0e41..00000000 --- a/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; -using StardewValley.TerrainFeatures; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldLargeTerrainFeatureListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The large terrain features added to the location. - public IEnumerable Added { get; } - - /// The large terrain features removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The large terrain features added to the location. - /// The large terrain features removed from the location. - public WorldLargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs deleted file mode 100644 index 8bc26a43..00000000 --- a/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldLocationListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The added locations. - public IEnumerable Added { get; } - - /// The removed locations. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The added locations. - /// The removed locations. - public WorldLocationListChangedEventArgs(IEnumerable added, IEnumerable removed) - { - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs deleted file mode 100644 index e251f894..00000000 --- a/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldNpcListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The NPCs added to the location. - public IEnumerable Added { get; } - - /// The NPCs removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The NPCs added to the location. - /// The NPCs removed from the location. - public WorldNpcListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs deleted file mode 100644 index 5623a49b..00000000 --- a/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using StardewValley; -using Object = StardewValley.Object; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldObjectListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The objects added to the location. - public IEnumerable> Added { get; } - - /// The objects removed from the location. - public IEnumerable> Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The objects added to the location. - /// The objects removed from the location. - public WorldObjectListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs deleted file mode 100644 index cb089811..00000000 --- a/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using StardewValley; -using StardewValley.TerrainFeatures; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldTerrainFeatureListChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The terrain features added to the location. - public IEnumerable> Added { get; } - - /// The terrain features removed from the location. - public IEnumerable> Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The terrain features added to the location. - /// The terrain features removed from the location. - public WorldTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 168ddde0..1435976a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -15,52 +15,52 @@ namespace StardewModdingAPI.Framework.Events ** Game loop ****/ /// Raised after the game is launched, right before the first update tick. - public readonly ManagedEvent GameLoop_Launched; + public readonly ManagedEvent GameLaunched; /// Raised before the game performs its overall update tick (≈60 times per second). - public readonly ManagedEvent GameLoop_Updating; + public readonly ManagedEvent UpdateTicking; /// Raised after the game performs its overall update tick (≈60 times per second). - public readonly ManagedEvent GameLoop_Updated; + public readonly ManagedEvent UpdateTicked; /**** ** Input ****/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; + public readonly ManagedEvent ButtonPressed; /// Raised after the player released a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; + public readonly ManagedEvent ButtonReleased; /// Raised after the player moves the in-game cursor. - public readonly ManagedEvent Input_CursorMoved; + public readonly ManagedEvent CursorMoved; /// Raised after the player scrolls the mouse wheel. - public readonly ManagedEvent Input_MouseWheelScrolled; + public readonly ManagedEvent MouseWheelScrolled; /**** ** World ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent World_LocationListChanged; + public readonly ManagedEvent LocationListChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent World_BuildingListChanged; + public readonly ManagedEvent BuildingListChanged; /// Raised after debris are added or removed in a location. - public readonly ManagedEvent World_DebrisListChanged; + public readonly ManagedEvent DebrisListChanged; /// Raised after large terrain features (like bushes) are added or removed in a location. - public readonly ManagedEvent World_LargeTerrainFeatureListChanged; + public readonly ManagedEvent LargeTerrainFeatureListChanged; /// Raised after NPCs are added or removed in a location. - public readonly ManagedEvent World_NpcListChanged; + public readonly ManagedEvent NpcListChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent World_ObjectListChanged; + public readonly ManagedEvent ObjectListChanged; /// Raised after terrain features (like floors and trees) are added or removed in a location. - public readonly ManagedEvent World_TerrainFeatureListChanged; + public readonly ManagedEvent TerrainFeatureListChanged; /********* @@ -70,185 +70,185 @@ namespace StardewModdingAPI.Framework.Events ** ContentEvents ****/ /// Raised after the content language changes. - public readonly ManagedEvent> Content_LocaleChanged; + public readonly ManagedEvent> Legacy_LocaleChanged; /**** ** ControlEvents ****/ /// Raised when the changes. That happens when the player presses or releases a key. - public readonly ManagedEvent Legacy_Control_KeyboardChanged; + public readonly ManagedEvent Legacy_KeyboardChanged; /// Raised after the player presses a keyboard key. - public readonly ManagedEvent Legacy_Control_KeyPressed; + public readonly ManagedEvent Legacy_KeyPressed; /// Raised after the player releases a keyboard key. - public readonly ManagedEvent Legacy_Control_KeyReleased; + public readonly ManagedEvent Legacy_KeyReleased; /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public readonly ManagedEvent Legacy_Control_MouseChanged; + public readonly ManagedEvent Legacy_MouseChanged; /// The player pressed a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Legacy_Control_ControllerButtonPressed; + public readonly ManagedEvent Legacy_ControllerButtonPressed; /// The player released a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Legacy_Control_ControllerButtonReleased; + public readonly ManagedEvent Legacy_ControllerButtonReleased; /// The player pressed a controller trigger button. - public readonly ManagedEvent Legacy_Control_ControllerTriggerPressed; + public readonly ManagedEvent Legacy_ControllerTriggerPressed; /// The player released a controller trigger button. - public readonly ManagedEvent Legacy_Control_ControllerTriggerReleased; + public readonly ManagedEvent Legacy_ControllerTriggerReleased; /**** ** GameEvents ****/ /// Raised once after the game initialises and all methods have been called. - public readonly ManagedEvent Game_FirstUpdateTick; + public readonly ManagedEvent Legacy_FirstUpdateTick; /// Raised when the game updates its state (≈60 times per second). - public readonly ManagedEvent Game_UpdateTick; + public readonly ManagedEvent Legacy_UpdateTick; /// Raised every other tick (≈30 times per second). - public readonly ManagedEvent Game_SecondUpdateTick; + public readonly ManagedEvent Legacy_SecondUpdateTick; /// Raised every fourth tick (≈15 times per second). - public readonly ManagedEvent Game_FourthUpdateTick; + public readonly ManagedEvent Legacy_FourthUpdateTick; /// Raised every eighth tick (≈8 times per second). - public readonly ManagedEvent Game_EighthUpdateTick; + public readonly ManagedEvent Legacy_EighthUpdateTick; /// Raised every 15th tick (≈4 times per second). - public readonly ManagedEvent Game_QuarterSecondTick; + public readonly ManagedEvent Legacy_QuarterSecondTick; /// Raised every 30th tick (≈twice per second). - public readonly ManagedEvent Game_HalfSecondTick; + public readonly ManagedEvent Legacy_HalfSecondTick; /// Raised every 60th tick (≈once per second). - public readonly ManagedEvent Game_OneSecondTick; + public readonly ManagedEvent Legacy_OneSecondTick; /**** ** GraphicsEvents ****/ /// Raised after the game window is resized. - public readonly ManagedEvent Graphics_Resize; + public readonly ManagedEvent Legacy_Resize; /// Raised before drawing the world to the screen. - public readonly ManagedEvent Graphics_OnPreRenderEvent; + public readonly ManagedEvent Legacy_OnPreRenderEvent; /// Raised after drawing the world to the screen. - public readonly ManagedEvent Graphics_OnPostRenderEvent; + public readonly ManagedEvent Legacy_OnPostRenderEvent; /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public readonly ManagedEvent Graphics_OnPreRenderHudEvent; + public readonly ManagedEvent Legacy_OnPreRenderHudEvent; /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public readonly ManagedEvent Graphics_OnPostRenderHudEvent; + public readonly ManagedEvent Legacy_OnPostRenderHudEvent; /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public readonly ManagedEvent Graphics_OnPreRenderGuiEvent; + public readonly ManagedEvent Legacy_OnPreRenderGuiEvent; /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public readonly ManagedEvent Graphics_OnPostRenderGuiEvent; + public readonly ManagedEvent Legacy_OnPostRenderGuiEvent; /**** ** InputEvents ****/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Legacy_Input_ButtonPressed; + public readonly ManagedEvent Legacy_ButtonPressed; /// Raised after the player releases a keyboard key on the keyboard, controller, or mouse. - public readonly ManagedEvent Legacy_Input_ButtonReleased; + public readonly ManagedEvent Legacy_ButtonReleased; /**** ** LocationEvents ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent Legacy_Location_LocationsChanged; + public readonly ManagedEvent Legacy_LocationsChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent Legacy_Location_BuildingsChanged; + public readonly ManagedEvent Legacy_BuildingsChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent Legacy_Location_ObjectsChanged; + public readonly ManagedEvent Legacy_ObjectsChanged; /**** ** MenuEvents ****/ /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. - public readonly ManagedEvent Menu_Changed; + public readonly ManagedEvent Legacy_MenuChanged; /// Raised after a game menu is closed. - public readonly ManagedEvent Menu_Closed; + public readonly ManagedEvent Legacy_MenuClosed; /**** ** MultiplayerEvents ****/ /// Raised before the game syncs changes from other players. - public readonly ManagedEvent Multiplayer_BeforeMainSync; + public readonly ManagedEvent Legacy_BeforeMainSync; /// Raised after the game syncs changes from other players. - public readonly ManagedEvent Multiplayer_AfterMainSync; + public readonly ManagedEvent Legacy_AfterMainSync; /// Raised before the game broadcasts changes to other players. - public readonly ManagedEvent Multiplayer_BeforeMainBroadcast; + public readonly ManagedEvent Legacy_BeforeMainBroadcast; /// Raised after the game broadcasts changes to other players. - public readonly ManagedEvent Multiplayer_AfterMainBroadcast; + public readonly ManagedEvent Legacy_AfterMainBroadcast; /**** ** MineEvents ****/ /// Raised after the player warps to a new level of the mine. - public readonly ManagedEvent Mine_LevelChanged; + public readonly ManagedEvent Legacy_MineLevelChanged; /**** ** PlayerEvents ****/ /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public readonly ManagedEvent Player_InventoryChanged; + public readonly ManagedEvent Legacy_InventoryChanged; /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. - public readonly ManagedEvent Player_LeveledUp; + public readonly ManagedEvent Legacy_LeveledUp; /// Raised after the player warps to a new location. - public readonly ManagedEvent Player_Warped; + public readonly ManagedEvent Legacy_PlayerWarped; /**** ** SaveEvents ****/ /// Raised before the game creates the save file. - public readonly ManagedEvent Save_BeforeCreate; + public readonly ManagedEvent Legacy_BeforeCreateSave; /// Raised after the game finishes creating the save file. - public readonly ManagedEvent Save_AfterCreate; + public readonly ManagedEvent Legacy_AfterCreateSave; /// Raised before the game begins writes data to the save file. - public readonly ManagedEvent Save_BeforeSave; + public readonly ManagedEvent Legacy_BeforeSave; /// Raised after the game finishes writing data to the save file. - public readonly ManagedEvent Save_AfterSave; + public readonly ManagedEvent Legacy_AfterSave; /// Raised after the player loads a save slot. - public readonly ManagedEvent Save_AfterLoad; + public readonly ManagedEvent Legacy_AfterLoad; /// Raised after the game returns to the title screen. - public readonly ManagedEvent Save_AfterReturnToTitle; + public readonly ManagedEvent Legacy_AfterReturnToTitle; /**** ** SpecialisedEvents ****/ /// Raised when the game updates its state (≈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 method will trigger a stability warning in the SMAPI console. - public readonly ManagedEvent Specialised_UnvalidatedUpdateTick; + public readonly ManagedEvent Legacy_UnvalidatedUpdateTick; /**** ** TimeEvents ****/ /// Raised after the game begins a new day, including when loading a save. - public readonly ManagedEvent Time_AfterDayStarted; + public readonly ManagedEvent Legacy_AfterDayStarted; /// Raised after the in-game clock changes. - public readonly ManagedEvent Time_TimeOfDayChanged; + public readonly ManagedEvent Legacy_TimeOfDayChanged; /********* @@ -264,84 +264,84 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) - this.GameLoop_Launched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Launched)); - this.GameLoop_Updating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating)); - this.GameLoop_Updated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated)); - - this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); - this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); - this.Input_MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); - - this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); - this.World_DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); - this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); - this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); - this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); - this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); - this.World_TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + 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)); + + 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.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)); + this.LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); + this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); // init events (old) - this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); - - this.Legacy_Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); - this.Legacy_Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); - this.Legacy_Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); - this.Legacy_Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); - this.Legacy_Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); - this.Legacy_Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); - this.Legacy_Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); - this.Legacy_Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); - - this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); - this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); - this.Game_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); - this.Game_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); - this.Game_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); - this.Game_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); - this.Game_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); - this.Game_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); - - this.Graphics_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); - this.Graphics_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); - this.Graphics_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); - this.Graphics_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); - this.Graphics_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); - this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); - this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); - - this.Legacy_Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); - this.Legacy_Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - - this.Legacy_Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Legacy_Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); - this.Legacy_Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); - - this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); - this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); - - this.Multiplayer_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); - this.Multiplayer_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); - this.Multiplayer_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); - this.Multiplayer_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); - - this.Mine_LevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); - - this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); - this.Player_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); - this.Player_Warped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); - - this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); - this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); - this.Save_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); - this.Save_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); - this.Save_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); - this.Save_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); - - this.Specialised_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); - - this.Time_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); - this.Time_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); + this.Legacy_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); + + this.Legacy_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Legacy_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Legacy_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Legacy_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Legacy_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Legacy_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Legacy_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Legacy_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + + this.Legacy_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); + this.Legacy_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); + this.Legacy_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); + this.Legacy_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); + this.Legacy_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); + this.Legacy_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); + this.Legacy_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); + this.Legacy_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); + + this.Legacy_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); + this.Legacy_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); + this.Legacy_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); + this.Legacy_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); + this.Legacy_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); + this.Legacy_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); + this.Legacy_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); + + this.Legacy_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Legacy_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + + this.Legacy_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Legacy_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); + this.Legacy_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); + + this.Legacy_MenuChanged = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); + this.Legacy_MenuClosed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + + this.Legacy_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); + this.Legacy_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); + this.Legacy_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); + this.Legacy_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); + + this.Legacy_MineLevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); + + this.Legacy_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); + this.Legacy_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + this.Legacy_PlayerWarped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); + + this.Legacy_BeforeCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); + this.Legacy_AfterCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); + this.Legacy_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); + this.Legacy_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); + this.Legacy_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); + this.Legacy_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); + + this.Legacy_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); + + this.Legacy_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); + this.Legacy_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); } } } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 379a4e96..781597ef 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -10,24 +10,24 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after the game is launched, right before the first update tick. - public event EventHandler Launched + public event EventHandler GameLaunched { - add => this.EventManager.GameLoop_Launched.Add(value); - remove => this.EventManager.GameLoop_Launched.Remove(value); + add => this.EventManager.GameLaunched.Add(value); + remove => this.EventManager.GameLaunched.Remove(value); } /// Raised before the game performs its overall update tick (≈60 times per second). - public event EventHandler Updating + public event EventHandler UpdateTicking { - add => this.EventManager.GameLoop_Updating.Add(value); - remove => this.EventManager.GameLoop_Updating.Remove(value); + add => this.EventManager.UpdateTicking.Add(value); + remove => this.EventManager.UpdateTicking.Remove(value); } /// Raised after the game performs its overall update tick (≈60 times per second). - public event EventHandler Updated + public event EventHandler UpdateTicked { - add => this.EventManager.GameLoop_Updated.Add(value); - remove => this.EventManager.GameLoop_Updated.Remove(value); + add => this.EventManager.UpdateTicked.Add(value); + remove => this.EventManager.UpdateTicked.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index feca34f3..6a4298b4 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -10,31 +10,31 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public event EventHandler ButtonPressed + public event EventHandler ButtonPressed { - add => this.EventManager.Input_ButtonPressed.Add(value); - remove => this.EventManager.Input_ButtonPressed.Remove(value); + add => this.EventManager.ButtonPressed.Add(value); + remove => this.EventManager.ButtonPressed.Remove(value); } /// Raised after the player releases a button on the keyboard, controller, or mouse. - public event EventHandler ButtonReleased + public event EventHandler ButtonReleased { - add => this.EventManager.Input_ButtonReleased.Add(value); - remove => this.EventManager.Input_ButtonReleased.Remove(value); + add => this.EventManager.ButtonReleased.Add(value); + remove => this.EventManager.ButtonReleased.Remove(value); } /// Raised after the player moves the in-game cursor. - public event EventHandler CursorMoved + public event EventHandler CursorMoved { - add => this.EventManager.Input_CursorMoved.Add(value); - remove => this.EventManager.Input_CursorMoved.Remove(value); + add => this.EventManager.CursorMoved.Add(value); + remove => this.EventManager.CursorMoved.Remove(value); } /// Raised after the player scrolls the mouse wheel. - public event EventHandler MouseWheelScrolled + public event EventHandler MouseWheelScrolled { - add => this.EventManager.Input_MouseWheelScrolled.Add(value); - remove => this.EventManager.Input_MouseWheelScrolled.Remove(value); + add => this.EventManager.MouseWheelScrolled.Add(value); + remove => this.EventManager.MouseWheelScrolled.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index dc9c0f4c..b85002a3 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -10,52 +10,52 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after a game location is added or removed. - public event EventHandler LocationListChanged + public event EventHandler LocationListChanged { - add => this.EventManager.World_LocationListChanged.Add(value, this.Mod); - remove => this.EventManager.World_LocationListChanged.Remove(value); + add => this.EventManager.LocationListChanged.Add(value, this.Mod); + remove => this.EventManager.LocationListChanged.Remove(value); } /// Raised after buildings are added or removed in a location. - public event EventHandler BuildingListChanged + public event EventHandler BuildingListChanged { - add => this.EventManager.World_BuildingListChanged.Add(value, this.Mod); - remove => this.EventManager.World_BuildingListChanged.Remove(value); + add => this.EventManager.BuildingListChanged.Add(value, this.Mod); + remove => this.EventManager.BuildingListChanged.Remove(value); } /// Raised after debris are added or removed in a location. - public event EventHandler DebrisListChanged + public event EventHandler DebrisListChanged { - add => this.EventManager.World_DebrisListChanged.Add(value, this.Mod); - remove => this.EventManager.World_DebrisListChanged.Remove(value); + add => this.EventManager.DebrisListChanged.Add(value, this.Mod); + remove => this.EventManager.DebrisListChanged.Remove(value); } /// Raised after large terrain features (like bushes) are added or removed in a location. - public event EventHandler LargeTerrainFeatureListChanged + public event EventHandler LargeTerrainFeatureListChanged { - add => this.EventManager.World_LargeTerrainFeatureListChanged.Add(value, this.Mod); - remove => this.EventManager.World_LargeTerrainFeatureListChanged.Remove(value); + add => this.EventManager.LargeTerrainFeatureListChanged.Add(value, this.Mod); + remove => this.EventManager.LargeTerrainFeatureListChanged.Remove(value); } /// Raised after NPCs are added or removed in a location. - public event EventHandler NpcListChanged + public event EventHandler NpcListChanged { - add => this.EventManager.World_NpcListChanged.Add(value); - remove => this.EventManager.World_NpcListChanged.Remove(value); + add => this.EventManager.NpcListChanged.Add(value); + remove => this.EventManager.NpcListChanged.Remove(value); } /// Raised after objects are added or removed in a location. - public event EventHandler ObjectListChanged + public event EventHandler ObjectListChanged { - add => this.EventManager.World_ObjectListChanged.Add(value); - remove => this.EventManager.World_ObjectListChanged.Remove(value); + add => this.EventManager.ObjectListChanged.Add(value); + remove => this.EventManager.ObjectListChanged.Remove(value); } /// Raised after terrain features (like floors and trees) are added or removed in a location. - public event EventHandler TerrainFeatureListChanged + public event EventHandler TerrainFeatureListChanged { - add => this.EventManager.World_TerrainFeatureListChanged.Add(value); - remove => this.EventManager.World_TerrainFeatureListChanged.Remove(value); + add => this.EventManager.TerrainFeatureListChanged.Add(value); + remove => this.EventManager.TerrainFeatureListChanged.Remove(value); } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a9b80bc7..a432e844 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -259,7 +259,7 @@ namespace StardewModdingAPI.Framework if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); - this.Events.Specialised_UnvalidatedUpdateTick.Raise(); + this.Events.Legacy_UnvalidatedUpdateTick.Raise(); return; } @@ -323,7 +323,7 @@ namespace StardewModdingAPI.Framework { this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - this.Events.Save_BeforeCreate.Raise(); + this.Events.Legacy_BeforeCreateSave.Raise(); } // raise before-save @@ -331,12 +331,12 @@ namespace StardewModdingAPI.Framework { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); - this.Events.Save_BeforeSave.Raise(); + this.Events.Legacy_BeforeSave.Raise(); } // suppress non-save events base.Update(gameTime); - this.Events.Specialised_UnvalidatedUpdateTick.Raise(); + this.Events.Legacy_UnvalidatedUpdateTick.Raise(); return; } if (this.IsBetweenCreateEvents) @@ -344,15 +344,15 @@ namespace StardewModdingAPI.Framework // raise after-create this.IsBetweenCreateEvents = false; this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - this.Events.Save_AfterCreate.Raise(); + this.Events.Legacy_AfterCreateSave.Raise(); } if (this.IsBetweenSaveEvents) { // raise after-save this.IsBetweenSaveEvents = false; this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - this.Events.Save_AfterSave.Raise(); - this.Events.Time_AfterDayStarted.Raise(); + this.Events.Legacy_AfterSave.Raise(); + this.Events.Legacy_AfterDayStarted.Raise(); } /********* @@ -382,7 +382,7 @@ namespace StardewModdingAPI.Framework var now = this.Watchers.LocaleWatcher.CurrentValue; this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace); - this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(was.ToString(), now.ToString())); + this.Events.Legacy_LocaleChanged.Raise(new EventArgsValueChanged(was.ToString(), now.ToString())); this.Watchers.LocaleWatcher.Reset(); } @@ -393,7 +393,7 @@ namespace StardewModdingAPI.Framework if (wasWorldReady && !Context.IsWorldReady) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); - this.Events.Save_AfterReturnToTitle.Raise(); + this.Events.Legacy_AfterReturnToTitle.Raise(); } else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) { @@ -410,8 +410,8 @@ namespace StardewModdingAPI.Framework // raise events this.RaisedAfterLoadEvent = true; - this.Events.Save_AfterLoad.Raise(); - this.Events.Time_AfterDayStarted.Raise(); + this.Events.Legacy_AfterLoad.Raise(); + this.Events.Legacy_AfterDayStarted.Raise(); } /********* @@ -425,7 +425,7 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); - this.Events.Graphics_Resize.Raise(); + this.Events.Legacy_Resize.Raise(); this.Watchers.WindowSizeWatcher.Reset(); } @@ -447,7 +447,7 @@ namespace StardewModdingAPI.Framework ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; this.Watchers.CursorWatcher.Reset(); - this.Events.Input_CursorMoved.Raise(new InputCursorMovedEventArgs(was, now)); + this.Events.CursorMoved.Raise(new CursorMovedEventArgs(was, now)); } // raise mouse wheel scrolled @@ -459,7 +459,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); - this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now)); + this.Events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); } // raise input button events @@ -473,21 +473,21 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); - this.Events.Input_ButtonPressed.Raise(new InputButtonPressedEventArgs(button, cursor, inputState)); - this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + this.Events.Legacy_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Legacy_Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Legacy_Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Legacy_Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); + this.Events.Legacy_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) @@ -495,30 +495,30 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); - this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedEventArgs(button, cursor, inputState)); - this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + this.Events.Legacy_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Legacy_Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Legacy_Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Legacy_Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + this.Events.Legacy_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.RealKeyboard != previousInputState.RealKeyboard) - this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + this.Events.Legacy_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); + this.Events.Legacy_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); } } @@ -536,9 +536,9 @@ namespace StardewModdingAPI.Framework // raise menu events if (now != null) - this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(was, now)); + this.Events.Legacy_MenuChanged.Raise(new EventArgsClickableMenuChanged(was, now)); else - this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(was)); + this.Events.Legacy_MenuClosed.Raise(new EventArgsClickableMenuClosed(was)); } /********* @@ -565,8 +565,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } - this.Events.World_LocationListChanged.Raise(new WorldLocationListChangedEventArgs(added, removed)); - this.Events.Legacy_Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); + this.Events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); + this.Events.Legacy_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } // raise location contents changed @@ -582,8 +582,8 @@ namespace StardewModdingAPI.Framework Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); - this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); - this.Events.Legacy_Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); + this.Events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed)); + this.Events.Legacy_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } // debris changed @@ -594,7 +594,7 @@ namespace StardewModdingAPI.Framework Debris[] removed = watcher.DebrisWatcher.Removed.ToArray(); watcher.DebrisWatcher.Reset(); - this.Events.World_DebrisListChanged.Raise(new WorldDebrisListChangedEventArgs(location, added, removed)); + this.Events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, added, removed)); } // large terrain features changed @@ -605,7 +605,7 @@ namespace StardewModdingAPI.Framework LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); watcher.LargeTerrainFeaturesWatcher.Reset(); - this.Events.World_LargeTerrainFeatureListChanged.Raise(new WorldLargeTerrainFeatureListChangedEventArgs(location, added, removed)); + this.Events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, added, removed)); } // NPCs changed @@ -616,7 +616,7 @@ namespace StardewModdingAPI.Framework NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); watcher.NpcsWatcher.Reset(); - this.Events.World_NpcListChanged.Raise(new WorldNpcListChangedEventArgs(location, added, removed)); + this.Events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, added, removed)); } // objects changed @@ -627,8 +627,8 @@ namespace StardewModdingAPI.Framework KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); - this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); - this.Events.Legacy_Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + this.Events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed)); + this.Events.Legacy_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } // terrain features changed @@ -639,7 +639,7 @@ namespace StardewModdingAPI.Framework KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); watcher.TerrainFeaturesWatcher.Reset(); - this.Events.World_TerrainFeatureListChanged.Raise(new WorldTerrainFeatureListChangedEventArgs(location, added, removed)); + this.Events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, added, removed)); } } } @@ -657,7 +657,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); - this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); + this.Events.Legacy_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } else this.Watchers.TimeWatcher.Reset(); @@ -672,7 +672,7 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); - this.Events.Player_Warped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation)); + this.Events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation)); } // raise player leveled up a skill @@ -680,7 +680,7 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); + this.Events.Legacy_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); } // raise player inventory changed @@ -689,7 +689,7 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); + this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); } // raise mine level changed @@ -697,7 +697,7 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); - this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); + this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); } } this.Watchers.CurrentPlayerTracker?.Reset(); @@ -711,8 +711,8 @@ namespace StardewModdingAPI.Framework *********/ this.TicksElapsed++; if (this.TicksElapsed == 1) - this.Events.GameLoop_Launched.Raise(new GameLoopLaunchedEventArgs()); - this.Events.GameLoop_Updating.Raise(new GameLoopUpdatingEventArgs(this.TicksElapsed)); + this.Events.GameLaunched.Raise(new GameLaunchedEventArgs()); + this.Events.UpdateTicking.Raise(new UpdateTickingEventArgs(this.TicksElapsed)); try { this.Input.UpdateSuppression(); @@ -722,27 +722,27 @@ namespace StardewModdingAPI.Framework { this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } - this.Events.GameLoop_Updated.Raise(new GameLoopUpdatedEventArgs(this.TicksElapsed)); + this.Events.UpdateTicked.Raise(new UpdateTickedEventArgs(this.TicksElapsed)); /********* ** Update events *********/ - this.Events.Specialised_UnvalidatedUpdateTick.Raise(); + this.Events.Legacy_UnvalidatedUpdateTick.Raise(); if (this.TicksElapsed == 1) - this.Events.Game_FirstUpdateTick.Raise(); - this.Events.Game_UpdateTick.Raise(); + this.Events.Legacy_FirstUpdateTick.Raise(); + this.Events.Legacy_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) - this.Events.Game_SecondUpdateTick.Raise(); + this.Events.Legacy_SecondUpdateTick.Raise(); if (this.CurrentUpdateTick % 4 == 0) - this.Events.Game_FourthUpdateTick.Raise(); + this.Events.Legacy_FourthUpdateTick.Raise(); if (this.CurrentUpdateTick % 8 == 0) - this.Events.Game_EighthUpdateTick.Raise(); + this.Events.Legacy_EighthUpdateTick.Raise(); if (this.CurrentUpdateTick % 15 == 0) - this.Events.Game_QuarterSecondTick.Raise(); + this.Events.Legacy_QuarterSecondTick.Raise(); if (this.CurrentUpdateTick % 30 == 0) - this.Events.Game_HalfSecondTick.Raise(); + this.Events.Legacy_HalfSecondTick.Raise(); if (this.CurrentUpdateTick % 60 == 0) - this.Events.Game_OneSecondTick.Raise(); + this.Events.Legacy_OneSecondTick.Raise(); this.CurrentUpdateTick += 1; if (this.CurrentUpdateTick >= 60) this.CurrentUpdateTick = 0; @@ -830,9 +830,9 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); try { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + this.Events.Legacy_OnPreRenderGuiEvent.Raise(); activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -859,9 +859,9 @@ namespace StardewModdingAPI.Framework try { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -920,9 +920,9 @@ namespace StardewModdingAPI.Framework { try { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -1001,7 +1001,7 @@ namespace StardewModdingAPI.Framework Game1.bloom.BeginDraw(); this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - this.Events.Graphics_OnPreRenderEvent.Raise(); + this.Events.Legacy_OnPreRenderEvent.Raise(); if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -1312,9 +1312,9 @@ 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.Graphics_OnPreRenderHudEvent.Raise(); + this.Events.Legacy_OnPreRenderHudEvent.Raise(); this.drawHUD(); - this.Events.Graphics_OnPostRenderHudEvent.Raise(); + this.Events.Legacy_OnPostRenderHudEvent.Raise(); } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); @@ -1422,9 +1422,9 @@ namespace StardewModdingAPI.Framework { try { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + this.Events.Legacy_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + this.Events.Legacy_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -1463,11 +1463,11 @@ namespace StardewModdingAPI.Framework /// Whether to create a new sprite batch. private void RaisePostRender(bool needsNewBatch = false) { - if (this.Events.Graphics_OnPostRenderEvent.HasListeners()) + if (this.Events.Legacy_OnPostRenderEvent.HasListeners()) { if (needsNewBatch) Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - this.Events.Graphics_OnPostRenderEvent.Raise(); + this.Events.Legacy_OnPostRenderEvent.Raise(); if (needsNewBatch) Game1.spriteBatch.End(); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 687b1922..4923a202 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -31,17 +31,17 @@ namespace StardewModdingAPI.Framework /// Handle sync messages from other players and perform other initial sync logic. public override void UpdateEarly() { - this.EventManager.Multiplayer_BeforeMainSync.Raise(); + this.EventManager.Legacy_BeforeMainSync.Raise(); base.UpdateEarly(); - this.EventManager.Multiplayer_AfterMainSync.Raise(); + this.EventManager.Legacy_AfterMainSync.Raise(); } /// Broadcast sync messages to other players and perform other final sync logic. public override void UpdateLate(bool forceSync = false) { - this.EventManager.Multiplayer_BeforeMainBroadcast.Raise(); + this.EventManager.Legacy_BeforeMainBroadcast.Raise(); base.UpdateLate(forceSync); - this.EventManager.Multiplayer_AfterMainBroadcast.Raise(); + this.EventManager.Legacy_AfterMainBroadcast.Raise(); } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 08fd25ce..5ddee30c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,25 +79,25 @@ Properties\GlobalAssemblyInfo.cs - - - - - - + + + + + + - - - - - - - - + + + + + + + + -- cgit From 14fab29370310a762a000c50b23075326b4e95da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Oct 2018 00:25:48 -0400 Subject: add save and day-started events for 3.0 (#310) --- src/SMAPI/Events/DayStartedEventArgs.cs | 7 +++++ src/SMAPI/Events/IGameLoopEvents.cs | 18 +++++++++++ src/SMAPI/Events/SaveCreatedEventArgs.cs | 7 +++++ src/SMAPI/Events/SaveCreatingEventArgs.cs | 7 +++++ src/SMAPI/Events/SaveLoadedEventArgs.cs | 7 +++++ src/SMAPI/Events/SavedEventArgs.cs | 7 +++++ src/SMAPI/Events/SavingEventArgs.cs | 7 +++++ src/SMAPI/Framework/Events/EventManager.cs | 24 ++++++++++++++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 42 +++++++++++++++++++++++++ src/SMAPI/Framework/InternalExtensions.cs | 12 +++++++ src/SMAPI/Framework/SGame.cs | 9 ++++++ src/SMAPI/Framework/Singleton.cs | 10 ++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +++++ 13 files changed, 164 insertions(+) create mode 100644 src/SMAPI/Events/DayStartedEventArgs.cs create mode 100644 src/SMAPI/Events/SaveCreatedEventArgs.cs create mode 100644 src/SMAPI/Events/SaveCreatingEventArgs.cs create mode 100644 src/SMAPI/Events/SaveLoadedEventArgs.cs create mode 100644 src/SMAPI/Events/SavedEventArgs.cs create mode 100644 src/SMAPI/Events/SavingEventArgs.cs create mode 100644 src/SMAPI/Framework/Singleton.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/DayStartedEventArgs.cs b/src/SMAPI/Events/DayStartedEventArgs.cs new file mode 100644 index 00000000..45823628 --- /dev/null +++ b/src/SMAPI/Events/DayStartedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class DayStartedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 8ab86c9e..165aa0ce 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -13,5 +13,23 @@ namespace StardewModdingAPI.Events /// Raised after the game state is updated (≈60 times per second). event EventHandler UpdateTicked; + + /// Raised before the game creates a new save file. + event EventHandler SaveCreating; + + /// Raised after the game finishes creating the save file. + event EventHandler SaveCreated; + + /// Raised before the game begins writes data to the save file (except the initial save creation). + event EventHandler Saving; + + /// Raised after the game finishes writing data to the save file (except the initial save creation). + event EventHandler Saved; + + /// Raised after the player loads a save slot. + event EventHandler SaveLoaded; + + /// Raised after the game begins a new day (including when the player loads a save). + event EventHandler DayStarted; } } diff --git a/src/SMAPI/Events/SaveCreatedEventArgs.cs b/src/SMAPI/Events/SaveCreatedEventArgs.cs new file mode 100644 index 00000000..5ae22531 --- /dev/null +++ b/src/SMAPI/Events/SaveCreatedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveCreatedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SaveCreatingEventArgs.cs b/src/SMAPI/Events/SaveCreatingEventArgs.cs new file mode 100644 index 00000000..3c83f421 --- /dev/null +++ b/src/SMAPI/Events/SaveCreatingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveCreatingEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SaveLoadedEventArgs.cs b/src/SMAPI/Events/SaveLoadedEventArgs.cs new file mode 100644 index 00000000..f8aaa7f7 --- /dev/null +++ b/src/SMAPI/Events/SaveLoadedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveLoadedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SavedEventArgs.cs b/src/SMAPI/Events/SavedEventArgs.cs new file mode 100644 index 00000000..a4e90729 --- /dev/null +++ b/src/SMAPI/Events/SavedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/SavingEventArgs.cs b/src/SMAPI/Events/SavingEventArgs.cs new file mode 100644 index 00000000..f323ca9e --- /dev/null +++ b/src/SMAPI/Events/SavingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavingEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 1435976a..023c45de 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -23,6 +23,24 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game performs its overall update tick (≈60 times per second). public readonly ManagedEvent UpdateTicked; + /// Raised before the game creates the save file. + public readonly ManagedEvent SaveCreating; + + /// Raised after the game finishes creating the save file. + public readonly ManagedEvent SaveCreated; + + /// Raised before the game begins writes data to the save file (except the initial save creation). + public readonly ManagedEvent Saving; + + /// Raised after the game finishes writing data to the save file (except the initial save creation). + public readonly ManagedEvent Saved; + + /// Raised after the player loads a save slot. + public readonly ManagedEvent SaveLoaded; + + /// Raised after the game begins a new day, including when loading a save. + public readonly ManagedEvent DayStarted; + /**** ** Input ****/ @@ -267,6 +285,12 @@ namespace StardewModdingAPI.Framework.Events 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)); + this.SaveCreating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); + this.SaveCreated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); + this.Saving = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); + this.Saved = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saved)); + this.SaveLoaded = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveLoaded)); + this.DayStarted = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayStarted)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 781597ef..cf7e54aa 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -30,6 +30,48 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.UpdateTicked.Remove(value); } + /// Raised before the game creates a new save file. + public event EventHandler SaveCreating + { + add => this.EventManager.SaveCreating.Add(value); + remove => this.EventManager.SaveCreating.Remove(value); + } + + /// Raised after the game finishes creating the save file. + public event EventHandler SaveCreated + { + add => this.EventManager.SaveCreated.Add(value); + remove => this.EventManager.SaveCreated.Remove(value); + } + + /// Raised before the game begins writes data to the save file. + public event EventHandler Saving + { + add => this.EventManager.Saving.Add(value); + remove => this.EventManager.Saving.Remove(value); + } + + /// Raised after the game finishes writing data to the save file. + public event EventHandler Saved + { + add => this.EventManager.Saved.Add(value); + remove => this.EventManager.Saved.Remove(value); + } + + /// Raised after the player loads a save slot. + public event EventHandler SaveLoaded + { + add => this.EventManager.SaveLoaded.Add(value); + remove => this.EventManager.SaveLoaded.Remove(value); + } + + /// Raised after the game begins a new day (including when the player loads a save). + public event EventHandler DayStarted + { + add => this.EventManager.DayStarted.Add(value); + remove => this.EventManager.DayStarted.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ff3925fb..b51ff6a8 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reflection; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -39,6 +40,17 @@ namespace StardewModdingAPI.Framework metadata.Monitor.Log(message, level); } + /**** + ** ManagedEvent + ****/ + /// Raise the event and notify all handlers. + /// The empty event arguments type to construct. + /// The event to raise. + public static void RaiseEmpty(this ManagedEvent @event) where TEventArgs : new() + { + @event.Raise(Singleton.Instance); + } + /**** ** Exceptions ****/ diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a432e844..ef851afc 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -323,6 +323,7 @@ namespace StardewModdingAPI.Framework { this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); + this.Events.SaveCreating.RaiseEmpty(); this.Events.Legacy_BeforeCreateSave.Raise(); } @@ -331,6 +332,7 @@ namespace StardewModdingAPI.Framework { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); + this.Events.Saving.RaiseEmpty(); this.Events.Legacy_BeforeSave.Raise(); } @@ -344,6 +346,7 @@ namespace StardewModdingAPI.Framework // raise after-create this.IsBetweenCreateEvents = false; this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.SaveCreated.RaiseEmpty(); this.Events.Legacy_AfterCreateSave.Raise(); } if (this.IsBetweenSaveEvents) @@ -351,6 +354,9 @@ namespace StardewModdingAPI.Framework // raise after-save this.IsBetweenSaveEvents = false; this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.Saved.RaiseEmpty(); + this.Events.DayStarted.RaiseEmpty(); + this.Events.Legacy_AfterSave.Raise(); this.Events.Legacy_AfterDayStarted.Raise(); } @@ -410,6 +416,9 @@ namespace StardewModdingAPI.Framework // raise events this.RaisedAfterLoadEvent = true; + this.Events.SaveLoaded.RaiseEmpty(); + this.Events.DayStarted.RaiseEmpty(); + this.Events.Legacy_AfterLoad.Raise(); this.Events.Legacy_AfterDayStarted.Raise(); } diff --git a/src/SMAPI/Framework/Singleton.cs b/src/SMAPI/Framework/Singleton.cs new file mode 100644 index 00000000..399a8bf0 --- /dev/null +++ b/src/SMAPI/Framework/Singleton.cs @@ -0,0 +1,10 @@ +namespace StardewModdingAPI.Framework +{ + /// Provides singleton instances of a given type. + /// The instance type. + internal static class Singleton where T : new() + { + /// The singleton instance. + public static T Instance { get; } = new T(); + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5ddee30c..4c8f2ffa 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,6 +79,12 @@ Properties\GlobalAssemblyInfo.cs + + + + + + @@ -125,6 +131,7 @@ + -- cgit From 79705448f57c962e9331fb802097c24d2424476c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Oct 2018 00:51:45 -0400 Subject: add DayEnding event (#310) --- src/SMAPI/Events/DayEndingEventArgs.cs | 7 +++++ src/SMAPI/Events/IGameLoopEvents.cs | 3 +++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 7 +++++ src/SMAPI/Framework/SGame.cs | 7 +++++ src/SMAPI/Framework/SModHooks.cs | 34 +++++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 63 insertions(+) create mode 100644 src/SMAPI/Events/DayEndingEventArgs.cs create mode 100644 src/SMAPI/Framework/SModHooks.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/DayEndingEventArgs.cs b/src/SMAPI/Events/DayEndingEventArgs.cs new file mode 100644 index 00000000..5cb433bc --- /dev/null +++ b/src/SMAPI/Events/DayEndingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class DayEndingEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 165aa0ce..5f531ba7 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -31,5 +31,8 @@ namespace StardewModdingAPI.Events /// Raised after the game begins a new day (including when the player loads a save). event EventHandler DayStarted; + + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + event EventHandler DayEnding; } } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 023c45de..616db7d9 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -41,6 +41,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game begins a new day, including when loading a save. public readonly ManagedEvent DayStarted; + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + public readonly ManagedEvent DayEnding; + /**** ** Input ****/ @@ -291,6 +294,7 @@ namespace StardewModdingAPI.Framework.Events this.Saved = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saved)); 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.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index cf7e54aa..d1def91b 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -72,6 +72,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.DayStarted.Remove(value); } + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + public event EventHandler DayEnding + { + add => this.EventManager.DayEnding.Add(value); + remove => this.EventManager.DayEnding.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index ef851afc..bd266ce1 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -151,6 +151,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); + Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables Game1.locations = new ObservableCollection(); @@ -182,6 +183,12 @@ namespace StardewModdingAPI.Framework /**** ** Intercepted methods & events ****/ + /// A callback invoked before runs. + protected void OnNewDayAfterFade() + { + this.Events.DayEnding.RaiseEmpty(); + } + /// 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/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs new file mode 100644 index 00000000..9f0201c8 --- /dev/null +++ b/src/SMAPI/Framework/SModHooks.cs @@ -0,0 +1,34 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// Invokes callbacks for mod hooks provided by the game. + internal class SModHooks : ModHooks + { + /********* + ** Properties + *********/ + /// A callback to invoke before runs. + private readonly Action BeforeNewDayAfterFade; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A callback to invoke before runs. + public SModHooks(Action beforeNewDayAfterFade) + { + this.BeforeNewDayAfterFade = beforeNewDayAfterFade; + } + + /// A hook invoked when is called. + /// The vanilla logic. + public override void OnGame1_NewDayAfterFade(Action action) + { + this.BeforeNewDayAfterFade?.Invoke(); + action(); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4c8f2ffa..efb85c52 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,6 +79,7 @@ Properties\GlobalAssemblyInfo.cs + -- 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 'src/SMAPI/Framework') 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 5940f3283e7b06acae1873963d583f0d43487f94 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 Oct 2018 22:10:42 -0400 Subject: fix compile error on Linux/Mac --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index a8ec79a8..f970762a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -86,12 +86,14 @@ namespace StardewModdingAPI.Framework.Content // This performs a conventional alpha blend for the pixels, which are already // premultiplied by the content pipeline. The formula is derived from // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + // Note: don't use named arguments here since they're different between + // Linux/Mac and Windows. float alphaBelow = 1 - (above.A / 255f); newData[i] = new Color( - r: (int)(above.R + (below.R * alphaBelow)), - g: (int)(above.G + (below.G * alphaBelow)), - b: (int)(above.B + (below.B * alphaBelow)), - a: Math.Max(above.A, below.A) + (int)(above.R + (below.R * alphaBelow)), // r + (int)(above.G + (below.G * alphaBelow)), // g + (int)(above.B + (below.B * alphaBelow)), // b + Math.Max(above.A, below.A) // a ); } sourceData = newData; -- cgit From b31babd1b3dd8add92a26cd52df1244561e6b4f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 Oct 2018 18:33:30 -0400 Subject: fix second-pass dependency check failing for optional dependencies --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9f642649..47370afd 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -912,7 +912,7 @@ namespace StardewModdingAPI.Framework // 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) + foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired)) { if (this.ModRegistry.Get(dependency.UniqueID) == null) { -- 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 'src/SMAPI/Framework') 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 88ea1eae13f3c5e3bfcedfb2ac9139c6dc829bac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Oct 2018 22:08:00 -0400 Subject: add support for ignored mod folders --- docs/release-notes.md | 4 +++- src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 ++++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 ++-- src/SMAPI/Framework/SCore.cs | 9 ++++++--- src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 7 ++++++- .../Framework/ModScanning/ModScanner.cs | 6 +++++- 7 files changed, 31 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Framework') 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 e5e4ce411cc5a5e5066552978517904b21900066 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 31 Oct 2018 17:29:32 -0400 Subject: sync SMAPI context between players in multiplayer (#480) --- build/common.targets | 8 + .../Framework/ModHelpers/MultiplayerHelper.cs | 17 ++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 128 +++++++++++ .../Framework/Networking/MultiplayerPeerMod.cs | 30 +++ .../Framework/Networking/RemoteContextModModel.cs | 15 ++ .../Framework/Networking/RemoteContextModel.cs | 24 ++ src/SMAPI/Framework/Networking/SLidgrenClient.cs | 58 +++++ src/SMAPI/Framework/Networking/SLidgrenServer.cs | 36 +++ src/SMAPI/Framework/SCore.cs | 5 +- src/SMAPI/Framework/SGame.cs | 11 +- src/SMAPI/Framework/SMultiplayer.cs | 246 ++++++++++++++++++++- src/SMAPI/IMultiplayerHelper.cs | 11 + src/SMAPI/IMultiplayerPeer.cs | 41 ++++ src/SMAPI/IMultiplayerPeerMod.cs | 15 ++ src/SMAPI/Patches/NetworkingPatch.cs | 103 +++++++++ src/SMAPI/StardewModdingAPI.csproj | 9 + 16 files changed, 749 insertions(+), 8 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/MultiplayerPeer.cs create mode 100644 src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs create mode 100644 src/SMAPI/Framework/Networking/RemoteContextModModel.cs create mode 100644 src/SMAPI/Framework/Networking/RemoteContextModel.cs create mode 100644 src/SMAPI/Framework/Networking/SLidgrenClient.cs create mode 100644 src/SMAPI/Framework/Networking/SLidgrenServer.cs create mode 100644 src/SMAPI/IMultiplayerPeer.cs create mode 100644 src/SMAPI/IMultiplayerPeerMod.cs create mode 100644 src/SMAPI/Patches/NetworkingPatch.cs (limited to 'src/SMAPI/Framework') diff --git a/build/common.targets b/build/common.targets index b5cbbe67..e646e62c 100644 --- a/build/common.targets +++ b/build/common.targets @@ -56,6 +56,14 @@ + + $(GamePath)\GalaxyCSharp.dll + False + + + $(GamePath)\Lidgren.Network.dll + False + $(GamePath)\Netcode.dll False diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index c449a51b..86f8e012 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using StardewModdingAPI.Framework.Networking; using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers @@ -36,5 +37,21 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.Multiplayer.getNewID(); } + + /// Get a connected player. + /// The player's unique ID. + /// Returns the connected player, or null if no such player is connected. + public IMultiplayerPeer GetConnectedPlayer(long id) + { + return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer) + ? peer + : null; + } + + /// Get all connected players. + public IEnumerable GetConnectedPlayers() + { + return this.Multiplayer.Peers.Values; + } } } diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs new file mode 100644 index 00000000..e97e36bc --- /dev/null +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Lidgren.Network; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about a connected player. + internal class MultiplayerPeer : IMultiplayerPeer + { + /********* + ** Properties + *********/ + /// The server through which to send messages, if this is an incoming farmhand. + private readonly SLidgrenServer Server; + + /// The client through which to send messages, if this is the host player. + private readonly SLidgrenClient Client; + + /// The network connection to the player. + private readonly NetConnection ServerConnection; + + + /********* + ** Accessors + *********/ + /// The player's unique ID. + public long PlayerID { get; } + + /// Whether this is a connection to the host player. + public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID; + + /// Whether the player has SMAPI installed. + public bool HasSmapi => this.ApiVersion != null; + + /// The player's OS platform, if is true. + public GamePlatform? Platform { get; } + + /// The installed version of Stardew Valley, if is true. + public ISemanticVersion GameVersion { get; } + + /// The installed version of SMAPI, if is true. + public ISemanticVersion ApiVersion { get; } + + /// The installed mods, if is true. + public IEnumerable Mods { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's unique ID. + /// The metadata to copy. + /// 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) + { + this.PlayerID = playerID; + if (model != null) + { + this.Platform = model.Platform; + this.GameVersion = model.GameVersion; + this.ApiVersion = model.ApiVersion; + this.Mods = model.Mods.Select(mod => new MultiplayerPeerMod(mod)).ToArray(); + } + this.Server = server; + this.ServerConnection = serverConnection; + this.Client = client; + } + + /// Construct an instance for a connection to an incoming farmhand. + /// The player's unique ID. + /// The metadata to copy, if available. + /// The server through which to send messages. + /// The server connection through which to send messages. + public static MultiplayerPeer ForConnectionToFarmhand(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection) + { + return new MultiplayerPeer( + playerID: playerID, + model: model, + server: server, + serverConnection: serverConnection, + client: null + ); + } + + /// Construct an instance for a connection to the host player. + /// The player's unique ID. + /// The metadata to copy. + /// The client through which to send messages. + public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client) + { + return new MultiplayerPeer( + playerID: playerID, + model: model, + server: null, + serverConnection: null, + client: client + ); + } + + /// Get metadata for a mod installed by the player. + /// The unique mod ID. + /// Returns the mod info, or null if the player doesn't have that mod. + public IMultiplayerPeerMod GetMod(string id) + { + if (string.IsNullOrWhiteSpace(id)) + return null; + + id = id.Trim(); + return this.Mods.FirstOrDefault(mod => mod.ID != null && mod.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Send a message to the given peer, bypassing the game's normal validation to allow messages before the connection is approved. + /// The message to send. + public void SendMessage(OutgoingMessage message) + { + if (this.IsHostPlayer) + this.Client.sendMessage(message); + else + this.Server.SendMessage(this.ServerConnection, message); + } + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs new file mode 100644 index 00000000..1b324bcd --- /dev/null +++ b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Framework.Networking +{ + internal class MultiplayerPeerMod : IMultiplayerPeerMod + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string Name { get; } + + /// The unique mod ID. + public string ID { get; } + + /// The mod version. + public ISemanticVersion Version { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + public MultiplayerPeerMod(RemoteContextModModel mod) + { + this.Name = mod.Name; + this.ID = mod.ID?.Trim(); + this.Version = mod.Version; + } + } +} diff --git a/src/SMAPI/Framework/Networking/RemoteContextModModel.cs b/src/SMAPI/Framework/Networking/RemoteContextModModel.cs new file mode 100644 index 00000000..9795d971 --- /dev/null +++ b/src/SMAPI/Framework/Networking/RemoteContextModModel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about an installed mod exchanged with connected computers. + public class RemoteContextModModel + { + /// The mod's display name. + public string Name { get; set; } + + /// The unique mod ID. + public string ID { get; set; } + + /// The mod version. + public ISemanticVersion Version { get; set; } + } +} diff --git a/src/SMAPI/Framework/Networking/RemoteContextModel.cs b/src/SMAPI/Framework/Networking/RemoteContextModel.cs new file mode 100644 index 00000000..7befb151 --- /dev/null +++ b/src/SMAPI/Framework/Networking/RemoteContextModel.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about the game, SMAPI, and installed mods exchanged with connected computers. + internal class RemoteContextModel + { + /********* + ** Accessors + *********/ + /// Whether this player is the host player. + public bool IsHost { get; set; } + + /// The game's platform version. + public GamePlatform Platform { get; set; } + + /// The installed version of Stardew Valley. + public ISemanticVersion GameVersion { get; set; } + + /// The installed version of SMAPI. + public ISemanticVersion ApiVersion { get; set; } + + /// The installed mods. + public RemoteContextModModel[] Mods { get; set; } + } +} diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs new file mode 100644 index 00000000..9dfdba15 --- /dev/null +++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs @@ -0,0 +1,58 @@ +using System; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer client used to connect to a hosted server. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SLidgrenClient : LidgrenClient + { + /********* + ** Properties + *********/ + /// Get the metadata to include in a metadata message sent to other players. + private readonly Func GetMetadataMessageFields; + + /// The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed. + private readonly Func TryProcessMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The remote address being connected. + /// Get the metadata to include in a metadata message sent to other players. + /// The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed.. + public SLidgrenClient(string address, Func getMetadataMessageFields, Func tryProcessMessage) + : base(address) + { + this.GetMetadataMessageFields = getMetadataMessageFields; + this.TryProcessMessage = tryProcessMessage; + } + + /// Send the metadata needed to connect with a remote server. + public override void sendPlayerIntroduction() + { + // send custom intro + if (this.getUserID() != "") + Game1.player.userID.Value = this.getUserID(); + this.sendMessage(SMultiplayer.ContextSyncMessageID, this.GetMetadataMessageFields()); + base.sendPlayerIntroduction(); + } + + + /********* + ** Protected methods + *********/ + /// Process an incoming network message. + /// The message to process. + protected override void processIncomingMessage(IncomingMessage message) + { + if (this.TryProcessMessage(this, message)) + return; + + base.processIncomingMessage(message); + } + } +} diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs new file mode 100644 index 00000000..971eb66d --- /dev/null +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Lidgren.Network; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SLidgrenServer : LidgrenServer + { + /********* + ** Properties + *********/ + /// A method which sends a message through a specific connection. + private readonly MethodInfo SendMessageToConnectionMethod; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + public SLidgrenServer(IGameServer gameServer) + : base(gameServer) + { + this.SendMessageToConnectionMethod = typeof(LidgrenServer).GetMethod(nameof(LidgrenServer.sendMessage), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(NetConnection), typeof(OutgoingMessage) }, null); + } + + /// Send a message to a remote server. + /// The network connection. + /// The message to send. + public void SendMessage(NetConnection connection, OutgoingMessage message) + { + this.SendMessageToConnectionMethod.Invoke(this, new object[] { connection, message }); + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a17af91e..d59051fa 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -161,7 +161,8 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new DialoguePatch(this.MonitorForGame, this.Reflection) + new DialogueErrorPatch(this.MonitorForGame, this.Reflection), + new NetworkingPatch() ); } @@ -208,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.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 57f48d11..6b19f538 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -15,9 +15,11 @@ using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; @@ -130,9 +132,11 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging on the game's behalf. /// Simplifies access to private game code. /// Manages SMAPI events for mods. + /// Encapsulates SMAPI's JSON file parsing. + /// 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, Action onGameInitialised, Action onGameExiting) + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting) { SGame.ConstructorHack = null; @@ -151,7 +155,7 @@ namespace StardewModdingAPI.Framework this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -181,9 +185,6 @@ namespace StardewModdingAPI.Framework this.OnGameExiting?.Invoke(); } - /**** - ** Intercepted methods & events - ****/ /// A callback invoked before runs. protected void OnNewDayAfterFade() { diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 4923a202..a151272e 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,5 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using Lidgren.Network; +using Newtonsoft.Json; using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; +using StardewValley.Network; namespace StardewModdingAPI.Framework { @@ -12,9 +20,34 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + /// Simplifies access to private code. + private readonly Reflector Reflection; + /// Manages SMAPI events. private readonly EventManager EventManager; + /// The players who are currently disconnecting. + private readonly IList DisconnectingFarmers; + + /// Whether SMAPI should log more detailed information. + private readonly bool VerboseLogging; + + + /********* + ** Accessors + *********/ + /// The message ID for a SMAPI message containing context about a player. + public const byte ContextSyncMessageID = 255; + + /// The metadata for each connected peer. + public IDictionary Peers { get; } = new Dictionary(); + /********* ** Public methods @@ -22,10 +55,20 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging. /// Manages SMAPI events. - public SMultiplayer(IMonitor monitor, EventManager eventManager) + /// Encapsulates SMAPI's JSON file parsing. + /// 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) { this.Monitor = monitor; this.EventManager = eventManager; + this.JsonHelper = jsonHelper; + this.ModRegistry = modRegistry; + this.Reflection = reflection; + this.VerboseLogging = verboseLogging; + + this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); } /// Handle sync messages from other players and perform other initial sync logic. @@ -43,5 +86,206 @@ namespace StardewModdingAPI.Framework base.UpdateLate(forceSync); this.EventManager.Legacy_AfterMainBroadcast.Raise(); } + + /// Initialise a client before the game connects to a remote server. + /// The client to initialise. + public override Client InitClient(Client client) + { + if (client is LidgrenClient) + { + string address = this.Reflection.GetField(client, "address").GetValue(); + return new SLidgrenClient(address, this.GetContextSyncMessageFields, this.TryProcessMessageFromServer); + } + + return client; + } + + /// Initialise a server before the game connects to an incoming player. + /// The server to initialise. + public override Server InitServer(Server server) + { + if (server is LidgrenServer) + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SLidgrenServer(gameServer); + } + + return server; + } + + /// Process an incoming network message from an unknown farmhand. + /// The server instance that received the connection. + /// The raw network message that was received. + /// The message to process. + public void ProcessMessageFromUnknownFarmhand(Server server, NetIncomingMessage rawMessage, IncomingMessage message) + { + // ignore invalid message (farmhands should only receive messages from the server) + if (!Game1.IsMasterGame) + return; + + // sync SMAPI context with connected instances + if (message.MessageType == 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 + 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))); + } + + // 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)) + { + if (this.VerboseLogging) + this.Monitor.Log($" Forwarding context to player {otherPeer.PlayerID}...", LogLevel.Trace); + otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + } + } + } + + // handle intro from unmodded player + else if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) + { + // 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); + } + } + + /// Process an incoming network message from the server. + /// The client instance that received the connection. + /// The message to process. + /// 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) + { + // 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; + } + + // 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); + } + + return false; + } + + /// Remove players who are disconnecting. + protected override void removeDisconnectedFarmers() + { + foreach (long playerID in this.DisconnectingFarmers) + { + this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); + this.Peers.Remove(playerID); + } + + base.removeDisconnectedFarmers(); + } + + + /********* + ** Private methods + *********/ + /// Get the fields to include in a context sync message sent to other players. + private object[] GetContextSyncMessageFields() + { + RemoteContextModel model = new RemoteContextModel + { + IsHost = Context.IsWorldReady && Context.IsMainPlayer, + Platform = Constants.TargetPlatform, + ApiVersion = Constants.ApiVersion, + GameVersion = Constants.GameVersion, + Mods = this.ModRegistry + .GetAll() + .Select(mod => new RemoteContextModModel + { + ID = mod.Manifest.UniqueID, + Name = mod.Manifest.Name, + Version = mod.Manifest.Version + }) + .ToArray() + }; + + return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; + } + + /// Get the fields to include in a context sync message sent to other players. + /// The peer whose data to represent. + private object[] GetContextSyncMessageFields(IMultiplayerPeer peer) + { + if (!peer.HasSmapi) + return new object[] { "{}" }; + + RemoteContextModel model = new RemoteContextModel + { + IsHost = peer.IsHostPlayer, + Platform = peer.Platform.Value, + ApiVersion = peer.ApiVersion, + GameVersion = peer.GameVersion, + Mods = peer.Mods + .Select(mod => new RemoteContextModModel + { + ID = mod.ID, + Name = mod.Name, + Version = mod.Version + }) + .ToArray() + }; + + return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; + } } } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs index 43a0ac95..b01a7bed 100644 --- a/src/SMAPI/IMultiplayerHelper.cs +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -11,5 +11,16 @@ 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. + IMultiplayerPeer GetConnectedPlayer(long id); + + /// Get all connected players. + IEnumerable GetConnectedPlayers(); + */ } } diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs new file mode 100644 index 00000000..e314eba5 --- /dev/null +++ b/src/SMAPI/IMultiplayerPeer.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Metadata about a connected player. + public interface IMultiplayerPeer + { + /********* + ** Accessors + *********/ + /// The player's unique ID. + long PlayerID { get; } + + /// Whether this is a connection to the host player. + bool IsHostPlayer { get; } + + /// Whether the player has SMAPI installed. + bool HasSmapi { get; } + + /// The player's OS platform, if is true. + GamePlatform? Platform { get; } + + /// The installed version of Stardew Valley, if is true. + ISemanticVersion GameVersion { get; } + + /// The installed version of SMAPI, if is true. + ISemanticVersion ApiVersion { get; } + + /// The installed mods, if is true. + IEnumerable Mods { get; } + + + /********* + ** Methods + *********/ + /// Get metadata for a mod installed by the player. + /// The unique mod ID. + /// Returns the mod info, or null if the player doesn't have that mod. + IMultiplayerPeerMod GetMod(string id); + } +} diff --git a/src/SMAPI/IMultiplayerPeerMod.cs b/src/SMAPI/IMultiplayerPeerMod.cs new file mode 100644 index 00000000..005408b1 --- /dev/null +++ b/src/SMAPI/IMultiplayerPeerMod.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI +{ + /// Metadata about a mod installed by a connected player. + public interface IMultiplayerPeerMod + { + /// The mod's display name. + string Name { get; } + + /// The unique mod ID. + string ID { get; } + + /// The mod version. + ISemanticVersion Version { get; } + } +} diff --git a/src/SMAPI/Patches/NetworkingPatch.cs b/src/SMAPI/Patches/NetworkingPatch.cs new file mode 100644 index 00000000..12ccf84c --- /dev/null +++ b/src/SMAPI/Patches/NetworkingPatch.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using Harmony; +using Lidgren.Network; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch to enable the SMAPI multiplayer metadata handshake. + internal class NetworkingPatch : IHarmonyPatch + { + /********* + ** Properties + *********/ + /// The constructor for the internal NetBufferReadStream type. + private static readonly ConstructorInfo NetBufferReadStreamConstructor = NetworkingPatch.GetNetBufferReadStreamConstructor(); + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(NetworkingPatch)}"; + + + /********* + ** Public methods + *********/ + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(NetworkingPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the method. + /// The instance being patched. + /// The raw network message to parse. + /// The private peers field on the instance. + /// The private gameServer field on the instance. + /// 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_LidgrenServer_ParseDataMessageFromClient(LidgrenServer __instance, NetIncomingMessage dataMsg, Bimap ___peers, IGameServer ___gameServer) + { + // get SMAPI overrides + SMultiplayer multiplayer = ((SGame)Game1.game1).Multiplayer; + SLidgrenServer server = (SLidgrenServer)__instance; + + // add hook to call multiplayer core + NetConnection peer = dataMsg.SenderConnection; + using (IncomingMessage message = new IncomingMessage()) + using (Stream readStream = (Stream)NetworkingPatch.NetBufferReadStreamConstructor.Invoke(new object[] { dataMsg })) + using (BinaryReader reader = new BinaryReader(readStream)) + { + while (dataMsg.LengthBits - dataMsg.Position >= 8) + { + message.Read(reader); + if (___peers.ContainsLeft(message.FarmerID) && ___peers[message.FarmerID] == peer) + ___gameServer.processIncomingMessage(message); + else if (message.MessageType == Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); + ___gameServer.checkFarmhandRequest("", farmer, msg => server.SendMessage(peer, msg), () => ___peers[farmer.Value.UniqueMultiplayerID] = peer); + } + else + multiplayer.ProcessMessageFromUnknownFarmhand(__instance, dataMsg, message); // added hook + } + } + + return false; + } + + /// Get the constructor for the internal NetBufferReadStream type. + private static ConstructorInfo GetNetBufferReadStreamConstructor() + { + // get type + string typeName = $"StardewValley.Network.NetBufferReadStream, {Constants.GameAssemblyName}"; + Type type = Type.GetType(typeName); + if (type == null) + throw new InvalidOperationException($"Can't find type: {typeName}"); + + // get constructor + ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); + if (constructor == null) + throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); + + return constructor; + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4ce0892e..2fdf4d97 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -164,6 +164,12 @@ + + + + + + @@ -243,9 +249,11 @@ + + @@ -310,6 +318,7 @@ + -- cgit From 6f23aaf2954f7eeb93b7cc5aad2d3f9b237883f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 2 Nov 2018 23:55:35 -0400 Subject: add internal mod.HasID helper (#480) --- src/SMAPI/Framework/IModMetadata.cs | 4 ++++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 9 +++++++++ src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/ModRegistry.cs | 2 +- src/SMAPI/Framework/SCore.cs | 6 +++--- 5 files changed, 18 insertions(+), 5 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index bda9429f..7ada7dea 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -88,6 +88,10 @@ namespace StardewModdingAPI.Framework /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). bool HasID(); + /// Whether the mod has the given ID. + /// The mod ID to check. + bool HasID(string id); + /// Get the defined update keys. /// Only return valid update keys. IEnumerable GetUpdateKeys(bool validOnly = true); diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 04aa679b..0cb62a75 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -153,6 +153,15 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } + /// Whether the mod has the given ID. + /// The mod ID to check. + public bool HasID(string id) + { + return + this.HasID() + && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.InvariantCultureIgnoreCase); + } + /// Get the defined update keys. /// Only return valid update keys. public IEnumerable GetUpdateKeys(bool validOnly = false) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9992cc78..3ff70d64 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -379,7 +379,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The loaded mods. private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) { - IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id)); // yield dependencies if (manifest.Dependencies != null) diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index e7d4f89a..da68fce3 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + return this.GetAll().FirstOrDefault(p => p.HasID(uniqueID)); } /// Get the mod metadata from one of its assemblies. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d59051fa..69b33699 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -750,7 +750,7 @@ namespace StardewModdingAPI.Framework // log loaded content packs if (loadedContentPacks.Any()) { - string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName; this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) @@ -907,7 +907,7 @@ namespace StardewModdingAPI.Framework if (this.ModRegistry.Get(dependency.UniqueID) == null) { string dependencyName = mods - .FirstOrDefault(otherMod => otherMod.HasID() && dependency.UniqueID.Equals(otherMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID)) ?.DisplayName ?? dependency.UniqueID; errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; return false; @@ -971,7 +971,7 @@ namespace StardewModdingAPI.Framework // get content packs IContentPack[] contentPacks = this.ModRegistry .GetAll(assemblyMods: false) - .Where(p => p.IsContentPack && mod.Manifest.UniqueID.Equals(p.Manifest.ContentPackFor.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID)) .Select(p => p.ContentPack) .ToArray(); -- cgit From 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 'src/SMAPI/Framework') 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 bfb40202793f2f7f2c9c73272f01a477b23edfa2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 21:34:48 -0500 Subject: rewrite multiplayer sync to use generic callbacks from client/server for better extensibility (#480) --- src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 2 +- src/SMAPI/Framework/Networking/MessageType.cs | 26 +++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 6 +- src/SMAPI/Framework/Networking/SLidgrenClient.cs | 37 ++-- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 122 +++++++++++- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SMultiplayer.cs | 226 +++++++++++++--------- src/SMAPI/Patches/LidgrenServerPatch.cs | 89 +++++++++ src/SMAPI/Patches/NetworkingPatch.cs | 103 ---------- src/SMAPI/StardewModdingAPI.csproj | 3 +- 10 files changed, 383 insertions(+), 233 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/MessageType.cs create mode 100644 src/SMAPI/Patches/LidgrenServerPatch.cs delete mode 100644 src/SMAPI/Patches/NetworkingPatch.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs index b1960a22..49366ec6 100644 --- a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs +++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Framework.Networking; namespace StardewModdingAPI.Events { - /// Event arguments when a mod receives a message over the network. + /// Event arguments for an event. public class ModMessageReceivedEventArgs : EventArgs { /********* diff --git a/src/SMAPI/Framework/Networking/MessageType.cs b/src/SMAPI/Framework/Networking/MessageType.cs new file mode 100644 index 00000000..bd9acfa9 --- /dev/null +++ b/src/SMAPI/Framework/Networking/MessageType.cs @@ -0,0 +1,26 @@ +using StardewValley; + +namespace StardewModdingAPI.Framework.Networking +{ + /// Network message types recognised by SMAPI and Stardew Valley. + internal enum MessageType : byte + { + /********* + ** SMAPI + *********/ + /// A data message intended for mods to consume. + ModMessage = 254, + + /// Metadata context about a player synced by SMAPI. + ModContext = 255, + + /********* + ** Vanilla + *********/ + /// Metadata about the host server sent to a farmhand. + ServerIntroduction = Multiplayer.serverIntroduction, + + /// Metadata about a player sent to a farmhand or server. + PlayerIntroduction = Multiplayer.playerIntroduction + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index c7f8ffad..e703dbb1 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Lidgren.Network; -using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -95,7 +94,8 @@ namespace StardewModdingAPI.Framework.Networking /// The player's unique ID. /// The metadata to copy. /// The client through which to send messages. - public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client) + /// Whether this connection is for the host player. + public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client, bool isHost) { return new MultiplayerPeer( playerID: playerID, @@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework.Networking server: null, serverConnection: null, client: client, - isHost: true + isHost: isHost ); } diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs index 9dfdba15..c05e6b76 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenClient.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs @@ -1,44 +1,38 @@ using System; -using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking { - /// A multiplayer client used to connect to a hosted server. This is an implementation of that adds support for SMAPI's metadata context exchange. + /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. internal class SLidgrenClient : LidgrenClient { /********* ** Properties *********/ - /// Get the metadata to include in a metadata message sent to other players. - private readonly Func GetMetadataMessageFields; - - /// The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed. - private readonly Func TryProcessMessage; + /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. + private readonly Action OnProcessingMessage; + /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. + private readonly Action OnSendingMessage; /********* ** Public methods *********/ /// Construct an instance. /// The remote address being connected. - /// Get the metadata to include in a metadata message sent to other players. - /// The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed.. - public SLidgrenClient(string address, Func getMetadataMessageFields, Func tryProcessMessage) + /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. + public SLidgrenClient(string address, Action onProcessingMessage, Action onSendingMessage) : base(address) { - this.GetMetadataMessageFields = getMetadataMessageFields; - this.TryProcessMessage = tryProcessMessage; + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; } - /// Send the metadata needed to connect with a remote server. - public override void sendPlayerIntroduction() + /// Send a message to the connected peer. + public override void sendMessage(OutgoingMessage message) { - // send custom intro - if (this.getUserID() != "") - Game1.player.userID.Value = this.getUserID(); - this.sendMessage(SMultiplayer.ContextSyncMessageID, this.GetMetadataMessageFields()); - base.sendPlayerIntroduction(); + this.OnSendingMessage(this, message, () => base.sendMessage(message)); } @@ -49,10 +43,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to process. protected override void processIncomingMessage(IncomingMessage message) { - if (this.TryProcessMessage(this, message)) - return; - - base.processIncomingMessage(message); + this.OnProcessingMessage(this, message, () => base.processIncomingMessage(message)); } } } diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 971eb66d..060b433b 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -1,5 +1,11 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Reflection; using Lidgren.Network; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Patches; +using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -10,8 +16,27 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - /// A method which sends a message through a specific connection. - private readonly MethodInfo SendMessageToConnectionMethod; + + /// The constructor for the internal NetBufferReadStream type. + private readonly ConstructorInfo NetBufferReadStreamConstructor = SLidgrenServer.GetNetBufferReadStreamConstructor(); + + /// The constructor for the internal NetBufferWriteStream type. + private readonly ConstructorInfo NetBufferWriteStreamConstructor = SLidgrenServer.GetNetBufferWriteStreamConstructor(); + + /// A method which reads farmer data from the given binary reader. + private readonly Func ReadFarmer; + + /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. + private readonly Action OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, target player ID, and a callback to run the default logic. + private readonly Action OnSendingMessage; + + /// The peer connections. + private readonly Bimap Peers; + + /// The underlying net server. + private readonly IReflectedField Server; /********* @@ -19,18 +44,105 @@ namespace StardewModdingAPI.Framework.Networking *********/ /// Construct an instance. /// The underlying game server. - public SLidgrenServer(IGameServer gameServer) + /// Simplifies access to private code. + /// A method which reads farmer data from the given binary reader. + /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, and a callback to run the default logic. + public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action onProcessingMessage, Action onSendingMessage) : base(gameServer) { - this.SendMessageToConnectionMethod = typeof(LidgrenServer).GetMethod(nameof(LidgrenServer.sendMessage), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(NetConnection), typeof(OutgoingMessage) }, null); + this.ReadFarmer = readFarmer; + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; + this.Peers = reflection.GetField>(this, "peers").GetValue(); + this.Server = reflection.GetField(this, "server"); } /// Send a message to a remote server. /// The network connection. /// The message to send. + /// This is an implementation of which calls . This method is invoked via . public void SendMessage(NetConnection connection, OutgoingMessage message) { - this.SendMessageToConnectionMethod.Invoke(this, new object[] { connection, message }); + this.OnSendingMessage(this, connection, message, () => + { + NetServer server = this.Server.GetValue(); + NetOutgoingMessage netMessage = server.CreateMessage(); + using (Stream bufferWriteStream = (Stream)this.NetBufferWriteStreamConstructor.Invoke(new object[] { netMessage })) + using (BinaryWriter writer = new BinaryWriter(bufferWriteStream)) + message.Write(writer); + + server.SendMessage(netMessage, connection, NetDeliveryMethod.ReliableOrdered); + }); + } + + /// Parse a data message from a client. + /// The raw network message to parse. + /// This is an implementation of which calls . This method is invoked via . + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] + public bool ParseDataMessageFromClient(NetIncomingMessage rawMessage) + { + // add hook to call multiplayer core + NetConnection peer = rawMessage.SenderConnection; + using (IncomingMessage message = new IncomingMessage()) + using (Stream readStream = (Stream)this.NetBufferReadStreamConstructor.Invoke(new object[] { rawMessage })) + using (BinaryReader reader = new BinaryReader(readStream)) + { + while (rawMessage.LengthBits - rawMessage.Position >= 8) + { + message.Read(reader); + this.OnProcessingMessage(this, rawMessage, message, () => + { + if (this.Peers.ContainsLeft(message.FarmerID) && this.Peers[message.FarmerID] == peer) + this.gameServer.processIncomingMessage(message); + else if (message.MessageType == Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = this.ReadFarmer(message.Reader); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.SendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); + } + }); + } + } + + return false; + } + + + /********* + ** Private methods + *********/ + /// Get the constructor for the internal NetBufferReadStream type. + private static ConstructorInfo GetNetBufferReadStreamConstructor() + { + // get type + string typeName = $"StardewValley.Network.NetBufferReadStream, {Constants.GameAssemblyName}"; + Type type = Type.GetType(typeName); + if (type == null) + throw new InvalidOperationException($"Can't find type: {typeName}"); + + // get constructor + ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); + if (constructor == null) + throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); + + return constructor; + } + + /// Get the constructor for the internal NetBufferWriteStream type. + private static ConstructorInfo GetNetBufferWriteStreamConstructor() + { + // get type + string typeName = $"StardewValley.Network.NetBufferWriteStream, {Constants.GameAssemblyName}"; + Type type = Type.GetType(typeName); + if (type == null) + throw new InvalidOperationException($"Can't find type: {typeName}"); + + // get constructor + ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); + if (constructor == null) + throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); + + return constructor; } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ca343389..f078acba 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -162,7 +162,7 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new NetworkingPatch() + new LidgrenServerPatch() ); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index e4f912d2..70f1a89a 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -14,6 +14,17 @@ using StardewValley.Network; namespace StardewModdingAPI.Framework { /// SMAPI's implementation of the game's core multiplayer logic. + /// + /// SMAPI syncs mod context to all players through the host as such: + /// 1. Farmhand sends ModContext + PlayerIntro. + /// 2. If host receives ModContext: it stores the context, replies with known contexts, and forwards it to other farmhands. + /// 3. If host receives PlayerIntro before ModContext: it stores a 'vanilla player' context, and forwards it to other farmhands. + /// 4. If farmhand receives ModContext: it stores it. + /// 5. If farmhand receives ServerIntro without a preceding ModContext: it stores a 'vanilla host' context. + /// 6. If farmhand receives PlayerIntro without a preceding ModContext AND it's not the host peer: it stores a 'vanilla player' context. + /// + /// Once a farmhand/server stored a context, messages can be sent to that player through the SMAPI APIs. + /// internal class SMultiplayer : Multiplayer { /********* @@ -47,12 +58,6 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// 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(); @@ -107,7 +112,7 @@ namespace StardewModdingAPI.Framework if (client is LidgrenClient) { string address = this.Reflection.GetField(client, "address").GetValue(); - return new SLidgrenClient(address, this.GetContextSyncMessageFields, this.TryProcessMessageFromServer); + return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); } return client; @@ -120,56 +125,78 @@ namespace StardewModdingAPI.Framework if (server is LidgrenServer) { IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SLidgrenServer(gameServer); + return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage, this.OnServerSendingMessage); } return server; } - /// Process an incoming network message from an unknown farmhand, usually a player whose connection hasn't been approved yet. + /// A callback raised when sending a network message as the host player. + /// The server sending the message. + /// The connection to which a message is being sent. + /// The message being sent. + /// Send the underlying message. + protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) + { + resume(); + } + + /// A callback raised when sending a message as a farmhand. + /// The client sending the message. + /// The message being sent. + /// Send the underlying message. + protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) + { + switch (message.MessageType) + { + // sync mod context (step 1) + case (byte)MessageType.PlayerIntroduction: + client.sendMessage((byte)MessageType.ModContext, this.GetContextSyncMessageFields()); + resume(); + break; + + // run default logic + default: + resume(); + break; + } + } + + /// Process an incoming network message as the host player. /// The server instance that received the connection. /// The raw network message that was received. /// The message to process. - public void ProcessMessageFromUnknownFarmhand(Server server, NetIncomingMessage rawMessage, IncomingMessage message) + /// Process the message using the game's default logic. + public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) { - // ignore invalid message (farmhands should only receive messages from the server) - if (!Game1.IsMasterGame) - return; switch (message.MessageType) { - // sync SMAPI context with connected instances - case SMultiplayer.ContextSyncMessageID: + // sync mod context (step 2) + case (byte)MessageType.ModContext: { - // get server - if (!(server is SLidgrenServer customServer)) + // parse message + RemoteContextModel model = this.ReadContext(message.Reader); + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer newPeer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, server, rawMessage.SenderConnection); + if (this.Peers.ContainsKey(message.FarmerID)) { - 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); + this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); return; } + this.Peers[message.FarmerID] = newPeer; - // 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 own context + this.VerboseLog(" Replying with host context..."); + newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); - // reply with known contexts - this.VerboseLog(" Replying with context for current player..."); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, 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}..."); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); + newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); } // forward to other peers @@ -179,107 +206,103 @@ namespace StardewModdingAPI.Framework 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)); + otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } } break; - // handle intro from unmodded player - case Multiplayer.playerIntroduction: - if (!this.Peers.ContainsKey(message.FarmerID)) + // handle player intro + case (byte)MessageType.PlayerIntroduction: { - // get server - if (!(server is SLidgrenServer customServer)) + // get peer + if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - 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; + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); } - // 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; - } - } - - /// Process an incoming message from an approved connection. - /// The message to process. - public override void processIncomingMessage(IncomingMessage message) - { - switch (message.MessageType) - { - // handle mod message - case SMultiplayer.ModMessageID: + case (byte)MessageType.ModMessage: this.ReceiveModMessage(message); break; - // let game process message default: - base.processIncomingMessage(message); + resume(); break; } - } - /// Process an incoming network message from the server. + /// Process an incoming network message as a farmhand. /// The client instance that received the connection. /// The message to process. + /// Process the message using the game's default logic. /// Returns whether the message was handled. - public bool TryProcessMessageFromServer(SLidgrenClient client, IncomingMessage message) + public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { + if (message.MessageType != Multiplayer.farmerDelta && message.MessageType != Multiplayer.locationDelta && message.MessageType != Multiplayer.teamDelta && message.MessageType != Multiplayer.worldDelta) + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Alert); + switch (message.MessageType) { - // receive SMAPI context from a connected player - case SMultiplayer.ContextSyncMessageID: + // mod context sync (step 4) + case (byte)MessageType.ModContext: { // 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); + RemoteContextModel model = this.ReadContext(message.Reader); + this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); // store peer - MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client, model?.IsHost ?? this.HostPeer == null); + if (peer.IsHost && this.HostPeer != null) + { + this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); + return; + } this.Peers[message.FarmerID] = peer; if (peer.IsHost) this.HostPeer = peer; } - return true; + break; - // handle intro from unmodded player - case Multiplayer.playerIntroduction: - if (!this.Peers.ContainsKey(message.FarmerID)) + // handle server intro + case (byte)MessageType.ServerIntroduction: { // 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; + if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) + { + this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true); + } + resume(); + break; + } + + // handle player intro + case (byte)MessageType.PlayerIntroduction: + { + // store peer + if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) + { + peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); + this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } } - return false; // handle mod message - case SMultiplayer.ModMessageID: + case (byte)MessageType.ModMessage: this.ReceiveModMessage(message); - return true; + break; default: - return false; + resume(); + break; } } @@ -352,12 +375,12 @@ namespace StardewModdingAPI.Framework if (playerIDs == null || playerIDs.Contains(peer.PlayerID)) { model.ToPlayerIDs = new[] { peer.PlayerID }; - peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, data)); + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data)); } } } else if (this.HostPeer != null && this.HostPeer.HasSmapi) - this.HostPeer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, this.HostPeer.PlayerID, data)); + 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."); @@ -367,6 +390,17 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// Read the metadata context for a player. + /// The stream reader. + private RemoteContextModel ReadContext(BinaryReader reader) + { + string data = reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + return model.ApiVersion != null + ? model + : null; // no data available for unmodded players + } + /// Receive a mod message sent from another player's mods. /// The raw message to parse. private void ReceiveModMessage(IncomingMessage message) @@ -392,7 +426,7 @@ namespace StardewModdingAPI.Framework { 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))); + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); } } } diff --git a/src/SMAPI/Patches/LidgrenServerPatch.cs b/src/SMAPI/Patches/LidgrenServerPatch.cs new file mode 100644 index 00000000..6f937665 --- /dev/null +++ b/src/SMAPI/Patches/LidgrenServerPatch.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using Harmony; +using Lidgren.Network; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch to let SMAPI override methods. + internal class LidgrenServerPatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(LidgrenServerPatch)}"; + + + /********* + ** Public methods + *********/ + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + // override parseDataMessageFromClient + { + MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + + // override sendMessage + { + MethodInfo method = typeof(LidgrenServer).GetMethod("sendMessage", BindingFlags.NonPublic | BindingFlags.Instance, null, new [] { typeof(NetConnection), typeof(OutgoingMessage) }, null); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_SendMessage)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the method. + /// The instance being patched. + /// The raw network message to parse. + /// The private peers field on the instance. + /// The private gameServer field on the instance. + /// 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_LidgrenServer_ParseDataMessageFromClient(LidgrenServer __instance, NetIncomingMessage dataMsg, Bimap ___peers, IGameServer ___gameServer) + { + if (__instance is SLidgrenServer smapiServer) + { + smapiServer.ParseDataMessageFromClient(dataMsg); + return false; + } + + return true; + } + + /// The method to call instead of the method. + /// The instance being patched. + /// The connection to which to send the message. + /// The private peers field on the instance. + /// The private gameServer field on the instance. + /// 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_LidgrenServer_SendMessage(LidgrenServer __instance, NetConnection connection, OutgoingMessage message, Bimap ___peers, IGameServer ___gameServer) + { + if (__instance is SLidgrenServer smapiServer) + { + smapiServer.SendMessage(connection, message); + return false; + } + + return true; + } + } +} diff --git a/src/SMAPI/Patches/NetworkingPatch.cs b/src/SMAPI/Patches/NetworkingPatch.cs deleted file mode 100644 index 12ccf84c..00000000 --- a/src/SMAPI/Patches/NetworkingPatch.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using Harmony; -using Lidgren.Network; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.Patching; -using StardewValley; -using StardewValley.Network; - -namespace StardewModdingAPI.Patches -{ - /// A Harmony patch to enable the SMAPI multiplayer metadata handshake. - internal class NetworkingPatch : IHarmonyPatch - { - /********* - ** Properties - *********/ - /// The constructor for the internal NetBufferReadStream type. - private static readonly ConstructorInfo NetBufferReadStreamConstructor = NetworkingPatch.GetNetBufferReadStreamConstructor(); - - - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(NetworkingPatch)}"; - - - /********* - ** Public methods - *********/ - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(NetworkingPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); - harmony.Patch(method, new HarmonyMethod(prefix), null); - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of the method. - /// The instance being patched. - /// The raw network message to parse. - /// The private peers field on the instance. - /// The private gameServer field on the instance. - /// 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_LidgrenServer_ParseDataMessageFromClient(LidgrenServer __instance, NetIncomingMessage dataMsg, Bimap ___peers, IGameServer ___gameServer) - { - // get SMAPI overrides - SMultiplayer multiplayer = ((SGame)Game1.game1).Multiplayer; - SLidgrenServer server = (SLidgrenServer)__instance; - - // add hook to call multiplayer core - NetConnection peer = dataMsg.SenderConnection; - using (IncomingMessage message = new IncomingMessage()) - using (Stream readStream = (Stream)NetworkingPatch.NetBufferReadStreamConstructor.Invoke(new object[] { dataMsg })) - using (BinaryReader reader = new BinaryReader(readStream)) - { - while (dataMsg.LengthBits - dataMsg.Position >= 8) - { - message.Read(reader); - if (___peers.ContainsLeft(message.FarmerID) && ___peers[message.FarmerID] == peer) - ___gameServer.processIncomingMessage(message); - else if (message.MessageType == Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); - ___gameServer.checkFarmhandRequest("", farmer, msg => server.SendMessage(peer, msg), () => ___peers[farmer.Value.UniqueMultiplayerID] = peer); - } - else - multiplayer.ProcessMessageFromUnknownFarmhand(__instance, dataMsg, message); // added hook - } - } - - return false; - } - - /// Get the constructor for the internal NetBufferReadStream type. - private static ConstructorInfo GetNetBufferReadStreamConstructor() - { - // get type - string typeName = $"StardewValley.Network.NetBufferReadStream, {Constants.GameAssemblyName}"; - Type type = Type.GetType(typeName); - if (type == null) - throw new InvalidOperationException($"Can't find type: {typeName}"); - - // get constructor - ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); - if (constructor == null) - throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); - - return constructor; - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 11fa5d35..dfe55eb9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -174,6 +174,7 @@ + @@ -322,7 +323,7 @@ - + -- cgit From 222265816d803e8e145c0a500568412d03dd49da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 22:41:31 -0500 Subject: add ContextReceived event (#480) --- src/SMAPI/Events/ContextReceivedEventArgs.cs | 25 +++++++++++ src/SMAPI/Events/IMultiplayerEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 4 ++ src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 7 +++ src/SMAPI/Framework/SMultiplayer.cs | 50 +++++++++++++++------- src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Events/ContextReceivedEventArgs.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/ContextReceivedEventArgs.cs b/src/SMAPI/Events/ContextReceivedEventArgs.cs new file mode 100644 index 00000000..c715cf1c --- /dev/null +++ b/src/SMAPI/Events/ContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to whom a connection is being established. + internal ContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index a6ac6fd3..91e0789c 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,6 +5,9 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + event EventHandler ContextReceived; + /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 519cf48a..63ac17ee 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -101,6 +101,9 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + public readonly ManagedEvent ContextReceived; + /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; @@ -380,6 +383,7 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ContextReceived)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index a830a54a..432a92d3 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -16,6 +16,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ModMessageReceived.Remove(value); } + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + public event EventHandler ContextReceived + { + add => this.EventManager.ContextReceived.Add(value); + remove => this.EventManager.ContextReceived.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 70f1a89a..9f649639 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Lidgren.Network; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; @@ -186,7 +188,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); return; } - this.Peers[message.FarmerID] = newPeer; + this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); // reply with own context this.VerboseLog(" Replying with host context..."); @@ -209,20 +211,23 @@ namespace StardewModdingAPI.Framework otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } + + // raise event + this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(newPeer)); } break; // handle player intro case (byte)MessageType.PlayerIntroduction: + // store peer if new + if (!this.Peers.ContainsKey(message.FarmerID)) { - // get peer - if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) - { - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); - } - + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); + this.AddPeer(peer, canBeHost: false); } + + resume(); break; // handle mod message @@ -262,9 +267,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); return; } - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + this.AddPeer(peer, canBeHost: true); } break; @@ -275,7 +278,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true); + this.AddPeer(MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true), canBeHost: false); } resume(); break; @@ -289,10 +292,11 @@ namespace StardewModdingAPI.Framework { peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + this.AddPeer(peer, canBeHost: true); } + + resume(); + break; } // handle mod message @@ -390,6 +394,22 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// Save a received peer. + /// The peer to add. + /// Whether to track the peer as the host if applicable. + /// Whether to raise the event. + private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true) + { + // store + this.Peers[peer.PlayerID] = peer; + if (canBeHost && peer.IsHost) + this.HostPeer = peer; + + // raise event + if (raiseEvent) + this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(peer)); + } + /// Read the metadata context for a player. /// The stream reader. private RemoteContextModel ReadContext(BinaryReader reader) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index dfe55eb9..09ecddcd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ + -- cgit From b4a5b3829f0f738e5b7e05048068eaec9d2d01d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:07:10 -0500 Subject: add PeerDisconnected event (#480) --- src/SMAPI/Events/ContextReceivedEventArgs.cs | 25 --------------------- src/SMAPI/Events/IMultiplayerEvents.cs | 7 ++++-- src/SMAPI/Events/PeerContextReceivedEventArgs.cs | 25 +++++++++++++++++++++ src/SMAPI/Events/PeerDisconnectedEventArgs.cs | 25 +++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 10 ++++++--- src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 15 +++++++++---- src/SMAPI/Framework/SMultiplayer.cs | 26 ++++++++++++++++------ src/SMAPI/StardewModdingAPI.csproj | 3 ++- 8 files changed, 94 insertions(+), 42 deletions(-) delete mode 100644 src/SMAPI/Events/ContextReceivedEventArgs.cs create mode 100644 src/SMAPI/Events/PeerContextReceivedEventArgs.cs create mode 100644 src/SMAPI/Events/PeerDisconnectedEventArgs.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/ContextReceivedEventArgs.cs b/src/SMAPI/Events/ContextReceivedEventArgs.cs deleted file mode 100644 index c715cf1c..00000000 --- a/src/SMAPI/Events/ContextReceivedEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class ContextReceivedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The player whose metadata was received. - public IMultiplayerPeer Peer { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player to whom a connection is being established. - internal ContextReceivedEventArgs(IMultiplayerPeer peer) - { - this.Peer = peer; - } - } -} diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index 91e0789c..4a31f48e 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,10 +5,13 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - event EventHandler ContextReceived; + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + event EventHandler PeerContextReceived; /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; + + /// Raised after the connection with a peer is severed. + event EventHandler PeerDisconnected; } } diff --git a/src/SMAPI/Events/PeerContextReceivedEventArgs.cs b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs new file mode 100644 index 00000000..151a295c --- /dev/null +++ b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer whose metadata was received. + internal PeerContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Events/PeerDisconnectedEventArgs.cs b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs new file mode 100644 index 00000000..8517988a --- /dev/null +++ b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerDisconnectedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer who disconnected. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer who disconnected. + internal PeerDisconnectedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 63ac17ee..b9d1c453 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -101,12 +101,15 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - public readonly ManagedEvent ContextReceived; + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public readonly ManagedEvent PeerContextReceived; /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; + /// Raised after the connection with a peer is severed. + public readonly ManagedEvent PeerDisconnected; + /**** ** Player ****/ @@ -383,8 +386,9 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); - this.ContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ContextReceived)); + this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.PeerDisconnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 432a92d3..152c4e0c 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public event EventHandler PeerContextReceived + { + add => this.EventManager.PeerContextReceived.Add(value); + remove => this.EventManager.PeerContextReceived.Remove(value); + } + /// Raised after a mod message is received over the network. public event EventHandler ModMessageReceived { @@ -16,11 +23,11 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ModMessageReceived.Remove(value); } - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - public event EventHandler ContextReceived + /// Raised after the connection with a peer is severed. + public event EventHandler PeerDisconnected { - add => this.EventManager.ContextReceived.Add(value); - remove => this.EventManager.ContextReceived.Remove(value); + add => this.EventManager.PeerDisconnected.Add(value); + remove => this.EventManager.PeerDisconnected.Remove(value); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 9f649639..2d0f8b9b 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -140,6 +140,9 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + resume(); } @@ -149,6 +152,9 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + switch (message.MessageType) { // sync mod context (step 1) @@ -171,6 +177,8 @@ 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) + this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) { @@ -213,7 +221,7 @@ namespace StardewModdingAPI.Framework } // raise event - this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(newPeer)); + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer)); } break; @@ -248,8 +256,8 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { - if (message.MessageType != Multiplayer.farmerDelta && message.MessageType != Multiplayer.locationDelta && message.MessageType != Multiplayer.teamDelta && message.MessageType != Multiplayer.worldDelta) - this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Alert); + if (this.VerboseLogging) + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) { @@ -315,8 +323,12 @@ namespace StardewModdingAPI.Framework { foreach (long playerID in this.DisconnectingFarmers) { - this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); - this.Peers.Remove(playerID); + if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) + { + this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); + this.Peers.Remove(playerID); + this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer)); + } } base.removeDisconnectedFarmers(); @@ -397,7 +409,7 @@ namespace StardewModdingAPI.Framework /// Save a received peer. /// The peer to add. /// Whether to track the peer as the host if applicable. - /// Whether to raise the event. + /// Whether to raise the event. private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true) { // store @@ -407,7 +419,7 @@ namespace StardewModdingAPI.Framework // raise event if (raiseEvent) - this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(peer)); + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer)); } /// Read the metadata context for a player. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 09ecddcd..8c12c1f9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,7 +85,6 @@ - @@ -137,6 +136,8 @@ + + -- cgit From 0717f926d879881e1f510a34942163bf275807dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:41:58 -0500 Subject: fix unit tests --- src/SMAPI.Tests/Core/ModResolverTests.cs | 1 + src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index a38621f8..51b839df 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -513,6 +513,7 @@ namespace StardewModdingAPI.Tests.Core mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); mod.Setup(p => p.Manifest).Returns(manifest); + mod.Setup(p => p.HasID(It.IsAny())).Returns((string id) => manifest.UniqueID == id); if (allowStatusChange) { mod diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 3ff70d64..0264f61f 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -42,7 +42,9 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded).SetStatus(status, folder.ManifestParseError ?? "disabled by dot convention"); + + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded) + .SetStatus(status, !folder.ShouldBeLoaded ? "disabled by dot convention" : folder.ManifestParseError); } } -- cgit From 724d3fccb1c7a5ec842314436126271c5697129a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:48:55 -0500 Subject: update mod compatibility page URLs --- src/SMAPI.Web/Controllers/ModsApiController.cs | 10 +++++----- src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs | 2 +- src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/appsettings.json | 2 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/SCore.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 6e517a97..05568d5e 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -47,8 +47,8 @@ namespace StardewModdingAPI.Web.Controllers /// The internal mod metadata list. private readonly ModDatabase ModDatabase; - /// The web URL for the wiki compatibility list. - private readonly string WikiCompatibilityPageUrl; + /// The web URL for the compatibility list. + private readonly string CompatibilityPageUrl; /********* @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Web.Controllers { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; - this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl; + this.CompatibilityPageUrl = config.CompatibilityPageUrl; this.Cache = cache; this.SuccessCacheMinutes = config.SuccessCacheMinutes; @@ -163,7 +163,7 @@ namespace StardewModdingAPI.Web.Controllers // get unofficial version if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, this.WikiCompatibilityPageUrl); + result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Web.Controllers if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) - ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, this.WikiCompatibilityPageUrl) + ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") : null; } else diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 5eef7c55..bde566c0 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -17,6 +17,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels public string SemanticVersionRegex { get; set; } /// The web URL for the wiki compatibility list. - public string WikiCompatibilityPageUrl { get; set; } + public string CompatibilityPageUrl { get; set; } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 82abf17d..60a16053 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -154,7 +154,7 @@ namespace StardewModdingAPI.Web // shortcut redirects redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1")); - redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility")); + redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://mods.smapi.io")); redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI")); diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 401b885f..aba8c448 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -48,6 +48,6 @@ "SuccessCacheMinutes": 60, "ErrorCacheMinutes": 5, "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", - "WikiCompatibilityPageUrl": "https://smapi.io/compat" + "CompatibilityPageUrl": "https://mods.smapi.io" } } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 0264f61f..ace84054 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModLoading updateUrls.Add(mod.DataRecord.AlternativeUrl); // default update URL - updateUrls.Add("https://smapi.io/compat"); + updateUrls.Add("https://mods.smapi.io"); // build error string error = $"{reasonPhrase}. Please check for a "; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f078acba..5f2e90aa 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -942,7 +942,7 @@ namespace StardewModdingAPI.Framework } catch (IncompatibleInstructionException) // details already in trace logs { - string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); + string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://mods.smapi.io" }.Where(p => p != null).ToArray(); errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."; return false; } -- cgit From 49e944d06fa7040889b569b62cb0025c057834df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:52:59 -0500 Subject: update HTTP URLs to HTTPS --- .editorconfig | 2 +- docs/README.md | 2 +- docs/release-notes.md | 6 +++--- docs/technical-docs.md | 4 ++-- src/SMAPI.Installer/unix-install.sh | 2 +- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/SemanticVersion.cs | 2 +- src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src/SMAPI/Framework') 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 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 'src/SMAPI/Framework') 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 60237f4d39eef619d030bd8f3334a13cbd2caffe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 02:07:23 -0500 Subject: fix error if peer.GetMod is called on a peer who doesn't have SMAPI (#480) --- src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index e703dbb1..7f0fa4f7 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -112,7 +112,7 @@ namespace StardewModdingAPI.Framework.Networking /// Returns the mod info, or null if the player doesn't have that mod. public IMultiplayerPeerMod GetMod(string id) { - if (string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id) || this.Mods == null || !this.Mods.Any()) return null; id = id.Trim(); -- cgit From 0b03b4f16a446e4b139efd33f6378598ae66efee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 12:04:20 -0500 Subject: deprecate old manifest version format --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 6 ++++++ src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 7 ++++++- .../Serialisation/Converters/SemanticVersionConverter.cs | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 57fb33a4..4e2d7cab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -48,6 +48,7 @@ * Fixed `Context.IsPlayerFree` being true before the player finishes transitioning to a new location in multiplayer. * Suppressed the game's 'added crickets' debug output. * Updated dependencies (Harmony 1.0.9.1 → 1.2.0.1, Mono.Cecil 0.10 → 0.10.1). + * **Deprecation:** non-string manifest versions are now deprecated and will no longer work in SMAPI 3.0. * **Breaking change:** `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods. * **Breaking change:** most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mod releases, but you'll need to update the build config NuGet package. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 3e8766c2..6cb564ce 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -895,6 +895,12 @@ namespace StardewModdingAPI.Framework return false; } + // add deprecation warning for old version format + { + if (mod.Manifest?.Version is Toolkit.SemanticVersion version && version.IsLegacyFormat) + this.DeprecationManager.Warn(mod.DisplayName, "non-string manifest version", "2.8", DeprecationLevel.Notice); + } + // validate dependencies // Although dependences are validated before mods are loaded, a dependency may have failed to load. if (mod.Manifest.Dependencies?.Any() == true) diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index 32fea3d7..de33df2f 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -42,6 +42,9 @@ namespace StardewModdingAPI.Toolkit /// An optional prerelease tag. public string Build { get; } + /// Whether the version was parsed from the legacy object format. + public bool IsLegacyFormat { get; } + /********* ** Public methods @@ -51,12 +54,14 @@ namespace StardewModdingAPI.Toolkit /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible fixes. /// An optional prerelease tag. - public SemanticVersion(int major, int minor, int patch, string tag = null) + /// Whether the version was parsed from the legacy object format. + public SemanticVersion(int major, int minor, int patch, string tag = null, bool isLegacyFormat = false) { this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; this.Build = this.GetNormalisedTag(tag); + this.IsLegacyFormat = isLegacyFormat; this.AssertValid(); } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 070f2c97..e0e185c9 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters if (build == "0") build = null; // '0' from incorrect examples in old SMAPI documentation - return new SemanticVersion(major, minor, patch, build); + return new SemanticVersion(major, minor, patch, build, isLegacyFormat: true); } /// Read a JSON string. -- cgit From c0738296273304e71183ddc537aaf9ffc6d3da46 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 12:17:53 -0500 Subject: deprecate version build field --- docs/release-notes.md | 9 ++++++--- src/SMAPI/Framework/SCore.cs | 9 +++++---- src/SMAPI/SemanticVersion.cs | 17 ++++++++++++++++- .../ISemanticVersion.cs | 4 ++++ src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 10 +++++++--- 5 files changed, 38 insertions(+), 11 deletions(-) (limited to 'src/SMAPI/Framework') 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 a379726ad9dbc8a8b85dd698a742343a9a163e46 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 12:36:38 -0500 Subject: print deprecation messages in batches for easier reading --- src/SMAPI/Framework/DeprecationManager.cs | 49 ++++++++++++++++++++----------- src/SMAPI/Framework/DeprecationWarning.cs | 38 ++++++++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SGame.cs | 9 +++++- src/SMAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 src/SMAPI/Framework/DeprecationWarning.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index 7a824a05..0fde67ee 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace StardewModdingAPI.Framework { @@ -18,6 +19,9 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. private readonly ModRegistry ModRegistry; + /// The queued deprecation warnings to display. + private readonly IList QueuedWarnings = new List(); + /********* ** Public methods @@ -51,29 +55,40 @@ namespace StardewModdingAPI.Framework if (!this.MarkWarned(source ?? "", nounPhrase, version)) return; - // build message - string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase} is deprecated since SMAPI {version})."; - if (source == null) - message += $"{Environment.NewLine}{Environment.StackTrace}"; + // queue warning + this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity)); + } - // log message - switch (severity) + /// Print any queued messages. + public void PrintQueued() + { + foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase)) { - case DeprecationLevel.Notice: - this.Monitor.Log(message, LogLevel.Trace); - break; + // build message + string message = $"{warning.ModName ?? "An unknown mod"} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; + if (warning.ModName == null) + message += $"{Environment.NewLine}{Environment.StackTrace}"; + + // log message + switch (warning.Level) + { + case DeprecationLevel.Notice: + this.Monitor.Log(message, LogLevel.Trace); + break; - case DeprecationLevel.Info: - this.Monitor.Log(message, LogLevel.Debug); - break; + case DeprecationLevel.Info: + this.Monitor.Log(message, LogLevel.Debug); + break; - case DeprecationLevel.PendingRemoval: - this.Monitor.Log(message, LogLevel.Warn); - break; + case DeprecationLevel.PendingRemoval: + this.Monitor.Log(message, LogLevel.Warn); + break; - default: - throw new NotSupportedException($"Unknown deprecation level '{severity}'"); + default: + throw new NotSupportedException($"Unknown deprecation level '{warning.Level}'."); + } } + this.QueuedWarnings.Clear(); } /// Mark a deprecation warning as already logged. diff --git a/src/SMAPI/Framework/DeprecationWarning.cs b/src/SMAPI/Framework/DeprecationWarning.cs new file mode 100644 index 00000000..25415012 --- /dev/null +++ b/src/SMAPI/Framework/DeprecationWarning.cs @@ -0,0 +1,38 @@ +namespace StardewModdingAPI.Framework +{ + /// A deprecation warning for a mod. + internal class DeprecationWarning + { + /********* + ** Accessors + *********/ + /// The affected mod's display name. + public string ModName { get; } + + /// A noun phrase describing what is deprecated. + public string NounPhrase { get; } + + /// The SMAPI version which deprecated it. + public string Version { get; } + + /// The deprecation level for the affected code. + public DeprecationLevel Level { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The affected mod's display name. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// The deprecation level for the affected code. + public DeprecationWarning(string modName, string nounPhrase, string version, DeprecationLevel level) + { + this.ModName = modName; + this.NounPhrase = nounPhrase; + this.Version = version; + this.Level = level; + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 7567c6db..ee175a1b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -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.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, this.Settings.VerboseLogging); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c7f5962f..04db9632 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -54,6 +54,9 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. private readonly ModRegistry ModRegistry; + /// Manages deprecation warnings. + private readonly DeprecationManager DeprecationManager; + /// Whether SMAPI should log more information about the game context. private readonly bool VerboseLogging; @@ -137,10 +140,11 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. + /// 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, 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, bool verboseLogging) { SGame.ConstructorHack = null; @@ -157,6 +161,7 @@ namespace StardewModdingAPI.Framework this.Events = eventManager; this.ModRegistry = modRegistry; this.Reflection = reflection; + this.DeprecationManager = deprecationManager; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; this.VerboseLogging = verboseLogging; @@ -237,6 +242,8 @@ namespace StardewModdingAPI.Framework { try { + this.DeprecationManager.PrintQueued(); + /********* ** Special cases *********/ diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f606ef44..7f6444d1 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -163,6 +163,7 @@ + -- 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 'src/SMAPI/Framework') 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 10773c088a475cd7adee45caaf598459669337e8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Nov 2018 18:52:24 -0500 Subject: fix console output during early game init not being logged This doesn't affect any current output, it only affects test code. --- src/SMAPI/Framework/SCore.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 7e5b6e02..890058b0 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -136,6 +136,10 @@ namespace StardewModdingAPI.Framework this.EventManager = new EventManager(this.Monitor, this.ModRegistry); 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); + // inject deprecation managers SemanticVersion.DeprecationManager = this.DeprecationManager; @@ -344,10 +348,6 @@ namespace StardewModdingAPI.Framework /// Initialise SMAPI and mods after the game starts. private void InitialiseAfterGameStart() { - // redirect direct console output - if (this.MonitorForGame.WriteToConsole) - this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); - // add headers if (this.Settings.DeveloperMode) this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); -- cgit From 90ecd377c88ba5a4c08a3c7e67618435358e5685 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 00:11:09 -0500 Subject: rework multiplayer code to allow for upcoming Galaxy server overrides (#480) --- src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 60 +++-------------------- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 59 +++------------------- src/SMAPI/Framework/SMultiplayer.cs | 42 ++++++---------- src/SMAPI/Patches/LidgrenServerPatch.cs | 30 ------------ 4 files changed, 28 insertions(+), 163 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index 7f0fa4f7..44a71978 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Lidgren.Network; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -12,14 +11,8 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - /// The server through which to send messages, if this is an incoming farmhand. - private readonly SLidgrenServer Server; - - /// The client through which to send messages, if this is the host player. - private readonly SLidgrenClient Client; - - /// The network connection to the player. - private readonly NetConnection ServerConnection; + /// A method which sends a message to the peer. + private readonly Action SendMessageImpl; /********* @@ -53,11 +46,9 @@ namespace StardewModdingAPI.Framework.Networking /// Construct an instance. /// The player's unique ID. /// The metadata to copy. - /// The server through which to send messages. - /// The server connection through which to send messages. - /// The client through which to send messages. + /// A method which sends a message to the peer. /// Whether this is a connection to the host player. - public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client, bool isHost) + public MultiplayerPeer(long playerID, RemoteContextModel model, Action sendMessage, bool isHost) { this.PlayerID = playerID; this.IsHost = isHost; @@ -68,43 +59,7 @@ namespace StardewModdingAPI.Framework.Networking this.ApiVersion = model.ApiVersion; this.Mods = model.Mods.Select(mod => new MultiplayerPeerMod(mod)).ToArray(); } - this.Server = server; - this.ServerConnection = serverConnection; - this.Client = client; - } - - /// Construct an instance for a connection to an incoming farmhand. - /// The player's unique ID. - /// The metadata to copy, if available. - /// The server through which to send messages. - /// The server connection through which to send messages. - public static MultiplayerPeer ForConnectionToFarmhand(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection) - { - return new MultiplayerPeer( - playerID: playerID, - model: model, - server: server, - serverConnection: serverConnection, - client: null, - isHost: false - ); - } - - /// Construct an instance for a connection to the host player. - /// The player's unique ID. - /// The metadata to copy. - /// The client through which to send messages. - /// Whether this connection is for the host player. - public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client, bool isHost) - { - return new MultiplayerPeer( - playerID: playerID, - model: model, - server: null, - serverConnection: null, - client: client, - isHost: isHost - ); + this.SendMessageImpl = sendMessage; } /// Get metadata for a mod installed by the player. @@ -123,10 +78,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to send. public void SendMessage(OutgoingMessage message) { - if (this.IsHost) - this.Client.sendMessage(message); - else - this.Server.SendMessage(this.ServerConnection, message); + this.SendMessageImpl(message); } } } diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 060b433b..36f96bc3 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -20,24 +20,15 @@ namespace StardewModdingAPI.Framework.Networking /// The constructor for the internal NetBufferReadStream type. private readonly ConstructorInfo NetBufferReadStreamConstructor = SLidgrenServer.GetNetBufferReadStreamConstructor(); - /// The constructor for the internal NetBufferWriteStream type. - private readonly ConstructorInfo NetBufferWriteStreamConstructor = SLidgrenServer.GetNetBufferWriteStreamConstructor(); - /// A method which reads farmer data from the given binary reader. private readonly Func ReadFarmer; - /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. - private readonly Action OnProcessingMessage; - - /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, target player ID, and a callback to run the default logic. - private readonly Action OnSendingMessage; + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; /// The peer connections. private readonly Bimap Peers; - /// The underlying net server. - private readonly IReflectedField Server; - /********* ** Public methods @@ -46,34 +37,13 @@ namespace StardewModdingAPI.Framework.Networking /// The underlying game server. /// Simplifies access to private code. /// A method which reads farmer data from the given binary reader. - /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. - /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, and a callback to run the default logic. - public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action onProcessingMessage, Action onSendingMessage) + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action, Action> onProcessingMessage) : base(gameServer) { this.ReadFarmer = readFarmer; this.OnProcessingMessage = onProcessingMessage; - this.OnSendingMessage = onSendingMessage; this.Peers = reflection.GetField>(this, "peers").GetValue(); - this.Server = reflection.GetField(this, "server"); - } - - /// Send a message to a remote server. - /// The network connection. - /// The message to send. - /// This is an implementation of which calls . This method is invoked via . - public void SendMessage(NetConnection connection, OutgoingMessage message) - { - this.OnSendingMessage(this, connection, message, () => - { - NetServer server = this.Server.GetValue(); - NetOutgoingMessage netMessage = server.CreateMessage(); - using (Stream bufferWriteStream = (Stream)this.NetBufferWriteStreamConstructor.Invoke(new object[] { netMessage })) - using (BinaryWriter writer = new BinaryWriter(bufferWriteStream)) - message.Write(writer); - - server.SendMessage(netMessage, connection, NetDeliveryMethod.ReliableOrdered); - }); } /// Parse a data message from a client. @@ -91,14 +61,14 @@ namespace StardewModdingAPI.Framework.Networking while (rawMessage.LengthBits - rawMessage.Position >= 8) { message.Read(reader); - this.OnProcessingMessage(this, rawMessage, message, () => + this.OnProcessingMessage(message, outgoing => this.sendMessage(rawMessage.SenderConnection, outgoing), () => { if (this.Peers.ContainsLeft(message.FarmerID) && this.Peers[message.FarmerID] == peer) this.gameServer.processIncomingMessage(message); else if (message.MessageType == Multiplayer.playerIntroduction) { NetFarmerRoot farmer = this.ReadFarmer(message.Reader); - this.gameServer.checkFarmhandRequest("", farmer, msg => this.SendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); } }); } @@ -127,22 +97,5 @@ namespace StardewModdingAPI.Framework.Networking return constructor; } - - /// Get the constructor for the internal NetBufferWriteStream type. - private static ConstructorInfo GetNetBufferWriteStreamConstructor() - { - // get type - string typeName = $"StardewValley.Network.NetBufferWriteStream, {Constants.GameAssemblyName}"; - Type type = Type.GetType(typeName); - if (type == null) - throw new InvalidOperationException($"Can't find type: {typeName}"); - - // get constructor - ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); - if (constructor == null) - throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); - - return constructor; - } } } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index dc9b8b68..843cd1fb 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Lidgren.Network; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StardewModdingAPI.Events; @@ -119,26 +118,18 @@ namespace StardewModdingAPI.Framework /// The server to initialise. public override Server InitServer(Server server) { - if (server is LidgrenServer) + switch (server) { - IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage, this.OnServerSendingMessage); - } + case LidgrenServer _: + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); + } - return server; - } - /// A callback raised when sending a network message as the host player. - /// The server sending the message. - /// The connection to which a message is being sent. - /// The message being sent. - /// Send the underlying message. - protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); - - resume(); + default: + return server; + } } /// A callback raised when sending a message as a farmhand. @@ -166,11 +157,10 @@ namespace StardewModdingAPI.Framework } /// Process an incoming network message as the host player. - /// The server instance that received the connection. - /// The raw network message that was received. /// The message to process. + /// A method which sends the given message to the client. /// Process the message using the game's default logic. - public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) + public void OnServerProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) { if (this.Monitor.IsVerbose) this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); @@ -185,7 +175,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); // store peer - MultiplayerPeer newPeer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, server, rawMessage.SenderConnection); + MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false); if (this.Peers.ContainsKey(message.FarmerID)) { this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); @@ -226,7 +216,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID)) { this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - MultiplayerPeer peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false); this.AddPeer(peer, canBeHost: false); } @@ -264,7 +254,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); // store peer - MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client, model?.IsHost ?? this.HostPeer == null); + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, client.sendMessage, isHost: model?.IsHost ?? this.HostPeer == null); if (peer.IsHost && this.HostPeer != null) { this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); @@ -281,7 +271,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.AddPeer(MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true), canBeHost: false); + this.AddPeer(new MultiplayerPeer(message.FarmerID, null, client.sendMessage, isHost: true), canBeHost: false); } resume(); break; @@ -293,7 +283,7 @@ namespace StardewModdingAPI.Framework // store peer if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); + peer = new MultiplayerPeer(message.FarmerID, null, client.sendMessage, isHost: this.HostPeer == null); this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); this.AddPeer(peer, canBeHost: true); } diff --git a/src/SMAPI/Patches/LidgrenServerPatch.cs b/src/SMAPI/Patches/LidgrenServerPatch.cs index 6f937665..47acd4c4 100644 --- a/src/SMAPI/Patches/LidgrenServerPatch.cs +++ b/src/SMAPI/Patches/LidgrenServerPatch.cs @@ -1,13 +1,9 @@ -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Reflection; using Harmony; using Lidgren.Network; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Patching; -using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Patches @@ -35,13 +31,6 @@ namespace StardewModdingAPI.Patches MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); harmony.Patch(method, new HarmonyMethod(prefix), null); } - - // override sendMessage - { - MethodInfo method = typeof(LidgrenServer).GetMethod("sendMessage", BindingFlags.NonPublic | BindingFlags.Instance, null, new [] { typeof(NetConnection), typeof(OutgoingMessage) }, null); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_SendMessage)); - harmony.Patch(method, new HarmonyMethod(prefix), null); - } } @@ -66,24 +55,5 @@ namespace StardewModdingAPI.Patches return true; } - - /// The method to call instead of the method. - /// The instance being patched. - /// The connection to which to send the message. - /// The private peers field on the instance. - /// The private gameServer field on the instance. - /// 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_LidgrenServer_SendMessage(LidgrenServer __instance, NetConnection connection, OutgoingMessage message, Bimap ___peers, IGameServer ___gameServer) - { - if (__instance is SLidgrenServer smapiServer) - { - smapiServer.SendMessage(connection, message); - return false; - } - - return true; - } } } -- cgit From 77a7a0fe58186e49e24c41f47a2a33661afbc81c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 00:35:36 -0500 Subject: rework multiplayer code to allow for upcoming Galaxy client overrides (#480) --- src/SMAPI/Framework/Networking/SLidgrenClient.cs | 19 ++++++------- src/SMAPI/Framework/SMultiplayer.cs | 34 +++++++++++++----------- 2 files changed, 29 insertions(+), 24 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs index c05e6b76..02d9d68f 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenClient.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs @@ -9,20 +9,21 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. - private readonly Action OnProcessingMessage; + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + private readonly Action, Action> OnSendingMessage; - /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. - private readonly Action OnSendingMessage; /********* ** Public methods *********/ /// Construct an instance. /// The remote address being connected. - /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. - /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. - public SLidgrenClient(string address, Action onProcessingMessage, Action onSendingMessage) + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + public SLidgrenClient(string address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) : base(address) { this.OnProcessingMessage = onProcessingMessage; @@ -32,7 +33,7 @@ namespace StardewModdingAPI.Framework.Networking /// Send a message to the connected peer. public override void sendMessage(OutgoingMessage message) { - this.OnSendingMessage(this, message, () => base.sendMessage(message)); + this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); } @@ -43,7 +44,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to process. protected override void processIncomingMessage(IncomingMessage message) { - this.OnProcessingMessage(this, message, () => base.processIncomingMessage(message)); + this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); } } } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 843cd1fb..6ce7596d 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -105,13 +105,17 @@ namespace StardewModdingAPI.Framework /// The client to initialise. public override Client InitClient(Client client) { - if (client is LidgrenClient) + switch (client) { - string address = this.Reflection.GetField(client, "address").GetValue(); - return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); - } + case LidgrenClient _: + { + string address = this.Reflection.GetField(client, "address").GetValue(); + return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); + } - return client; + default: + return client; + } } /// Initialise a server before the game connects to an incoming player. @@ -133,10 +137,10 @@ namespace StardewModdingAPI.Framework } /// A callback raised when sending a message as a farmhand. - /// The client sending the message. /// The message being sent. - /// Send the underlying message. - protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) + /// Send an arbitrary message through the client. + /// Resume sending the underlying message. + protected void OnClientSendingMessage(OutgoingMessage message, Action sendMessage, Action resume) { if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); @@ -145,7 +149,7 @@ namespace StardewModdingAPI.Framework { // sync mod context (step 1) case (byte)MessageType.PlayerIntroduction: - client.sendMessage((byte)MessageType.ModContext, this.GetContextSyncMessageFields()); + sendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); resume(); break; @@ -235,11 +239,11 @@ namespace StardewModdingAPI.Framework } /// Process an incoming network message as a farmhand. - /// The client instance that received the connection. /// The message to process. - /// Process the message using the game's default logic. + /// Send an arbitrary message through the client. + /// Resume processing the message using the game's default logic. /// Returns whether the message was handled. - public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) + public void OnClientProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) { if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); @@ -254,7 +258,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); // store peer - MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, client.sendMessage, isHost: model?.IsHost ?? this.HostPeer == null); + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null); if (peer.IsHost && this.HostPeer != null) { this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); @@ -271,7 +275,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.AddPeer(new MultiplayerPeer(message.FarmerID, null, client.sendMessage, isHost: true), canBeHost: false); + this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false); } resume(); break; @@ -283,7 +287,7 @@ namespace StardewModdingAPI.Framework // store peer if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - peer = new MultiplayerPeer(message.FarmerID, null, client.sendMessage, isHost: this.HostPeer == null); + peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null); this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); this.AddPeer(peer, canBeHost: true); } -- cgit From 8e1d45b3100453a11bcdf663d3567935c592098c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 00:57:52 -0500 Subject: override Galaxy client/server to support context sync (#480) This commit assumes the changes I requested in the game code to make GalaxyNetClient and GalaxyNetServer public are implemented in 1.3.32. --- src/SMAPI/Framework/Networking/SGalaxyNetClient.cs | 52 ++++++++++++ src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 96 ++++++++++++++++++++++ src/SMAPI/Framework/SMultiplayer.cs | 13 +++ src/SMAPI/StardewModdingAPI.csproj | 6 +- 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetClient.cs create mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs new file mode 100644 index 00000000..fddd423d --- /dev/null +++ b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs @@ -0,0 +1,52 @@ +using System; +using Galaxy.Api; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. + internal class SGalaxyNetClient : GalaxyNetClient + { + /********* + ** Properties + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + private readonly Action, Action> OnSendingMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The remote address being connected. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + public SGalaxyNetClient(GalaxyID address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) + : base(address) + { + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; + } + + /// Send a message to the connected peer. + public override void sendMessage(OutgoingMessage message) + { + this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); + } + + + /********* + ** Protected methods + *********/ + /// Process an incoming network message. + /// The message to process. + protected override void processIncomingMessage(IncomingMessage message) + { + this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); + } + } +} diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs new file mode 100644 index 00000000..99eae8ad --- /dev/null +++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using Galaxy.Api; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SGalaxyNetServer : GalaxyNetServer + { + /********* + ** Properties + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// The peer connections. + private readonly Bimap Peers; + + /// The underlying net server. + private readonly IReflectedField Server; + + /// The underlying method which handles incoming connections. + private readonly Action BaseReceiveConnection; + + /// The underlying method which handles incoming disconnections. + private readonly Action BaseReceiveDisconnect; + + /// The underlying method which handles incoming errors. + private readonly Action BaseReceiveError; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + /// Simplifies access to private code. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SGalaxyNetServer(IGameServer gameServer, Reflector reflection, Action, Action> onProcessingMessage) + : base(gameServer) + { + this.OnProcessingMessage = onProcessingMessage; + this.Peers = reflection.GetField>(this, "peers").GetValue(); + this.Server = reflection.GetField(this, "server"); + + this.BaseReceiveConnection = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveConnection").MethodInfo); + this.BaseReceiveDisconnect = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveDisconnect").MethodInfo); + this.BaseReceiveError = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveError").MethodInfo); + } + + /// Receive and process messages from the client. + public override void receiveMessages() + { + GalaxySocket server = this.Server.GetValue(); + if (server == null) + return; + + server.Receive(this.BaseReceiveConnection, this.OnReceiveMessage, this.BaseReceiveDisconnect, this.BaseReceiveError); + server.Heartbeat(server.LobbyMembers()); + foreach (GalaxyID connection in server.Connections) + { + if (server.GetPingWith(connection) > 30000L) + server.Kick(connection); + } + } + + /// Read and process a message from the client. + /// The Galaxy peer ID. + /// The data to process. + private void OnReceiveMessage(GalaxyID peerID, Stream data) + { + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(data)) + { + message.Read(reader); + this.OnProcessingMessage(message, outgoing => this.sendMessage(peerID, outgoing), () => + { + if (this.Peers.ContainsLeft(message.FarmerID) && (long)this.Peers[message.FarmerID] == (long)peerID.ToUint64()) + { + this.gameServer.processIncomingMessage(message); + } + else if (message.MessageType == Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = Game1.multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peerID.ToUint64()); + this.gameServer.checkFarmhandRequest(Convert.ToString(peerID.ToUint64()), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + } + } +} diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 6ce7596d..5a8aa3e5 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Galaxy.Api; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StardewModdingAPI.Events; @@ -11,6 +12,7 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.Network; +using StardewValley.SDKs; namespace StardewModdingAPI.Framework { @@ -113,6 +115,12 @@ namespace StardewModdingAPI.Framework return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); } + case GalaxyNetClient _: + { + GalaxyID address = this.Reflection.GetField(client, "lobbyId").GetValue(); + return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); + } + default: return client; } @@ -130,6 +138,11 @@ namespace StardewModdingAPI.Framework return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); } + case GalaxyNetServer _: + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); + } default: return server; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 471e98fc..29c9f7fa 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -55,7 +55,7 @@ - + @@ -174,13 +174,15 @@ - + + + -- cgit From 15acbc8f230dd2c4ba394960cdcc12a22a831bbf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 01:36:43 -0500 Subject: patch GalaxyNetServer to support context sync pending game code changes to make it public (#480) --- src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 96 ------------------- src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Framework/SMultiplayer.cs | 10 +- src/SMAPI/Patches/GalaxyNetServerPatch.cs | 106 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 2 +- 5 files changed, 114 insertions(+), 103 deletions(-) delete mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs create mode 100644 src/SMAPI/Patches/GalaxyNetServerPatch.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs deleted file mode 100644 index 99eae8ad..00000000 --- a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.IO; -using Galaxy.Api; -using StardewModdingAPI.Framework.Reflection; -using StardewValley; -using StardewValley.Network; -using StardewValley.SDKs; - -namespace StardewModdingAPI.Framework.Networking -{ - /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. - internal class SGalaxyNetServer : GalaxyNetServer - { - /********* - ** Properties - *********/ - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - private readonly Action, Action> OnProcessingMessage; - - /// The peer connections. - private readonly Bimap Peers; - - /// The underlying net server. - private readonly IReflectedField Server; - - /// The underlying method which handles incoming connections. - private readonly Action BaseReceiveConnection; - - /// The underlying method which handles incoming disconnections. - private readonly Action BaseReceiveDisconnect; - - /// The underlying method which handles incoming errors. - private readonly Action BaseReceiveError; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying game server. - /// Simplifies access to private code. - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - public SGalaxyNetServer(IGameServer gameServer, Reflector reflection, Action, Action> onProcessingMessage) - : base(gameServer) - { - this.OnProcessingMessage = onProcessingMessage; - this.Peers = reflection.GetField>(this, "peers").GetValue(); - this.Server = reflection.GetField(this, "server"); - - this.BaseReceiveConnection = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveConnection").MethodInfo); - this.BaseReceiveDisconnect = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveDisconnect").MethodInfo); - this.BaseReceiveError = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveError").MethodInfo); - } - - /// Receive and process messages from the client. - public override void receiveMessages() - { - GalaxySocket server = this.Server.GetValue(); - if (server == null) - return; - - server.Receive(this.BaseReceiveConnection, this.OnReceiveMessage, this.BaseReceiveDisconnect, this.BaseReceiveError); - server.Heartbeat(server.LobbyMembers()); - foreach (GalaxyID connection in server.Connections) - { - if (server.GetPingWith(connection) > 30000L) - server.Kick(connection); - } - } - - /// Read and process a message from the client. - /// The Galaxy peer ID. - /// The data to process. - private void OnReceiveMessage(GalaxyID peerID, Stream data) - { - using (IncomingMessage message = new IncomingMessage()) - using (BinaryReader reader = new BinaryReader(data)) - { - message.Read(reader); - this.OnProcessingMessage(message, outgoing => this.sendMessage(peerID, outgoing), () => - { - if (this.Peers.ContainsLeft(message.FarmerID) && (long)this.Peers[message.FarmerID] == (long)peerID.ToUint64()) - { - this.gameServer.processIncomingMessage(message); - } - else if (message.MessageType == Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = Game1.multiplayer.readFarmer(message.Reader); - GalaxyID capturedPeer = new GalaxyID(peerID.ToUint64()); - this.gameServer.checkFarmhandRequest(Convert.ToString(peerID.ToUint64()), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); - } - }); - } - } - } -} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 890058b0..6ad118ce 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -170,7 +170,8 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new LidgrenServerPatch() + new LidgrenServerPatch(), + new GalaxyNetServerPatch(() => this.GameInstance.Multiplayer) ); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 5a8aa3e5..1777a261 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -138,11 +138,11 @@ namespace StardewModdingAPI.Framework return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); } - case GalaxyNetServer _: - { - IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); - } + //case GalaxyNetServer _: + // { + // IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + // return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); + // } default: return server; diff --git a/src/SMAPI/Patches/GalaxyNetServerPatch.cs b/src/SMAPI/Patches/GalaxyNetServerPatch.cs new file mode 100644 index 00000000..e01ac329 --- /dev/null +++ b/src/SMAPI/Patches/GalaxyNetServerPatch.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using Galaxy.Api; +using Harmony; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Patching; +using StardewValley.Network; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch to let SMAPI override methods. + internal class GalaxyNetServerPatch : IHarmonyPatch + { + /********* + ** Properties + *********/ + /// SMAPI's implementation of the game's core multiplayer logic. + private static Lazy Multiplayer; + + /// The name of the internal GalaxyNetServer class. + private static readonly string ServerTypeName = $"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"; + + /// The method which sends an arbitrary message. + private static MethodInfo SendMessageMethod; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(GalaxyNetServerPatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's implementation of the game's core multiplayer logic. + public GalaxyNetServerPatch(Func multiplayer) + { + // init + GalaxyNetServerPatch.Multiplayer = new Lazy(multiplayer); + + // get server.sendMessage method + Type type = Type.GetType(GalaxyNetServerPatch.ServerTypeName); + if (type == null) + throw new InvalidOperationException($"Can't find type '{GalaxyNetServerPatch.ServerTypeName}'."); + GalaxyNetServerPatch.SendMessageMethod = type.GetMethod("sendMessage", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(GalaxyID), typeof(OutgoingMessage) }, null); + if (GalaxyNetServerPatch.SendMessageMethod == null) + throw new InvalidOperationException($"Can't find method 'sendMessage' on '{GalaxyNetServerPatch.ServerTypeName}'."); + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + // override parseDataMessageFromClient + { + MethodInfo method = AccessTools.Method(Type.GetType($"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"), "onReceiveMessage"); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GalaxyNetServerPatch.Prefix_GalaxyNetServer_OnReceiveMessage)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the method. + /// The instance being patched. + /// The Galaxy peer ID. + /// The data to process. + /// The private peers field on the instance. + /// The private gameServer field on the instance. + /// 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_GalaxyNetServer_OnReceiveMessage(Server __instance, GalaxyID peer, Stream messageStream, Bimap ___peers, IGameServer ___gameServer) + { + SMultiplayer multiplayer = GalaxyNetServerPatch.Multiplayer.Value; + + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(messageStream)) + { + message.Read(reader); + multiplayer.OnServerProcessingMessage(message, outgoing => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { peer, outgoing }), () => + { + if (___peers.ContainsLeft(message.FarmerID) && (long)___peers[message.FarmerID] == (long)peer.ToUint64()) + { + ___gameServer.processIncomingMessage(message); + } + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peer.ToUint64()); + ___gameServer.checkFarmhandRequest(Convert.ToString(peer.ToUint64()), farmer, msg => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { capturedPeer, msg }), () => ___peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + + return false; + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 29c9f7fa..f16087bc 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -182,7 +182,6 @@ - @@ -325,6 +324,7 @@ + -- cgit From 0a50cdb162d7ba1c9219d1982dccbb5828b070c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 18:18:32 -0500 Subject: update multiplayer code for Stardew Valley 1.3.22 (#480) --- src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 63 ++++++++++++ src/SMAPI/Framework/Networking/SLidgrenServer.cs | 59 +++--------- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/Framework/SMultiplayer.cs | 19 ++-- src/SMAPI/Patches/GalaxyNetServerPatch.cs | 106 --------------------- src/SMAPI/Patches/LidgrenServerPatch.cs | 59 ------------ src/SMAPI/StardewModdingAPI.csproj | 3 +- 7 files changed, 83 insertions(+), 230 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs delete mode 100644 src/SMAPI/Patches/GalaxyNetServerPatch.cs delete mode 100644 src/SMAPI/Patches/LidgrenServerPatch.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs new file mode 100644 index 00000000..2fc92737 --- /dev/null +++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Galaxy.Api; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SGalaxyNetServer : GalaxyNetServer + { + /********* + ** Properties + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// SMAPI's implementation of the game's core multiplayer logic. + private readonly SMultiplayer Multiplayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + /// SMAPI's implementation of the game's core multiplayer logic. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SGalaxyNetServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) + : base(gameServer) + { + this.Multiplayer = multiplayer; + this.OnProcessingMessage = onProcessingMessage; + } + + /// Read and process a message from the client. + /// The Galaxy peer ID. + /// The data to process. + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] + protected override void onReceiveMessage(GalaxyID peer, Stream messageStream) + { + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(messageStream)) + { + message.Read(reader); + this.OnProcessingMessage(message, outgoing => this.sendMessage(peer, outgoing), () => + { + if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peer.ToUint64()) + { + this.gameServer.processIncomingMessage(message); + } + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peer.ToUint64()); + this.gameServer.checkFarmhandRequest(Convert.ToString(peer.ToUint64()), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + } + } +} diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 36f96bc3..37e546a1 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -1,11 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reflection; using Lidgren.Network; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Patches; -using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -16,46 +12,36 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - - /// The constructor for the internal NetBufferReadStream type. - private readonly ConstructorInfo NetBufferReadStreamConstructor = SLidgrenServer.GetNetBufferReadStreamConstructor(); - - /// A method which reads farmer data from the given binary reader. - private readonly Func ReadFarmer; + /// SMAPI's implementation of the game's core multiplayer logic. + private readonly SMultiplayer Multiplayer; /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. private readonly Action, Action> OnProcessingMessage; - /// The peer connections. - private readonly Bimap Peers; - /********* ** Public methods *********/ /// Construct an instance. + /// SMAPI's implementation of the game's core multiplayer logic. /// The underlying game server. - /// Simplifies access to private code. - /// A method which reads farmer data from the given binary reader. /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action, Action> onProcessingMessage) + public SLidgrenServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) : base(gameServer) { - this.ReadFarmer = readFarmer; + this.Multiplayer = multiplayer; this.OnProcessingMessage = onProcessingMessage; - this.Peers = reflection.GetField>(this, "peers").GetValue(); } /// Parse a data message from a client. /// The raw network message to parse. - /// This is an implementation of which calls . This method is invoked via . [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] - public bool ParseDataMessageFromClient(NetIncomingMessage rawMessage) + protected override void parseDataMessageFromClient(NetIncomingMessage rawMessage) { // add hook to call multiplayer core NetConnection peer = rawMessage.SenderConnection; using (IncomingMessage message = new IncomingMessage()) - using (Stream readStream = (Stream)this.NetBufferReadStreamConstructor.Invoke(new object[] { rawMessage })) + using (Stream readStream = new NetBufferReadStream(rawMessage)) using (BinaryReader reader = new BinaryReader(readStream)) { while (rawMessage.LengthBits - rawMessage.Position >= 8) @@ -63,39 +49,16 @@ namespace StardewModdingAPI.Framework.Networking message.Read(reader); this.OnProcessingMessage(message, outgoing => this.sendMessage(rawMessage.SenderConnection, outgoing), () => { - if (this.Peers.ContainsLeft(message.FarmerID) && this.Peers[message.FarmerID] == peer) + if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) this.gameServer.processIncomingMessage(message); - else if (message.MessageType == Multiplayer.playerIntroduction) + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) { - NetFarmerRoot farmer = this.ReadFarmer(message.Reader); - this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer); } }); } } - - return false; - } - - - /********* - ** Private methods - *********/ - /// Get the constructor for the internal NetBufferReadStream type. - private static ConstructorInfo GetNetBufferReadStreamConstructor() - { - // get type - string typeName = $"StardewValley.Network.NetBufferReadStream, {Constants.GameAssemblyName}"; - Type type = Type.GetType(typeName); - if (type == null) - throw new InvalidOperationException($"Can't find type: {typeName}"); - - // get constructor - ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); - if (constructor == null) - throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); - - return constructor; } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6ad118ce..128659c7 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -169,9 +169,7 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new LidgrenServerPatch(), - new GalaxyNetServerPatch(() => this.GameInstance.Multiplayer) + new DialogueErrorPatch(this.MonitorForGame, this.Reflection) ); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 1777a261..629fce1d 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -48,9 +48,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events. private readonly EventManager EventManager; - /// The players who are currently disconnecting. - private readonly IList DisconnectingFarmers; - /// A callback to invoke when a mod message is received. private readonly Action OnModMessageReceived; @@ -83,8 +80,6 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.OnModMessageReceived = onModMessageReceived; - - this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); } /// Handle sync messages from other players and perform other initial sync logic. @@ -135,14 +130,14 @@ namespace StardewModdingAPI.Framework case LidgrenServer _: { IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); + return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage); } - //case GalaxyNetServer _: - // { - // IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - // return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); - // } + case GalaxyNetServer _: + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage); + } default: return server; @@ -323,7 +318,7 @@ namespace StardewModdingAPI.Framework /// Remove players who are disconnecting. protected override void removeDisconnectedFarmers() { - foreach (long playerID in this.DisconnectingFarmers) + foreach (long playerID in this.disconnectingFarmers) { if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) { diff --git a/src/SMAPI/Patches/GalaxyNetServerPatch.cs b/src/SMAPI/Patches/GalaxyNetServerPatch.cs deleted file mode 100644 index e01ac329..00000000 --- a/src/SMAPI/Patches/GalaxyNetServerPatch.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using Galaxy.Api; -using Harmony; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Patching; -using StardewValley.Network; - -namespace StardewModdingAPI.Patches -{ - /// A Harmony patch to let SMAPI override methods. - internal class GalaxyNetServerPatch : IHarmonyPatch - { - /********* - ** Properties - *********/ - /// SMAPI's implementation of the game's core multiplayer logic. - private static Lazy Multiplayer; - - /// The name of the internal GalaxyNetServer class. - private static readonly string ServerTypeName = $"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"; - - /// The method which sends an arbitrary message. - private static MethodInfo SendMessageMethod; - - - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(GalaxyNetServerPatch)}"; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's implementation of the game's core multiplayer logic. - public GalaxyNetServerPatch(Func multiplayer) - { - // init - GalaxyNetServerPatch.Multiplayer = new Lazy(multiplayer); - - // get server.sendMessage method - Type type = Type.GetType(GalaxyNetServerPatch.ServerTypeName); - if (type == null) - throw new InvalidOperationException($"Can't find type '{GalaxyNetServerPatch.ServerTypeName}'."); - GalaxyNetServerPatch.SendMessageMethod = type.GetMethod("sendMessage", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(GalaxyID), typeof(OutgoingMessage) }, null); - if (GalaxyNetServerPatch.SendMessageMethod == null) - throw new InvalidOperationException($"Can't find method 'sendMessage' on '{GalaxyNetServerPatch.ServerTypeName}'."); - } - - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - // override parseDataMessageFromClient - { - MethodInfo method = AccessTools.Method(Type.GetType($"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"), "onReceiveMessage"); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GalaxyNetServerPatch.Prefix_GalaxyNetServer_OnReceiveMessage)); - harmony.Patch(method, new HarmonyMethod(prefix), null); - } - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of the method. - /// The instance being patched. - /// The Galaxy peer ID. - /// The data to process. - /// The private peers field on the instance. - /// The private gameServer field on the instance. - /// 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_GalaxyNetServer_OnReceiveMessage(Server __instance, GalaxyID peer, Stream messageStream, Bimap ___peers, IGameServer ___gameServer) - { - SMultiplayer multiplayer = GalaxyNetServerPatch.Multiplayer.Value; - - using (IncomingMessage message = new IncomingMessage()) - using (BinaryReader reader = new BinaryReader(messageStream)) - { - message.Read(reader); - multiplayer.OnServerProcessingMessage(message, outgoing => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { peer, outgoing }), () => - { - if (___peers.ContainsLeft(message.FarmerID) && (long)___peers[message.FarmerID] == (long)peer.ToUint64()) - { - ___gameServer.processIncomingMessage(message); - } - else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); - GalaxyID capturedPeer = new GalaxyID(peer.ToUint64()); - ___gameServer.checkFarmhandRequest(Convert.ToString(peer.ToUint64()), farmer, msg => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { capturedPeer, msg }), () => ___peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); - } - }); - } - - return false; - } - } -} diff --git a/src/SMAPI/Patches/LidgrenServerPatch.cs b/src/SMAPI/Patches/LidgrenServerPatch.cs deleted file mode 100644 index 47acd4c4..00000000 --- a/src/SMAPI/Patches/LidgrenServerPatch.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Harmony; -using Lidgren.Network; -using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.Patching; -using StardewValley.Network; - -namespace StardewModdingAPI.Patches -{ - /// A Harmony patch to let SMAPI override methods. - internal class LidgrenServerPatch : IHarmonyPatch - { - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(LidgrenServerPatch)}"; - - - /********* - ** Public methods - *********/ - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - // override parseDataMessageFromClient - { - MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); - harmony.Patch(method, new HarmonyMethod(prefix), null); - } - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of the method. - /// The instance being patched. - /// The raw network message to parse. - /// The private peers field on the instance. - /// The private gameServer field on the instance. - /// 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_LidgrenServer_ParseDataMessageFromClient(LidgrenServer __instance, NetIncomingMessage dataMsg, Bimap ___peers, IGameServer ___gameServer) - { - if (__instance is SLidgrenServer smapiServer) - { - smapiServer.ParseDataMessageFromClient(dataMsg); - return false; - } - - return true; - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f16087bc..70fe2bed 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -182,6 +182,7 @@ + @@ -324,8 +325,6 @@ - - -- cgit From dcfae980bf74386c624b0d059a83e95ec1aedc0b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Nov 2018 21:29:28 -0500 Subject: fix content packs always failing to load if they declare a dependency on a SMAPI mod --- docs/release-notes.md | 3 ++- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 9 ++++---- src/SMAPI/Framework/ModRegistry.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 32 +++++++++++++++-------------- 4 files changed, 26 insertions(+), 21 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1631c153..d3036135 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -44,7 +44,8 @@ * 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 content packs' `ReadJsonFile` allowing non-relative paths. + * Fixed content pack always failing to load if they declare a dependency on a SMAPI mod. * 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. diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index ae0368f0..5e190e55 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Toolkit.Serialisation; @@ -17,7 +16,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Properties *********/ /// The content packs loaded for this mod. - private readonly IContentPack[] ContentPacks; + private readonly Lazy ContentPacks; /// Create a transitional content pack. private readonly Func CreateContentPack; @@ -84,7 +83,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, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, Func contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -104,7 +103,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); - this.ContentPacks = contentPacks.ToArray(); + this.ContentPacks = new Lazy(contentPacks); this.CreateContentPack = createContentPack; this.DeprecationManager = deprecationManager; this.Events = events; @@ -204,7 +203,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Get all content packs loaded for this mod. public IEnumerable GetContentPacks() { - return this.ContentPacks; + return this.ContentPacks.Value; } /**** diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index da68fce3..8ce3172c 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -18,6 +18,9 @@ namespace StardewModdingAPI.Framework /// An assembly full name => mod lookup. private readonly IDictionary ModNamesByAssembly = new Dictionary(); + /// Whether all mod assemblies have been loaded. + public bool AreAllModsLoaded { get; set; } + /// Whether all mods have been initialised and their method called. public bool AreAllModsInitialised { get; set; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 128659c7..4b95917b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -713,15 +713,8 @@ namespace StardewModdingAPI.Framework mod.SetStatus(ModMetadataStatus.Failed, errorPhrase); } - // load content packs first (so they're available to mods) - foreach (IModMetadata contentPack in mods.Where(p => p.IsContentPack)) - { - if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) - LogSkip(contentPack, errorPhrase, errorDetails); - } - - // load SMAPI mods - foreach (IModMetadata contentPack in mods.Where(p => !p.IsContentPack)) + // load mods + foreach (IModMetadata contentPack in mods) { if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) LogSkip(contentPack, errorPhrase, errorDetails); @@ -730,6 +723,9 @@ namespace StardewModdingAPI.Framework IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); + // unlock content packs + this.ModRegistry.AreAllModsLoaded = true; + // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) @@ -972,11 +968,17 @@ namespace StardewModdingAPI.Framework return false; // get content packs - IContentPack[] contentPacks = this.ModRegistry - .GetAll(assemblyMods: false) - .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID)) - .Select(p => p.ContentPack) - .ToArray(); + IContentPack[] GetContentPacks() + { + if (!this.ModRegistry.AreAllModsLoaded) + throw new InvalidOperationException("Can't access content packs before SMAPI finishes loading mods."); + + return this.ModRegistry + .GetAll(assemblyMods: false) + .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID)) + .Select(p => p.ContentPack) + .ToArray(); + } // init mod helpers IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); @@ -998,7 +1000,7 @@ namespace StardewModdingAPI.Framework return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, GetContentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod -- cgit From d21bfc3ada2d9a1d6bf6084f7e54d49120f4d82e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Nov 2018 21:31:07 -0500 Subject: fix error when entering an incorrect console command --- src/SMAPI/Framework/SGame.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 3de97aea..75cf4c52 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -303,7 +303,10 @@ namespace StardewModdingAPI.Framework try { 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); + continue; + } } catch (Exception ex) { -- cgit From fb9ef6efda9ae9f59104c40b19fcb2daa6027297 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Nov 2018 15:24:07 -0500 Subject: fix errors in rare cases when sending a message through LidgrenClient after an error packet is received (#480) --- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 37e546a1..251e5268 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -47,7 +47,8 @@ namespace StardewModdingAPI.Framework.Networking while (rawMessage.LengthBits - rawMessage.Position >= 8) { message.Read(reader); - this.OnProcessingMessage(message, outgoing => this.sendMessage(rawMessage.SenderConnection, outgoing), () => + NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused + this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () => { if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) this.gameServer.processIncomingMessage(message); -- cgit From fe56c6396a3d0817077ca9ae3b179f322957dfc4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Nov 2018 20:39:28 -0500 Subject: remove rewrite rules only used by mods that broke in SDV 1.3 --- .../Rewriters/VirtualEntryCallRemover.cs | 90 ---------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 9 --- src/SMAPI/StardewModdingAPI.csproj | 1 - 3 files changed, 100 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs deleted file mode 100644 index 322a7df1..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites virtual calls to the method. - internal class VirtualEntryCallRemover : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The type containing the method. - private readonly Type ToType; - - /// The name of the method. - private readonly string MethodName; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public VirtualEntryCallRemover() - { - this.ToType = typeof(Mod); - this.MethodName = nameof(Mod.Entry); - this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - // get instructions comprising method call - int index = cil.Body.Instructions.IndexOf(instruction); - Instruction loadArg0 = cil.Body.Instructions[index - 2]; - Instruction loadArg1 = cil.Body.Instructions[index - 1]; - if (loadArg0.OpCode != OpCodes.Ldarg_0) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); - if (loadArg1.OpCode != OpCodes.Ldarg_1) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); - - // remove method call - cil.Remove(loadArg0); - cil.Remove(loadArg1); - cil.Remove(instruction); - return InstructionHandleResult.Rewritten; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.ToType.FullName - && methodRef.Name == this.MethodName; - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 5a47fb1a..ff8d54e3 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -34,15 +34,6 @@ namespace StardewModdingAPI.Metadata // rewrite for crossplatform compatibility new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), - // rewrite for SMAPI 2.0 - new VirtualEntryCallRemover(), - - // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces) - new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), - // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 70fe2bed..5a098b8a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -227,7 +227,6 @@ - -- cgit