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') 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) --- src/SMAPI/Framework/ContentPack.cs | 10 ++++++++++ src/SMAPI/IContentPack.cs | 8 +++++++- 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'src/SMAPI') 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) --- src/SMAPI/Framework/ContentPack.cs | 8 ++++++++ src/SMAPI/IContentPack.cs | 2 ++ 2 files changed, 10 insertions(+) (limited to 'src/SMAPI') 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. -- 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) --- 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 + 6 files changed, 240 insertions(+), 20 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/DataHelper.cs create mode 100644 src/SMAPI/IDataHelper.cs (limited to 'src/SMAPI') 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 @@ + -- 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) --- src/SMAPI/Constants.cs | 7 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 1 + src/SMAPI/Program.cs | 85 ++++++++++++++++-------- 3 files changed, 63 insertions(+), 30 deletions(-) (limited to 'src/SMAPI') 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) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') 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); } } -- 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 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'src/SMAPI') 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); } } -- 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') 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 464d6b2275f52b9db5560e242c6073fbd816bcb3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 23:41:03 -0400 Subject: fix error handling when resolving assemblies (#582) --- src/SMAPI/Program.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 64eeb45a..f5a32b4f 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -370,21 +370,21 @@ namespace StardewModdingAPI /// The event arguments. private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { - AssemblyName name = new AssemblyName(e.Name); - foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) + try { - try + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) return Assembly.LoadFrom(dll.FullName); } - catch (Exception ex) - { - throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting the smapi-lib folder and reinstalling SMAPI.", ex); - } + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error resolving assembly: {ex}"); + return null; } - - return null; } /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. -- 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') 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(SpriteSortMo