From 51b7b9fe06d91d59461f9b9d6d3c1b1c736dc34d Mon Sep 17 00:00:00 2001 From: Ameisen <14104310+ameisen@users.noreply.github.com> Date: Wed, 9 Feb 2022 18:00:15 -0600 Subject: Cleanup and performance/allocation improvement for AssetDataForImage.PatchImage --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 529fb93a..c75514bc 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -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(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(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; -- cgit From a2190df08cc3f1b4a8dcb394056d65921d10702e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 18 Feb 2022 15:39:49 -0500 Subject: add AssetName to encapsulate asset name handling (#766) --- docs/release-notes.md | 2 + src/SMAPI.Tests/Core/AssetNameTests.cs | 295 +++++++++++++++++++++ src/SMAPI/Framework/Content/AssetData.cs | 4 +- .../Framework/Content/AssetDataForDictionary.cs | 4 +- src/SMAPI/Framework/Content/AssetDataForImage.cs | 4 +- src/SMAPI/Framework/Content/AssetDataForMap.cs | 4 +- src/SMAPI/Framework/Content/AssetDataForObject.cs | 12 +- src/SMAPI/Framework/Content/AssetInfo.cs | 16 +- .../Framework/Content/AssetInterceptorChange.cs | 4 +- src/SMAPI/Framework/Content/AssetName.cs | 173 ++++++++++++ src/SMAPI/Framework/ContentCoordinator.cs | 45 ++-- .../ContentManagers/BaseContentManager.cs | 51 +--- .../ContentManagers/GameContentManager.cs | 94 +++---- .../Framework/ContentManagers/IContentManager.cs | 3 - .../Framework/ContentManagers/ModContentManager.cs | 26 +- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 5 +- src/SMAPI/IAssetInfo.cs | 5 + src/SMAPI/IAssetName.cs | 44 +++ src/SMAPI/Metadata/CoreAssetPropagator.cs | 261 ++++++++---------- 19 files changed, 736 insertions(+), 316 deletions(-) create mode 100644 src/SMAPI.Tests/Core/AssetNameTests.cs create mode 100644 src/SMAPI/Framework/Content/AssetName.cs create mode 100644 src/SMAPI/IAssetName.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index d549b99c..b84f8a06 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,8 @@ * Improved translations. Thanks to ChulkyBow (updated Ukrainian)! * For mod authors: + * Added `IAssetName` field to the asset info received by `IAssetEditor` and `IAssetLoader` methods. + _This provides utility methods for working with asset names, parsed locales, etc. The `asset.AssetNameEquals` method is now deprecated in favor of `asset.Name.IsEquivalentTo`_. * The `SDate` constructor is no longer case-sensitive for season names. * Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead. * Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`). diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs new file mode 100644 index 00000000..8785aab8 --- /dev/null +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace SMAPI.Tests.Core +{ + /// Unit tests for . + [TestFixture] + internal class AssetNameTests + { + /********* + ** Unit tests + *********/ + /**** + ** Constructor + ****/ + [Test(Description = $"Assert that the {nameof(AssetName)} constructor creates an instance with the expected values.")] + [TestCase("SimpleName", "SimpleName", null, null)] + [TestCase("Data/Achievements", "Data/Achievements", null, null)] + [TestCase("Characters/Dialogue/Abigail", "Characters/Dialogue/Abigail", null, null)] + [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] + [TestCase("Characters/Dialogue\\Abigail.fr-FR", "Characters/Dialogue/Abigail.fr-FR", null, null)] + [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] + public void Constructor_Valid(string name, string expectedBaseName, string expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode) + { + // arrange + name = PathUtilities.NormalizeAssetName(name); + + // act + string calledWithLocale = null; + IAssetName assetName = AssetName.Parse(name, parseLocale: locale => expectedLanguageCode); + + // assert + assetName.Name.Should() + .NotBeNull() + .And.Be(name.Replace("\\", "/")); + assetName.BaseName.Should() + .NotBeNull() + .And.Be(expectedBaseName); + assetName.LocaleCode.Should() + .Be(expectedLocale); + assetName.LanguageCode.Should() + .Be(expectedLanguageCode); + } + + [Test(Description = $"Assert that the {nameof(AssetName)} constructor throws an exception if the value is invalid.")] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("\t")] + [TestCase(" \t ")] + public void Constructor_NullOrWhitespace(string name) + { + // act + ArgumentException exception = Assert.Throws(() => _ = AssetName.Parse(name, null)); + + // assert + exception!.ParamName.Should().Be("rawName"); + exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')"); + } + + + /**** + ** IsEquivalentTo + ****/ + [Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is included.")] + + // exact match (ignore case) + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + + // exact match (ignore formatting) + [TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)] + [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + + // whitespace-sensitive + [TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = false)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] + + // other is null or whitespace + [TestCase("Data/Achievements", null, ExpectedResult = false)] + [TestCase("Data/Achievements", "", ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = true)] + public bool IsEquivalentTo_Name(string mainAssetName, string otherAssetName) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr); + + // assert + return name.IsEquivalentTo(otherAssetName); + } + + [Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is excluded.")] + + // a few samples from previous test to make sure + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = false)] + public bool IsEquivalentTo_BaseName(string mainAssetName, string otherAssetName) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr); + + // assert + return name.IsEquivalentTo(otherAssetName, useBaseName: true); + } + + + /**** + ** StartsWith + ****/ + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for inputs that aren't affected by the input options.")] + + // exact match (ignore case and formatting) + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)] + [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + + // leading-whitespace-sensitive + [TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = false)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] + + // invalid prefixes + [TestCase("Data/Achievements", null, ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] + public bool StartsWith_SimpleCases(string mainAssetName, string prefix) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(prefix, true, true); + foreach (bool allowPartialWord in new[] { true, false }) + { + foreach (bool allowSubfolder in new[] { true, true }) + { + if (allowPartialWord && allowSubfolder) + continue; + + name.StartsWith(prefix, allowPartialWord, allowSubfolder) + .Should().Be(result, $"the value returned for options ({nameof(allowPartialWord)}: {allowPartialWord}, {nameof(allowSubfolder)}: {allowSubfolder}) should match the base case"); + } + } + + // assert value + return result; + } + + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowPartialWord' option.")] + [TestCase("Data/AchievementsToIgnore", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/AchievementsToIgnore", "Data/Achievements", false, ExpectedResult = false)] + [TestCase("Data/Achievements X", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements X", "Data/Achievements", false, ExpectedResult = true)] + [TestCase("Data/Achievements.X", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements.X", "Data/Achievements", false, ExpectedResult = true)] + + // with locale codes + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", false, ExpectedResult = true)] + public bool StartsWith_PartialWord(string mainAssetName, string prefix, bool allowPartialWord) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(prefix, allowPartialWord: allowPartialWord, allowSubfolder: true); + name.StartsWith(prefix, allowPartialWord, allowSubfolder: false) + .Should().Be(result, "specifying allowSubfolder should have no effect for these inputs"); + + // assert value + return result; + } + + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowSubfolder' option.")] + + // simple cases + [TestCase("Data/Achievements/Path", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/Achievements", false, ExpectedResult = false)] + [TestCase("Data/Achievements/Path", "Data\\Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data\\Achievements", false, ExpectedResult = false)] + + // trailing slash + [TestCase("Data/Achievements/Path", "Data/", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/", false, ExpectedResult = false)] + + // normalize slash style + [TestCase("Data/Achievements/Path", "Data\\", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data\\", false, ExpectedResult = false)] + [TestCase("Data/Achievements/Path", "Data/\\/", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/\\/", false, ExpectedResult = false)] + + // with locale code + [TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", false, ExpectedResult = false)] + public bool StartsWith_Subfolder(string mainAssetName, string otherAssetName, bool allowSubfolder) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder); + name.StartsWith(otherAssetName, allowPartialWord: false, allowSubfolder: allowSubfolder) + .Should().Be(result, "specifying allowPartialWord should have no effect for these inputs"); + + // assert value + return result; + } + + + /**** + ** GetHashCode + ****/ + [Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates the same hash code for two asset names which differ only by capitalization.")] + public void GetHashCode_IsCaseInsensitive() + { + // arrange + string left = "data/ACHIEVEMENTS"; + string right = "DATA/achievements"; + + // act + int leftHash = AssetName.Parse(left, _ => null).GetHashCode(); + int rightHash = AssetName.Parse(right, _ => null).GetHashCode(); + + // assert + leftHash.Should().Be(rightHash, "two asset names which differ only by capitalization should produce the same hash code"); + } + + [Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates few hash code collisions for an arbitrary set of asset names.")] + public void GetHashCode_HasFewCollisions() + { + // generate list of names + List names = new(); + { + Random random = new(); + string characters = "abcdefghijklmnopqrstuvwxyz1234567890/"; + + while (names.Count < 1000) + { + char[] name = new char[random.Next(5, 20)]; + for (int i = 0; i < name.Length; i++) + name[i] = characters[random.Next(0, characters.Length)]; + + names.Add(new string(name)); + } + } + + // get distinct hash codes + HashSet hashCodes = new(); + foreach (string name in names) + hashCodes.Add(AssetName.Parse(name, _ => null).GetHashCode()); + + // assert a collision frequency under 0.1% + float collisionFrequency = 1 - (hashCodes.Count / (names.Count * 1f)); + collisionFrequency.Should().BeLessOrEqualTo(0.001f, "hash codes should be relatively distinct with a collision rate under 0.1% for a small sample set"); + } + } +} 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 *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetData(string locale, string assetName, TValue data, Func getNormalizedPath, Action onDataReplaced) + public AssetData(string locale, IAssetName assetName, TValue data, Func getNormalizedPath, Action 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 *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalizedPath, Action> onDataReplaced) + public AssetDataForDictionary(string locale, IAssetName assetName, IDictionary data, Func getNormalizedPath, Action> onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } } } diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index c75514bc..b0f1b5c7 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -21,11 +21,11 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalizedPath, Action onDataReplaced) + public AssetDataForImage(string locale, IAssetName assetName, Texture2D data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// 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 *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForMap(string locale, string assetName, Map data, Func getNormalizedPath, Action onDataReplaced) + public AssetDataForMap(string locale, IAssetName assetName, Map data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// 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 *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. - public AssetDataForObject(string locale, string assetName, object data, Func getNormalizedPath) + public AssetDataForObject(string locale, IAssetName assetName, object data, Func getNormalizedPath) : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { } /// Construct an instance. @@ -24,24 +24,24 @@ namespace StardewModdingAPI.Framework.Content /// The content data being read. /// Normalizes an asset key to match the cache key. public AssetDataForObject(IAssetInfo info, object data, Func getNormalizedPath) - : this(info.Locale, info.AssetName, data, getNormalizedPath) { } + : this(info.Locale, info.Name, data, getNormalizedPath) { } /// public IAssetDataForDictionary AsDictionary() { - return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForDictionary(this.Locale, this.Name, this.GetData>(), this.GetNormalizedPath, this.ReplaceWith); } /// public IAssetDataForImage AsImage() { - return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForImage(this.Locale, this.Name, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } /// public IAssetDataForMap AsMap() { - return new AssetDataForMap(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForMap(this.Locale, this.Name, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } /// 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; } /// - public string AssetName { get; } + public IAssetName Name { get; } + + /// + [Obsolete($"Use {nameof(Name)} instead.")] + public string AssetName => this.Name.Name; /// public Type DataType { get; } @@ -31,22 +35,22 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content type being read. /// Normalizes an asset key to match the cache key. - public AssetInfo(string locale, string assetName, Type type, Func getNormalizedPath) + public AssetInfo(string locale, IAssetName assetName, Type type, Func getNormalizedPath) { this.Locale = locale; - this.AssetName = assetName; + this.Name = assetName; this.DataType = type; this.GetNormalizedPath = getNormalizedPath; } /// + [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 +{ + /// An asset name that can be loaded through the content pipeline. + internal class AssetName : IAssetName + { + /********* + ** Fields + *********/ + /// A lowercase version of used for consistent hash codes and equality checks. + private readonly string ComparableName; + + + /********* + ** Accessors + *********/ + /// + public string Name { get; } + + /// + public string BaseName { get; } + + /// + public string LocaleCode { get; } + + /// + public LocalizedContentManager.LanguageCode? LanguageCode { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base asset name without the locale code. + /// The locale code specified in the , if it's a valid code recognized by the game content. + /// The language code matching the , if applicable. + 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(); + } + + /// Parse a raw asset name into an instance. + /// The raw asset name to parse. + /// Get the language code for a given locale, if it's valid. + /// The is null or empty. + public static AssetName Parse(string rawName, Func 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); + } + + /// + 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); + } + + /// + 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); + } + + /// + public bool Equals(IAssetName other) + { + return other switch + { + null => false, + AssetName otherImpl => this.ComparableName == otherImpl.ComparableName, + _ => StringComparer.OrdinalIgnoreCase.Equals(this.Name, other.Name) + }; + } + + /// + public override int GetHashCode() + { + return this.ComparableName.GetHashCode(); + } + + /// + public override string ToString() + { + return this.Name; + } + } +} diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 61cefd12..97a37b3f 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -248,6 +248,16 @@ namespace StardewModdingAPI.Framework this.InvalidateCache((contentManager, key, type) => contentManager is GameContentManager); } + /// Parse a raw asset name. + /// The raw asset name to parse. + /// The is null or empty. + 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)); + } + /// Get whether this asset is mapped to a mod folder. /// The asset key. public bool IsManagedAssetKey(string key) @@ -306,11 +316,12 @@ namespace StardewModdingAPI.Framework /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns the invalidated asset keys. - public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + public IEnumerable InvalidateCache(Func 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); @@ -320,10 +331,10 @@ namespace StardewModdingAPI.Framework /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns the invalidated asset names. - public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { // invalidate cache & track removed assets - IDictionary removedAssets = new Dictionary(StringComparer.OrdinalIgnoreCase); + IDictionary removedAssets = new Dictionary(); this.ContentManagerLock.InReadLock(() => { // cached assets @@ -331,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.Value.GetType(); } } @@ -346,8 +358,8 @@ namespace StardewModdingAPI.Framework continue; // get map path - string mapPath = this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value); - if (!removedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath, typeof(Map))) + AssetName mapPath = this.ParseAssetName(this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value)); + if (!removedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map))) removedAssets[mapPath] = typeof(Map); } } @@ -360,17 +372,17 @@ namespace StardewModdingAPI.Framework this.CoreAssets.Propagate( assets: removedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded, - out IDictionary propagated, + out IDictionary propagated, out bool updatedNpcWarps ); // log summary StringBuilder report = new(); { - string[] invalidatedKeys = removedAssets.Keys.ToArray(); - string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); + IAssetName[] invalidatedKeys = removedAssets.Keys.ToArray(); + IAssetName[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); - string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); + string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.Select(p => p.Name).OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); report.AppendLine($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)})."); report.AppendLine(propagated.Count > 0 @@ -422,15 +434,6 @@ namespace StardewModdingAPI.Framework return tilesheets ?? Array.Empty(); } - /// Get the language enum which corresponds to a locale code (e.g. given fr-FR). - /// The locale code to search. This must exactly match the language; no fallback is performed. - /// The matched language enum, if any. - /// Returns whether a valid language was found. - public bool TryGetLanguageEnum(string locale, out LocalizedContentManager.LanguageCode language) - { - return this.LocaleCodes.Value.TryGetValue(locale, out language); - } - /// Get the locale code which corresponds to a language enum (e.g. fr-FR given ). /// The language enum to search. public string GetLocaleCode(LocalizedContentManager.LanguageCode language) diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 5645c0fa..be892b33 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -160,14 +160,6 @@ namespace StardewModdingAPI.Framework.ContentManagers return this.IsNormalizedKeyLoaded(assetName, language); } - /// - public IEnumerable GetAssetKeys() - { - return this.Cache.Keys - .Select(this.GetAssetName) - .Distinct(); - } - /**** ** Cache invalidation ****/ @@ -177,13 +169,13 @@ namespace StardewModdingAPI.Framework.ContentManagers IDictionary removeAssets = new Dictionary(StringComparer.OrdinalIgnoreCase); this.Cache.Remove((key, asset) => { - this.ParseCacheKey(key, out string assetName, out _); + string baseAssetName = this.Coordinator.ParseAssetName(key).BaseName; // check if asset should be removed - bool remove = removeAssets.ContainsKey(assetName); - if (!remove && predicate(assetName, asset.GetType())) + bool remove = removeAssets.ContainsKey(baseAssetName); + if (!remove && predicate(baseAssetName, asset.GetType())) { - removeAssets[assetName] = asset; + removeAssets[baseAssetName] = asset; remove = true; } @@ -275,44 +267,9 @@ namespace StardewModdingAPI.Framework.ContentManagers this.BaseDisposableReferences.Clear(); } - /// Parse a cache key into its component parts. - /// The input cache key. - /// The original asset name. - /// The asset locale code (or null if not localized). - protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) - { - // handle localized key - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.Ordinal); - if (lastSepIndex >= 0) - { - string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.Coordinator.TryGetLanguageEnum(suffix, out _)) - { - assetName = cacheKey.Substring(0, lastSepIndex); - localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - return; - } - } - } - - // handle simple key - assetName = cacheKey; - localeCode = null; - } - /// Get whether an asset has already been loaded. /// The normalized asset name. /// The language to check. protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language); - - /// Get the asset name from a cache key. - /// The input cache key. - private string GetAssetName(string cacheKey) - { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; - } } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index ab198076..0ca9e277 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -73,46 +73,46 @@ namespace StardewModdingAPI.Framework.ContentManagers } // normalize asset name - assetName = this.AssertAndNormalizeAssetName(assetName); - if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) - return this.Load(newAssetName, newLanguage, useCache); + IAssetName parsedName = this.Coordinator.ParseAssetName(assetName); + if (parsedName.LanguageCode.HasValue) + return this.Load(parsedName.BaseName, parsedName.LanguageCode.Value, useCache); // get from cache - if (useCache && this.IsLoaded(assetName, language)) - return this.RawLoad(assetName, language, useCache: true); + if (useCache && this.IsLoaded(parsedName.Name, language)) + return this.RawLoad(parsedName.Name, language, useCache: true); // get managed asset - if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + if (this.Coordinator.TryParseManagedAssetKey(parsedName.Name, out string contentManagerID, out string relativePath)) { T managedAsset = this.Coordinator.LoadManagedAsset(contentManagerID, relativePath); - this.TrackAsset(assetName, managedAsset, language, useCache); + this.TrackAsset(parsedName.Name, managedAsset, language, useCache); return managedAsset; } // load asset T data; - if (this.AssetsBeingLoaded.Contains(assetName)) + if (this.AssetsBeingLoaded.Contains(parsedName.Name)) { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Broke loop while loading asset '{parsedName.Name}'.", LogLevel.Warn); this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}"); - data = this.RawLoad(assetName, language, useCache); + data = this.RawLoad(parsedName.Name, language, useCache); } else { - data = this.AssetsBeingLoaded.Track(assetName, () => + data = this.AssetsBeingLoaded.Track(parsedName.Name, () => { string locale = this.GetLocale(language); - IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName); + IAssetInfo info = new AssetInfo(locale, parsedName, typeof(T), this.AssertAndNormalizeAssetName); IAssetData asset = this.ApplyLoader(info) - ?? new AssetDataForObject(info, this.RawLoad(assetName, language, useCache), this.AssertAndNormalizeAssetName); + ?? new AssetDataForObject(info, this.RawLoad(parsedName.Name, language, useCache), this.AssertAndNormalizeAssetName); asset = this.ApplyEditors(info, asset); return (T)asset.Data; }); } // update cache & return data - this.TrackAsset(assetName, data, language, useCache); + this.TrackAsset(parsedName.Name, data, language, useCache); return data; } @@ -124,13 +124,16 @@ namespace StardewModdingAPI.Framework.ContentManagers // find assets for which a translatable version was loaded HashSet removeAssetNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string key in this.LocalizedAssetNames.Where(p => p.Key != p.Value).Select(p => p.Key)) - removeAssetNames.Add(this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) ? assetName : key); + { + IAssetName assetName = this.Coordinator.ParseAssetName(key); + removeAssetNames.Add(assetName.BaseName); + } // invalidate translatable assets string[] invalidated = this .InvalidateCache((key, type) => removeAssetNames.Contains(key) - || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) + || removeAssetNames.Contains(this.Coordinator.ParseAssetName(key).BaseName) ) .Select(p => p.Key) .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) @@ -168,9 +171,10 @@ namespace StardewModdingAPI.Framework.ContentManagers { // handle explicit language in asset name { - if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) + IAssetName parsedName = this.Coordinator.ParseAssetName(assetName); + if (parsedName.LanguageCode.HasValue) { - this.TrackAsset(newAssetName, value, newLanguage, useCache); + this.TrackAsset(parsedName.BaseName, value, parsedName.LanguageCode.Value, useCache); return; } } @@ -238,30 +242,6 @@ namespace StardewModdingAPI.Framework.ContentManagers } } - /// Parse an asset key that contains an explicit language into its asset name and language, if applicable. - /// The asset key to parse. - /// The asset name without the language code. - /// The language code removed from the asset name. - /// Returns whether the asset key contains an explicit language and was successfully parsed. - private bool TryParseExplicitLanguageAssetKey(string rawAsset, out string assetName, out LanguageCode language) - { - if (string.IsNullOrWhiteSpace(rawAsset)) - throw new SContentLoadException("The asset key is empty."); - - // extract language code - int splitIndex = rawAsset.LastIndexOf('.'); - if (splitIndex != -1 && this.Coordinator.TryGetLanguageEnum(rawAsset.Substring(splitIndex + 1), out language)) - { - assetName = rawAsset.Substring(0, splitIndex); - return true; - } - - // no explicit language code found - assetName = rawAsset; - language = this.Language; - return false; - } - /// Load the initial asset from the registered . /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. @@ -277,7 +257,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) { - entry.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + entry.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -289,7 +269,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (loaders.Length > 1) { string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); - this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + this.Monitor.Log($"Multiple mods want to provide the '{info.Name}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); return null; } @@ -300,11 +280,11 @@ namespace StardewModdingAPI.Framework.ContentManagers try { data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'.", LogLevel.Trace); } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when loading asset '{info.Name}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return null; } @@ -349,7 +329,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -358,22 +338,22 @@ namespace StardewModdingAPI.Framework.ContentManagers try { editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}."); + this.Monitor.Log($"{mod.DisplayName} edited {info.Name}."); } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when editing asset '{info.Name}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit if (asset.Data == null) { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{info.Name}' to a null value; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } else if (!(asset.Data is T)) { - mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{asset.Name}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } } @@ -393,21 +373,21 @@ namespace StardewModdingAPI.Framework.ContentManagers // can't load a null asset if (data == null) { - mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': mod incorrectly set asset to a null value.", LogLevel.Error); + mod.LogAsMod($"SMAPI blocked asset replacement for '{info.Name}': mod incorrectly set asset to a null value.", LogLevel.Error); return false; } // when replacing a map, the vanilla tilesheets must have the same order and IDs if (data is Map loadedMap) { - TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName); + TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.Name.Name); foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs) { // add missing tilesheet if (loadedMap.GetTileSheet(vanillaSheet.Id) == null) { mod.Monitor.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn); - this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.AssetName}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource})."); + this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.Name}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource})."); loadedMap.AddTileSheet(new TileSheet(vanillaSheet.Id, loadedMap, vanillaSheet.ImageSource, vanillaSheet.SheetSize, vanillaSheet.TileSize)); } @@ -417,17 +397,17 @@ namespace StardewModdingAPI.Framework.ContentManagers { // only show warning if not farm map // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. - bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); + bool isFarmMap = info.Name.IsEquivalentTo("Maps/Farm") || info.Name.IsEquivalentTo("Maps/Farm_Combat") || info.Name.IsEquivalentTo("Maps/Farm_Fishing") || info.Name.IsEquivalentTo("Maps/Farm_Foraging") || info.Name.IsEquivalentTo("Maps/Farm_FourCorners") || info.Name.IsEquivalentTo("Maps/Farm_Island") || info.Name.IsEquivalentTo("Maps/Farm_Mining"); string reason = $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); if (isFarmMap) { - mod.LogAsMod($"SMAPI blocked '{info.AssetName}' map load: {reason}", LogLevel.Error); + mod.LogAsMod($"SMAPI blocked '{info.Name}' map load: {reason}", LogLevel.Error); return false; } - mod.LogAsMod($"SMAPI found an issue with '{info.AssetName}' map load: {reason}", LogLevel.Warn); + mod.LogAsMod($"SMAPI found an issue with '{info.Name}' map load: {reason}", LogLevel.Warn); } } } diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index d7963305..ba7dbc06 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -58,9 +58,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The language. bool IsLoaded(string assetName, LocalizedContentManager.LanguageCode language); - /// Get the cached asset keys. - IEnumerable GetAssetKeys(); - /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index beb90a5d..21f88d47 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // normalize key bool isXnbFile = Path.GetExtension(assetName).ToLower() == ".xnb"; - assetName = this.AssertAndNormalizeAssetName(assetName); + IAssetName parsedName = this.Coordinator.ParseAssetName(assetName); // disable caching // This is necessary to avoid assets being shared between content managers, which can @@ -97,21 +97,21 @@ namespace StardewModdingAPI.Framework.ContentManagers // resolve managed asset key { - if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + if (this.Coordinator.TryParseManagedAssetKey(parsedName.Name, out string contentManagerID, out string relativePath)) { if (contentManagerID != this.Name) - throw new SContentLoadException($"Can't load managed asset key '{assetName}' through content manager '{this.Name}' for a different mod."); - assetName = relativePath; + throw new SContentLoadException($"Can't load managed asset key '{parsedName}' through content manager '{this.Name}' for a different mod."); + parsedName = this.Coordinator.ParseAssetName(relativePath); } } // get local asset - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}"); + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{parsedName}' from {this.Name}: {reasonPhrase}"); T asset; try { // get file - FileInfo file = this.GetModFile(isXnbFile ? $"{assetName}.xnb" : assetName); // .xnb extension is stripped from asset names passed to the content manager + FileInfo file = this.GetModFile(isXnbFile ? $"{parsedName}.xnb" : parsedName.Name); // .xnb extension is stripped from asset names passed to the content manager if (!file.Exists) throw GetContentError("the specified path doesn't exist."); @@ -121,11 +121,11 @@ namespace StardewModdingAPI.Framework.ContentManagers // XNB file case ".xnb": { - asset = this.RawLoad(assetName, useCache: false); + asset = this.RawLoad(parsedName.Name, useCache: false); if (asset is Map map) { - map.assetPath = assetName; - this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: true); + map.assetPath = parsedName.Name; + this.FixTilesheetPaths(map, relativeMapPath: parsedName.Name, fixEagerPathPrefixes: true); } } break; @@ -173,8 +173,8 @@ namespace StardewModdingAPI.Framework.ContentManagers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - map.assetPath = assetName; - this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: false); + map.assetPath = parsedName.Name; + this.FixTilesheetPaths(map, relativeMapPath: parsedName.Name, fixEagerPathPrefixes: false); asset = (T)(object)map; } break; @@ -185,11 +185,11 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) when (!(ex is SContentLoadException)) { - throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex); + throw new SContentLoadException($"The content manager failed loading content asset '{parsedName}' from {this.Name}.", ex); } // track & return asset - this.TrackAsset(assetName, asset, language, useCache); + this.TrackAsset(parsedName.Name, asset, language, useCache); return asset; } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index bfca2264..a01248a8 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -129,7 +129,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); - return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any(); + return this.ContentCore.InvalidateCache(asset => asset.Name.IsEquivalentTo(actualKey)).Any(); } /// @@ -153,7 +153,8 @@ namespace StardewModdingAPI.Framework.ModHelpers throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); assetName ??= $"temp/{Guid.NewGuid():N}"; - return new AssetDataForObject(this.CurrentLocale, assetName, data, this.NormalizeAssetName); + + return new AssetDataForObject(this.CurrentLocale, this.ContentCore.ParseAssetName(assetName), data, this.NormalizeAssetName); } diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index 6cdf01ee..6ac8358d 100644 --- a/src/SMAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs @@ -11,7 +11,11 @@ namespace StardewModdingAPI /// The content's locale code, if the content is localized. string Locale { get; } + /// The asset name being read. + public IAssetName Name { get; } + /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. + [Obsolete($"Use {nameof(Name)} instead.")] string AssetName { get; } /// The content data type. @@ -23,6 +27,7 @@ namespace StardewModdingAPI *********/ /// Get whether the asset name being loaded matches a given name after normalization. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] bool AssetNameEquals(string path); } } diff --git a/src/SMAPI/IAssetName.cs b/src/SMAPI/IAssetName.cs new file mode 100644 index 00000000..a5bfea93 --- /dev/null +++ b/src/SMAPI/IAssetName.cs @@ -0,0 +1,44 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI +{ + /// The name for an asset loaded through the content pipeline. + public interface IAssetName : IEquatable + { + /********* + ** Accessors + *********/ + /// The full normalized asset name, including the locale if applicable (like Data/Achievements.fr-FR). + string Name { get; } + + /// The base asset name without the locale code. + string BaseName { get; } + + /// The locale code specified in the , if it's a valid code recognized by the game content. + string LocaleCode { get; } + + /// The language code matching the , if applicable. + LocalizedContentManager.LanguageCode? LanguageCode { get; } + + + /********* + ** Public methods + *********/ + /// Get whether the given asset name is equivalent, ignoring capitalization and formatting. + /// The asset name to compare this instance to. + /// Whether to compare the given name with the (if true) or (if false). This has no effect on any locale included in the given . + bool IsEquivalentTo(string assetName, bool useBaseName = false); + + /// Get whether the asset name starts with the given value, ignoring capitalization and formatting. This can be used with a trailing slash to test for an asset folder, like Data/. + /// The prefix to match. + /// Whether to match if the prefix occurs mid-word, so Data/AchievementsToIgnore matches prefix Data/Achievements. If this is false, the prefix only matches if the asset name starts with the prefix followed by a non-alphanumeric character (including ., /, or \\) or the end of string. + /// Whether to match the prefix if there's a subfolder path after it, so Data/Achievements/Example matches prefix Data/Achievements. If this is false, the prefix only matches if the asset name has no / or \\ characters after the prefix. + bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true); + + /// Get whether the asset is directly within the given asset path. + /// For example, Characters/Dialogue/Abigail is directly under Characters/Dialogue but not Characters or Characters/Dialogue/Ab. To allow sub-paths, use instead. + /// The asset path to check. This doesn't need a trailing slash. + bool IsDirectlyUnderPath(string assetFolder); + } +} diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index a6c4bb24..e7fac578 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -87,22 +87,22 @@ namespace StardewModdingAPI.Metadata /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// A lookup of asset names to whether they've been propagated. /// Whether the NPC pathfinding cache was reloaded. - public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool updatedNpcWarps) + public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool updatedNpcWarps) { // group into optimized lists var buckets = assets.GroupBy(p => { - if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters/Monsters")) + if (p.Key.IsDirectlyUnderPath("Characters") || p.Key.IsDirectlyUnderPath("Characters/Monsters")) return AssetBucket.Sprite; - if (this.IsInFolder(p.Key, "Portraits")) + if (p.Key.IsDirectlyUnderPath("Portraits")) return AssetBucket.Portrait; return AssetBucket.Other; }); // reload assets - propagatedAssets = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase); + propagatedAssets = assets.ToDictionary(p => p.Key, _ => false); updatedNpcWarps = false; foreach (var bucket in buckets) { @@ -149,16 +149,16 @@ namespace StardewModdingAPI.Metadata ** Private methods *********/ /// Reload one of the game's core assets (if applicable). - /// The asset key to reload. + /// The asset name to reload. /// The asset type to reload. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// Whether any map warps were changed as part of this propagation. /// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true. [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(string key, Type type, bool ignoreWorld, out bool changedWarps) + private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarps) { var content = this.MainContentManager; - key = this.AssertAndNormalizeAssetName(key); + string key = assetName.Name; changedWarps = false; /**** @@ -170,7 +170,7 @@ namespace StardewModdingAPI.Metadata { foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) { - if (this.IsSameAssetKey(tilesheet.ImageSource, key)) + if (assetName.IsEquivalentTo(tilesheet.ImageSource)) Game1.mapDisplayDevice.LoadTileSheet(tilesheet); } } @@ -188,7 +188,7 @@ namespace StardewModdingAPI.Metadata { GameLocation location = info.Location; - if (this.IsSameAssetKey(location.mapPath.Value, key)) + if (assetName.IsEquivalentTo(location.mapPath.Value)) { static ISet GetWarpSet(GameLocation location) { @@ -213,14 +213,13 @@ namespace StardewModdingAPI.Metadata /**** ** Propagate by key ****/ - Reflector reflection = this.Reflection; - switch (key.ToLower().Replace("\\", "/")) // normalized key so we can compare statically + switch (assetName.Name.ToLower().Replace("\\", "/")) // normalized key so we can compare statically { /**** ** Animals ****/ case "animals/horse": - return !ignoreWorld && this.ReloadPetOrHorseSprites(content, key); + return !ignoreWorld && this.ReloadPetOrHorseSprites(content, assetName); /**** ** Buildings @@ -231,7 +230,7 @@ namespace StardewModdingAPI.Metadata case "buildings/houses_paintmask": // Farm { - bool removedFromCache = this.RemoveFromPaintMaskCache(key); + bool removedFromCache = this.RemoveFromPaintMaskCache(assetName); Farm farm = Game1.getFarm(); farm?.ApplyHousePaint(); @@ -250,7 +249,7 @@ namespace StardewModdingAPI.Metadata case "characters/farmer/farmer_base_bald": case "characters/farmer/farmer_girl_base": case "characters/farmer/farmer_girl_base_bald": - return !ignoreWorld && this.ReloadPlayerSprites(key); + return !ignoreWorld && this.ReloadPlayerSprites(assetName); case "characters/farmer/hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key); @@ -313,7 +312,7 @@ namespace StardewModdingAPI.Metadata return true; case "data/npcdispositions": // NPC constructor - return !ignoreWorld && this.ReloadNpcDispositions(content, key); + return !ignoreWorld && this.ReloadNpcDispositions(content, assetName); case "data/npcgifttastes": // Game1.LoadContent Game1.NPCGiftTastes = content.Load>(key); @@ -393,7 +392,7 @@ namespace StardewModdingAPI.Metadata } if (!ignoreWorld) - this.ReloadDoorSprites(content, key); + this.ReloadDoorSprites(content, assetName); return true; case "loosesprites/cursors2": // Game1.LoadContent @@ -425,7 +424,7 @@ namespace StardewModdingAPI.Metadata return true; case "loosesprites/suspensionbridge": // SuspensionBridge constructor - return !ignoreWorld && this.ReloadSuspensionBridges(content, key); + return !ignoreWorld && this.ReloadSuspensionBridges(content, assetName); /**** ** Content\Maps @@ -456,7 +455,7 @@ namespace StardewModdingAPI.Metadata return false; case "minigames/titlebuttons": // TitleMenu - return this.ReloadTitleButtons(content, key); + return this.ReloadTitleButtons(content, assetName); /**** ** Content\Strings @@ -480,14 +479,14 @@ namespace StardewModdingAPI.Metadata return true; case "tilesheets/chairtiles": // Game1.LoadContent - return this.ReloadChairTiles(content, key, ignoreWorld); + return this.ReloadChairTiles(content, assetName, ignoreWorld); case "tilesheets/craftables": // Game1.LoadContent Game1.bigCraftableSpriteSheet = content.Load(key); return true; case "tilesheets/critters": // Critter constructor - return !ignoreWorld && this.ReloadCritterTextures(content, key) > 0; + return !ignoreWorld && this.ReloadCritterTextures(content, assetName) > 0; case "tilesheets/crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); @@ -541,7 +540,7 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/grass": // from Grass - return !ignoreWorld && this.ReloadGrassTextures(content, key); + return !ignoreWorld && this.ReloadGrassTextures(content, assetName); case "terrainfeatures/hoedirt": // from HoeDirt HoeDirt.lightTexture = content.Load(key); @@ -556,27 +555,27 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/mushroom_tree": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.mushroomTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.mushroomTree); case "terrainfeatures/tree_palm": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.palmTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.palmTree); case "terrainfeatures/tree1_fall": // from Tree case "terrainfeatures/tree1_spring": // from Tree case "terrainfeatures/tree1_summer": // from Tree case "terrainfeatures/tree1_winter": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.bushyTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.bushyTree); case "terrainfeatures/tree2_fall": // from Tree case "terrainfeatures/tree2_spring": // from Tree case "terrainfeatures/tree2_summer": // from Tree case "terrainfeatures/tree2_winter": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.leafyTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.leafyTree); case "terrainfeatures/tree3_fall": // from Tree case "terrainfeatures/tree3_spring": // from Tree case "terrainfeatures/tree3_winter": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.pineTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.pineTree); } /**** @@ -585,25 +584,25 @@ namespace StardewModdingAPI.Metadata if (!ignoreWorld) { // dynamic textures - if (this.KeyStartsWith(key, "animals/cat")) - return this.ReloadPetOrHorseSprites(content, key); - if (this.KeyStartsWith(key, "animals/dog")) - return this.ReloadPetOrHorseSprites(content, key); - if (this.IsInFolder(key, "Animals")) - return this.ReloadFarmAnimalSprites(content, key); + if (assetName.StartsWith("animals/cat")) + return this.ReloadPetOrHorseSprites(content, assetName); + if (assetName.StartsWith("animals/dog")) + return this.ReloadPetOrHorseSprites(content, assetName); + if (assetName.IsDirectlyUnderPath("Animals")) + return this.ReloadFarmAnimalSprites(content, assetName); - if (this.IsInFolder(key, "Buildings")) - return this.ReloadBuildings(key); + if (assetName.IsDirectlyUnderPath("Buildings")) + return this.ReloadBuildings(assetName); - if (this.KeyStartsWith(key, "LooseSprites/Fence")) - return this.ReloadFenceTextures(key); + if (assetName.StartsWith("LooseSprites/Fence")) + return this.ReloadFenceTextures(assetName); // dynamic data - if (this.IsInFolder(key, "Characters/Dialogue")) - return this.ReloadNpcDialogue(key); + if (assetName.IsDirectlyUnderPath("Characters/Dialogue")) + return this.ReloadNpcDialogue(assetName); - if (this.IsInFolder(key, "Characters/schedules")) - return this.ReloadNpcSchedules(key); + if (assetName.IsDirectlyUnderPath("Characters/schedules")) + return this.ReloadNpcSchedules(assetName); } return false; @@ -618,14 +617,14 @@ namespace StardewModdingAPI.Metadata ****/ /// Reload buttons on the title screen. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. /// Derived from the constructor and . - private bool ReloadTitleButtons(LocalizedContentManager content, string key) + private bool ReloadTitleButtons(LocalizedContentManager content, IAssetName assetName) { if (Game1.activeClickableMenu is TitleMenu titleMenu) { - Texture2D texture = content.Load(key); + Texture2D texture = content.Load(assetName.Name); titleMenu.titleButtonsTexture = texture; titleMenu.backButton.texture = texture; @@ -645,21 +644,21 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for matching pets or horses. /// The animal type. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadPetOrHorseSprites(LocalizedContentManager content, string key) + private bool ReloadPetOrHorseSprites(LocalizedContentManager content, IAssetName assetName) where TAnimal : NPC { // find matches TAnimal[] animals = this.GetCharacters() .OfType() - .Where(p => this.IsSameAssetKey(p.Sprite?.Texture?.Name, key)) + .Where(p => assetName.IsEquivalentTo(p.Sprite?.Texture?.Name)) .ToArray(); if (!animals.Any()) return false; // update sprites - Texture2D texture = content.Load(key); + Texture2D texture = content.Load(assetName.Name); foreach (TAnimal animal in animals) animal.Sprite.spriteTexture = texture; return true; @@ -667,10 +666,10 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for matching farm animals. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. /// Derived from . - private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key) + private bool ReloadFarmAnimalSprites(LocalizedContentManager content, IAssetName assetName) { // find matches FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); @@ -678,7 +677,7 @@ namespace StardewModdingAPI.Metadata return false; // update sprites - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (FarmAnimal animal in animals) { // get expected key @@ -690,23 +689,23 @@ namespace StardewModdingAPI.Metadata expectedKey = $"Animals/{expectedKey}"; // reload asset - if (this.IsSameAssetKey(expectedKey, key)) + if (assetName.IsEquivalentTo(expectedKey)) animal.Sprite.spriteTexture = texture.Value; } return texture.IsValueCreated; } /// Reload building textures. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadBuildings(string key) + private bool ReloadBuildings(IAssetName assetName) { // get paint mask info const string paintMaskSuffix = "_PaintMask"; - bool isPaintMask = key.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase); + bool isPaintMask = assetName.BaseName.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase); // get building type - string type = Path.GetFileName(key); + string type = Path.GetFileName(assetName.Name)!; if (isPaintMask) type = type.Substring(0, type.Length - paintMaskSuffix.Length); @@ -718,7 +717,7 @@ namespace StardewModdingAPI.Metadata .ToArray(); // remove from paint mask cache - bool removedFromCache = this.RemoveFromPaintMaskCache(key); + bool removedFromCache = this.RemoveFromPaintMaskCache(assetName); // reload textures if (buildings.Any()) @@ -734,12 +733,12 @@ namespace StardewModdingAPI.Metadata /// Reload map seat textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// Returns whether any textures were reloaded. - private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld) + private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld) { - MapSeat.mapChairTexture = content.Load(key); + MapSeat.mapChairTexture = content.Load(assetName.Name); if (!ignoreWorld) { @@ -747,7 +746,7 @@ namespace StardewModdingAPI.Metadata { foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) { - if (this.IsSameAssetKey(seat._loadedTextureFile, key)) + if (assetName.IsEquivalentTo(seat._loadedTextureFile)) seat.overlayTexture = MapSeat.mapChairTexture; } } @@ -758,9 +757,9 @@ namespace StardewModdingAPI.Metadata /// Reload critter textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns the number of reloaded assets. - private int ReloadCritterTextures(LocalizedContentManager content, string key) + private int ReloadCritterTextures(LocalizedContentManager content, IAssetName assetName) { // get critters Critter[] critters = @@ -768,7 +767,7 @@ namespace StardewModdingAPI.Metadata from location in this.GetLocations() where location.critters != null from Critter critter in location.critters - where this.IsSameAssetKey(critter.sprite?.Texture?.Name, key) + where assetName.IsEquivalentTo(critter.sprite?.Texture?.Name) select critter ) .ToArray(); @@ -776,7 +775,7 @@ namespace StardewModdingAPI.Metadata return 0; // update sprites - Texture2D texture = content.Load(key); + Texture2D texture = content.Load(assetName.Name); foreach (var entry in critters) entry.sprite.spriteTexture = texture; @@ -785,11 +784,11 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for interior doors. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any doors were affected. - private bool ReloadDoorSprites(LocalizedContentManager content, string key) + private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName) { - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (GameLocation location in this.GetLocations()) { @@ -803,7 +802,7 @@ namespace StardewModdingAPI.Metadata continue; string curKey = this.Reflection.GetField(door.Sprite, "textureName").GetValue(); - if (this.IsSameAssetKey(curKey, key)) + if (assetName.IsEquivalentTo(curKey)) door.Sprite.texture = texture.Value; } } @@ -827,12 +826,12 @@ namespace StardewModdingAPI.Metadata } /// Reload the sprites for a fence type. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadFenceTextures(string key) + private bool ReloadFenceTextures(IAssetName assetName) { - // get fence type - if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType)) + // get fence type (e.g. LooseSprites/Fence3 => 3) + if (!int.TryParse(this.GetSegments(assetName.BaseName)[1].Substring("Fence".Length), out int fenceType)) return false; // get fences @@ -855,22 +854,22 @@ namespace StardewModdingAPI.Metadata /// Reload tree textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadGrassTextures(LocalizedContentManager content, string key) + private bool ReloadGrassTextures(LocalizedContentManager content, IAssetName assetName) { Grass[] grasses = ( from location in this.GetLocations() from grass in location.terrainFeatures.Values.OfType() - where this.IsSameAssetKey(grass.textureName(), key) + where assetName.IsEquivalentTo(grass.textureName()) select grass ) .ToArray(); if (grasses.Any()) { - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (Grass grass in grasses) grass.texture = texture; return true; @@ -932,11 +931,11 @@ namespace StardewModdingAPI.Metadata /// Reload the disposition data for matching NPCs. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any NPCs were affected. - private bool ReloadNpcDispositions(LocalizedContentManager content, string key) + private bool ReloadNpcDispositions(LocalizedContentManager content, IAssetName assetName) { - IDictionary data = content.Load>(key); + IDictionary data = content.Load>(assetName.Name); bool changed = false; foreach (NPC npc in this.GetCharacters()) { @@ -953,16 +952,16 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for matching NPCs. /// The asset keys to reload. /// The asset keys which have been propagated. - private void ReloadNpcSprites(IEnumerable keys, IDictionary propagated) + private void ReloadNpcSprites(IEnumerable keys, IDictionary propagated) { // get NPCs - HashSet lookup = new HashSet(keys, StringComparer.OrdinalIgnoreCase); + IDictionary lookup = keys.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); var characters = ( from npc in this.GetCharacters() let key = this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name) - where key != null && lookup.Contains(key) - select new { Npc = npc, Key = key } + where key != null && lookup.ContainsKey(key) + select new { Npc = npc, AssetName = lookup[key] } ) .ToArray(); if (!characters.Any()) @@ -971,56 +970,56 @@ namespace StardewModdingAPI.Metadata // update sprite foreach (var target in characters) { - target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.Key); - propagated[target.Key] = true; + target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.Name); + propagated[target.AssetName] = true; } } /// Reload the portraits for matching NPCs. /// The asset key to reload. /// The asset keys which have been propagated. - private void ReloadNpcPortraits(IEnumerable keys, IDictionary propagated) + private void ReloadNpcPortraits(IEnumerable keys, IDictionary propagated) { // get NPCs - HashSet lookup = new HashSet(keys, StringComparer.OrdinalIgnoreCase); + IDictionary lookup = keys.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); var characters = ( from npc in this.GetCharacters() where npc.isVillager() let key = this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name) - where key != null && lookup.Contains(key) - select new { Npc = npc, Key = key } + where key != null && lookup.ContainsKey(key) + select new { Npc = npc, AssetName = lookup[key] } ) .ToList(); // special case: Gil is a private NPC field on the AdventureGuild class (only used for the portrait) { string gilKey = this.NormalizeAssetNameIgnoringEmpty("Portraits/Gil"); - if (lookup.Contains(gilKey)) + if (lookup.TryGetValue(gilKey, out IAssetName assetName)) { GameLocation adventureGuild = Game1.getLocationFromName("AdventureGuild"); if (adventureGuild != null) - characters.Add(new { Npc = this.Reflection.GetField(adventureGuild, "Gil").GetValue(), Key = gilKey }); + characters.Add(new { Npc = this.Reflection.GetField(adventureGuild, "Gil").GetValue(), AssetName = assetName }); } } // update portrait foreach (var target in characters) { - target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.Key); - propagated[target.Key] = true; + target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.AssetName.Name); + propagated[target.AssetName] = true; } } /// Reload the sprites for matching players. - /// The asset key to reload. - private bool ReloadPlayerSprites(string key) + /// The asset name to reload. + private bool ReloadPlayerSprites(IAssetName assetName) { Farmer[] players = ( from player in Game1.getOnlineFarmers() - where this.IsSameAssetKey(player.getTexture(), key) + where assetName.IsEquivalentTo(player.getTexture()) select player ) .ToArray(); @@ -1036,11 +1035,11 @@ namespace StardewModdingAPI.Metadata /// Reload suspension bridge textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadSuspensionBridges(LocalizedContentManager content, string key) + private bool ReloadSuspensionBridges(LocalizedContentManager content, IAssetName assetName) { - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (GameLocation location in this.GetLocations(buildingInteriors: false)) { @@ -1059,10 +1058,10 @@ namespace StardewModdingAPI.Metadata /// Reload tree textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// The type to reload. /// Returns whether any textures were reloaded. - private bool ReloadTreeTextures(LocalizedContentManager content, string key, int type) + private bool ReloadTreeTextures(LocalizedContentManager content, IAssetName assetName, int type) { Tree[] trees = this.GetLocations() .SelectMany(p => p.terrainFeatures.Values.OfType()) @@ -1071,7 +1070,7 @@ namespace StardewModdingAPI.Metadata if (trees.Any()) { - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (Tree tree in trees) tree.texture = texture; return true; @@ -1084,12 +1083,12 @@ namespace StardewModdingAPI.Metadata ** Reload data methods ****/ /// Reload the dialogue data for matching NPCs. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any assets were reloaded. - private bool ReloadNpcDialogue(string key) + private bool ReloadNpcDialogue(IAssetName assetName) { // get NPCs - string name = Path.GetFileName(key); + string name = Path.GetFileName(assetName.Name); NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -1114,12 +1113,12 @@ namespace StardewModdingAPI.Metadata } /// Reload the schedules for matching NPCs. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any assets were reloaded. - private bool ReloadNpcSchedules(string key) + private bool ReloadNpcSchedules(IAssetName assetName) { // get NPCs - string name = Path.GetFileName(key); + string name = Path.GetFileName(assetName.Name); NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -1243,39 +1242,6 @@ namespace StardewModdingAPI.Metadata return this.AssertAndNormalizeAssetName(path); } - /// Get whether a given asset key is equivalent to a normalized asset key, ignoring unimportant differences like capitalization and formatting. - /// The actual key to check. - /// The key to match, already normalized via or . - private bool IsSameAssetKey(string actualKey, string normalizedKey) - { - if (actualKey is null || normalizedKey is null) - return false; - - return normalizedKey.Equals(PathUtilities.NormalizeAssetName(actualKey), StringComparison.OrdinalIgnoreCase); - } - - /// Get whether a key starts with a substring after the substring is normalized. - /// The key to check. - /// The substring to normalize and find. - private bool KeyStartsWith(string key, string rawSubstring) - { - if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(rawSubstring)) - return false; - - return key.StartsWith(this.NormalizeAssetNameIgnoringEmpty(rawSubstring), StringComparison.OrdinalIgnoreCase); - } - - /// Get whether a normalized asset key is in the given folder. - /// The normalized asset key (like Animals/cat). - /// The key folder (like Animals); doesn't need to be normalized. - /// Whether to return true if the key is inside a subfolder of the . - private bool IsInFolder(string key, string folder, bool allowSubfolders = false) - { - return - this.KeyStartsWith(key, $"{folder}/") - && (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1); - } - /// Get the segments in a path (e.g. 'a/b' is 'a' and 'b'). /// The path to check. private string[] GetSegments(string path) @@ -1285,13 +1251,6 @@ namespace StardewModdingAPI.Metadata : Array.Empty(); } - /// Count the number of segments in a path (e.g. 'a/b' is 2). - /// The path to check. - private int CountSegments(string path) - { - return this.GetSegments(path).Length; - } - /// Load a texture, and dispose the old one if is enabled and it's different from the new instance. /// The previous texture to dispose. /// The asset key to load. @@ -1315,8 +1274,8 @@ namespace StardewModdingAPI.Metadata } /// Remove a case-insensitive key from the paint mask cache. - /// The paint mask asset key. - private bool RemoveFromPaintMaskCache(string key) + /// The paint mask asset name. + private bool RemoveFromPaintMaskCache(IAssetName assetName) { // make cache case-insensitive // This is needed for cache invalidation since mods may specify keys with a different capitalization @@ -1324,7 +1283,7 @@ namespace StardewModdingAPI.Metadata BuildingPainter.paintMaskLookup = new Dictionary>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase); // remove key from cache - return BuildingPainter.paintMaskLookup.Remove(key); + return BuildingPainter.paintMaskLookup.Remove(assetName.Name); } /// Metadata about a location used in asset propagation. -- cgit From a42926868ae5878ed59d6406ca085b587299ba07 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 20 Mar 2022 12:53:27 -0400 Subject: encapsulate editor/loader operations (#766) These will be used by the new content API, and allow handling the old one the same way. --- src/SMAPI/Framework/Content/AssetEditOperation.cs | 30 +++++ src/SMAPI/Framework/Content/AssetLoadOperation.cs | 30 +++++ .../ContentManagers/GameContentManager.cs | 127 ++++++++++++++------- 3 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 src/SMAPI/Framework/Content/AssetEditOperation.cs create mode 100644 src/SMAPI/Framework/Content/AssetLoadOperation.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs new file mode 100644 index 00000000..fa189d44 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Framework.Content +{ + /// An edit to apply to an asset when it's requested from the content pipeline. + internal class AssetEditOperation + { + /********* + ** Accessors + *********/ + /// The mod applying the edit. + public IModMetadata Mod { get; } + + /// Apply the edit to an asset. + public Action ApplyEdit { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod applying the edit. + /// Apply the edit to an asset. + public AssetEditOperation(IModMetadata mod, Action applyEdit) + { + this.Mod = mod; + this.ApplyEdit = applyEdit; + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs new file mode 100644 index 00000000..d773cadd --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Framework.Content +{ + /// An operation which provides the initial instance of an asset when it's requested from the content pipeline. + internal class AssetLoadOperation + { + /********* + ** Accessors + *********/ + /// The mod applying the edit. + public IModMetadata Mod { get; } + + /// Load the initial value for an asset. + public Func GetData { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod applying the edit. + /// Load the initial value for an asset. + public AssetLoadOperation(IModMetadata mod, Func getData) + { + this.Mod = mod; + this.GetData = getData; + } + } +} diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 9b8125ad..7ed1fcda 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Framework.ContentManagers ** Fields *********/ /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. - private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + private readonly ContextHash AssetsBeingLoaded = new(); /// Interceptors which provide the initial versions of matching assets. private IList> Loaders => this.Coordinator.Loaders; @@ -79,12 +79,10 @@ namespace StardewModdingAPI.Framework.ContentManagers // custom asset from a loader string locale = this.GetLocale(); IAssetInfo info = new AssetInfo(locale, assetName, typeof(object), this.AssertAndNormalizeAssetName); - ModLinked[] loaders = this.GetLoaders(info).ToArray(); - if (loaders.Length > 1) - { - string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); - this.Monitor.Log($"Multiple mods want to provide the '{info.Name}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); - } + AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); + + if (!this.AssertMaxOneLoader(info, loaders, out string error)) + this.Monitor.Log(error, LogLevel.Warn); return loaders.Length == 1; } @@ -261,7 +259,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // try base asset return base.RawLoad(assetName.Name, useCache); } - catch (ContentLoadException ex) when (ex.InnerException is FileNotFoundException innerEx && innerEx.InnerException == null) + catch (ContentLoadException ex) when (ex.InnerException is FileNotFoundException { InnerException: null }) { throw new SContentLoadException($"Error loading \"{assetName}\": it isn't in the Content folder and no mod provided it."); } @@ -272,27 +270,31 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Returns the loaded asset metadata, or null if no loader matched. private IAssetData ApplyLoader(IAssetInfo info) { - // find matching loaders - var loaders = this.GetLoaders(info).ToArray(); - - // validate loaders - if (!loaders.Any()) - return null; - if (loaders.Length > 1) + // find matching loader + AssetLoadOperation loader; { - string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); - this.Monitor.Log($"Multiple mods want to provide the '{info.Name}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); - return null; + AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); + + if (!this.AssertMaxOneLoader(info, loaders, out string error)) + { + this.Monitor.Log(error, LogLevel.Warn); + return null; + } + + loader = loaders.FirstOrDefault(); } + // no loader found + if (loader == null) + return null; + // fetch asset from loader - IModMetadata mod = loaders[0].Mod; - IAssetLoader loader = loaders[0].Data; + IModMetadata mod = loader.Mod; T data; try { - data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'.", LogLevel.Trace); + data = (T)loader.GetData(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'."); } catch (Exception ex) { @@ -322,34 +324,23 @@ namespace StardewModdingAPI.Framework.ContentManagers if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary<,>) || actualOpenType == typeof(List<>) || actualType == typeof(Texture2D) || actualType == typeof(Map))) { return (IAssetData)this.GetType() - .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance) + .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)! .MakeGenericMethod(actualType) .Invoke(this, new object[] { info, asset }); } } // edit asset - foreach (var entry in this.Editors) + AssetEditOperation[] editors = this.GetEditors(info).ToArray(); + foreach (AssetEditOperation editor in editors) { - // check for match - IModMetadata mod = entry.Mod; - IAssetEditor editor = entry.Data; - try - { - if (!editor.CanEdit(info)) - continue; - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } + IModMetadata mod = editor.Mod; // try edit object prevAsset = asset.Data; try { - editor.Edit(asset); + editor.ApplyEdit(asset); this.Monitor.Log($"{mod.DisplayName} edited {info.Name}."); } catch (Exception ex) @@ -374,24 +365,72 @@ namespace StardewModdingAPI.Framework.ContentManagers return asset; } - /// Get the asset loaders which handle the asset. + /// Get the asset loaders which handle an asset. /// The asset type. /// The basic asset metadata. - private IEnumerable> GetLoaders(IAssetInfo info) + private IEnumerable GetLoaders(IAssetInfo info) { return this.Loaders - .Where(entry => + .Where(loader => { try { - return entry.Data.CanLoad(info); + return loader.Data.CanLoad(info); } catch (Exception ex) { - entry.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } - }); + }) + .Select( + loader => new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load(assetInfo)) + ); + } + + /// Get the asset editors to apply to an asset. + /// The asset type. + /// The basic asset metadata. + private IEnumerable GetEditors(IAssetInfo info) + { + return this.Editors + .Where(editor => + { + try + { + return editor.Data.CanEdit(info); + } + catch (Exception ex) + { + editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .Select( + editor => new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit(assetData)) + ); + } + + /// Assert that at most one loader will be applied to an asset. + /// The basic asset metadata. + /// The asset loaders to apply. + /// The error message to show to the user, if the method returns false. + /// Returns true if only one loader will apply, else false. + private bool AssertMaxOneLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error) + { + if (loaders.Length <= 1) + { + error = null; + return true; + } + + string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); + string errorPhrase = loaderNames.Length > 1 + ? $"Multiple mods want to provide '{info.Name}' asset ({string.Join(", ", loaderNames)})" + : $"The '{loaderNames[0]}' mod wants to provide the '{info.Name}' asset multiple times"; + + error = $"{errorPhrase}, but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)"; + return false; } /// Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible. -- cgit From b07d2340a9a6da22ee0fd95f2c6ccca3939cb7ab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 22 Mar 2022 23:00:18 -0400 Subject: encapsulate & cache asset operation groups (#766) This is needed for the upcoming Stardew Valley 1.6 to avoid duplicate checks between DoesAssetExist and Load calls, and to make sure the answer doesn't change between them. --- docs/release-notes.md | 1 + src/SMAPI/Framework/Content/AssetOperationGroup.cs | 33 +++++++ src/SMAPI/Framework/ContentCoordinator.cs | 100 ++++++++++++++++++--- .../ContentManagers/GameContentManager.cs | 44 ++------- .../Framework/Utilities/TickCacheDictionary.cs | 51 +++++++++++ 5 files changed, 180 insertions(+), 49 deletions(-) create mode 100644 src/SMAPI/Framework/Content/AssetOperationGroup.cs create mode 100644 src/SMAPI/Framework/Utilities/TickCacheDictionary.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2598dad5..b9385e3f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ * Added `Constants.ContentPath`. * Added `IAssetName Name` field to the info received by `IAssetEditor` and `IAssetLoader` methods. _This adds methods for working with asset names, parsed locales, etc._ + * If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated via `helper.Content.InvalidateCache`. * Fixed the `SDate` constructor being case-sensitive. * Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`). * Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead. 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 +{ + /// A set of operations to apply to an asset for a given or implementation. + internal class AssetOperationGroup + { + /********* + ** Accessors + *********/ + /// The mod applying the changes. + public IModMetadata Mod { get; } + + /// The load operations to apply. + public AssetLoadOperation[] LoadOperations { get; } + + /// The edit operations to apply. + public AssetEditOperation[] EditOperations { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod applying the changes. + /// The load operations to apply. + /// The edit operations to apply. + public AssetOperationGroup(IModMetadata mod, AssetLoadOperation[] loadOperations, AssetEditOperation[] editOperations) + { + this.Mod = mod; + this.LoadOperations = loadOperations; + this.EditOperations = editOperations; + } + } +} diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index fbbbe2d2..bf944e23 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -10,6 +10,8 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; @@ -68,6 +70,9 @@ namespace StardewModdingAPI.Framework /// The language enum values indexed by locale code. private Lazy> LocaleCodes; + /// The cached asset load/edit operations to apply, indexed by asset name. + private readonly TickCacheDictionary AssetOperationsByKey = new(); + /********* ** Accessors @@ -351,17 +356,17 @@ namespace StardewModdingAPI.Framework public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { // invalidate cache & track removed assets - IDictionary removedAssets = new Dictionary(); + IDictionary invalidatedAssets = new Dictionary(); this.ContentManagerLock.InReadLock(() => { // cached assets foreach (IContentManager contentManager in this.ContentManagers) { - foreach (var entry in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose)) + foreach ((string key, object asset) in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose)) { - AssetName assetName = this.ParseAssetName(entry.Key); - if (!removedAssets.ContainsKey(assetName)) - removedAssets[assetName] = entry.Value.GetType(); + AssetName assetName = this.ParseAssetName(key); + if (!invalidatedAssets.ContainsKey(assetName)) + invalidatedAssets[assetName] = asset.GetType(); } } @@ -376,18 +381,22 @@ namespace StardewModdingAPI.Framework // get map path AssetName mapPath = this.ParseAssetName(this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value)); - if (!removedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map))) - removedAssets[mapPath] = typeof(Map); + if (!invalidatedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath.Name, typeof(Map))) + invalidatedAssets[mapPath] = typeof(Map); } } }); + // clear cached editor checks + foreach (IAssetName name in invalidatedAssets.Keys) + this.AssetOperationsByKey.Remove(name); + // reload core game assets - if (removedAssets.Any()) + if (invalidatedAssets.Any()) { // propagate changes to the game this.CoreAssets.Propagate( - assets: removedAssets.ToDictionary(p => p.Key, p => p.Value), + assets: invalidatedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded, out IDictionary propagated, out bool updatedNpcWarps @@ -396,7 +405,7 @@ namespace StardewModdingAPI.Framework // log summary StringBuilder report = new(); { - IAssetName[] invalidatedKeys = removedAssets.Keys.ToArray(); + IAssetName[] invalidatedKeys = invalidatedAssets.Keys.ToArray(); IAssetName[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.Select(p => p.Name).OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); @@ -414,7 +423,18 @@ namespace StardewModdingAPI.Framework else this.Monitor.Log("Invalidated 0 cache entries."); - return removedAssets.Keys; + return invalidatedAssets.Keys; + } + + /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now. + /// The asset type. + /// The asset info to load or edit. + public IEnumerable GetAssetOperations(IAssetInfo info) + { + return this.AssetOperationsByKey.GetOrSet( + info.Name, + () => this.GetAssetOperationsWithoutCache(info).ToArray() + ); } /// Get all loaded instances of an asset name. @@ -534,5 +554,63 @@ namespace StardewModdingAPI.Framework return map; } + + /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now, ignoring the cache. + /// The asset type. + /// The asset info to load or edit. + private IEnumerable GetAssetOperationsWithoutCache(IAssetInfo info) + { + // legacy load operations + foreach (ModLinked loader in this.Loaders) + { + // check if loader applies + try + { + if (!loader.Data.CanLoad(info)) + continue; + } + catch (Exception ex) + { + loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // add operation + yield return new AssetOperationGroup( + mod: loader.Mod, + loadOperations: new[] + { + new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load(assetInfo)) + }, + editOperations: Array.Empty() + ); + } + + // legacy edit operations + foreach (var editor in this.Editors) + { + // check if editor applies + try + { + if (!editor.Data.CanEdit(info)) + continue; + } + catch (Exception ex) + { + editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // add operation + yield return new AssetOperationGroup( + mod: editor.Mod, + loadOperations: Array.Empty(), + editOperations: new[] + { + new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit(assetData)) + } + ); + } + } } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 7ed1fcda..642e526c 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -26,12 +26,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. private readonly ContextHash AssetsBeingLoaded = new(); - /// Interceptors which provide the initial versions of matching assets. - private IList> Loaders => this.Coordinator.Loaders; - - /// Interceptors which edit matching assets after they're loaded. - private IList> Editors => this.Coordinator.Editors; - /// Maps asset names to their localized form, like LooseSprites\Billboard => LooseSprites\Billboard.fr-FR (localized) or Maps\AnimalShop => Maps\AnimalShop (not localized). private IDictionary LocalizedAssetNames => LocalizedContentManager.localizedAssetNames; @@ -370,22 +364,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The basic asset metadata. private IEnumerable GetLoaders(IAssetInfo info) { - return this.Loaders - .Where(loader => - { - try - { - return loader.Data.CanLoad(info); - } - catch (Exception ex) - { - loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return false; - } - }) - .Select( - loader => new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load(assetInfo)) - ); + return this.Coordinator + .GetAssetOperations(info) + .SelectMany(p => p.LoadOperations); } /// Get the asset editors to apply to an asset. @@ -393,22 +374,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The basic asset metadata. private IEnumerable GetEditors(IAssetInfo info) { - return this.Editors - .Where(editor => - { - try - { - return editor.Data.CanEdit(info); - } - catch (Exception ex) - { - editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return false; - } - }) - .Select( - editor => new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit(assetData)) - ); + return this.Coordinator + .GetAssetOperations(info) + .SelectMany(p => p.EditOperations); } /// Assert that at most one loader will be applied to an asset. diff --git a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs new file mode 100644 index 00000000..1613a480 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// An in-memory dictionary cache that stores data for the duration of a game update tick. + /// The dictionary key type. + /// The dictionary value type. + internal class TickCacheDictionary + { + /********* + ** Fields + *********/ + /// The last game tick for which data was cached. + private int LastGameTick = -1; + + /// The underlying cached data. + private readonly Dictionary Cache = new(); + + + /********* + ** Public methods + *********/ + /// Get a value from the cache, fetching it first if it's not cached yet. + /// The unique key for the cached value. + /// Get the latest data if it's not in the cache yet. + public TValue GetOrSet(TKey cacheKey, Func get) + { + // clear cache on new tick + if (Game1.ticks != this.LastGameTick) + { + this.Cache.Clear(); + this.LastGameTick = Game1.ticks; + } + + // fetch value + if (!this.Cache.TryGetValue(cacheKey, out TValue cached)) + this.Cache[cacheKey] = cached = get(); + return cached; + } + + /// Remove an entry from the cache. + /// The unique key for the cached value. + /// Returns whether the key was present in the dictionary. + public bool Remove(TKey cacheKey) + { + return this.Cache.Remove(cacheKey); + } + } +} -- cgit From e1fc566e0afeb6eb92418bb039365611abd33829 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 Mar 2022 21:46:37 -0400 Subject: add content pack labels (#766) --- docs/release-notes.md | 2 +- src/SMAPI/Events/AssetRequestedEventArgs.cs | 30 ++++++++++++++++---- src/SMAPI/Framework/Content/AssetEditOperation.cs | 7 ++++- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 9 ++++-- src/SMAPI/Framework/ContentCoordinator.cs | 12 ++++++-- .../ContentManagers/GameContentManager.cs | 26 ++++++++++++------ src/SMAPI/Framework/InternalExtensions.cs | 9 ++++++ src/SMAPI/Framework/SCore.cs | 32 +++++++++++++++++++++- 8 files changed, 106 insertions(+), 21 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8bf451a5..2a8d142f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,7 +11,7 @@ * Fixed `--no-terminal` still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!). * For mod authors: - * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. + * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. These include new features not supported by the old API like content pack labels. * Overhauled [mod-provided APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) (thanks to Shockah!). _This adds support for many previously-unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more._ * Added `Constants.ContentPath`. diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index b17250b0..774ab808 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI.Events /// The mod handling the event. private readonly IModMetadata Mod; + /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. + private readonly Func GetOnBehalfOf; + /********* ** Accessors @@ -36,14 +39,17 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The mod handling the event. /// The name of the asset being requested. - internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name) + /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. + internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, Func getOnBehalfOf) { this.Mod = mod; this.Name = name; + this.GetOnBehalfOf = getOnBehalfOf; } /// Provide the initial instance for the asset, instead of trying to load it from the game's Content folder. /// Get the initial instance of an asset. + /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. /// /// Usage notes: /// @@ -51,10 +57,14 @@ namespace StardewModdingAPI.Events /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFrom(Func load) + public void LoadFrom(Func load, string onBehalfOf = null) { this.LoadOperations.Add( - new AssetLoadOperation(this.Mod, _ => load()) + new AssetLoadOperation( + mod: this.Mod, + onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), + getData: _ => load() + ) ); } @@ -71,12 +81,16 @@ namespace StardewModdingAPI.Events public void LoadFromModFile(string relativePath) { this.LoadOperations.Add( - new AssetLoadOperation(this.Mod, _ => this.Mod.Mod.Helper.Content.Load(relativePath)) + new AssetLoadOperation( + mod: this.Mod, + onBehalfOf: null, + _ => this.Mod.Mod.Helper.Content.Load(relativePath)) ); } /// Edit the asset after it's loaded. /// Apply changes to the asset. + /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. /// /// Usage notes: /// @@ -84,10 +98,14 @@ namespace StardewModdingAPI.Events /// You can apply any number of edits to the asset. Each edit will be applied on top of the previous one (i.e. it'll see the merged asset from all previous edits as its input). /// /// - public void Edit(Action apply) + public void Edit(Action apply, string onBehalfOf = null) { this.EditOperations.Add( - new AssetEditOperation(this.Mod, apply) + new AssetEditOperation( + mod: this.Mod, + onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"), + apply + ) ); } } diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs index fa189d44..14db231c 100644 --- a/src/SMAPI/Framework/Content/AssetEditOperation.cs +++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs @@ -11,6 +11,9 @@ namespace StardewModdingAPI.Framework.Content /// The mod applying the edit. public IModMetadata Mod { get; } + /// The content pack on whose behalf the edit is being applied, if any. + public IModMetadata OnBehalfOf { get; } + /// Apply the edit to an asset. public Action ApplyEdit { get; } @@ -20,10 +23,12 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// The content pack on whose behalf the edit is being applied, if any. /// Apply the edit to an asset. - public AssetEditOperation(IModMetadata mod, Action applyEdit) + public AssetEditOperation(IModMetadata mod, IModMetadata onBehalfOf, Action applyEdit) { this.Mod = mod; + this.OnBehalfOf = onBehalfOf; this.ApplyEdit = applyEdit; } } diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index d773cadd..29bf1518 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -8,9 +8,12 @@ namespace StardewModdingAPI.Framework.Content /********* ** Accessors *********/ - /// The mod applying the edit. + /// The mod loading the asset. public IModMetadata Mod { get; } + /// The content pack on whose behalf the asset is being loaded, if any. + public IModMetadata OnBehalfOf { get; } + /// Load the initial value for an asset. public Func GetData { get; } @@ -20,10 +23,12 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - public AssetLoadOperation(IModMetadata mod, Func getData) + public AssetLoadOperation(IModMetadata mod, IModMetadata onBehalfOf, Func getData) { this.Mod = mod; + this.OnBehalfOf = onBehalfOf; this.GetData = getData; } } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 4dbbae15..3b304f0d 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -606,7 +606,11 @@ namespace StardewModdingAPI.Framework mod: loader.Mod, loadOperations: new[] { - new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load(assetInfo)) + new AssetLoadOperation( + mod: loader.Mod, + onBehalfOf: null, + getData: assetInfo => loader.Data.Load(assetInfo) + ) }, editOperations: Array.Empty() ); @@ -633,7 +637,11 @@ namespace StardewModdingAPI.Framework loadOperations: Array.Empty(), editOperations: new[] { - new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit(assetData)) + new AssetEditOperation( + mod: editor.Mod, + onBehalfOf: null, + applyEdit: assetData => editor.Data.Edit(assetData) + ) } ); } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 12ed5506..58e36128 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -267,7 +267,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } } - /// Load the initial asset from the registered . + /// Load the initial asset from the registered loaders. /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. private IAssetData ApplyLoader(IAssetInfo info) @@ -296,11 +296,11 @@ namespace StardewModdingAPI.Framework.ContentManagers try { data = (T)loader.GetData(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'."); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'{this.GetOnBehalfOfLabel(loader.OnBehalfOf)}."); } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when loading asset '{info.Name}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when loading asset '{info.Name}'{this.GetOnBehalfOfLabel(loader.OnBehalfOf)}. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return null; } @@ -310,7 +310,7 @@ namespace StardewModdingAPI.Framework.ContentManagers : null; } - /// Apply any to a loaded asset. + /// Apply any editors to a loaded asset. /// The asset type. /// The basic asset metadata. /// The loaded asset. @@ -343,22 +343,22 @@ namespace StardewModdingAPI.Framework.ContentManagers try { editor.ApplyEdit(asset); - this.Monitor.Log($"{mod.DisplayName} edited {info.Name}."); + this.Monitor.Log($"{mod.DisplayName} edited {info.Name}{this.GetOnBehalfOfLabel(editor.OnBehalfOf)}."); } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when editing asset '{info.Name}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when editing asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)}, which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit if (asset.Data == null) { - mod.LogAsMod($"Mod incorrectly set asset '{info.Name}' to a null value; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to a null value; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } else if (!(asset.Data is T)) { - mod.LogAsMod($"Mod incorrectly set asset '{asset.Name}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{asset.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } } @@ -409,6 +409,16 @@ namespace StardewModdingAPI.Framework.ContentManagers return false; } + /// Get a parenthetical label for log messages for the content pack on whose behalf the action is being performed, if any. + /// The content pack on whose behalf the action is being performed. + private string GetOnBehalfOfLabel(IModMetadata onBehalfOf) + { + if (onBehalfOf == null) + return string.Empty; + + return $" (for the '{onBehalfOf.Manifest.Name}' content pack)"; + } + /// Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible. /// The asset type. /// The basic asset metadata. diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 4cb77a45..fe10b045 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -45,6 +45,15 @@ namespace StardewModdingAPI.Framework metadata.Monitor.Log(message, level); } + /// Log a message using the mod's monitor, but only if it hasn't already been logged since the last game launch. + /// The mod whose monitor to use. + /// The message to log. + /// The log severity level. + public static void LogAsModOnce(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) + { + metadata.Monitor.LogOnce(message, level); + } + /**** ** ManagedEvent ****/ diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9d97ec7d..dd682e40 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1133,7 +1133,7 @@ namespace StardewModdingAPI.Framework this.EventManager.AssetRequested.Raise( invoke: (mod, invoke) => { - AssetRequestedEventArgs args = new(mod, asset.Name); + AssetRequestedEventArgs args = new(mod, asset.Name, this.GetOnBehalfOfContentPack); invoke(args); @@ -1149,6 +1149,36 @@ namespace StardewModdingAPI.Framework return operations; } + /// Get the mod metadata for a content pack whose ID matches , if it's a valid content pack for the given . + /// The mod requesting to act on the content pack's behalf. + /// The content pack ID. + /// The verb phrase indicating what action will be performed, like 'load assets' or 'edit assets'. + /// Returns the content pack metadata if valid, else null. + private IModMetadata GetOnBehalfOfContentPack(IModMetadata mod, string id, string verb) + { + if (id == null) + return null; + + string errorPrefix = $"Can't {verb} on behalf of content pack ID '{id}'"; + + // get target mod + IModMetadata onBehalfOf = this.ModRegistry.Get(id); + if (onBehalfOf == null) + { + mod.LogAsModOnce($"{errorPrefix}: there's no content pack installed with that ID.", LogLevel.Warn); + return null; + } + + // make sure it's a content pack for the requesting mod + if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest?.ContentPackFor?.UniqueID, mod.Manifest.UniqueID)) + { + mod.LogAsModOnce($"{errorPrefix}: that isn't a content pack for this mod.", LogLevel.Warn); + return null; + } + + return onBehalfOf; + } + /// Raised immediately before the player returns to the title screen. private void OnReturningToTitle() { -- cgit From 021891ff0ceb6b327bc196c336aa56ddfaf99b0e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 Mar 2022 22:49:14 -0400 Subject: add load conflict resolution option (#766) --- docs/release-notes.md | 3 ++- src/SMAPI/Events/AssetRequestedEventArgs.cs | 10 +++++++--- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 7 ++++++- src/SMAPI/Framework/ContentCoordinator.cs | 1 + .../Framework/ContentManagers/GameContentManager.cs | 17 ++++++++++------- 5 files changed, 26 insertions(+), 12 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7cdd093a..c8c87db3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,7 +12,8 @@ * Fixed warning text when a mod causes an asset load conflict with itself. * For mod authors: - * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. These include new features not supported by the old API like content pack labels. + * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. + _These include new features not supported by the old API, like load conflict resolution and content pack labels. They also support new cases like easily detecting when an asset has been changed._ * Overhauled [mod-provided APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) (thanks to Shockah!). _This adds support for many previously-unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more._ * Added `Constants.ContentPath`. diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index 774ab808..9942079b 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -50,18 +50,20 @@ namespace StardewModdingAPI.Events /// Provide the initial instance for the asset, instead of trying to load it from the game's Content folder. /// Get the initial instance of an asset. /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. + /// When there are multiple loads that apply to the same asset, this indicates whether this one can be skipped to resolve the conflict. If all loads allow skipping, the first one that was registered will be applied. If this is false, SMAPI will raise an error and apply none of them. /// /// Usage notes: /// /// The asset doesn't need to exist in the game's Content folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder. - /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. + /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFrom(Func load, string onBehalfOf = null) + public void LoadFrom(Func load, string onBehalfOf = null, bool allowSkipOnConflict = false) { this.LoadOperations.Add( new AssetLoadOperation( mod: this.Mod, + allowSkipOnConflict: allowSkipOnConflict, onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), getData: _ => load() ) @@ -71,6 +73,7 @@ namespace StardewModdingAPI.Events /// Provide the initial instance for the asset from a file in your mod folder, instead of trying to load it from the game's Content folder. /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. /// The relative path to the file in your mod folder. + /// When there are multiple loads that apply to the same asset, this indicates whether this one can be skipped to resolve the conflict. If all loads allow skipping, the first one that was registered will be applied. If this is false, SMAPI will raise an error and apply none of them. /// /// Usage notes: /// @@ -78,11 +81,12 @@ namespace StardewModdingAPI.Events /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFromModFile(string relativePath) + public void LoadFromModFile(string relativePath, bool allowSkipOnConflict = false) { this.LoadOperations.Add( new AssetLoadOperation( mod: this.Mod, + allowSkipOnConflict: allowSkipOnConflict, onBehalfOf: null, _ => this.Mod.Mod.Helper.Content.Load(relativePath)) ); diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index 29bf1518..36baf1aa 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Framework.Content /// The content pack on whose behalf the asset is being loaded, if any. public IModMetadata OnBehalfOf { get; } + /// Whether to allow skipping this operation to resolve a load conflict. + public bool AllowSkipOnConflict { get; } + /// Load the initial value for an asset. public Func GetData { get; } @@ -23,11 +26,13 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// Whether to allow skipping this operation to resolve a load conflict. /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - public AssetLoadOperation(IModMetadata mod, IModMetadata onBehalfOf, Func getData) + public AssetLoadOperation(IModMetadata mod, bool allowSkipOnConflict, IModMetadata onBehalfOf, Func getData) { this.Mod = mod; + this.AllowSkipOnConflict = allowSkipOnConflict; this.OnBehalfOf = onBehalfOf; this.GetData = getData; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 3b304f0d..43cebcbe 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -608,6 +608,7 @@ namespace StardewModdingAPI.Framework { new AssetLoadOperation( mod: loader.Mod, + allowSkipOnConflict: false, onBehalfOf: null, getData: assetInfo => loader.Data.Load(assetInfo) ) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 1d9c8b4c..b3e98648 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Framework.ContentManagers IAssetInfo info = new AssetInfo(locale, assetName, typeof(object), this.AssertAndNormalizeAssetName); AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); - if (!this.AssertMaxOneLoader(info, loaders, out string error)) + if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error)) this.Monitor.Log(error, LogLevel.Warn); return loaders.Length == 1; @@ -277,13 +277,15 @@ namespace StardewModdingAPI.Framework.ContentManagers { AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); - if (!this.AssertMaxOneLoader(info, loaders, out string error)) + if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error)) { this.Monitor.Log(error, LogLevel.Warn); return null; } - loader = loaders.FirstOrDefault(); + loader = + loaders.FirstOrDefault(p => !p.AllowSkipOnConflict) + ?? loaders.FirstOrDefault(); } // no loader found @@ -392,20 +394,21 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset loaders to apply. /// The error message to show to the user, if the method returns false. /// Returns true if only one loader will apply, else false. - private bool AssertMaxOneLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error) + private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error) { - if (loaders.Length <= 1) + AssetLoadOperation[] required = loaders.Where(p => !p.AllowSkipOnConflict).ToArray(); + if (required.Length <= 1) { error = null; return true; } - string[] loaderNames = loaders + string[] loaderNames = required .Select(p => p.Mod.DisplayName + this.GetOnBehalfOfLabel(p.OnBehalfOf)) .Distinct() .ToArray(); string errorPhrase = loaderNames.Length > 1 - ? $"Multiple mods want to provide '{info.Name}' asset: {string.Join(", ", loaderNames)}" + ? $"Multiple mods want to provide the '{info.Name}' asset: {string.Join(", ", loaderNames)}" : $"The '{loaderNames[0]}' mod wants to provide the '{info.Name}' asset multiple times"; error = $"{errorPhrase}. An asset can't be loaded multiple times, so SMAPI will use the default asset instead. Uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)"; -- cgit From 3707f481a567df5149aea00ffb14cddb7b14fccb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 Mar 2022 23:53:30 -0400 Subject: extend load conflict resolution into load priority (#766) --- src/SMAPI/Events/AssetLoadPriority.cs | 19 +++++++++++++++++++ src/SMAPI/Events/AssetRequestedEventArgs.cs | 14 +++++++------- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 11 ++++++----- src/SMAPI/Framework/ContentCoordinator.cs | 2 +- .../Framework/ContentManagers/GameContentManager.cs | 10 +++++----- 5 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 src/SMAPI/Events/AssetLoadPriority.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI/Events/AssetLoadPriority.cs b/src/SMAPI/Events/AssetLoadPriority.cs new file mode 100644 index 00000000..e07b5a40 --- /dev/null +++ b/src/SMAPI/Events/AssetLoadPriority.cs @@ -0,0 +1,19 @@ +namespace StardewModdingAPI.Events +{ + /// The priority for an asset load when multiple apply for the same asset. + /// If multiple non- loads have the same priority, the one registered first will be selected. You can also specify arbitrary intermediate values, like AssetLoadPriority.Low + 5. + public enum AssetLoadPriority + { + /// This load is optional and can safely be skipped if there are higher-priority loads. + Low = -1000, + + /// The load is optional and can safely be skipped if there are higher-priority loads, but it should still be preferred over any -priority loads. + Medium = 0, + + /// The load is optional and can safely be skipped if there are higher-priority loads, but it should still be preferred over any - or -priority loads. + High = 1000, + + /// The load is not optional. If more than one loader has priority, SMAPI will log an error and ignore all of them. + Exclusive = int.MaxValue + } +} diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index 9942079b..d022a4de 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -49,21 +49,21 @@ namespace StardewModdingAPI.Events /// Provide the initial instance for the asset, instead of trying to load it from the game's Content folder. /// Get the initial instance of an asset. + /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. - /// When there are multiple loads that apply to the same asset, this indicates whether this one can be skipped to resolve the conflict. If all loads allow skipping, the first one that was registered will be applied. If this is false, SMAPI will raise an error and apply none of them. /// /// Usage notes: /// /// The asset doesn't need to exist in the game's Content folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder. - /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. + /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFrom(Func load, string onBehalfOf = null, bool allowSkipOnConflict = false) + public void LoadFrom(Func load, AssetLoadPriority priority, string onBehalfOf = null) { this.LoadOperations.Add( new AssetLoadOperation( mod: this.Mod, - allowSkipOnConflict: allowSkipOnConflict, + priority: priority, onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), getData: _ => load() ) @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Events /// Provide the initial instance for the asset from a file in your mod folder, instead of trying to load it from the game's Content folder. /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. /// The relative path to the file in your mod folder. - /// When there are multiple loads that apply to the same asset, this indicates whether this one can be skipped to resolve the conflict. If all loads allow skipping, the first one that was registered will be applied. If this is false, SMAPI will raise an error and apply none of them. + /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. /// /// Usage notes: /// @@ -81,12 +81,12 @@ namespace StardewModdingAPI.Events /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFromModFile(string relativePath, bool allowSkipOnConflict = false) + public void LoadFromModFile(string relativePath, AssetLoadPriority priority) { this.LoadOperations.Add( new AssetLoadOperation( mod: this.Mod, - allowSkipOnConflict: allowSkipOnConflict, + priority: priority, onBehalfOf: null, _ => this.Mod.Mod.Helper.Content.Load(relativePath)) ); diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index 36baf1aa..b12958d6 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -1,4 +1,5 @@ using System; +using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework.Content { @@ -14,8 +15,8 @@ namespace StardewModdingAPI.Framework.Content /// The content pack on whose behalf the asset is being loaded, if any. public IModMetadata OnBehalfOf { get; } - /// Whether to allow skipping this operation to resolve a load conflict. - public bool AllowSkipOnConflict { get; } + /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. + public AssetLoadPriority Priority { get; } /// Load the initial value for an asset. public Func GetData { get; } @@ -26,13 +27,13 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. - /// Whether to allow skipping this operation to resolve a load conflict. + /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - public AssetLoadOperation(IModMetadata mod, bool allowSkipOnConflict, IModMetadata onBehalfOf, Func getData) + public AssetLoadOperation(IModMetadata mod, AssetLoadPriority priority, IModMetadata onBehalfOf, Func getData) { this.Mod = mod; - this.AllowSkipOnConflict = allowSkipOnConflict; + this.Priority = priority; this.OnBehalfOf = onBehalfOf; this.GetData = getData; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 43cebcbe..144832b2 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -608,7 +608,7 @@ namespace StardewModdingAPI.Framework { new AssetLoadOperation( mod: loader.Mod, - allowSkipOnConflict: false, + priority: AssetLoadPriority.Exclusive, onBehalfOf: null, getData: assetInfo => loader.Data.Load(assetInfo) ) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index b3e98648..16eddb00 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; @@ -275,7 +276,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // find matching loader AssetLoadOperation loader; { - AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); + AssetLoadOperation[] loaders = this.GetLoaders(info).OrderByDescending(p => p.Priority).ToArray(); if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error)) { @@ -283,9 +284,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return null; } - loader = - loaders.FirstOrDefault(p => !p.AllowSkipOnConflict) - ?? loaders.FirstOrDefault(); + loader = loaders.FirstOrDefault(); } // no loader found @@ -396,7 +395,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Returns true if only one loader will apply, else false. private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error) { - AssetLoadOperation[] required = loaders.Where(p => !p.AllowSkipOnConflict).ToArray(); + AssetLoadOperation[] required = loaders.Where(p => p.Priority == AssetLoadPriority.Exclusive).ToArray(); if (required.Length <= 1) { error = null; @@ -405,6 +404,7 @@ namespace StardewModdingAPI.Framework.ContentManagers string[] loaderNames = required .Select(p => p.Mod.DisplayName + this.GetOnBehalfOfLabel(p.OnBehalfOf)) + .OrderBy(p => p) .Distinct() .ToArray(); string errorPhrase = loaderNames.Length > 1 -- cgit From e40907ab8b97bd8a557adf683a406413646b1fc5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Mar 2022 01:19:44 -0400 Subject: add NameWithoutLocale fields (#766) --- src/SMAPI/Events/AssetReadyEventArgs.cs | 8 +++++++- src/SMAPI/Events/AssetRequestedEventArgs.cs | 8 +++++++- src/SMAPI/Events/AssetsInvalidatedEventArgs.cs | 9 +++++++-- src/SMAPI/Framework/Content/AssetInfo.cs | 12 ++++++++---- src/SMAPI/Framework/Content/AssetName.cs | 9 +++++++++ src/SMAPI/Framework/ContentCoordinator.cs | 6 +++--- src/SMAPI/Framework/SCore.cs | 8 ++++---- src/SMAPI/IAssetInfo.cs | 8 ++++++-- src/SMAPI/IAssetName.cs | 3 +++ 9 files changed, 54 insertions(+), 17 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI/Events/AssetReadyEventArgs.cs b/src/SMAPI/Events/AssetReadyEventArgs.cs index 946c9173..2c308f18 100644 --- a/src/SMAPI/Events/AssetReadyEventArgs.cs +++ b/src/SMAPI/Events/AssetReadyEventArgs.cs @@ -11,15 +11,21 @@ namespace StardewModdingAPI.Events /// The name of the asset being requested. public IAssetName Name { get; } + /// The with any locale codes stripped. + /// For example, if contains a locale like Data/Bundles.fr-FR, this will be the name without locale like Data/Bundles. If the name has no locale, this field is equivalent. + public IAssetName NameWithoutLocale { get; } + /********* ** Public methods *********/ /// Construct an instance. /// The name of the asset being requested. - internal AssetReadyEventArgs(IAssetName name) + /// The with any locale codes stripped. + internal AssetReadyEventArgs(IAssetName name, IAssetName nameWithoutLocale) { this.Name = name; + this.NameWithoutLocale = nameWithoutLocale; } } } diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index d022a4de..9e2cde7f 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -26,6 +26,10 @@ namespace StardewModdingAPI.Events /// The name of the asset being requested. public IAssetName Name { get; } + /// The with any locale codes stripped. + /// For example, if contains a locale like Data/Bundles.fr-FR, this will be the name without locale like Data/Bundles. If the name has no locale, this field is equivalent. + public IAssetName NameWithoutLocale { get; } + /// The load operations requested by the event handler. internal IList LoadOperations { get; } = new List(); @@ -39,11 +43,13 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The mod handling the event. /// The name of the asset being requested. + /// The with any locale codes stripped. /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. - internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, Func getOnBehalfOf) + internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, IAssetName nameWithoutLocale, Func getOnBehalfOf) { this.Mod = mod; this.Name = name; + this.NameWithoutLocale = nameWithoutLocale; this.GetOnBehalfOf = getOnBehalfOf; } diff --git a/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs b/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs index f3d83dd6..614cdf49 100644 --- a/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs +++ b/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; namespace StardewModdingAPI.Events { @@ -14,15 +13,21 @@ namespace StardewModdingAPI.Events /// The asset names that were invalidated. public IReadOnlySet Names { get; } + /// The with any locale codes stripped. + /// For example, if contains a locale like Data/Bundles.fr-FR, this will have the name without locale like Data/Bundles. If the name has no locale, this field is equivalent. + public IReadOnlySet NamesWithoutLocale { get; } + /********* ** Public methods *********/ /// Construct an instance. /// The asset names that were invalidated. - internal AssetsInvalidatedEventArgs(IEnumerable names) + /// The with any locale codes stripped. + internal AssetsInvalidatedEventArgs(IEnumerable names, IEnumerable namesWithoutLocale) { this.Names = names.ToImmutableHashSet(); + this.NamesWithoutLocale = namesWithoutLocale.ToImmutableHashSet(); } } } diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index 6a5b4f31..556f1c2a 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -23,8 +23,11 @@ namespace StardewModdingAPI.Framework.Content public IAssetName Name { get; } /// - [Obsolete($"Use {nameof(Name)} instead.")] - public string AssetName => this.Name.Name; + public IAssetName NameWithoutLocale { get; } + + /// + [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead.")] + public string AssetName => this.NameWithoutLocale.Name; /// public Type DataType { get; } @@ -42,15 +45,16 @@ namespace StardewModdingAPI.Framework.Content { this.Locale = locale; this.Name = assetName; + this.NameWithoutLocale = assetName.GetBaseAssetName(); this.DataType = type; this.GetNormalizedPath = getNormalizedPath; } /// - [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] + [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] public bool AssetNameEquals(string path) { - return this.Name.IsEquivalentTo(path); + return this.NameWithoutLocale.IsEquivalentTo(path); } diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 992647f8..7ce0f8ee 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -142,11 +142,20 @@ namespace StardewModdingAPI.Framework.Content } + /// public bool IsDirectlyUnderPath(string assetFolder) { return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false); } + /// + IAssetName IAssetName.GetBaseAssetName() + { + return this.LocaleCode == null + ? this + : new AssetName(this.BaseName, null, null); + } + /// public bool Equals(IAssetName other) { diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 144832b2..fc137e50 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework private readonly Action OnAssetLoaded; /// A callback to invoke when any asset names have been invalidated from the cache. - private readonly Action> OnAssetsInvalidated; + private readonly Action> OnAssetsInvalidated; /// Get the load/edit operations to apply to an asset by querying registered event handlers. private readonly Func> RequestAssetOperations; @@ -118,7 +118,7 @@ namespace StardewModdingAPI.Framework /// Whether to enable more aggressive memory optimizations. /// A callback to invoke when any asset names have been invalidated from the cache. /// Get the load/edit operations to apply to an asset by querying registered event handlers. - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, bool aggressiveMemoryOptimizations, Action> onAssetsInvalidated, Func> requestAssetOperations) + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, bool aggressiveMemoryOptimizations, Action> onAssetsInvalidated, Func> requestAssetOperations) { this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); @@ -414,7 +414,7 @@ namespace StardewModdingAPI.Framework this.AssetOperationsByKey.Remove(name); // raise event - this.OnAssetsInvalidated(invalidatedAssets.Keys); + this.OnAssetsInvalidated(invalidatedAssets.Keys.ToArray()); // propagate changes to the game this.CoreAssets.Propagate( diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index dd682e40..dd952dee 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1113,15 +1113,15 @@ namespace StardewModdingAPI.Framework private void OnAssetLoaded(IContentManager contentManager, IAssetName assetName) { if (this.EventManager.AssetReady.HasListeners()) - this.EventManager.AssetReady.Raise(new AssetReadyEventArgs(assetName)); + this.EventManager.AssetReady.Raise(new AssetReadyEventArgs(assetName, assetName.GetBaseAssetName())); } /// A callback invoked after assets have been invalidated from the content cache. /// The invalidated asset names. - private void OnAssetsInvalidated(IEnumerable assetNames) + private void OnAssetsInvalidated(IList assetNames) { if (this.EventManager.AssetsInvalidated.HasListeners()) - this.EventManager.AssetsInvalidated.Raise(new AssetsInvalidatedEventArgs(assetNames)); + this.EventManager.AssetsInvalidated.Raise(new AssetsInvalidatedEventArgs(assetNames, assetNames.Select(p => p.GetBaseAssetName()))); } /// Get the load/edit operations to apply to an asset by querying registered event handlers. @@ -1133,7 +1133,7 @@ namespace StardewModdingAPI.Framework this.EventManager.AssetRequested.Raise( invoke: (mod, invoke) => { - AssetRequestedEventArgs args = new(mod, asset.Name, this.GetOnBehalfOfContentPack); + AssetRequestedEventArgs args = new(mod, asset.Name, asset.NameWithoutLocale, this.GetOnBehalfOfContentPack); invoke(args); diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index 6ac8358d..c7b2ab62 100644 --- a/src/SMAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs @@ -14,8 +14,12 @@ namespace StardewModdingAPI /// The asset name being read. public IAssetName Name { get; } + /// The with any locale codes stripped. + /// For example, if contains a locale like Data/Bundles.fr-FR, this will be the name without locale like Data/Bundles. If the name has no locale, this field is equivalent. + public IAssetName NameWithoutLocale { get; } + /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. - [Obsolete($"Use {nameof(Name)} instead.")] + [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead.")] string AssetName { get; } /// The content data type. @@ -27,7 +31,7 @@ namespace StardewModdingAPI *********/ /// Get whether the asset name being loaded matches a given name after normalization. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] + [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] bool AssetNameEquals(string path); } } diff --git a/src/SMAPI/IAssetName.cs b/src/SMAPI/IAssetName.cs index a5bfea93..89f02adf 100644 --- a/src/SMAPI/IAssetName.cs +++ b/src/SMAPI/IAssetName.cs @@ -40,5 +40,8 @@ namespace StardewModdingAPI /// For example, Characters/Dialogue/Abigail is directly under Characters/Dialogue but not Characters or Characters/Dialogue/Ab. To allow sub-paths, use instead. /// The asset path to check. This doesn't need a trailing slash. bool IsDirectlyUnderPath(string assetFolder); + + /// Get an asset name representing the without locale. + internal IAssetName GetBaseAssetName(); } } -- cgit From ad8912047beaf84ce34f4918703d55841be13ff0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Mar 2022 01:43:40 -0400 Subject: add asset edit priority (#766) --- docs/release-notes.md | 2 +- src/SMAPI/Events/AssetEditPriority.cs | 16 ++++++++++++++++ src/SMAPI/Events/AssetRequestedEventArgs.cs | 4 +++- src/SMAPI/Framework/Content/AssetEditOperation.cs | 8 +++++++- src/SMAPI/Framework/ContentCoordinator.cs | 1 + .../Framework/ContentManagers/GameContentManager.cs | 7 +++++-- 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI/Events/AssetEditPriority.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index c8c87db3..a8f8ccfd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,7 +13,7 @@ * For mod authors: * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. - _These include new features not supported by the old API, like load conflict resolution and content pack labels. They also support new cases like easily detecting when an asset has been changed._ + _These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has been changed._ * Overhauled [mod-provided APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) (thanks to Shockah!). _This adds support for many previously-unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more._ * Added `Constants.ContentPath`. diff --git a/src/SMAPI/Events/AssetEditPriority.cs b/src/SMAPI/Events/AssetEditPriority.cs new file mode 100644 index 00000000..d41dfd7d --- /dev/null +++ b/src/SMAPI/Events/AssetEditPriority.cs @@ -0,0 +1,16 @@ +namespace StardewModdingAPI.Events +{ + /// The priority for an asset edit when multiple apply for the same asset. + /// You can also specify arbitrary intermediate values, like AssetLoadPriority.Low + 5. + public enum AssetEditPriority + { + /// This edit should be applied before (i.e. 'under') edits. + Early = -1000, + + /// The default priority. + Default = 0, + + /// This edit should be applied after (i.e. 'on top of') edits. + Late = 1000 + } +} diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index 9e2cde7f..4d9ee236 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -100,6 +100,7 @@ namespace StardewModdingAPI.Events /// Edit the asset after it's loaded. /// Apply changes to the asset. + /// If there are multiple edits that apply to the same asset, the priority with which this one should be applied. /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. /// /// Usage notes: @@ -108,11 +109,12 @@ namespace StardewModdingAPI.Events /// You can apply any number of edits to the asset. Each edit will be applied on top of the previous one (i.e. it'll see the merged asset from all previous edits as its input). /// /// - public void Edit(Action apply, string onBehalfOf = null) + public void Edit(Action apply, AssetEditPriority priority = AssetEditPriority.Default, string onBehalfOf = null) { this.EditOperations.Add( new AssetEditOperation( mod: this.Mod, + priority: priority, onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"), apply ) diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs index 14db231c..818209fa 100644 --- a/src/SMAPI/Framework/Content/AssetEditOperation.cs +++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs @@ -1,4 +1,5 @@ using System; +using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework.Content { @@ -11,6 +12,9 @@ namespace StardewModdingAPI.Framework.Content /// The mod applying the edit. public IModMetadata Mod { get; } + /// If there are multiple edits that apply to the same asset, the priority with which this one should be applied. + public AssetEditPriority Priority { get; } + /// The content pack on whose behalf the edit is being applied, if any. public IModMetadata OnBehalfOf { get; } @@ -23,11 +27,13 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// If there are multiple edits that apply to the same asset, the priority with which this one should be applied. /// The content pack on whose behalf the edit is being applied, if any. /// Apply the edit to an asset. - public AssetEditOperation(IModMetadata mod, IModMetadata onBehalfOf, Action applyEdit) + public AssetEditOperation(IModMetadata mod, AssetEditPriority priority, IModMetadata onBehalfOf, Action applyEdit) { this.Mod = mod; + this.Priority = priority; this.OnBehalfOf = onBehalfOf; this.ApplyEdit = applyEdit; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index fc137e50..8e7465de 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -640,6 +640,7 @@ namespace StardewModdingAPI.Framework { new AssetEditOperation( mod: editor.Mod, + priority: AssetEditPriority.Default, onBehalfOf: null, applyEdit: assetData => editor.Data.Edit(assetData) ) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 16eddb00..a121f4c0 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -82,9 +82,12 @@ namespace StardewModdingAPI.Framework.ContentManagers AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error)) + { this.Monitor.Log(error, LogLevel.Warn); + return false; + } - return loaders.Length == 1; + return loaders.Any(); } /// @@ -334,7 +337,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // edit asset - AssetEditOperation[] editors = this.GetEditors(info).ToArray(); + AssetEditOperation[] editors = this.GetEditors(info).OrderBy(p => p.Priority).ToArray(); foreach (AssetEditOperation editor in editors) { IModMetadata mod = editor.Mod; -- cgit From 8d704153762fa73416a3ccb44ee71032952802eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Mar 2022 15:02:11 -0400 Subject: add deprecation notices for SMAPI 4.0.0 (#766) --- docs/release-notes.md | 37 +++++++++++-------------- src/SMAPI/Constants.cs | 17 ++++++++++-- src/SMAPI/Framework/Content/AssetInfo.cs | 23 ++++++++++++++- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 34 ++++++++++++++++++++--- src/SMAPI/Framework/SCore.cs | 19 +++++++++++++ src/SMAPI/IAssetInfo.cs | 1 + 6 files changed, 103 insertions(+), 28 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index 98392c17..464049b9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,37 +3,32 @@ # Release notes ## Upcoming release * For players: - * Improved translations. Thanks to ChulkyBow (updated Ukrainian)! - * Fixed `player_add` console command's handling of Journal Scraps and Secret Notes. - * Fixed `set_farm_type` console command not updating warps if they moved. - * Improved [command-line arguments](technical/smapi.md#command-line-arguments) on Linux/macOS: + * Fixed support for `_international` content assets (used in the movie theater). + * Fixed the warning text when a mod causes an asset load conflict with itself. + * Improved Linux/macOS [command-line arguments](technical/smapi.md#command-line-arguments): * Added `--use-current-shell` to avoid opening a separate terminal window. * Fixed `--no-terminal` still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!). - * Fixed warning text when a mod causes an asset load conflict with itself. - * Fixed support for `_international` content assets (used in the movie theater). + * Improved translations. Thanks to ChulkyBow (updated Ukrainian)! + +* For the Console Commands mod: + * Fixed `player_add` not handling journal scraps and secret notes correctly. + * Fixed `set_farm_type` not updating warps. * For mod authors: - * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. - _These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has been changed._ - * Overhauled [mod-provided APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) (thanks to Shockah!). - _This adds support for many previously-unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more._ + * **Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.** + _These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has changed, and avoid data corruption issues in some edge cases._ + * **Overhauled [mod-provided API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) proxying** (thanks to Shockah!). + _This adds support for many previously unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more. Existing mod APIs should work fine as-is._ + * **Deprecation warning:** The upcoming SMAPI 4.0 will remove deprecated APIs and break mods which haven't updated yet. + _See [_Migrate to SMAPI 4.0_](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) for help updating your mod code. You can update your mod code now, there's no need to wait for the 4.0.0 release (which will happen in at least three months, and possibly later if needed to update open-source mods)._ * Added `Constants.ContentPath`. - * Added `IAssetName Name` field to the info received by `IAssetEditor` and `IAssetLoader` methods. + * Added `IAssetName` fields to the info received by `IAssetEditor` and `IAssetLoader` methods. _This adds methods for working with asset names, parsed locales, etc._ - * If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated via `helper.Content.InvalidateCache`. + * If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated by `helper.Content.InvalidateCache`. * Fixed the `SDate` constructor being case-sensitive. * Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`). * Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead. -* **Deprecation warning for mod authors:** - These APIs are now deprecated and will be removed in the upcoming SMAPI 4.0.0. - - API | how to update code - :-- | :----------------- - `Constants.ExecutionPath` | Use `Constants.GamePath` instead. - `IAssetInfo.AssetName`
`IAssetData.AssetName` | Use `Name` instead, which changes the type from `string` to the new `AssetName`. - `IAssetInfo.AssetNameEquals`
`IAssetData.AssetNameEquals` | Use `Name.IsEquivalentTo` instead. - * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.25.0. * Added `data-*` attributes to log parser page for external tools. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index b736ca59..3351e5c4 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -77,8 +77,21 @@ namespace StardewModdingAPI public static GameFramework GameFramework { get; } = EarlyConstants.GameFramework; /// The path to the game folder. - [Obsolete($"Use {nameof(GamePath)} instead.")] - public static string ExecutionPath => Constants.GamePath; + [Obsolete($"Use {nameof(Constants)}.{nameof(GamePath)} instead.")] + public static string ExecutionPath + { + get + { + SCore.DeprecationManager.Warn( + source: SCore.DeprecationManager.GetSourceNameFromStack(), + nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return Constants.GamePath; + } + } /// The path to the game folder. public static string GamePath { get; } = EarlyConstants.GamePath; diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index 556f1c2a..6e93c33c 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -27,7 +27,20 @@ namespace StardewModdingAPI.Framework.Content /// [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead.")] - public string AssetName => this.NameWithoutLocale.Name; + public string AssetName + { + get + { + SCore.DeprecationManager.Warn( + source: SCore.DeprecationManager.GetSourceNameFromStack(), + nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.NameWithoutLocale.Name; + } + } /// public Type DataType { get; } @@ -54,6 +67,14 @@ namespace StardewModdingAPI.Framework.Content [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] public bool AssetNameEquals(string path) { + SCore.DeprecationManager.Warn( + source: SCore.DeprecationManager.GetSourceNameFromStack(), + nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.NameWithoutLocale.IsEquivalentTo(path); } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 3416c286..3727b909 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -44,16 +44,42 @@ namespace StardewModdingAPI.Framework.ModHelpers public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; /// The observable implementation of . - internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); + internal ObservableCollection ObservableAssetEditors { get; } = new(); /// The observable implementation of . - internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); + internal ObservableCollection ObservableAssetLoaders { get; } = new(); /// - public IList AssetLoaders => this.ObservableAssetLoaders; + public IList AssetLoaders + { + get + { + SCore.DeprecationManager.Warn( + source: this.ModName, + nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.ObservableAssetLoaders; + } + } /// - public IList AssetEditors => this.ObservableAssetEditors; + public IList AssetEditors + { + get + { + SCore.DeprecationManager.Warn( + source: this.ModName, + nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.ObservableAssetEditors; + } + } /********* diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index dd952dee..eab977ac 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1609,9 +1609,28 @@ namespace StardewModdingAPI.Framework { // ReSharper disable SuspiciousTypeConversion.Global if (metadata.Mod is IAssetEditor editor) + { + SCore.DeprecationManager.Warn( + source: metadata.DisplayName, + nounPhrase: $"{nameof(IAssetEditor)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + this.ContentCore.Editors.Add(new ModLinked(metadata, editor)); + } + if (metadata.Mod is IAssetLoader loader) + { + SCore.DeprecationManager.Warn( + source: metadata.DisplayName, + nounPhrase: $"{nameof(IAssetLoader)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + this.ContentCore.Loaders.Add(new ModLinked(metadata, loader)); + } // ReSharper restore SuspiciousTypeConversion.Global helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index c7b2ab62..c3753b97 100644 --- a/src/SMAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs @@ -12,6 +12,7 @@ namespace StardewModdingAPI string Locale { get; } /// The asset name being read. + /// NOTE: when reading this field from an or implementation, it's always equivalent to for backwards compatibility. public IAssetName Name { get; } /// The with any locale codes stripped. -- cgit From eebd8d54dc068cff2b5127a4b8f03d0b54b89542 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Mar 2022 18:34:49 -0400 Subject: expand obsolete attributes (#766) --- src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/Content/AssetInfo.cs | 4 ++-- src/SMAPI/GameFramework.cs | 2 +- src/SMAPI/IAssetEditor.cs | 6 +++++- src/SMAPI/IAssetInfo.cs | 4 ++-- src/SMAPI/IAssetLoader.cs | 6 +++++- src/SMAPI/ICommandHelper.cs | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 3351e5c4..76f4ef87 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -77,7 +77,7 @@ namespace StardewModdingAPI public static GameFramework GameFramework { get; } = EarlyConstants.GameFramework; /// The path to the game folder. - [Obsolete($"Use {nameof(Constants)}.{nameof(GamePath)} instead.")] + [Obsolete($"Use {nameof(Constants)}.{nameof(GamePath)} instead. This property will be removed in SMAPI 4.0.0.")] public static string ExecutionPath { get diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index 6e93c33c..f5da5d69 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Framework.Content public IAssetName NameWithoutLocale { get; } /// - [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead.")] + [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")] public string AssetName { get @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.Content } /// - [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] + [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")] public bool AssetNameEquals(string path) { SCore.DeprecationManager.Warn( diff --git a/src/SMAPI/GameFramework.cs b/src/SMAPI/GameFramework.cs index a0154329..60fbe56e 100644 --- a/src/SMAPI/GameFramework.cs +++ b/src/SMAPI/GameFramework.cs @@ -6,7 +6,7 @@ namespace StardewModdingAPI public enum GameFramework { /// The XNA Framework, previously used on Windows. - [Obsolete("Stardew Valley no longer uses XNA Framework on any supported platform.")] + [Obsolete("Stardew Valley no longer uses XNA Framework on any supported platform. This value will be removed in SMAPI 4.0.0.")] Xna, /// The MonoGame framework. diff --git a/src/SMAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs index d2c6f295..9f22ed83 100644 --- a/src/SMAPI/IAssetEditor.cs +++ b/src/SMAPI/IAssetEditor.cs @@ -1,6 +1,10 @@ -namespace StardewModdingAPI +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI { /// Edits matching content assets. + [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This interface will be removed in SMAPI 4.0.0.")] public interface IAssetEditor { /********* diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index c3753b97..64d10b35 100644 --- a/src/SMAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI public IAssetName NameWithoutLocale { get; } /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. - [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead.")] + [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")] string AssetName { get; } /// The content data type. @@ -32,7 +32,7 @@ namespace StardewModdingAPI *********/ /// Get whether the asset name being loaded matches a given name after normalization. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] + [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")] bool AssetNameEquals(string path); } } diff --git a/src/SMAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs index ad97b941..96b98793 100644 --- a/src/SMAPI/IAssetLoader.cs +++ b/src/SMAPI/IAssetLoader.cs @@ -1,6 +1,10 @@ -namespace StardewModdingAPI +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI { /// Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use instead. + [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This interface will be removed in SMAPI 4.0.0.")] public interface IAssetLoader { /********* diff --git a/src/SMAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs index b92e5162..9f1c345c 100644 --- a/src/SMAPI/ICommandHelper.cs +++ b/src/SMAPI/ICommandHelper.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI /// The command name. /// The command arguments. /// Returns whether a matching command was triggered. - [Obsolete("Manually triggering console commands will no longer be supported in SMAPI 4.0.0.")] + [Obsolete("Use mod-provided APIs to integrate with mods instead. This method will be removed in SMAPI 4.0.0.")] bool Trigger(string name, string[] arguments); } } -- cgit From 1d3c99cc25f6c0d504fd5e43ea71ef327b6e9066 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 27 Mar 2022 13:42:14 -0400 Subject: split helper.Content API into game/mod content APIs --- docs/release-notes.md | 2 + src/SMAPI/Framework/Content/AssetName.cs | 12 ++ src/SMAPI/Framework/ContentPack.cs | 24 ++-- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 9 +- .../Framework/ModHelpers/GameContentHelper.cs | 129 +++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModContentHelper.cs | 75 ++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 38 +++++- src/SMAPI/Framework/SCore.cs | 12 +- src/SMAPI/IAssetName.cs | 5 + src/SMAPI/IContentHelper.cs | 10 +- src/SMAPI/IContentPack.cs | 5 + src/SMAPI/IGameContentHelper.cs | 73 ++++++++++++ src/SMAPI/IModContentHelper.cs | 32 +++++ src/SMAPI/IModHelper.cs | 10 ++ 14 files changed, 400 insertions(+), 36 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/GameContentHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/ModContentHelper.cs create mode 100644 src/SMAPI/IGameContentHelper.cs create mode 100644 src/SMAPI/IModContentHelper.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/docs/release-notes.md b/docs/release-notes.md index 99ac86df..2e09240c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,10 +17,12 @@ * For mod authors: * **Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.** _These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has changed, and avoid data corruption issues in some edge cases._ + * **Added `helper.GameContent` and `helper.ModContent`, which will replace `helper.Content` in SMAPI 4.0.0.** * **Overhauled [mod-provided API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) proxying** (thanks to Shockah!). _This adds support for many previously unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more. Existing mod APIs should work fine as-is._ * **Deprecation warning:** The upcoming SMAPI 4.0 will remove deprecated APIs and break mods which haven't updated yet. _See [_Migrate to SMAPI 4.0_](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) for help updating your mod code. You can update your mod code now, there's no need to wait for the 4.0.0 release (which will happen in at least three months, and possibly later if needed to update open-source mods)._ + * Added `IContentPack.ModContent` property. * Added `Constants.ContentPath`. * Added `IAssetName` fields to the info received by `IAssetEditor` and `IAssetLoader` methods. _This adds methods for working with asset names, parsed locales, etc._ diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 7ce0f8ee..a1d37b0b 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -100,6 +100,18 @@ namespace StardewModdingAPI.Framework.Content return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase); } + /// + 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); + } + /// public bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true) { diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index b6add7b5..3920354e 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -13,9 +13,6 @@ namespace StardewModdingAPI.Framework /********* ** Fields *********/ - /// Provides an API for loading content assets. - private readonly IContentHelper Content; - /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; @@ -35,6 +32,9 @@ namespace StardewModdingAPI.Framework /// public ITranslationHelper Translation => this.TranslationImpl; + /// + public IModContentHelper ModContent { get; } + /// The underlying translation helper. internal TranslationHelper TranslationImpl { get; set; } @@ -45,14 +45,14 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// The full path to the content pack's folder. /// The content pack's manifest. - /// Provides an API for loading content assets. + /// Provides an API for loading content assets from the content pack's folder. /// Provides translations stored in the content pack's i18n folder. /// Encapsulates SMAPI's JSON file parsing. - public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, TranslationHelper translation, JsonHelper jsonHelper) + public ContentPack(string directoryPath, IManifest manifest, IModContentHelper content, TranslationHelper translation, JsonHelper jsonHelper) { this.DirectoryPath = directoryPath; this.Manifest = manifest; - this.Content = content; + this.ModContent = content; this.TranslationImpl = translation; this.JsonHelper = jsonHelper; @@ -95,21 +95,17 @@ namespace StardewModdingAPI.Framework } /// + [Obsolete] public T LoadAsset(string key) { - key = PathUtilities.NormalizePath(key); - - key = this.GetCaseInsensitiveRelativePath(key); - return this.Content.Load(key, ContentSource.ModFolder); + return this.ModContent.Load(key); } /// + [Obsolete] public string GetActualAssetKey(string key) { - key = PathUtilities.NormalizePath(key); - - key = this.GetCaseInsensitiveRelativePath(key); - return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); + return this.ModContent.GetInternalAssetName(key)?.Name; } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 3a5c8938..b0064532 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -13,6 +13,7 @@ using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides an API for loading content assets. + [Obsolete] internal class ContentHelper : BaseHelper, IContentHelper { /********* @@ -44,11 +45,9 @@ namespace StardewModdingAPI.Framework.ModHelpers public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; /// The observable implementation of . - [Obsolete] internal ObservableCollection ObservableAssetEditors { get; } = new(); /// The observable implementation of . - [Obsolete] internal ObservableCollection ObservableAssetLoaders { get; } = new(); /// @@ -105,12 +104,6 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Monitor = monitor; } - /// - public IAssetName ParseAssetName(string rawName) - { - return this.ContentCore.ParseAssetName(rawName); - } - /// public T Load(string key, ContentSource source = ContentSource.ModFolder) { diff --git a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs new file mode 100644 index 00000000..42a4de20 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs @@ -0,0 +1,129 @@ +using System; +using System.Linq; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Exceptions; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// + internal class GameContentHelper : BaseHelper, IGameContentHelper + { + /********* + ** Fields + *********/ + /// SMAPI's core content logic. + private readonly ContentCoordinator ContentCore; + + /// The underlying game content manager. + private readonly IContentManager GameContentManager; + + /// The friendly mod name for use in errors. + private readonly string ModName; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Accessors + *********/ + /// + public string CurrentLocale => this.GameContentManager.GetLocale(); + + /// + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's core content logic. + /// The unique ID of the relevant mod. + /// The friendly mod name for use in errors. + /// Encapsulates monitoring and logging. + public GameContentHelper(ContentCoordinator contentCore, string modID, string modName, IMonitor monitor) + : base(modID) + { + string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID); + + this.ContentCore = contentCore; + this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content"); + this.ModName = modName; + this.Monitor = monitor; + } + + /// + public IAssetName ParseAssetName(string rawName) + { + return this.ContentCore.ParseAssetName(rawName); + } + + /// + public T Load(string key) + { + IAssetName assetName = this.ContentCore.ParseAssetName(key); + return this.Load(assetName); + } + + /// + public T Load(IAssetName assetName) + { + try + { + return this.GameContentManager.LoadLocalized(assetName, this.CurrentLocaleConstant, useCache: false); + } + catch (Exception ex) when (ex is not SContentLoadException) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{assetName}' from the game content.", ex); + } + } + + /// + public bool InvalidateCache(string key) + { + IAssetName assetName = this.ParseAssetName(key); + return this.InvalidateCache(assetName); + } + + /// + public bool InvalidateCache(IAssetName assetName) + { + this.Monitor.Log($"Requested cache invalidation for '{assetName}'."); + return this.ContentCore.InvalidateCache(asset => asset.Name.IsEquivalentTo(assetName)).Any(); + } + + /// + public bool InvalidateCache() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible."); + return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any(); + } + + /// + public bool InvalidateCache(Func predicate) + { + this.Monitor.Log("Requested cache invalidation for all assets matching a predicate."); + return this.ContentCore.InvalidateCache(predicate).Any(); + } + + /// + public IAssetData GetPatchHelper(T data, string assetName = null) + { + if (data == null) + throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); + + assetName ??= $"temp/{Guid.NewGuid():N}"; + + return new AssetDataForObject(this.CurrentLocale, this.ContentCore.ParseAssetName(assetName), data, key => this.ParseAssetName(key).Name); + } + + /// Get the underlying game content manager. + internal IContentManager GetUnderlyingContentManager() + { + return this.GameContentManager; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs new file mode 100644 index 00000000..45899dd7 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs @@ -0,0 +1,75 @@ +using System; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Exceptions; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// + internal class ModContentHelper : BaseHelper, IModContentHelper + { + /********* + ** Fields + *********/ + /// SMAPI's core content logic. + private readonly ContentCoordinator ContentCore; + + /// A content manager for this mod which manages files from the mod's folder. + private readonly ModContentManager ModContentManager; + + /// The friendly mod name for use in errors. + private readonly string ModName; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's core content logic. + /// The absolute path to the mod folder. + /// The unique ID of the relevant mod. + /// The friendly mod name for use in errors. + /// The game content manager used for map tilesheets not provided by the mod. + public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IContentManager gameContentManager) + : base(modID) + { + string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID); + + this.ContentCore = contentCore; + this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, gameContentManager); + this.ModName = modName; + } + + /// + public T Load(string relativePath) + { + IAssetName assetName = this.ContentCore.ParseAssetName(relativePath); + + try + { + return this.ModContentManager.LoadExact(assetName, useCache: false); + } + catch (Exception ex) when (ex is not SContentLoadException) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{relativePath}' from its mod folder.", ex); + } + } + + /// + public IAssetName GetInternalAssetName(string relativePath) + { + return this.ModContentManager.GetInternalAssetKey(relativePath); + } + + /// + public IAssetData GetPatchHelper(T data, string relativePath = null) + { + if (data == null) + throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); + + relativePath ??= $"temp/{Guid.NewGuid():N}"; + + return new AssetDataForObject(this.ContentCore.GetLocale(), this.ContentCore.ParseAssetName(relativePath), data, key => this.ContentCore.ParseAssetName(key).Name); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 058bff83..d28faacc 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -8,6 +8,14 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Provides simplified APIs for writing mods. internal class ModHelper : BaseHelper, IModHelper, IDisposable { + /********* + ** Fields + *********/ + /// The backing field for . + [Obsolete] + private readonly IContentHelper ContentImpl; + + /********* ** Accessors *********/ @@ -18,7 +26,27 @@ namespace StardewModdingAPI.Framework.ModHelpers public IModEvents Events { get; } /// - public IContentHelper Content { get; } + [Obsolete] + public IContentHelper Content + { + get + { + SCore.DeprecationManager.Warn( + source: SCore.DeprecationManager.GetSourceName(this.ModID), + nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.ContentImpl; + } + } + + /// + public IGameContentHelper GameContent { get; } + + /// + public IModContentHelper ModContent { get; } /// public IContentPackHelper ContentPacks { get; } @@ -54,6 +82,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages the game's input state for the current player instance. That may not be the main player in split-screen mode. /// Manages access to events raised by SMAPI. /// An API for loading content assets. + /// An API for loading content assets from the game's Content folder or via . + /// An API for loading content assets from your mod's files. /// An API for managing content packs. /// An API for managing console commands. /// An API for reading and writing persistent mod data. @@ -63,7 +93,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for reading translations stored in the mod's i18n folder. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, Func currentInputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) + public ModHelper(string modID, string modDirectory, Func currentInputState, IModEvents events, IContentHelper contentHelper, IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) : base(modID) { // validate directory @@ -74,7 +104,9 @@ namespace StardewModdingAPI.Framework.ModHelpers // initialize this.DirectoryPath = modDirectory; - this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.ContentImpl = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.GameContent = gameContentHelper ?? throw new ArgumentNullException(nameof(gameContentHelper)); + this.ModContent = modContentHelper ?? throw new ArgumentNullException(nameof(modContentHelper)); this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper)); this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper)); this.Input = new InputHelper(modID, currentInputState); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index efdfabe7..b4aa3595 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1775,9 +1775,10 @@ namespace StardewModdingAPI.Framework { IManifest manifest = mod.Manifest; IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); - IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); + GameContentHelper gameContentHelper = new(this.ContentCore, manifest.UniqueID, mod.DisplayName, monitor); + IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager()); TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); - IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, translationHelper, jsonHelper); + IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, modContentHelper, translationHelper, jsonHelper); mod.SetMod(contentPack, monitor, translationHelper); this.ModRegistry.Add(mod); @@ -1855,7 +1856,8 @@ namespace StardewModdingAPI.Framework IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + GameContentHelper gameContentHelper = new(contentCore, packManifest.UniqueID, packManifest.Name, packMonitor); + IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, gameContentHelper.GetUnderlyingContentManager()); TranslationHelper packTranslationHelper = new(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); @@ -1867,13 +1869,15 @@ namespace StardewModdingAPI.Framework IModEvents events = new ModEvents(mod, this.EventManager); ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); + GameContentHelper gameContentHelper = new(contentCore, manifest.UniqueID, mod.DisplayName, monitor); + IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager()); IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy(GetContentPacks), CreateFakeContentPack); IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.Multiplayer); - modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); + modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); } // init mod diff --git a/src/SMAPI/IAssetName.cs b/src/SMAPI/IAssetName.cs index 89f02adf..c91da266 100644 --- a/src/SMAPI/IAssetName.cs +++ b/src/SMAPI/IAssetName.cs @@ -30,6 +30,11 @@ namespace StardewModdingAPI /// Whether to compare the given name with the (if true) or (if false). This has no effect on any locale included in the given . bool IsEquivalentTo(string assetName, bool useBaseName = false); + /// Get whether the given asset name is equivalent, ignoring capitalization and formatting. + /// The asset name to compare this instance to. + /// Whether to compare the given name with the (if true) or (if false). + bool IsEquivalentTo(IAssetName assetName, bool useBaseName = false); + /// Get whether the asset name starts with the given value, ignoring capitalization and formatting. This can be used with a trailing slash to test for an asset folder, like Data/. /// The prefix to match. /// Whether to match if the prefix occurs mid-word, so Data/AchievementsToIgnore matches prefix Data/Achievements. If this is false, the prefix only matches if the asset name starts with the prefix followed by a non-alphanumeric character (including ., /, or \\) or the end of string. diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index 1d36abff..48f6bfd8 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -10,17 +10,18 @@ using xTile; namespace StardewModdingAPI { /// Provides an API for loading content assets. + [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.GameContent)} or {nameof(IMod.Helper)}.{nameof(IModHelper.ModContent)} instead. This interface will be removed in SMAPI 4.0.0.")] public interface IContentHelper : IModLinked { /********* ** Accessors *********/ /// Interceptors which provide the initial versions of matching content assets. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This interface will be removed in SMAPI 4.0.0.")] + [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This property will be removed in SMAPI 4.0.0.")] IList AssetLoaders { get; } /// Interceptors which edit matching content assets after they're loaded. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This interface will be removed in SMAPI 4.0.0.")] + [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This property will be removed in SMAPI 4.0.0.")] IList AssetEditors { get; } /// The game's current locale code (like pt-BR). @@ -33,11 +34,6 @@ namespace StardewModdingAPI /********* ** Public methods *********/ - /// Parse a raw asset name. - /// The raw asset name to parse. - /// The is null or empty. - IAssetName ParseAssetName(string rawName); - /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 9cc64dcd..3c66faff 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI /// Provides translations stored in the content pack's i18n folder. See for more info. ITranslationHelper Translation { get; } + /// An API for loading content assets from the content pack's files. + IModContentHelper ModContent { get; } + /********* ** Public methods @@ -47,11 +50,13 @@ namespace StardewModdingAPI /// The relative file path within the content pack (case-insensitive). /// The is empty or contains invalid characters. /// The content asset couldn't be loaded (e.g. because it doesn't exist). + [Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.Load)} instead. This method will be removed in SMAPI 4.0.0.")] T LoadAsset(string key); /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. /// The relative file path within the content pack (case-insensitive). /// The is empty or contains invalid characters. + [Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.GetInternalAssetName)} instead. This method will be removed in SMAPI 4.0.0.")] string GetActualAssetKey(string key); } } diff --git a/src/SMAPI/IGameContentHelper.cs b/src/SMAPI/IGameContentHelper.cs new file mode 100644 index 00000000..86bc3e0e --- /dev/null +++ b/src/SMAPI/IGameContentHelper.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Events; +using StardewValley; +using xTile; + +namespace StardewModdingAPI +{ + /// Provides an API for loading content assets from the game's Content folder or via . + public interface IGameContentHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// The game's current locale code (like pt-BR). + string CurrentLocale { get; } + + /// The game's current locale as an enum value. + LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } + + + /********* + ** Public methods + *********/ + /// Parse a raw asset name. + /// The raw asset name to parse. + /// The is null or empty. + IAssetName ParseAssetName(string rawName); + + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. + /// The asset name to load. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T Load(string assetName); + + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. + /// The asset name to load. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T Load(IAssetName assetName); + + /// Remove an asset from the content cache so it's 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. + /// The asset key to invalidate in the content folder. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + bool InvalidateCache(string assetName); + + /// Remove an asset from the content cache so it's 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. + /// The asset key to invalidate in the content folder. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + bool InvalidateCache(IAssetName assetName); + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + bool InvalidateCache(); + + /// 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. + /// A predicate matching the assets to invalidate. + /// Returns whether any cache entries were invalidated. + bool InvalidateCache(Func predicate); + + /// Get a patch helper for arbitrary data. + /// The data type. + /// The asset data. + /// The asset name. This is only used for tracking purposes and has no effect on the patch helper. + IAssetData GetPatchHelper(T data, string assetName = null); + } +} diff --git a/src/SMAPI/IModContentHelper.cs b/src/SMAPI/IModContentHelper.cs new file mode 100644 index 00000000..e3431365 --- /dev/null +++ b/src/SMAPI/IModContentHelper.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using xTile; + +namespace StardewModdingAPI +{ + /// Provides an API for loading content assets from the current mod's folder. + public interface IModContentHelper : IModLinked + { + /********* + ** Public methods + *********/ + /// Load content from the mod folder and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the mod folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T Load(string relativePath); + + /// Get the internal asset name which allows loading a mod file through any of the game's content managers. This can be used when passing asset names directly to the game (e.g. for map tilesheets), but should be avoided if you can use instead. This does not validate whether the asset exists. + /// The local path to a content file relative to the mod folder. + /// The is empty or contains invalid characters. + IAssetName GetInternalAssetName(string relativePath); + + /// Get a patch helper for arbitrary data. + /// The data type. + /// The asset data. + /// The local path to the content file being edited relative to the mod folder. This is only used for tracking purposes and has no effect on the patch helper. + IAssetData GetPatchHelper(T data, string relativePath = null); + } +} diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index cd746e06..15e4ed8d 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Events; namespace StardewModdingAPI @@ -17,13 +18,22 @@ namespace StardewModdingAPI /// An API for managing console commands. ICommandHelper ConsoleCommands { get; } + /// An API for loading content assets from the game's Content folder or using the events. + IGameContentHelper GameContent { get; } + + /// An API for loading content assets from your mod's files. + /// This API is intended for reading content assets from the mod files (like game data, images, etc); see also which is intended for persisting internal mod data. + IModContentHelper ModContent { get; } + /// An API for loading content assets. + [Obsolete($"Use {nameof(IGameContentHelper)} or {nameof(IModContentHelper)} instead.")] IContentHelper Content { get; } /// An API for managing content packs. IContentPackHelper ContentPacks { get; } /// An API for reading and writing persistent mod data. + /// This API is intended for persisting internal mod data; see also which is intended for reading content assets (like game data, images, etc). IDataHelper Data { get; } /// An API for checking and changing input state. -- cgit From a593eda30f82af474887d91458b0e9158f66fefc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 18:24:59 -0400 Subject: use target-typed new --- src/SMAPI.Installer/Framework/InstallerContext.cs | 2 +- src/SMAPI.Installer/InteractiveInstaller.cs | 18 ++++++------ src/SMAPI.Installer/Program.cs | 6 ++-- src/SMAPI.Internal.Patching/HarmonyPatcher.cs | 2 +- src/SMAPI.Internal.Patching/PatchHelper.cs | 2 +- .../Mock/StardewValley/Item.cs | 16 +++++------ .../Mock/StardewValley/Object.cs | 2 +- .../NetFieldAnalyzerTests.cs | 4 +-- .../ObsoleteFieldAnalyzerTests.cs | 2 +- .../NetFieldAnalyzer.cs | 4 +-- .../ObsoleteFieldAnalyzer.cs | 2 +- src/SMAPI.ModBuildConfig/DeployModTask.cs | 4 +-- .../Framework/ModFileManager.cs | 8 +++--- .../Framework/Commands/Player/AddCommand.cs | 2 +- .../Commands/Player/ListItemTypesCommand.cs | 2 +- .../Framework/Commands/Player/ListItemsCommand.cs | 2 +- .../Framework/ItemRepository.cs | 2 +- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 2 +- .../Patches/SaveGamePatcher.cs | 2 +- src/SMAPI.Mods.SaveBackup/ModEntry.cs | 12 ++++---- src/SMAPI.Tests/Core/TranslationTests.cs | 6 ++-- src/SMAPI.Tests/Sample.cs | 2 +- src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs | 22 +++++++-------- src/SMAPI.Tests/Utilities/SDateTests.cs | 4 +-- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 2 +- .../Framework/Clients/WebApi/WebApiClient.cs | 4 +-- .../Framework/Clients/Wiki/WikiClient.cs | 2 +- .../Framework/GameScanning/GameScanner.cs | 4 +-- .../Framework/LowLevelEnvironmentUtility.cs | 2 +- .../Framework/ModData/ModDataRecord.cs | 2 +- .../Framework/ModScanning/ModScanner.cs | 12 ++++---- src/SMAPI.Toolkit/ModToolkit.cs | 2 +- src/SMAPI.Toolkit/SemanticVersionComparer.cs | 2 +- src/SMAPI.Toolkit/Serialization/JsonHelper.cs | 2 +- src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 4 +-- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- .../Controllers/JsonValidatorController.cs | 2 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../Clients/Chucklefish/ChucklefishClient.cs | 2 +- .../Clients/CurseForge/CurseForgeClient.cs | 2 +- .../Framework/Clients/Nexus/NexusClient.cs | 2 +- src/SMAPI.Web/Framework/Compression/GzipHelper.cs | 8 +++--- src/SMAPI.Web/Framework/Extensions.cs | 4 +-- .../Framework/JobDashboardAuthorizationFilter.cs | 2 +- .../Framework/LogParsing/LogMessageBuilder.cs | 2 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 32 +++++++++++----------- src/SMAPI.Web/Framework/Storage/StorageProvider.cs | 4 +-- src/SMAPI.Web/ViewModels/LogParserModel.cs | 2 +- src/SMAPI/Context.cs | 4 +-- src/SMAPI/Framework/CommandManager.cs | 2 +- .../Framework/Commands/HarmonySummaryCommand.cs | 2 +- src/SMAPI/Framework/Content/AssetDataForMap.cs | 4 +-- src/SMAPI/Framework/DeprecationManager.cs | 2 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 2 +- src/SMAPI/Framework/Input/KeyboardStateBuilder.cs | 2 +- src/SMAPI/Framework/Input/SInputState.cs | 10 +++---- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 16 +++++------ src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- .../ModLoading/Symbols/SymbolReaderProvider.cs | 2 +- src/SMAPI/Framework/ModRegistry.cs | 4 +-- src/SMAPI/Framework/Models/SConfig.cs | 2 +- src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 6 ++-- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 4 +-- src/SMAPI/Framework/Reflection/Reflector.cs | 4 +-- src/SMAPI/Framework/SGame.cs | 8 +++--- src/SMAPI/Framework/SGameRunner.cs | 2 +- src/SMAPI/Framework/SMultiplayer.cs | 18 ++++++------ src/SMAPI/Framework/Singleton.cs | 2 +- src/SMAPI/Framework/SnapshotListDiff.cs | 4 +-- src/SMAPI/Framework/StateTracking/ChestTracker.cs | 4 +-- .../FieldWatchers/ComparableListWatcher.cs | 4 +-- .../FieldWatchers/ImmutableCollectionWatcher.cs | 2 +- .../FieldWatchers/NetCollectionWatcher.cs | 4 +-- .../FieldWatchers/ObservableCollectionWatcher.cs | 6 ++-- .../Framework/StateTracking/LocationTracker.cs | 2 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 2 +- .../StateTracking/Snapshots/LocationSnapshot.cs | 14 +++++----- .../StateTracking/Snapshots/PlayerSnapshot.cs | 4 +-- .../StateTracking/Snapshots/WatcherSnapshot.cs | 16 +++++------ .../Snapshots/WorldLocationsSnapshot.cs | 4 +-- .../Framework/TemporaryHacks/MiniMonoModHotfix.cs | 4 +-- src/SMAPI/Framework/WatcherCore.cs | 2 +- src/SMAPI/Program.cs | 2 +- 83 files changed, 203 insertions(+), 203 deletions(-) (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI.Installer/Framework/InstallerContext.cs b/src/SMAPI.Installer/Framework/InstallerContext.cs index bb973230..abc7adde 100644 --- a/src/SMAPI.Installer/Framework/InstallerContext.cs +++ b/src/SMAPI.Installer/Framework/InstallerContext.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Installer.Framework ** Fields *********/ /// The underlying toolkit game scanner. - private readonly GameScanner GameScanner = new GameScanner(); + private readonly GameScanner GameScanner = new(); /********* diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index b3bba883..a126c3b8 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -126,7 +126,7 @@ namespace StardewModdingApi.Installer /**** ** Get basic info & set window title ****/ - ModToolkit toolkit = new ModToolkit(); + ModToolkit toolkit = new(); var context = new InstallerContext(); Console.Title = $"SMAPI {context.GetInstallerVersion()} installer on {context.Platform} {context.PlatformName}"; Console.WriteLine(); @@ -246,7 +246,7 @@ namespace StardewModdingApi.Installer } // get folders - DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath); + DirectoryInfo bundleDir = new(this.BundlePath); paths = new InstallerPaths(bundleDir, installDir); } @@ -354,8 +354,8 @@ namespace StardewModdingApi.Installer // move global save data folder (changed in 3.2) { string dataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); - DirectoryInfo oldDir = new DirectoryInfo(Path.Combine(dataPath, "Saves", ".smapi")); - DirectoryInfo newDir = new DirectoryInfo(Path.Combine(dataPath, ".smapi")); + DirectoryInfo oldDir = new(Path.Combine(dataPath, "Saves", ".smapi")); + DirectoryInfo newDir = new(Path.Combine(dataPath, ".smapi")); if (oldDir.Exists) { @@ -428,7 +428,7 @@ namespace StardewModdingApi.Installer } // add or replace bundled mods - DirectoryInfo bundledModsDir = new DirectoryInfo(Path.Combine(paths.BundlePath, "Mods")); + DirectoryInfo bundledModsDir = new(Path.Combine(paths.BundlePath, "Mods")); if (bundledModsDir.Exists && bundledModsDir.EnumerateDirectories().Any()) { this.PrintDebug("Adding bundled mods..."); @@ -450,7 +450,7 @@ namespace StardewModdingApi.Installer // find target folder ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true); - DirectoryInfo defaultTargetFolder = new DirectoryInfo(Path.Combine(paths.ModsPath, sourceMod.Directory.Name)); + DirectoryInfo defaultTargetFolder = new(Path.Combine(paths.ModsPath, sourceMod.Directory.Name)); DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder; this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName ? $" adding {sourceMod.Manifest.Name}..." @@ -593,7 +593,7 @@ namespace StardewModdingApi.Installer break; case DirectoryInfo sourceDir: - DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); + DirectoryInfo targetSubfolder = new(Path.Combine(targetFolder.FullName, sourceDir.Name)); foreach (var entry in sourceDir.EnumerateFileSystemInfos()) this.RecursiveCopy(entry, targetSubfolder, filter); break; @@ -717,7 +717,7 @@ namespace StardewModdingApi.Installer // get directory if (File.Exists(path)) path = Path.GetDirectoryName(path); - DirectoryInfo directory = new DirectoryInfo(path); + DirectoryInfo directory = new(path); // validate path if (!directory.Exists) @@ -795,7 +795,7 @@ namespace StardewModdingApi.Installer // get path string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); - DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods")); + DirectoryInfo modDir = new(Path.Combine(appDataPath, "Mods")); // check if migration needed if (!modDir.Exists) diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index 45cfea75..2c9b2c0a 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -31,7 +31,7 @@ namespace StardewModdingApi.Installer public static void Main(string[] args) { // find install bundle - FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, "install.dat")); + FileInfo zipFile = new(Path.Combine(Program.InstallerPath, "install.dat")); if (!zipFile.Exists) { Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})"); @@ -40,7 +40,7 @@ namespace StardewModdingApi.Installer } // unzip bundle into temp folder - DirectoryInfo bundleDir = new DirectoryInfo(Program.ExtractedBundlePath); + DirectoryInfo bundleDir = new(Program.ExtractedBundlePath); Console.WriteLine("Extracting install files..."); ZipFile.ExtractToDirectory(zipFile.FullName, bundleDir.FullName); @@ -70,7 +70,7 @@ namespace StardewModdingApi.Installer { try { - AssemblyName name = new AssemblyName(e.Name); + AssemblyName name = new(e.Name); foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll")) { if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase)) diff --git a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs index c07e3b41..6f30c241 100644 --- a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs +++ b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs @@ -15,7 +15,7 @@ namespace StardewModdingAPI.Internal.Patching /// The patchers to apply. public static Harmony Apply(string id, IMonitor monitor, params IPatcher[] patchers) { - Harmony harmony = new Harmony(id); + Harmony harmony = new(id); foreach (IPatcher patcher in patchers) { diff --git a/src/SMAPI.Internal.Patching/PatchHelper.cs b/src/SMAPI.Internal.Patching/PatchHelper.cs index fc79ddf2..c9758616 100644 --- a/src/SMAPI.Internal.Patching/PatchHelper.cs +++ b/src/SMAPI.Internal.Patching/PatchHelper.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Internal.Patching /// The method generic types, or null if it's not generic. public static string GetMethodString(Type type, string name, Type[] parameters = null, Type[] generics = null) { - StringBuilder str = new StringBuilder(); + StringBuilder str = new(); // type str.Append(type.FullName); diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs index 1b6317c1..e8da92fa 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs @@ -7,27 +7,27 @@ namespace StardewValley public class Item { /// A net int field with an equivalent non-net Category property. - public readonly NetInt category = new NetInt { Value = 42 }; + public readonly NetInt category = new() { Value = 42 }; /// A generic net int field with no equivalent non-net property. - public readonly NetInt netIntField = new NetInt { Value = 42 }; + public readonly NetInt netIntField = new() { Value = 42 }; /// A generic net ref field with no equivalent non-net property. - public readonly NetRef netRefField = new NetRef(); + public readonly NetRef netRefField = new(); /// A generic net int property with no equivalent non-net property. - public NetInt netIntProperty = new NetInt { Value = 42 }; + public NetInt netIntProperty = new() { Value = 42 }; /// A generic net ref property with no equivalent non-net property. - public NetRef netRefProperty { get; } = new NetRef(); + public NetRef netRefProperty { get; } = new(); /// A sample net list. - public readonly NetList netList = new NetList(); + public readonly NetList netList = new(); /// A sample net object list. - public readonly NetObjectList netObjectList = new NetObjectList(); + public readonly NetObjectList netObjectList = new(); /// A sample net collection. - public readonly NetCollection netCollection = new NetCollection(); + public readonly NetCollection netCollection = new(); } } diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs index 3dd66a6d..151010a7 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs @@ -7,6 +7,6 @@ namespace StardewValley public class Object : Item { /// A net int field with an equivalent non-net property. - public NetInt type = new NetInt { Value = 42 }; + public NetInt type = new() { Value = 42 }; } } diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 89bd1be5..f11a59d3 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -93,7 +93,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests { // arrange string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); - DiagnosticResult expected = new DiagnosticResult + DiagnosticResult expected = new() { Id = "AvoidImplicitNetFieldCast", Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.", @@ -135,7 +135,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests { // arrange string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); - DiagnosticResult expected = new DiagnosticResult + DiagnosticResult expected = new() { Id = "AvoidNetField", Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/package/avoid-net-field for details.", diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs index 12641e1a..76607b8e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs @@ -64,7 +64,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests { // arrange string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); - DiagnosticResult expected = new DiagnosticResult + DiagnosticResult expected = new() { Id = "AvoidObsoleteField", Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/package/avoid-obsolete-field for details.", diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index e03c72de..8478dc54 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -132,7 +132,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer }; /// The diagnostic info for an implicit net field cast. - private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor( + private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new( id: "AvoidImplicitNetFieldCast", title: "Netcode types shouldn't be implicitly converted", messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.", @@ -143,7 +143,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer ); /// The diagnostic info for an avoidable net field access. - private readonly DiagnosticDescriptor AvoidNetFieldRule = new DiagnosticDescriptor( + private readonly DiagnosticDescriptor AvoidNetFieldRule = new( id: "AvoidNetField", title: "Avoid Netcode types when possible", messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/package/avoid-net-field for details.", diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index 722d5227..3184147a 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// Describes the diagnostic rule covered by the analyzer. private readonly IDictionary Rules = new Dictionary { - ["AvoidObsoleteField"] = new DiagnosticDescriptor( + ["AvoidObsoleteField"] = new( id: "AvoidObsoleteField", title: "Reference to obsolete field", messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/package/avoid-obsolete-field for details.", diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 140933bd..c7026ee1 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -88,7 +88,7 @@ namespace StardewModdingAPI.ModBuildConfig Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray(); // get mod info - ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip); + ModFileManager package = new(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip); // deploy mod files if (this.EnableModDeploy) @@ -246,7 +246,7 @@ namespace StardewModdingAPI.ModBuildConfig // create zip file Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!); using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write); - using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create); + using ZipArchive archive = new(zipStream, ZipArchiveMode.Create); foreach (var fileEntry in files) { diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index ad4ffdf9..80955f67 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -136,7 +136,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // project manifest bool hasProjectManifest = false; { - FileInfo manifest = new FileInfo(Path.Combine(projectDir, this.ManifestFileName)); + FileInfo manifest = new(Path.Combine(projectDir, this.ManifestFileName)); if (manifest.Exists) { yield return Tuple.Create(this.ManifestFileName, manifest); @@ -146,7 +146,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // project i18n files bool hasProjectTranslations = false; - DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n")); + DirectoryInfo translationsFolder = new(Path.Combine(projectDir, "i18n")); if (translationsFolder.Exists) { foreach (FileInfo file in translationsFolder.EnumerateFiles()) @@ -156,7 +156,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // project assets folder bool hasAssetsFolder = false; - DirectoryInfo assetsFolder = new DirectoryInfo(Path.Combine(projectDir, "assets")); + DirectoryInfo assetsFolder = new(Path.Combine(projectDir, "assets")); if (assetsFolder.Exists) { foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories)) @@ -168,7 +168,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework } // build output - DirectoryInfo buildFolder = new DirectoryInfo(targetDir); + DirectoryInfo buildFolder = new(targetDir); foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories)) { // get path info diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 0e8f7517..fae31c23 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ** Fields *********/ /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new ItemRepository(); + private readonly ItemRepository Items = new(); /// The type names recognized by this command. private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray(); diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs index 1f12e5f9..af362bcd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ** Fields *********/ /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new ItemRepository(); + private readonly ItemRepository Items = new(); /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index 67569298..46fc1d9d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ** Fields *********/ /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new ItemRepository(); + private readonly ItemRepository Items = new(); /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 8704a403..9b48fa99 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -285,7 +285,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework case SObject.flowersCategory: yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => { - SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false) + SObject honey = new(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false) { Name = $"{item.Name} Honey", preservedParentSheetIndex = { id } diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 91437fd3..5e594984 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The command arguments. private void HandleCommand(IConsoleCommand command, string commandName, string[] args) { - ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor); + ArgumentParser argParser = new(commandName, args, this.Monitor); command.Handle(this.Monitor, commandName, argParser); } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs index 0a7ed212..5f6dbcb3 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs @@ -121,7 +121,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches { try { - BluePrint _ = new BluePrint(building.buildingType.Value); + BluePrint _ = new(building.buildingType.Value); } catch (ContentLoadException) { diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index b89bb9c3..f6925707 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Mods.SaveBackup try { // init backup folder - DirectoryInfo backupFolder = new DirectoryInfo(this.BackupFolder); + DirectoryInfo backupFolder = new(this.BackupFolder); backupFolder.Create(); // back up & prune saves @@ -63,8 +63,8 @@ namespace StardewModdingAPI.Mods.SaveBackup try { // get target path - FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName)); - DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel)); + FileInfo targetFile = new(Path.Combine(backupFolder.FullName, this.FileName)); + DirectoryInfo fallbackDir = new(Path.Combine(backupFolder.FullName, this.BackupLabel)); if (targetFile.Exists || fallbackDir.Exists) { this.Monitor.Log("Already backed up today."); @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Mods.SaveBackup } // copy saves to fallback directory (ignore non-save files/folders) - DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath); + DirectoryInfo savesDir = new(Constants.SavesPath); if (!this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false)) { this.Monitor.Log("No saves found."); @@ -190,8 +190,8 @@ namespace StardewModdingAPI.Mods.SaveBackup /// The destination file to create. private void CompressUsingMacProcess(string sourcePath, FileInfo destination) { - DirectoryInfo saveFolder = new DirectoryInfo(sourcePath); - ProcessStartInfo startInfo = new ProcessStartInfo + DirectoryInfo saveFolder = new(sourcePath); + ProcessStartInfo startInfo = new() { FileName = "zip", Arguments = $"-rq \"{destination.FullName}\" \"{saveFolder.Name}\" -x \"*.DS_Store\" -x \"__MACOSX\"", diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs index 457f9fad..58bc59b1 100644 --- a/src/SMAPI.Tests/Core/TranslationTests.cs +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -116,7 +116,7 @@ namespace SMAPI.Tests.Core public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) { // act - Translation translation = new Translation("pt-BR", "key", text); + Translation translation = new("pt-BR", "key", text); // assert if (translation.HasValue()) @@ -129,7 +129,7 @@ namespace SMAPI.Tests.Core public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) { // act - Translation translation = new Translation("pt-BR", "key", text); + Translation translation = new("pt-BR", "key", text); // assert if (translation.HasValue()) @@ -182,7 +182,7 @@ namespace SMAPI.Tests.Core string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; // act - Translation translation = new Translation("pt-BR", "key", input); + Translation translation = new("pt-BR", "key", input); switch (structure) { case "anonymous object": diff --git a/src/SMAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs index f4f0d88e..9587a100 100644 --- a/src/SMAPI.Tests/Sample.cs +++ b/src/SMAPI.Tests/Sample.cs @@ -9,7 +9,7 @@ namespace SMAPI.Tests ** Fields *********/ /// A random number generator. - private static readonly Random Random = new Random(); + private static readonly Random Random = new(); /********* diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index ab4c2618..94819c2e 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -14,7 +14,7 @@ namespace SMAPI.Tests.Utilities /// Sample paths used in unit tests. public static readonly SamplePath[] SamplePaths = { // Windows absolute path - new SamplePath + new() { OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley", @@ -26,7 +26,7 @@ namespace SMAPI.Tests.Utilities }, // Windows absolute path (with trailing slash) - new SamplePath + new() { OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\", @@ -38,7 +38,7 @@ namespace SMAPI.Tests.Utilities }, // Windows relative path - new SamplePath + new() { OriginalPath = @"Content\Characters\Dialogue\Abigail", @@ -50,7 +50,7 @@ namespace SMAPI.Tests.Utilities }, // Windows relative path (with directory climbing) - new SamplePath + new() { OriginalPath = @"..\..\Content", @@ -62,7 +62,7 @@ namespace SMAPI.Tests.Utilities }, // Windows UNC path - new SamplePath + new() { OriginalPath = @"\\unc\path", @@ -74,7 +74,7 @@ namespace SMAPI.Tests.Utilities }, // Linux absolute path - new SamplePath + new() { OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley", @@ -86,7 +86,7 @@ namespace SMAPI.Tests.Utilities }, // Linux absolute path (with trailing slash) - new SamplePath + new() { OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley/", @@ -98,7 +98,7 @@ namespace SMAPI.Tests.Utilities }, // Linux absolute path (with ~) - new SamplePath + new() { OriginalPath = @"~/.steam/steam/steamapps/common/Stardew Valley", @@ -110,7 +110,7 @@ namespace SMAPI.Tests.Utilities }, // Linux relative path - new SamplePath + new() { OriginalPath = @"Content/Characters/Dialogue/Abigail", @@ -122,7 +122,7 @@ namespace SMAPI.Tests.Utilities }, // Linux relative path (with directory climbing) - new SamplePath + new() { OriginalPath = @"../../Content", @@ -134,7 +134,7 @@ namespace SMAPI.Tests.Utilities }, // Mixed directory separators - new SamplePath + new() { OriginalPath = @"C:\some/mixed\path/separators", diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index 374f4921..886f25cd 100644 --- a/src/SMAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -61,7 +61,7 @@ namespace SMAPI.Tests.Utilities public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.SampleSeasonValues))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year) { // act - SDate date = new SDate(day, season, year); + SDate date = new(day, season, year); // assert Assert.AreEqual(day, date.Day); @@ -254,7 +254,7 @@ namespace SMAPI.Tests.Utilities { foreach (int day in SDateTests.ValidDays) { - SDate date = new SDate(day, season, year); + SDate date = new(day, season, year); int hash = date.GetHashCode(); if (hashes.TryGetValue(hash, out SDate otherDate)) Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}."); diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index ac4ef39b..c8270373 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -395,7 +395,7 @@ namespace SMAPI.Tests.Utilities public void GameVersion(string versionStr) { // act - GameVersion version = new GameVersion(versionStr); + GameVersion version = new(versionStr); // assert Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value."); diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index c2d906a0..f7d26d21 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -62,9 +62,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi private TResult Post(string url, TBody content) { // note: avoid HttpClient for macOS compatibility - using WebClient client = new WebClient(); + using WebClient client = new(); - Uri fullUrl = new Uri(this.BaseUrl, url); + Uri fullUrl = new(this.BaseUrl, url); string data = JsonConvert.SerializeObject(content); client.Headers["Content-Type"] = "application/json"; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 0f5a0ec3..abbcdc81 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string pullRequestUrl = this.GetAttribute(node, "data-pr"); // parse stable compatibility - WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo + WikiCompatibilityInfo compatibility = new() { Status = this.GetAttributeAsEnum(node, "data-status") ?? WikiCompatibilityStatus.Ok, BrokeIn = this.GetAttribute(node, "data-broke-in"), diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 8d4198de..768beba1 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning // yield valid folders foreach (string path in paths) { - DirectoryInfo folder = new DirectoryInfo(path); + DirectoryInfo folder = new(path); if (this.LooksLikeGameFolder(folder)) yield return folder; } @@ -191,7 +191,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning yield break; // get targets file - FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets")); + FileInfo file = new(Path.Combine(homePath, "stardewvalley.targets")); if (!file.Exists) yield break; diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index 8b6eb5fb..c0332331 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -98,7 +98,7 @@ namespace StardewModdingAPI.Toolkit.Framework /// private static bool IsRunningAndroid() { - using Process process = new Process + using Process process = new() { StartInfo = { diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 5dd32acf..7e07ffde 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The manifest to match. public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) { - ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; + ModDataRecordVersionedFields parsed = new() { DisplayName = this.DisplayName, DataRecord = this }; foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) { switch (field.Key) diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index e6105f9c..d21ccec0 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning private readonly JsonHelper JsonHelper; /// A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod. - private readonly HashSet IgnoreFilesystemNames = new HashSet + private readonly HashSet IgnoreFilesystemNames = new() { new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // macOS @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// A list of file extensions to ignore when searching for mod files. - private readonly HashSet IgnoreFileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + private readonly HashSet IgnoreFileExtensions = new(StringComparer.OrdinalIgnoreCase) { // text ".doc", @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// The extensions for packed content files. - private readonly HashSet StrictXnbModExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + private readonly HashSet StrictXnbModExtensions = new(StringComparer.OrdinalIgnoreCase) { ".xgs", ".xnb", @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// The extensions for files which an XNB mod may contain, in addition to . - private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + private readonly HashSet PotentialXnbModExtensions = new(StringComparer.OrdinalIgnoreCase) { ".json", ".yaml" @@ -96,7 +96,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The root folder containing mods. public IEnumerable GetModFolders(string rootPath) { - DirectoryInfo root = new DirectoryInfo(rootPath); + DirectoryInfo root = new(rootPath); return this.GetModFolders(root, root); } @@ -260,7 +260,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning while (true) { // check for manifest in current folder - FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json")); + FileInfo file = new(Path.Combine(folder.FullName, "manifest.json")); if (file.Exists) return file; diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 38a67ae5..80008df7 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// Encapsulates SMAPI's JSON parsing. - public JsonHelper JsonHelper { get; } = new JsonHelper(); + public JsonHelper JsonHelper { get; } = new(); /********* diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs index 9f6b57a2..8eba2c9f 100644 --- a/src/SMAPI.Toolkit/SemanticVersionComparer.cs +++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// A singleton instance of the comparer. - public static SemanticVersionComparer Instance { get; } = new SemanticVersionComparer(); + public static SemanticVersionComparer Instance { get; } = new(); /********* diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs index 00db9903..5c465f3c 100644 --- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Serialization ** Accessors *********/ /// The JSON settings to use when serializing and deserializing files. - public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings + public JsonSerializerSettings JsonSettings { get; } = new() { Formatting = Formatting.Indented, ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 2e9e5eac..85e12bfa 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -100,8 +100,8 @@ namespace StardewModdingAPI.Toolkit.Utilities // though, this is only for compatibility with the mod build package. // convert to URIs - Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri from = new(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); if (from.Scheme != to.Scheme) throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 5097997c..69b54f47 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Web.Controllers // strip 'noinclude' blocks from release description if (release != null) { - HtmlDocument doc = new HtmlDocument(); + HtmlDocument doc = new(); doc.LoadHtml(release.Body); foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? Array.Empty()) node.Remove(); diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index e06c1236..bcd4097a 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -197,7 +197,7 @@ namespace StardewModdingAPI.Web.Controllers return null; // get matching file - DirectoryInfo schemaDir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "schemas")); + DirectoryInfo schemaDir = new(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "schemas")); foreach (FileInfo file in schemaDir.EnumerateFiles("*.json")) { if (file.Name.Equals($"{id}.json")) diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index dfe2504b..5329df99 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Web.Controllers bool isSmapiBeta = apiVersion.IsPrerelease() && apiVersion.PrereleaseTag.StartsWith("beta"); // get latest versions - ModEntryModel result = new ModEntryModel { ID = search.ID }; + ModEntryModel result = new() { ID = search.ID }; IList errors = new List(); ModEntryVersionModel main = null; ModEntryVersionModel optional = null; diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index b8b05878..9689807c 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -90,7 +90,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); + UriBuilder builder = new(this.Client.BaseClient.BaseAddress); builder.Path += string.Format(this.ModPageUrlFormat, id); return builder.Uri.ToString(); } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index d8008721..50a3336d 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge private readonly IClient Client; /// A regex pattern which matches a version number in a CurseForge mod file name. - private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled); + private readonly Regex VersionInNamePattern = new(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled); /********* diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 4ba94f81..571f06bc 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -195,7 +195,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new UriBuilder(this.WebClient.BaseClient.BaseAddress); + UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress); builder.Path += string.Format(this.WebModUrlFormat, id); return builder.Uri.ToString(); } diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs index 676d660d..93cde9d3 100644 --- a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs +++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs @@ -29,9 +29,9 @@ namespace StardewModdingAPI.Web.Framework.Compression // compressed byte[] compressedData; - using (MemoryStream stream = new MemoryStream()) + using (MemoryStream stream = new()) { - using (GZipStream zipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true)) + using (GZipStream zipStream = new(stream, CompressionLevel.Optimal, leaveOpen: true)) zipStream.Write(buffer, 0, buffer.Length); stream.Position = 0; @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.Compression return rawText; // decompress - using MemoryStream memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); { // read length prefix int dataLength = BitConverter.ToInt32(zipBuffer, 0); @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Web.Framework.Compression // read data byte[] buffer = new byte[dataLength]; memoryStream.Position = 0; - using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (GZipStream gZipStream = new(memoryStream, CompressionMode.Decompress)) gZipStream.Read(buffer, 0, buffer.Length); // return original string diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs index 5305b142..2e767b3d 100644 --- a/src/SMAPI.Web/Framework/Extensions.cs +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI.Web.Framework public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false) { // get route values - RouteValueDictionary valuesDict = new RouteValueDictionary(values); + RouteValueDictionary valuesDict = new(values); foreach (var value in helper.ActionContext.RouteData.Values) { if (!valuesDict.ContainsKey(value.Key)) @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework if (absoluteUrl) { HttpRequest request = helper.ActionContext.HttpContext.Request; - Uri baseUri = new Uri($"{request.Scheme}://{request.Host}"); + Uri baseUri = new($"{request.Scheme}://{request.Host}"); url = new Uri(baseUri, url).ToString(); } diff --git a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs index 385c0c91..3c1405eb 100644 --- a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs +++ b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Web.Framework ** Fields *********/ /// An authorization filter that allows local requests. - private static readonly LocalRequestsOnlyAuthorizationFilter LocalRequestsOnlyFilter = new LocalRequestsOnlyAuthorizationFilter(); + private static readonly LocalRequestsOnlyAuthorizationFilter LocalRequestsOnlyFilter = new(); /********* diff --git a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs index 992876ef..9da27d61 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing public string Mod { get; set; } /// The text for the next log message. - private readonly StringBuilder Text = new StringBuilder(); + private readonly StringBuilder Text = new(); /********* diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 887d0105..6a3ea222 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -14,38 +14,38 @@ namespace StardewModdingAPI.Web.Framework.LogParsing ** Fields *********/ /// A regex pattern matching the start of a SMAPI message. - private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?