summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-11-01 17:42:18 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-11-01 17:42:18 -0400
commite0b72374cd14298aacc6f71dc391fdc9814be37c (patch)
tree2e5d85937c34539c1a0df48423b5136508693ca8 /src/SMAPI
parent79118316065a01322d8ea12a14589ec016794c32 (diff)
parent089e6de749ae7cb109af00164d2597c6644c255e (diff)
downloadSMAPI-e0b72374cd14298aacc6f71dc391fdc9814be37c.tar.gz
SMAPI-e0b72374cd14298aacc6f71dc391fdc9814be37c.tar.bz2
SMAPI-e0b72374cd14298aacc6f71dc391fdc9814be37c.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Events/EventArgsInput.cs20
-rw-r--r--src/SMAPI/Events/InputEvents.cs15
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs150
-rw-r--r--src/SMAPI/Framework/GameVersion.cs3
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs236
-rw-r--r--src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs99
-rw-r--r--src/SMAPI/Framework/Reflection/PrivateProperty.cs30
-rw-r--r--src/SMAPI/Framework/Reflection/Reflector.cs16
-rw-r--r--src/SMAPI/Framework/SContentManager.cs618
-rw-r--r--src/SMAPI/Framework/SGame.cs20
-rw-r--r--src/SMAPI/Framework/Serialisation/JsonHelper.cs7
-rw-r--r--src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs37
-rw-r--r--src/SMAPI/Framework/Serialisation/StringEnumConverter.cs22
-rw-r--r--src/SMAPI/IContentHelper.cs8
-rw-r--r--src/SMAPI/Program.cs31
-rw-r--r--src/SMAPI/SButton.cs12
-rw-r--r--src/SMAPI/SemanticVersion.cs6
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj3
19 files changed, 865 insertions, 470 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 7721fd5e..a2dbdd98 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -29,7 +29,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0);
+ public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 1, 0);
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30");
diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs
index 66cb19f2..ff904675 100644
--- a/src/SMAPI/Events/EventArgsInput.cs
+++ b/src/SMAPI/Events/EventArgsInput.cs
@@ -2,7 +2,6 @@ using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
-using StardewModdingAPI.Utilities;
using StardewValley;
namespace StardewModdingAPI.Events
@@ -20,7 +19,14 @@ namespace StardewModdingAPI.Events
public ICursorPosition Cursor { get; set; }
/// <summary>Whether the input is considered a 'click' by the game for enabling action.</summary>
- public bool IsClick { get; }
+ [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1
+ public bool IsClick => this.IsActionButton;
+
+ /// <summary>Whether the input should trigger actions on the affected tile.</summary>
+ public bool IsActionButton { get; }
+
+ /// <summary>Whether the input should use tools on the affected tile.</summary>
+ public bool IsUseToolButton { get; }
/*********
@@ -29,12 +35,14 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="button">The button on the controller, keyboard, or mouse.</param>
/// <param name="cursor">The cursor position.</param>
- /// <param name="isClick">Whether the input is considered a 'click' by the game for enabling action.</param>
- public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick)
+ /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param>
+ /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param>
+ public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton)
{
this.Button = button;
this.Cursor = cursor;
- this.IsClick = isClick;
+ this.IsActionButton = isActionButton;
+ this.IsUseToolButton = isUseToolButton;
}
/// <summary>Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event.</summary>
@@ -49,7 +57,7 @@ namespace StardewModdingAPI.Events
{
// keyboard
if (this.Button.TryGetKeyboard(out Keys key))
- Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Except(new[] { key }).ToArray());
+ Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray());
// controller
else if (this.Button.TryGetController(out Buttons controllerButton))
diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs
index c31eb698..985aed99 100644
--- a/src/SMAPI/Events/InputEvents.cs
+++ b/src/SMAPI/Events/InputEvents.cs
@@ -1,6 +1,5 @@
using System;
using StardewModdingAPI.Framework;
-using StardewModdingAPI.Utilities;
namespace StardewModdingAPI.Events
{
@@ -24,20 +23,22 @@ namespace StardewModdingAPI.Events
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="button">The button on the controller, keyboard, or mouse.</param>
/// <param name="cursor">The cursor position.</param>
- /// <param name="isClick">Whether the input is considered a 'click' by the game for enabling action.</param>
- internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick)
+ /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param>
+ /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param>
+ internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton)
{
- monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick));
+ monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton));
}
/// <summary>Raise a <see cref="ButtonReleased"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="button">The button on the controller, keyboard, or mouse.</param>
/// <param name="cursor">The cursor position.</param>
- /// <param name="isClick">Whether the input is considered a 'click' by the game for enabling action.</param>
- internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick)
+ /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param>
+ /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param>
+ internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton)
{
- monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick));
+ monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton));
}
}
}
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
new file mode 100644
index 00000000..10c41d08
--- /dev/null
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Framework.Reflection;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localisation, loading content, etc. It assumes all keys passed in are already normalised.</summary>
+ internal class ContentCache
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying asset cache.</summary>
+ private readonly IDictionary<string, object> Cache;
+
+ /// <summary>The possible directory separator characters in an asset key.</summary>
+ private readonly char[] PossiblePathSeparators;
+
+ /// <summary>The preferred directory separator chaeacter in an asset key.</summary>
+ private readonly string PreferredPathSeparator;
+
+ /// <summary>Applies platform-specific asset key normalisation so it's consistent with the underlying cache.</summary>
+ private readonly Func<string, string> NormaliseAssetNameForPlatform;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Get or set the value of a raw cache entry.</summary>
+ /// <param name="key">The cache key.</param>
+ public object this[string key]
+ {
+ get => this.Cache[key];
+ set => this.Cache[key] = value;
+ }
+
+ /// <summary>The current cache keys.</summary>
+ public IEnumerable<string> Keys => this.Cache.Keys;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /****
+ ** Constructor
+ ****/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="contentManager">The underlying content manager whose cache to manage.</param>
+ /// <param name="reflection">Simplifies access to private game code.</param>
+ /// <param name="possiblePathSeparators">The possible directory separator characters in an asset key.</param>
+ /// <param name="preferredPathSeparator">The preferred directory separator chaeacter in an asset key.</param>
+ public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator)
+ {
+ // init
+ this.Cache = reflection.GetPrivateField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
+ this.PossiblePathSeparators = possiblePathSeparators;
+ this.PreferredPathSeparator = preferredPathSeparator;
+
+ // get key normalisation logic
+ if (Constants.TargetPlatform == Platform.Windows)
+ {
+ IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath");
+ this.NormaliseAssetNameForPlatform = path => method.Invoke<string>(path);
+ }
+ else
+ this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic
+ }
+
+ /****
+ ** Fetch
+ ****/
+ /// <summary>Get whether the cache contains a given key.</summary>
+ /// <param name="key">The cache key.</param>
+ public bool ContainsKey(string key)
+ {
+ return this.Cache.ContainsKey(key);
+ }
+
+
+ /****
+ ** Normalise
+ ****/
+ /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="NormaliseKey"/> instead.</summary>
+ /// <param name="path">The file path to normalise.</param>
+ [Pure]
+ public string NormalisePathSeparators(string path)
+ {
+ string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
+ string normalised = string.Join(this.PreferredPathSeparator, parts);
+ if (path.StartsWith(this.PreferredPathSeparator))
+ normalised = this.PreferredPathSeparator + normalised; // keep root slash
+ return normalised;
+ }
+
+ /// <summary>Normalise a cache key so it's consistent with the underlying cache.</summary>
+ /// <param name="key">The asset key.</param>
+ [Pure]
+ public string NormaliseKey(string key)
+ {
+ key = this.NormalisePathSeparators(key);
+ return key.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)
+ ? key.Substring(0, key.Length - 4)
+ : this.NormaliseAssetNameForPlatform(key);
+ }
+
+ /****
+ ** Remove
+ ****/
+ /// <summary>Remove an asset with the given key.</summary>
+ /// <param name="key">The cache key.</param>
+ /// <param name="dispose">Whether to dispose the entry value, if applicable.</param>
+ /// <returns>Returns the removed key (if any).</returns>
+ public bool Remove(string key, bool dispose)
+ {
+ // get entry
+ if (!this.Cache.TryGetValue(key, out object value))
+ return false;
+
+ // dispose & remove entry
+ if (dispose && value is IDisposable disposable)
+ disposable.Dispose();
+
+ return this.Cache.Remove(key);
+ }
+
+ /// <summary>Purge matched assets from the cache.</summary>
+ /// <param name="predicate">Matches the asset keys to invalidate.</param>
+ /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
+ /// <returns>Returns the removed keys (if any).</returns>
+ public IEnumerable<string> Remove(Func<string, Type, bool> predicate, bool dispose = false)
+ {
+ List<string> removed = new List<string>();
+ foreach (string key in this.Cache.Keys.ToArray())
+ {
+ Type type = this.Cache[key].GetType();
+ if (predicate(key, type))
+ {
+ this.Remove(key, dispose);
+ removed.Add(key);
+ }
+ }
+ return removed;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs
index 48159f61..1884afe9 100644
--- a/src/SMAPI/Framework/GameVersion.cs
+++ b/src/SMAPI/Framework/GameVersion.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Framework
@@ -22,6 +22,7 @@ namespace StardewModdingAPI.Framework
["1.06"] = "1.0.6",
["1.07"] = "1.0.7",
["1.07a"] = "1.0.8-prerelease1",
+ ["1.08"] = "1.0.8",
["1.11"] = "1.1.1"
};
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 4f5bd2f0..be9594ee 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
@@ -74,12 +73,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ContentManager = contentManager;
this.ModFolderPath = modFolderPath;
this.ModName = modName;
- this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath);
+ this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath);
this.Monitor = monitor;
}
/// <summary>Load content from the game folder or mod 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="Texture2D"/> and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
+ /// <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 asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
@@ -88,9 +87,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}.");
- this.AssertValidAssetKeyFormat(key);
try
{
+ this.AssertValidAssetKeyFormat(key);
switch (source)
{
case ContentSource.GameContent:
@@ -103,60 +102,32 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw GetContentError($"there's no matching file at path '{file.FullName}'.");
// get asset path
- string assetPath = this.GetModAssetPath(key, file.FullName);
+ string assetName = this.GetModAssetPath(key, file.FullName);
// try cache
- if (this.ContentManager.IsLoaded(assetPath))
- return this.ContentManager.Load<T>(assetPath);
+ if (this.ContentManager.IsLoaded(assetName))
+ return this.ContentManager.Load<T>(assetName);
- // load content
- switch (file.Extension.ToLower())
+ // fix map tilesheets
+ if (file.Extension.ToLower() == ".tbin")
{
- // XNB file
- case ".xnb":
- {
- T asset = this.ContentManager.Load<T>(assetPath);
- if (asset is Map)
- this.FixLocalMapTilesheets(asset as Map, key);
- return asset;
- }
-
- // unpacked map
- case ".tbin":
- {
- // validate
- if (typeof(T) != typeof(Map))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
-
- // fetch & cache
- FormatManager formatManager = FormatManager.Instance;
- Map map = formatManager.LoadMap(file.FullName);
- this.FixLocalMapTilesheets(map, key);
-
- // inject map
- this.ContentManager.Inject(assetPath, map);
- return (T)(object)map;
- }
-
- // unpacked image
- case ".png":
- // validate
- if (typeof(T) != typeof(Texture2D))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
-
- // fetch & cache
- using (FileStream stream = File.OpenRead(file.FullName))
- {
- Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
- texture = this.PremultiplyTransparency(texture);
- this.ContentManager.Inject(assetPath, texture);
- return (T)(object)texture;
- }
-
- default:
- throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'.");
+ // validate
+ if (typeof(T) != typeof(Map))
+ throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
+
+ // fetch & cache
+ FormatManager formatManager = FormatManager.Instance;
+ Map map = formatManager.LoadMap(file.FullName);
+ this.FixCustomTilesheetPaths(map, key);
+
+ // inject map
+ this.ContentManager.Inject(assetName, map, this.ContentManager);
+ return (T)(object)map;
}
+ // load through content manager
+ return this.ContentManager.Load<T>(assetName);
+
default:
throw GetContentError($"unknown content source '{source}'.");
}
@@ -193,9 +164,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <returns>Returns whether the given asset key was cached.</returns>
public bool InvalidateCache(string key)
{
- this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace);
string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent);
- return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase));
+ this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace);
+ return this.ContentManager.InvalidateCache(asset => asset.AssetNameEquals(actualKey));
}
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
@@ -207,28 +178,50 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type));
}
+ /// <summary>Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
+ /// <param name="predicate">A predicate matching the assets to invalidate.</param>
+ /// <returns>Returns whether any cache entries were invalidated.</returns>
+ public bool InvalidateCache(Func<IAssetInfo, bool> predicate)
+ {
+ this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace);
+ return this.ContentManager.InvalidateCache(predicate);
+ }
+
/*********
** Private methods
*********/
- /// <summary>Fix the tilesheets for a map loaded from the mod folder.</summary>
+ /// <summary>Assert that the given key has a valid format.</summary>
+ /// <param name="key">The asset key to check.</param>
+ /// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
+ [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
+ private void AssertValidAssetKeyFormat(string key)
+ {
+ this.ContentManager.AssertValidAssetKeyFormat(key);
+ if (Path.IsPathRooted(key))
+ throw new ArgumentException("The asset key must not be an absolute path.");
+ }
+
+ /// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
/// <param name="map">The map whose tilesheets to fix.</param>
/// <param name="mapKey">The map asset key within the mod folder.</param>
- /// <exception cref="ContentLoadException">The map tilesheets could not be loaded.</exception>
+ /// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception>
/// <remarks>
- /// The game's logic for tilesheets in <see cref="Game1.setGraphicsForSeason"/> is a bit specialised. It boils down to this:
- /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the <c>Content</c> folder.
+ /// The game's logic for tilesheets in <see cref="Game1.setGraphicsForSeason"/> is a bit specialised. It boils
+ /// down to this:
+ /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded
+ /// as-is relative to the <c>Content</c> folder.
/// * Else it's loaded from <c>Content\Maps</c> with a seasonal prefix.
///
/// That logic doesn't work well in our case, mainly because we have no location metadata at this point.
/// Instead we use a more heuristic approach: check relative to the map file first, then relative to
- /// <c>Content\Maps</c>, then <c>Content</c>. If the image source filename contains a seasonal prefix, we try
- /// for a seasonal variation and then an exact match.
+ /// <c>Content\Maps</c>, then <c>Content</c>. If the image source filename contains a seasonal prefix, try for a
+ /// seasonal variation and then an exact match.
///
/// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference.
/// </remarks>
- private void FixLocalMapTilesheets(Map map, string mapKey)
+ private void FixCustomTilesheetPaths(Map map, string mapKey)
{
- // check map info
+ // get map info
if (!map.TileSheets.Any())
return;
mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators
@@ -239,7 +232,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
string imageSource = tilesheet.ImageSource;
- // validate
+ // validate tilesheet path
if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains(".."))
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../).");
@@ -264,8 +257,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
try
{
string key =
- this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource)
- ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource);
+ this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
+ ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
if (key != null)
{
tilesheet.ImageSource = key;
@@ -282,33 +275,22 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
}
- /// <summary>Load a tilesheet image source if the file exists.</summary>
- /// <param name="relativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
+ /// <summary>Get the actual asset name for a tilesheet.</summary>
+ /// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
/// <param name="imageSource">The tilesheet image source to load.</param>
- /// <returns>Returns the loaded asset key (if it was loaded successfully).</returns>
- /// <remarks>See remarks on <see cref="FixLocalMapTilesheets"/>.</remarks>
- private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource)
+ /// <returns>Returns the asset name.</returns>
+ /// <remarks>See remarks on <see cref="FixCustomTilesheetPaths"/>.</remarks>
+ private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
{
if (imageSource == null)
return null;
// check relative to map file
{
- string localKey = Path.Combine(relativeMapFolder, imageSource);
+ string localKey = Path.Combine(modRelativeMapFolder, imageSource);
FileInfo localFile = this.GetModFile(localKey);
if (localFile.Exists)
- {
- try
- {
- this.Load<Texture2D>(localKey);
- }
- catch (Exception ex)
- {
- throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex);
- }
-
return this.GetActualAssetKey(localKey);
- }
}
// check relative to content folder
@@ -327,7 +309,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
catch
{
// ignore file-not-found errors
- // TODO: while it's useful to suppress a asset-not-found error here to avoid
+ // TODO: while it's useful to suppress an asset-not-found error here to avoid
// confusion, this is a pretty naive approach. Even if the file doesn't exist,
// the file may have been loaded through an IAssetLoader which failed. So even
// if the content file doesn't exist, that doesn't mean the error here is a
@@ -343,18 +325,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
return null;
}
- /// <summary>Assert that the given key has a valid format.</summary>
- /// <param name="key">The asset key to check.</param>
- /// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
- [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")]
- private void AssertValidAssetKeyFormat(string key)
- {
- if (string.IsNullOrWhiteSpace(key))
- throw new ArgumentException("The asset key or local path is empty.");
- if (key.Intersect(Path.GetInvalidPathChars()).Any())
- throw new ArgumentException("The asset key or local path contains invalid characters.");
- }
-
/// <summary>Get a file from the mod folder.</summary>
/// <param name="path">The asset path relative to the mod folder.</param>
private FileInfo GetModFile(string path)
@@ -400,81 +370,5 @@ namespace StardewModdingAPI.Framework.ModHelpers
return absolutePath;
#endif
}
-
- /// <summary>Get a directory path relative to a given root.</summary>
- /// <param name="rootPath">The root path from which the path should be relative.</param>
- /// <param name="targetPath">The target file path.</param>
- private string GetRelativePath(string rootPath, string targetPath)
- {
- // convert to URIs
- Uri from = new Uri(rootPath + "/");
- Uri to = new Uri(targetPath + "/");
- if (from.Scheme != to.Scheme)
- throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'.");
-
- // get relative path
- return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
- .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
- }
-
- /// <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>
- /// <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>
- 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,
-