summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/Content/AssetData.cs4
-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.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForObject.cs12
-rw-r--r--src/SMAPI/Framework/Content/AssetInfo.cs16
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetName.cs173
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs94
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs53
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs94
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs3
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs36
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs2
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs5
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs4
-rw-r--r--src/SMAPI/Framework/SCore.cs17
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs5
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs3
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs2
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs2
22 files changed, 363 insertions, 212 deletions
diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs
index 5c90d83b..05be8a3b 100644
--- a/src/SMAPI/Framework/Content/AssetData.cs
+++ b/src/SMAPI/Framework/Content/AssetData.cs
@@ -25,11 +25,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..735b651c 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..b0f1b5c7 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..26e4986e 100644
--- a/src/SMAPI/Framework/Content/AssetDataForMap.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs
@@ -18,11 +18,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 AssetDataForMap(string locale, string assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
+ public AssetDataForMap(string locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs
index b7e8dfeb..d91873ae 100644
--- a/src/SMAPI/Framework/Content/AssetDataForObject.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs
@@ -13,10 +13,10 @@ 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>
- public AssetDataForObject(string locale, string assetName, object data, Func<string, string> getNormalizedPath)
+ public AssetDataForObject(string locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { }
/// <summary>Construct an instance.</summary>
@@ -24,24 +24,24 @@ namespace StardewModdingAPI.Framework.Content
/// <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) { }
+ : this(info.Locale, info.Name, data, getNormalizedPath) { }
/// <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);
}
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs
index d8106439..6a5b4f31 100644
--- a/src/SMAPI/Framework/Content/AssetInfo.cs
+++ b/src/SMAPI/Framework/Content/AssetInfo.cs
@@ -20,7 +20,11 @@ namespace StardewModdingAPI.Framework.Content
public string Locale { get; }
/// <inheritdoc />
- public string AssetName { get; }
+ public IAssetName Name { get; }
+
+ /// <inheritdoc />
+ [Obsolete($"Use {nameof(Name)} instead.")]
+ public string AssetName => this.Name.Name;
/// <inheritdoc />
public Type DataType { get; }
@@ -31,22 +35,22 @@ 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.DataType = type;
this.GetNormalizedPath = getNormalizedPath;
}
/// <inheritdoc />
+ [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")]
public bool AssetNameEquals(string path)
{
- path = this.GetNormalizedPath(path);
- return this.AssetName.Equals(path, StringComparison.OrdinalIgnoreCase);
+ return this.Name.IsEquivalentTo(path);
}
diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
index 10488b84..981eed40 100644
--- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
+++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
@@ -70,7 +70,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 +84,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/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
new file mode 100644
index 00000000..992647f8
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -0,0 +1,173 @@
+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 StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true)
+ {
+ // asset keys never start with null
+ if (prefix is null)
+ return false;
+
+ // asset keys can't have a leading slash, but NormalizeAssetName will trim them
+ {
+ string trimmed = prefix.TrimStart();
+ if (trimmed.StartsWith('/') || trimmed.StartsWith('\\'))
+ return false;
+ }
+
+ // normalize prefix
+ {
+ string normalized = PathUtilities.NormalizeAssetName(prefix);
+
+ string trimmed = prefix.TrimEnd();
+ if (trimmed.EndsWith('/') || trimmed.EndsWith('\\'))
+ normalized += PathUtilities.PreferredAssetSeparator;
+
+ prefix = normalized;
+ }
+
+ // compare
+ 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)
+ );
+ }
+
+
+ public bool IsDirectlyUnderPath(string assetFolder)
+ {
+ return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false);
+ }
+
+ /// <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/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 99091f3e..00f9439c 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -14,6 +14,7 @@ using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
+using StardewValley.GameData;
using xTile;
namespace StardewModdingAPI.Framework
@@ -46,7 +47,7 @@ namespace StardewModdingAPI.Framework
private readonly Action OnLoadingFirstAsset;
/// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
- private readonly IList<IContentManager> ContentManagers = new List<IContentManager>();
+ private readonly List<IContentManager> ContentManagers = new();
/// <summary>The language code for language-agnostic mod assets.</summary>
private readonly LocalizedContentManager.LanguageCode DefaultLanguage = Constants.DefaultLanguage;
@@ -56,16 +57,16 @@ namespace StardewModdingAPI.Framework
/// <summary>A lock used to prevent asynchronous changes to the content manager list.</summary>
/// <remarks>The game may add content managers in asynchronous threads (e.g. when populating the load screen).</remarks>
- private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim();
+ private readonly ReaderWriterLockSlim ContentManagerLock = new();
/// <summary>A cache of ordered tilesheet IDs used by vanilla maps.</summary>
- private readonly IDictionary<string, TilesheetReference[]> VanillaTilesheets = new Dictionary<string, TilesheetReference[]>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, TilesheetReference[]> VanillaTilesheets = new(StringComparer.OrdinalIgnoreCase);
/// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary>
private readonly LocalizedContentManager VanillaContentManager;
/// <summary>The language enum values indexed by locale code.</summary>
- private Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
+ private Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
/*********
@@ -106,7 +107,7 @@ namespace StardewModdingAPI.Framework
this.Reflection = reflection;
this.JsonHelper = jsonHelper;
this.OnLoadingFirstAsset = onLoadingFirstAsset;
- this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory);
+ this.FullRootDirectory = Path.Combine(Constants.GamePath, rootDirectory);
this.ContentManagers.Add(
this.MainContentManager = new GameContentManager(
name: "Game1.content",
@@ -136,7 +137,7 @@ namespace StardewModdingAPI.Framework
this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, aggressiveMemoryOptimizations);
- this.LocaleCodes = new Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>>(this.GetLocaleCodes);
+ this.LocaleCodes = new Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>>(() => this.GetLocaleCodes(includeCustomLanguages: false));
}
/// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
@@ -145,7 +146,7 @@ namespace StardewModdingAPI.Framework
{
return this.ContentManagerLock.InWriteLock(() =>
{
- GameContentManager manager = new GameContentManager(
+ GameContentManager manager = new(
name: name,
serviceProvider: this.MainContentManager.ServiceProvider,
rootDirectory: this.MainContentManager.RootDirectory,
@@ -171,7 +172,7 @@ namespace StardewModdingAPI.Framework
{
return this.ContentManagerLock.InWriteLock(() =>
{
- ModContentManager manager = new ModContentManager(
+ ModContentManager manager = new(
name: name,
gameContentManager: gameContentManager,
serviceProvider: this.MainContentManager.ServiceProvider,
@@ -196,12 +197,17 @@ namespace StardewModdingAPI.Framework
return this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode);
}
- /// <summary>Perform any cleanup needed when the locale changes.</summary>
- public void OnLocaleChanged()
+ /// <summary>Perform any updates needed when the game loads custom languages from <c>Data/AdditionalLanguages</c>.</summary>
+ public void OnAdditionalLanguagesInitialized()
{
- // rebuild locale cache (which may change due to custom mod languages)
- this.LocaleCodes = new Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>>(this.GetLocaleCodes);
+ // update locale cache for custom languages, and load it now (since languages added later won't work)
+ this.LocaleCodes = new Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>>(() => this.GetLocaleCodes(includeCustomLanguages: true));
+ _ = this.LocaleCodes.Value;
+ }
+ /// <summary>Perform any updates needed when the locale changes.</summary>
+ public void OnLocaleChanged()
+ {
// reload affected content
this.ContentManagerLock.InReadLock(() =>
{
@@ -242,6 +248,16 @@ namespace StardewModdingAPI.Framework
this.InvalidateCache((contentManager, key, type) => contentManager is GameContentManager);
}
+ /// <summary>Parse a raw asset name.</summary>
+ /// <param name="rawName">The raw asset name to parse.</param>
+ /// <exception cref="ArgumentException">The <paramref name="rawName"/> is null or empty.</exception>
+ public AssetName ParseAssetName(string rawName)
+ {
+ return !string.IsNullOrWhiteSpace(rawName)
+ ? AssetName.Parse(rawName, parseLocale: locale => this.LocaleCodes.Value.TryGetValue(locale, out LocalizedContentManager.LanguageCode langCode) ? langCode : null)
+ : throw new ArgumentException("The asset name can't be null or empty.", nameof(rawName));
+ }
+
/// <summary>Get whether this asset is mapped to a mod folder.</summary>
/// <param name="key">The asset key.</param>
public bool IsManagedAssetKey(string key)
@@ -300,11 +316,12 @@ namespace StardewModdingAPI.Framework
/// <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 invalidated asset keys.</returns>
- public IEnumerable<string> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
+ public IEnumerable<IAssetName> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
{
string locale = this.GetLocale();
- return this.InvalidateCache((contentManager, assetName, type) =>
+ return this.InvalidateCache((contentManager, rawName, type) =>
{
+ IAssetName assetName = this.ParseAssetName(rawName);
IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormalizeAssetName);
return predicate(info);
}, dispose);
@@ -314,10 +331,10 @@ namespace StardewModdingAPI.Framework
/// <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 invalidated asset names.</returns>
- public IEnumerable<string> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
+ public IEnumerable<IAssetName> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
{
// invalidate cache & track removed assets
- IDictionary<string, Type> removedAssets = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
+ IDictionary<IAssetName, Type> removedAssets = new Dictionary<IAssetName, Type>();
this.ContentManagerLock.InReadLock(() =>
{
// cached assets
@@ -325,8 +342,9 @@ namespace StardewModdingAPI.Framework
{
foreach (var entry in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose))
{
- if (!removedAssets.ContainsKey(entry.Key))
- removedAssets[entry.Key] = entry.Value.GetType();
+ AssetName assetName = this.ParseAssetName(entry.Key);
+ if (!removedAssets.ContainsKey(assetName))
+ removedAssets[assetName] = entry.Valu