summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/Content
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework/Content')
-rw-r--r--src/SMAPI/Framework/Content/AssetData.cs7
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForDictionary.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs30
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForMap.cs93
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForObject.cs35
-rw-r--r--src/SMAPI/Framework/Content/AssetEditOperation.cs41
-rw-r--r--src/SMAPI/Framework/Content/AssetInfo.cs56
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs11
-rw-r--r--src/SMAPI/Framework/Content/AssetLoadOperation.cs41
-rw-r--r--src/SMAPI/Framework/Content/AssetName.cs199
-rw-r--r--src/SMAPI/Framework/Content/AssetOperationGroup.cs33
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs17
-rw-r--r--src/SMAPI/Framework/Content/TilesheetReference.cs1
13 files changed, 498 insertions, 70 deletions
diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs
index 5c90d83b..0367e999 100644
--- a/src/SMAPI/Framework/Content/AssetData.cs
+++ b/src/SMAPI/Framework/Content/AssetData.cs
@@ -5,12 +5,13 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>Base implementation for a content helper which encapsulates access and changes to content being read from a data file.</summary>
/// <typeparam name="TValue">The interface value type.</typeparam>
internal class AssetData<TValue> : AssetInfo, IAssetData<TValue>
+ where TValue : notnull
{
/*********
** Fields
*********/
/// <summary>A callback to invoke when the data is replaced (if any).</summary>
- private readonly Action<TValue> OnDataReplaced;
+ private readonly Action<TValue>? OnDataReplaced;
/*********
@@ -25,11 +26,11 @@ namespace StardewModdingAPI.Framework.Content
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locale">The content's locale code, if the content is localized.</param>
- /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="assetName">The asset name being read.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
- public AssetData(string locale, string assetName, TValue data, Func<string, string> getNormalizedPath, Action<TValue> onDataReplaced)
+ public AssetData(string? locale, IAssetName assetName, TValue data, Func<string, string> getNormalizedPath, Action<TValue>? onDataReplaced)
: base(locale, assetName, data.GetType(), getNormalizedPath)
{
this.Data = data;
diff --git a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs
index 26cbff5a..d9bfa7bf 100644
--- a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs
@@ -11,11 +11,11 @@ namespace StardewModdingAPI.Framework.Content
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locale">The content's locale code, if the content is localized.</param>
- /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="assetName">The asset name being read.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
- public AssetDataForDictionary(string locale, string assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalizedPath, Action<IDictionary<TKey, TValue>> onDataReplaced)
+ public AssetDataForDictionary(string? locale, IAssetName assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalizedPath, Action<IDictionary<TKey, TValue>> onDataReplaced)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
}
}
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index 529fb93a..97729c95 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -21,11 +21,11 @@ namespace StardewModdingAPI.Framework.Content
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locale">The content's locale code, if the content is localized.</param>
- /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="assetName">The asset name being read.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
- public AssetDataForImage(string locale, string assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
+ public AssetDataForImage(string? locale, IAssetName assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
/// <inheritdoc />
@@ -41,39 +41,40 @@ namespace StardewModdingAPI.Framework.Content
targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height));
// validate
- if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height)
+ if (!source.Bounds.Contains(sourceArea.Value))
throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
- if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height)
+ if (!target.Bounds.Contains(targetArea.Value))
throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture.");
- if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height)
+ if (sourceArea.Value.Size != targetArea.Value.Size)
throw new InvalidOperationException("The source and target areas must be the same size.");
// get source data
int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height;
- Color[] sourceData = new Color[pixelCount];
+ Color[] sourceData = GC.AllocateUninitializedArray<Color>(pixelCount);
source.GetData(0, sourceArea, sourceData, 0, pixelCount);
// merge data in overlay mode
if (patchMode == PatchMode.Overlay)
{
// get target data
- Color[] targetData = new Color[pixelCount];
+ Color[] targetData = GC.AllocateUninitializedArray<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 above = sourceData[i];
Color below = targetData[i];
// shortcut transparency
- if (above.A < AssetDataForImage.MinOpacity)
+ if (above.A < MinOpacity)
+ {
+ sourceData[i] = below;
continue;
- if (below.A < AssetDataForImage.MinOpacity)
+ }
+ if (below.A < MinOpacity)
{
- newData[i] = above;
+ sourceData[i] = above;
continue;
}
@@ -84,14 +85,13 @@ namespace StardewModdingAPI.Framework.Content
// Note: don't use named arguments here since they're different between
// Linux/macOS and Windows.
float alphaBelow = 1 - (above.A / 255f);
- newData[i] = new Color(
+ sourceData[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;
}
// patch target texture
@@ -105,7 +105,7 @@ namespace StardewModdingAPI.Framework.Content
return false;
Texture2D original = this.Data;
- Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight));
+ Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight));
this.ReplaceWith(texture);
this.PatchImage(original);
return true;
diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs
index 0a5fa7e7..b8722ead 100644
--- a/src/SMAPI/Framework/Content/AssetDataForMap.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs
@@ -2,11 +2,14 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
+using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using xTile;
+using xTile.Dimensions;
using xTile.Layers;
using xTile.Tiles;
+using Rectangle = Microsoft.Xna.Framework.Rectangle;
namespace StardewModdingAPI.Framework.Content
{
@@ -14,16 +17,27 @@ namespace StardewModdingAPI.Framework.Content
internal class AssetDataForMap : AssetData<Map>, IAssetDataForMap
{
/*********
+ ** Fields
+ *********/
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
+
+ /*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locale">The content's locale code, if the content is localized.</param>
- /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="assetName">The asset name being read.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
- public AssetDataForMap(string locale, string assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
- : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public AssetDataForMap(string? locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced, Reflector reflection)
+ : base(locale, assetName, data, getNormalizedPath, onDataReplaced)
+ {
+ this.Reflection = reflection;
+ }
/// <inheritdoc />
/// <remarks>Derived from <see cref="GameLocation.ApplyMapOverride(Map,string,Rectangle?,Rectangle?)"/> with a few changes:
@@ -85,8 +99,15 @@ namespace StardewModdingAPI.Framework.Content
}
// get target layers
- IDictionary<Layer, Layer> sourceToTargetLayers = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id));
- HashSet<Layer> orphanedTargetLayers = new HashSet<Layer>(target.Layers.Except(sourceToTargetLayers.Values));
+ Dictionary<Layer, Layer> sourceToTargetLayers =
+ (
+ from sourceLayer in source.Layers
+ let targetLayer = target.GetLayer(sourceLayer.Id)
+ where targetLayer != null
+ select (sourceLayer, targetLayer)
+ )
+ .ToDictionary(p => p.sourceLayer, p => p.targetLayer);
+ HashSet<Layer> orphanedTargetLayers = new(target.Layers.Except(sourceToTargetLayers.Values));
// apply tiles
bool replaceAll = patchMode == PatchMapMode.Replace;
@@ -96,8 +117,8 @@ namespace StardewModdingAPI.Framework.Content
for (int y = 0; y < sourceArea.Value.Height; y++)
{
// calculate tile positions
- Point sourcePos = new Point(sourceArea.Value.X + x, sourceArea.Value.Y + y);
- Point targetPos = new Point(targetArea.Value.X + x, targetArea.Value.Y + y);
+ Point sourcePos = new(sourceArea.Value.X + x, sourceArea.Value.Y + y);
+ Point targetPos = new(targetArea.Value.X + x, targetArea.Value.Y + y);
// replace tiles on target-only layers
if (replaceAll)
@@ -110,8 +131,7 @@ namespace StardewModdingAPI.Framework.Content
foreach (Layer sourceLayer in source.Layers)
{
// get layer
- Layer targetLayer = sourceToTargetLayers[sourceLayer];
- if (targetLayer == null)
+ if (!sourceToTargetLayers.TryGetValue(sourceLayer, out Layer? targetLayer))
{
target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize));
sourceToTargetLayers[sourceLayer] = target.GetLayer(sourceLayer.Id);
@@ -121,11 +141,13 @@ namespace StardewModdingAPI.Framework.Content
targetLayer.Properties.CopyFrom(sourceLayer.Properties);
// create new tile
- Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y];
- Tile newTile = sourceTile != null
- ? this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet])
- : null;
- newTile?.Properties.CopyFrom(sourceTile.Properties);
+ Tile? sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y];
+ Tile? newTile = null;
+ if (sourceTile != null)
+ {
+ newTile = this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet]);
+ newTile?.Properties.CopyFrom(sourceTile.Properties);
+ }
// replace tile
if (newTile != null || replaceByLayer || replaceAll)
@@ -135,6 +157,43 @@ namespace StardewModdingAPI.Framework.Content
}
}
+ /// <inheritdoc />
+ public bool ExtendMap(int minWidth = 0, int minHeight = 0)
+ {
+ bool resized = false;
+ Map map = this.Data;
+
+ // resize layers
+ foreach (Layer layer in map.Layers)
+ {
+ // check if resize needed
+ if (layer.LayerWidth >= minWidth && layer.LayerHeight >= minHeight)
+ continue;
+ resized = true;
+
+ // build new tile matrix
+ int width = Math.Max(minWidth, layer.LayerWidth);
+ int height = Math.Max(minHeight, layer.LayerHeight);
+ Tile[,] tiles = new Tile[width, height];
+ for (int x = 0; x < layer.LayerWidth; x++)
+ {
+ for (int y = 0; y < layer.LayerHeight; y++)
+ tiles[x, y] = layer.Tiles[x, y];
+ }
+
+ // update fields
+ this.Reflection.GetField<Tile[,]>(layer, "m_tiles").SetValue(tiles);
+ this.Reflection.GetField<TileArray>(layer, "m_tileArray").SetValue(new TileArray(layer, tiles));
+ this.Reflection.GetField<Size>(layer, "m_layerSize").SetValue(new Size(width, height));
+ }
+
+ // resize map
+ if (resized)
+ this.Reflection.GetMethod(map, "UpdateDisplaySize").Invoke();
+
+ return resized;
+ }
+
/*********
** Private methods
@@ -143,11 +202,11 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="sourceTile">The source tile to copy.</param>
/// <param name="targetLayer">The target layer.</param>
/// <param name="targetSheet">The target tilesheet.</param>
- private Tile CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet)
+ private Tile? CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet)
{
switch (sourceTile)
{
- case StaticTile _:
+ case StaticTile:
return new StaticTile(targetLayer, targetSheet, sourceTile.BlendMode, sourceTile.TileIndex);
case AnimatedTile animatedTile:
@@ -168,7 +227,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <summary>Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path.</summary>
/// <param name="path">The path to normalize.</param>
- private string NormalizeTilesheetPathForComparison(string path)
+ private string NormalizeTilesheetPathForComparison(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs
index b7e8dfeb..6c40f5f9 100644
--- a/src/SMAPI/Framework/Content/AssetDataForObject.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Reflection;
using xTile;
namespace StardewModdingAPI.Framework.Content
@@ -9,47 +10,61 @@ namespace StardewModdingAPI.Framework.Content
internal class AssetDataForObject : AssetData<object>, IAssetData
{
/*********
+ ** Fields
+ *********/
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
+
+ /*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locale">The content's locale code, if the content is localized.</param>
- /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="assetName">The asset name being read.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
- public AssetDataForObject(string locale, string assetName, object data, Func<string, string> getNormalizedPath)
- : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { }
+ /// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
+ public AssetDataForObject(string? locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath, Reflector reflection, Action<object>? onDataReplaced = null)
+ : base(locale, assetName, data, getNormalizedPath, onDataReplaced)
+ {
+ this.Reflection = reflection;
+ }
/// <summary>Construct an instance.</summary>
/// <param name="info">The asset metadata.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
- public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath)
- : this(info.Locale, info.AssetName, data, getNormalizedPath) { }
+ /// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
+ public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath, Reflector reflection, Action<object>? onDataReplaced = null)
+ : this(info.Locale, info.Name, data, getNormalizedPath, reflection, onDataReplaced) { }
/// <inheritdoc />
public IAssetDataForDictionary<TKey, TValue> AsDictionary<TKey, TValue>()
{
- return new AssetDataForDictionary<TKey, TValue>(this.Locale, this.AssetName, this.GetData<IDictionary<TKey, TValue>>(), this.GetNormalizedPath, this.ReplaceWith);
+ return new AssetDataForDictionary<TKey, TValue>(this.Locale, this.Name, this.GetData<IDictionary<TKey, TValue>>(), this.GetNormalizedPath, this.ReplaceWith);
}
/// <inheritdoc />
public IAssetDataForImage AsImage()
{
- return new AssetDataForImage(this.Locale, this.AssetName, this.GetData<Texture2D>(), this.GetNormalizedPath, this.ReplaceWith);
+ return new AssetDataForImage(this.Locale, this.Name, this.GetData<Texture2D>(), this.GetNormalizedPath, this.ReplaceWith);
}
/// <inheritdoc />
public IAssetDataForMap AsMap()
{
- return new AssetDataForMap(this.Locale, this.AssetName, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith);
+ return new AssetDataForMap(this.Locale, this.Name, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith, this.Reflection);
}
/// <inheritdoc />
public TData GetData<TData>()
{
- if (!(this.Data is TData))
+ if (this.Data is not TData data)
throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}.");
- return (TData)this.Data;
+ return data;
}
}
}
diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs
new file mode 100644
index 00000000..464948b0
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs
@@ -0,0 +1,41 @@
+using System;
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>An edit to apply to an asset when it's requested from the content pipeline.</summary>
+ internal class AssetEditOperation
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod applying the edit.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>If there are multiple edits that apply to the same asset, the priority with which this one should be applied.</summary>
+ public AssetEditPriority Priority { get; }
+
+ /// <summary>The content pack on whose behalf the edit is being applied, if any.</summary>
+ public IModMetadata? OnBehalfOf { get; }
+
+ /// <summary>Apply the edit to an asset.</summary>
+ public Action<IAssetData> ApplyEdit { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod applying the edit.</param>
+ /// <param name="priority">If there are multiple edits that apply to the same asset, the priority with which this one should be applied.</param>
+ /// <param name="onBehalfOf">The content pack on whose behalf the edit is being applied, if any.</param>
+ /// <param name="applyEdit">Apply the edit to an asset.</param>
+ public AssetEditOperation(IModMetadata mod, AssetEditPriority priority, IModMetadata? onBehalfOf, Action<IAssetData> applyEdit)
+ {
+ this.Mod = mod;
+ this.Priority = priority;
+ this.OnBehalfOf = onBehalfOf;
+ this.ApplyEdit = applyEdit;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs
index d8106439..363fffb3 100644
--- a/src/SMAPI/Framework/Content/AssetInfo.cs
+++ b/src/SMAPI/Framework/Content/AssetInfo.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Deprecations;
namespace StardewModdingAPI.Framework.Content
{
@@ -17,10 +18,35 @@ namespace StardewModdingAPI.Framework.Content
** Accessors
*********/
/// <inheritdoc />
- public string Locale { get; }
+ public string? Locale { get; }
/// <inheritdoc />
- public string AssetName { get; }
+ public IAssetName Name { get; }
+
+ /// <inheritdoc />
+ public IAssetName NameWithoutLocale { get; }
+
+ /// <inheritdoc />
+ [Obsolete($"Use {nameof(AssetInfo.Name)} or {nameof(AssetInfo.NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")]
+ public string AssetName
+ {
+ get
+ {
+ SCore.DeprecationManager.Warn(
+ source: SCore.DeprecationManager.GetModFromStack(),
+ nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
+ version: "3.14.0",
+ severity: DeprecationLevel.Notice,
+ unlessStackIncludes: new[]
+ {
+ $"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
+ $"{typeof(ContentCoordinator).FullName}.{nameof(ContentCoordinator.GetAssetOperations)}"
+ }
+ );
+
+ return this.NameWithoutLocale.Name;
+ }
+ }
/// <inheritdoc />
public Type DataType { get; }
@@ -31,22 +57,36 @@ namespace StardewModdingAPI.Framework.Content
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locale">The content's locale code, if the content is localized.</param>
- /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="assetName">The asset name being read.</param>
/// <param name="type">The content type being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
- public AssetInfo(string locale, string assetName, Type type, Func<string, string> getNormalizedPath)
+ public AssetInfo(string? locale, IAssetName assetName, Type type, Func<string, string> getNormalizedPath)
{
this.Locale = locale;
- this.AssetName = assetName;
+ this.Name = assetName;
+ this.NameWithoutLocale = assetName.GetBaseAssetName();
this.DataType = type;
this.GetNormalizedPath = getNormalizedPath;
}
/// <inheritdoc />
+ [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(AssetInfo.NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")]
public bool AssetNameEquals(string path)
{
- path = this.GetNormalizedPath(path);
- return this.AssetName.Equals(path, StringComparison.OrdinalIgnoreCase);
+ SCore.DeprecationManager.Warn(
+ source: SCore.DeprecationManager.GetModFromStack(),
+ nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
+ version: "3.14.0",
+ severity: DeprecationLevel.Notice,
+ unlessStackIncludes: new[]
+ {
+ $"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
+ $"{typeof(ContentCoordinator).FullName}.{nameof(ContentCoordinator.GetAssetOperations)}"
+ }
+ );
+
+
+ return this.NameWithoutLocale.IsEquivalentTo(path);
}
@@ -75,7 +115,7 @@ namespace StardewModdingAPI.Framework.Content
return "string";
// default
- return type.FullName;
+ return type.FullName!;
}
}
}
diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
index 10488b84..fc8199e8 100644
--- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
+++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
@@ -2,6 +2,7 @@ using System;
using System.Reflection;
using StardewModdingAPI.Internal;
+#pragma warning disable CS0618 // obsolete asset interceptors deliberately supported here
namespace StardewModdingAPI.Framework.Content
{
/// <summary>A wrapper for <see cref="IAssetEditor"/> and <see cref="IAssetLoader"/> for internal cache invalidation.</summary>
@@ -36,7 +37,7 @@ namespace StardewModdingAPI.Framework.Content
this.Instance = instance ?? throw new ArgumentNullException(nameof(instance));
this.WasAdded = wasAdded;
- if (!(instance is IAssetEditor) && !(instance is IAssetLoader))
+ if (instance is not (IAssetEditor or IAssetLoader))
throw new InvalidCastException($"The provided {nameof(instance)} value must be an {nameof(IAssetEditor)} or {nameof(IAssetLoader)} instance.");
}
@@ -44,11 +45,11 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="asset">Basic metadata about the asset being loaded.</param>
public bool CanIntercept(IAssetInfo asset)
{
- MethodInfo canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic);
+ MethodInfo? canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic);
if (canIntercept == null)
throw new InvalidOperationException($"SMAPI couldn't access the {nameof(AssetInterceptorChange)}.{nameof(this.CanInterceptImpl)} implementation.");
- return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset });
+ return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset })!;
}
@@ -70,7 +71,7 @@ namespace StardewModdingAPI.Framework.Content
}
catch (Exception ex)
{
- this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
}
@@ -84,7 +85,7 @@ namespace StardewModdingAPI.Framework.Content
}
catch (Exception ex)
{
- this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
}
diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs
new file mode 100644
index 00000000..b6cdec27
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs
@@ -0,0 +1,41 @@
+using System;
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>An operation which provides the initial instance of an asset when it's requested from the content pipeline.</summary>
+ internal class AssetLoadOperation
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod loading the asset.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>The content pack on whose behalf the asset is being loaded, if any.</summary>
+ public IModMetadata? OnBehalfOf { get; }
+
+ /// <summary>If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</summary>
+ public AssetLoadPriority Priority { get; }
+
+ /// <summary>Load the initial value for an asset.</summary>
+ public Func<IAssetInfo, object> GetData { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod applying the edit.</param>
+ /// <param name="priority">If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</param>
+ /// <param name="onBehalfOf">The content pack on whose behalf the asset is being loaded, if any.</param>
+ /// <param name="getData">Load the initial value for an asset.</param>
+ public AssetLoadOperation(IModMetadata mod, AssetLoadPriority priority, IModMetadata? onBehalfOf, Func<IAssetInfo, object> getData)
+ {
+ this.Mod = mod;
+ this.Priority = priority;
+ this.OnBehalfOf = onBehalfOf;
+ this.GetData = getData;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
new file mode 100644
index 00000000..148354a1
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -0,0 +1,199 @@
+using System;
+using StardewModdingAPI.Toolkit.Utilities;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>An asset name that can be loaded through the content pipeline.</summary>
+ internal class AssetName : IAssetName
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>A lowercase version of <see cref="Name"/> used for consistent hash codes and equality checks.</summary>
+ private readonly string ComparableName;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <inheritdoc />
+ public string Name { get; }
+
+ /// <inheritdoc />
+ public string BaseName { get; }
+
+ /// <inheritdoc />
+ public string? LocaleCode { get; }
+
+ /// <inheritdoc />
+ public LocalizedContentManager.LanguageCode? LanguageCode { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="baseName">The base asset name without the locale code.</param>
+ /// <param name="localeCode">The locale code specified in the <see cref="Name"/>, if it's a valid code recognized by the game content.</param>
+ /// <param name="languageCode">The language code matching the <see cref="LocaleCode"/>, if applicable.</param>
+ public AssetName(string baseName, string? localeCode, LocalizedContentManager.LanguageCode? languageCode)
+ {
+ // validate
+ if (string.IsNullOrWhiteSpace(baseName))
+ throw new ArgumentException("The asset name can't be null or empty.", nameof(baseName));
+ if (string.IsNullOrWhiteSpace(localeCode))
+ localeCode = null;
+
+ // set base values
+ this.BaseName = PathUtilities.NormalizeAssetName(baseName);
+ this.LocaleCode = localeCode;
+ this.LanguageCode = languageCode;
+
+ // set derived values
+ this.Name = localeCode != null
+ ? string.Concat(this.BaseName, '.', this.LocaleCode)
+ : this.BaseName;
+ this.ComparableName = this.Name.ToLowerInvariant();
+ }
+
+ /// <summary>Parse a raw asset name into an instance.</summary>
+ /// <param name="rawName">The raw asset name to parse.</param>
+ /// <param name="parseLocale">Get the language code for a given locale, if it's valid.</param>
+ /// <exception cref="ArgumentException">The <paramref name="rawName"/> is null or empty.</exception>
+ public static AssetName Parse(string rawName, Func<string, LocalizedContentManager.LanguageCode?> parseLocale)
+ {
+ if (string.IsNullOrWhiteSpace(rawName))
+ throw new ArgumentException("The asset name can't be null or empty.", nameof(rawName));
+
+ string baseName = rawName;
+ string? localeCode = null;
+ LocalizedContentManager.LanguageCode? languageCode = null;
+
+ int lastPeriodIndex = rawName.LastIndexOf('.');
+ if (lastPeriodIndex > 0 && rawName.Length > lastPeriodIndex + 1)
+ {
+ string possibleLocaleCode = rawName[(lastPeriodIndex + 1)..];
+ LocalizedContentManager.LanguageCode? possibleLanguageCode = parseLocale(possibleLocaleCode);
+
+ if (possibleLanguageCode != null)
+ {
+ baseName = rawName[..lastPeriodIndex];
+ localeCode = possibleLocaleCode;
+ languageCode = possibleLanguageCode;
+ }
+ }
+
+ return new AssetName(baseName, localeCode, languageCode);
+ }
+
+ /// <inheritdoc />
+ public bool IsEquivalentTo(string? assetName, bool useBaseName = false)
+ {
+ // empty asset key is never equivalent
+ if (string.IsNullOrWhiteSpace(assetName))
+ return false;
+
+ assetName = PathUtilities.NormalizeAssetName(assetName);
+
+ string compareTo = useBaseName ? this.BaseName : this.Name;
+ return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc />
+ public bool IsEquivalentTo(IAssetName? assetName, bool useBaseName = false)
+ {
+ if (useBaseName)
+ return this.BaseName.Equals(assetName?.BaseName, StringComparison.OrdinalIgnoreCase);
+
+ if (assetName is AssetName impl)
+ return this.ComparableName == impl.ComparableName;
+
+ return this.Name.Equals(assetName?.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc />
+ public bool StartsWith(string? prefix, bool allowPartialWord = true, bool allowSubfolder = true)
+ {
+ // asset keys never start with null
+ if (prefix is null)
+ return false;
+
+ string rawTrimmed = prefix.Trim();
+
+ // asset keys can't have a leading slash, but NormalizeAssetName will trim them
+ if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\'))
+ return false;
+
+ // normalize prefix
+ {
+ string normalized = PathUtilities.NormalizeAssetName(prefix);
+
+ // keep trailing slash
+ if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\'))
+ normalized += PathUtilities.PreferredAssetSeparator;
+
+ prefix = normalized;
+ }
+
+ // compare
+ if (prefix.Length == 0)
+ return true;
+
+ return
+ this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
+ && (
+ allowPartialWord
+ || this.Name.Length == prefix.Length
+ || !char.IsLetterOrDigit(prefix[^1]) // last character in suffix is word separator
+ || !char.IsLetterOrDigit(this.Name[prefix.Length]) // or first character after it is
+ )
+ && (
+ allowSubfolder
+ || this.Name.Length == prefix.Length
+ || !this.Name[prefix.Length..].Contains(PathUtilities.PreferredAssetSeparator)
+ );
+ }
+
+
+ /// <inheritdoc />
+ public bool IsDirectlyUnderPath(string? assetFolder)
+ {
+ if (assetFolder is null)
+ return false;
+
+ return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false);
+ }
+
+ /// <inheritdoc />
+ IAssetName IAssetName.GetBaseAssetName()
+ {
+ return this.LocaleCode == null
+ ? this
+ : new AssetName(this.BaseName, null, null);
+ }
+
+ /// <inheritdoc />
+ public bool Equals(IAssetName? other)
+ {
+ return other switch
+ {
+ null => false,
+ AssetName otherImpl => this.ComparableName == otherImpl.ComparableName,
+ _ => StringComparer.OrdinalIgnoreCase.Equals(this.Name, other.Name)
+ };
+ }
+
+ /// <inheritdoc />
+ public override int GetHashCode()
+ {
+ return this.ComparableName.GetHashCode();
+ }
+
+ /// <inheritdoc />
+ public override string ToString()
+ {
+ return this.Name;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/AssetOperationGroup.cs b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
new file mode 100644
index 00000000..a2fcb722
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
@@ -0,0 +1,33 @@
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>A set of operations to apply to an asset for a given <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/> implementation.</summary>
+ internal class AssetOperationGroup
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod applying the changes.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>The load operations to apply.</summary>
+ public AssetLoadOperation[] LoadOperations { get; }
+
+ /// <summary>The edit operations to apply.</summary>
+ public AssetEditOperation[] EditOperations { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod applying the changes.</param>
+ /// <param name="loadOperations">The load operations to apply.</param>
+ /// <param name="editOperations">The edit operations to apply.</param>
+ public AssetOperationGroup(IModMetadata mod, AssetLoadOperation[] loadOperations, AssetEditOperation[] editOperations)
+ {
+ this.Mod = mod;
+ this.LoadOperations = loadOperations;
+ this.EditOperations = editOperations;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index 8e0c6228..736ee5da 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
-using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
-using StardewValley;
namespace StardewModdingAPI.Framework.Content
{
@@ -40,11 +39,10 @@ namespace StardewModdingAPI.Framework.Content
** 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>
- public ContentCache(LocalizedContentManager contentManager, Reflector reflection)
+ /// <param name="loadedAssets">The asset cache for the underlying content manager.</param>
+ public ContentCache(Dictionary<string, object> loadedAssets)
{
- this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
+ this.Cache = loadedAssets;
}
/****
@@ -64,7 +62,8 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>Normalize path separators in an asset name.</summary>
/// <param name="path">The file path to normalize.</param>
[Pure]
- public string NormalizePathSeparators(string path)
+ [return: NotNullIfNotNull("path")]
+ public string? NormalizePathSeparators(string? path)
{
return PathUtilities.NormalizeAssetName(path);
}
@@ -77,7 +76,7 @@ namespace StardewModdingAPI.Framework.Content
{
key = this.NormalizePathSeparators(key);
return key.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)
- ? key.Substring(0, key.Length - 4)
+ ? key[..^4]
: key;
}
@@ -91,7 +90,7 @@ namespace StardewModdingAPI.Framework.Content
public bool Remove(string key, bool dispose)
{
// get entry
- if (!this.Cache.TryGetValue(key, out object value))
+ if (!this.Cache.TryGetValue(key, out object? value))
return false;
// dispose & remove entry
diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs
index 0919bb44..0339b802 100644
--- a/src/SMAPI/Framework/Content/TilesheetReference.cs
+++ b/src/SMAPI/Framework/Content/TilesheetReference.cs
@@ -1,4 +1,3 @@
-using System.Numerics;
using xTile.Dimensions;
namespace StardewModdingAPI.Framework.Content