diff options
| author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-11-19 13:48:19 -0500 |
|---|---|---|
| committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-11-19 13:48:19 -0500 |
| commit | 593723b7940ba72a786fc4c7366c56f9813d977b (patch) | |
| tree | 4d23fbef5bc5a20115f10ca04ae3379df78cc8e1 /src/SMAPI/Framework | |
| parent | 4f28ea33bd7cc65485402c5e85259083e86b49e1 (diff) | |
| parent | 3dc27a5681dcfc4ae30e95570d9966f2e14a4dd7 (diff) | |
| download | SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.gz SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.bz2 SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.zip | |
Merge branch 'develop' into stable
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 + ****/ + /// <summary>Raised after a game menu is opened, closed, or replaced.</summary> + public readonly ManagedEvent<MenuChangedEventArgs> MenuChanged; + + /// <summary>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.</summary> + public readonly ManagedEvent<RenderingEventArgs> Rendering; + + /// <summary>Raised after the game draws to the sprite patch in a draw tick, just before the |
