diff options
Diffstat (limited to 'src/SMAPI/Framework')
53 files changed, 3906 insertions, 623 deletions
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 *********/ - /// <summary>The friendly name for the mod that registered the command.</summary> - public string ModName { get; } + /// <summary>The mod that registered the command (or <c>null</c> if registered by SMAPI).</summary> + public IModMetadata Mod { get; } /// <summary>The command name, which the user must type to trigger it.</summary> public string Name { get; } @@ -25,13 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modName">The friendly name for the mod that registered the command.</param> + /// <param name="mod">The mod that registered the command (or <c>null</c> if registered by SMAPI).</param> /// <param name="name">The command name, which the user must type to trigger it.</param> /// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param> /// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param> - public Command(string modName, string name, string documentation, Action<string, string[]> callback) + public Command(IModMetadata mod, string name, string documentation, Action<string, string[]> 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 *********/ /// <summary>Add a console command.</summary> - /// <param name="modName">The friendly mod name for this instance.</param> + /// <param name="mod">The mod adding the command (or <c>null</c> for a SMAPI command).</param> /// <param name="name">The command name, which the user must type to trigger it.</param> /// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param> /// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param> @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework /// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception> /// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception> /// <exception cref="ArgumentException">There's already a command with that name.</exception> - public void Add(string modName, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false) + public void Add(IModMetadata mod, string name, string documentation, Action<string, string[]> 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)); } /// <summary>Get a command by its unique name.</summary> @@ -65,19 +65,30 @@ namespace StardewModdingAPI.Framework .OrderBy(p => p.Name); } - /// <summary>Trigger a command.</summary> - /// <param name="input">The raw command input.</param> - /// <returns>Returns whether a matching command was triggered.</returns> - public bool Trigger(string input) + /// <summary>Try to parse a raw line of user input into an executable command.</summary> + /// <param name="input">The raw user input.</param> + /// <param name="name">The parsed command name.</param> + /// <param name="args">The parsed command arguments.</param> + /// <param name="command">The command which can handle the input.</param> + /// <returns>Returns true if the input was successfully parsed and matched to a command; else false.</returns> + 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); } /// <summary>Trigger a command.</summary> diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 5c7b87de..f970762a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -8,6 +8,14 @@ namespace StardewModdingAPI.Framework.Content internal class AssetDataForImage : AssetData<Texture2D>, IAssetDataForImage { /********* + ** Properties + *********/ + /// <summary>The minimum value to consider non-transparent.</summary> + /// <remarks>On Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason.</remarks> + private const byte MinOpacity = 5; + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -53,13 +61,40 @@ 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. 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( + (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; } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 24ce69ea..ed76a925 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -165,63 +165,25 @@ namespace StardewModdingAPI.Framework.ContentManagers return file; } - /// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.</summary> + /// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game.</summary> /// <param name="texture">The texture to premultiply.</param> /// <returns>Returns a premultiplied texture.</returns> - /// <remarks>Based on <a href="https://gist.github.com/Layoric/6255384">code by Layoric</a>.</remarks> + /// <remarks>Based on <a href="https://gamedev.stackexchange.com/a/26037">code by David Gouveia</a>.</remarks> 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); - } - + // 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++) + data[i] = Color.FromNonPremultiplied(data[i].ToVector4()); + texture.SetData(data); return texture; } } diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 62d8b80d..49285388 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -51,14 +51,32 @@ namespace StardewModdingAPI.Framework /// <typeparam name="TModel">The model type.</typeparam> /// <param name="path">The file path relative to the contnet directory.</param> /// <returns>Returns the deserialised model, or <c>null</c> if the file doesn't exist or is empty.</returns> + /// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception> public TModel ReadJsonFile<TModel>(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 : null; } + /// <summary>Save data to a JSON file in the content pack's folder.</summary> + /// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam> + /// <param name="path">The file path relative to the mod folder.</param> + /// <param name="data">The arbitrary data to save.</param> + /// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception> + public void WriteJsonFile<TModel>(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); + } + /// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam> /// <param name="key">The local path to a content file relative to the content pack folder.</param> 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 /// <summary>Tracks the installed mods.</summary> private readonly ModRegistry ModRegistry; + /// <summary>The queued deprecation warnings to display.</summary> + private readonly IList<DeprecationWarning> QueuedWarnings = new List<DeprecationWarning>(); + /********* ** Public methods @@ -51,29 +55,40 @@ namespace StardewModdingAPI.Framework if (!this.MarkWarned(source ?? "<unknown>", 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) + /// <summary>Print any queued messages.</summary> + 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(); } /// <summary>Mark a deprecation warning as already logged.</summary> 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 +{ + /// <summary>A deprecation warning for a mod.</summary> + internal class DeprecationWarning + { + /********* + ** Accessors + *********/ + /// <summary>The affected mod's display name.</summary> + public string ModName { get; } + + /// <summary>A noun phrase describing what is deprecated.</summary> + public string NounPhrase { get; } + + /// <summary>The SMAPI version which deprecated it.</summary> + public string Version { get; } + + /// <summary>The deprecation level for the affected code.</summary> + public DeprecationLevel Level { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="modName">The affected mod's display name.</param> + /// <param name="nounPhrase">A noun phrase describing what is deprecated.</param> + /// <param name="version">The SMAPI version which deprecated it.</param> + /// <param name="level">The deprecation level for the affected code.</param> + 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/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 168ddde0..b9d1c453 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -12,55 +12,148 @@ namespace StardewModdingAPI.Framework.Events ** Events (new) *********/ /**** + ** Display + ****/ |
