add AssetName to encapsulate asset name handling (#766)
This commit is contained in:
parent
065859408f
commit
a2190df08c
|
@ -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`).
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>Unit tests for <see cref="AssetName"/>.</summary>
|
||||
[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<ArgumentException>(() => _ = 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<string> 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<int> 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,11 +25,11 @@ namespace StardewModdingAPI.Framework.Content
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locale">The content's locale code, if the content is localized.</param>
|
||||
/// <param name="assetName">The normalized asset name being read.</param>
|
||||
/// <param name="assetName">The asset name being read.</param>
|
||||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
|
||||
public AssetData(string locale, string assetName, TValue data, Func<string, string> getNormalizedPath, Action<TValue> onDataReplaced)
|
||||
public AssetData(string locale, IAssetName assetName, TValue data, Func<string, string> getNormalizedPath, Action<TValue> onDataReplaced)
|
||||
: base(locale, assetName, data.GetType(), getNormalizedPath)
|
||||
{
|
||||
this.Data = data;
|
||||
|
|
|
@ -11,11 +11,11 @@ namespace StardewModdingAPI.Framework.Content
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locale">The content's locale code, if the content is localized.</param>
|
||||
/// <param name="assetName">The normalized asset name being read.</param>
|
||||
/// <param name="assetName">The asset name being read.</param>
|
||||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
|
||||
public AssetDataForDictionary(string locale, string assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalizedPath, Action<IDictionary<TKey, TValue>> onDataReplaced)
|
||||
public AssetDataForDictionary(string locale, IAssetName assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalizedPath, Action<IDictionary<TKey, TValue>> onDataReplaced)
|
||||
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,11 +21,11 @@ namespace StardewModdingAPI.Framework.Content
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locale">The content's locale code, if the content is localized.</param>
|
||||
/// <param name="assetName">The normalized asset name being read.</param>
|
||||
/// <param name="assetName">The asset name being read.</param>
|
||||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
|
||||
public AssetDataForImage(string locale, string assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
|
||||
public AssetDataForImage(string locale, IAssetName assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
|
||||
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -18,11 +18,11 @@ namespace StardewModdingAPI.Framework.Content
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locale">The content's locale code, if the content is localized.</param>
|
||||
/// <param name="assetName">The normalized asset name being read.</param>
|
||||
/// <param name="assetName">The asset name being read.</param>
|
||||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
/// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
|
||||
public AssetDataForMap(string locale, string assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
|
||||
public AssetDataForMap(string locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
|
||||
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -13,10 +13,10 @@ namespace StardewModdingAPI.Framework.Content
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locale">The content's locale code, if the content is localized.</param>
|
||||
/// <param name="assetName">The normalized asset name being read.</param>
|
||||
/// <param name="assetName">The asset name being read.</param>
|
||||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
public AssetDataForObject(string locale, string assetName, object data, Func<string, string> getNormalizedPath)
|
||||
public AssetDataForObject(string locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath)
|
||||
: base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
|
@ -24,24 +24,24 @@ namespace StardewModdingAPI.Framework.Content
|
|||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath)
|
||||
: this(info.Locale, info.AssetName, data, getNormalizedPath) { }
|
||||
: this(info.Locale, info.Name, data, getNormalizedPath) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAssetDataForDictionary<TKey, TValue> AsDictionary<TKey, TValue>()
|
||||
{
|
||||
return new AssetDataForDictionary<TKey, TValue>(this.Locale, this.AssetName, this.GetData<IDictionary<TKey, TValue>>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
return new AssetDataForDictionary<TKey, TValue>(this.Locale, this.Name, this.GetData<IDictionary<TKey, TValue>>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAssetDataForImage AsImage()
|
||||
{
|
||||
return new AssetDataForImage(this.Locale, this.AssetName, this.GetData<Texture2D>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
return new AssetDataForImage(this.Locale, this.Name, this.GetData<Texture2D>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAssetDataForMap AsMap()
|
||||
{
|
||||
return new AssetDataForMap(this.Locale, this.AssetName, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
return new AssetDataForMap(this.Locale, this.Name, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -20,7 +20,11 @@ namespace StardewModdingAPI.Framework.Content
|
|||
public string Locale { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string AssetName { get; }
|
||||
public IAssetName Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete($"Use {nameof(Name)} instead.")]
|
||||
public string AssetName => this.Name.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type DataType { get; }
|
||||
|
@ -31,22 +35,22 @@ namespace StardewModdingAPI.Framework.Content
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locale">The content's locale code, if the content is localized.</param>
|
||||
/// <param name="assetName">The normalized asset name being read.</param>
|
||||
/// <param name="assetName">The asset name being read.</param>
|
||||
/// <param name="type">The content type being read.</param>
|
||||
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
|
||||
public AssetInfo(string locale, string assetName, Type type, Func<string, string> getNormalizedPath)
|
||||
public AssetInfo(string locale, IAssetName assetName, Type type, Func<string, string> getNormalizedPath)
|
||||
{
|
||||
this.Locale = locale;
|
||||
this.AssetName = assetName;
|
||||
this.Name = assetName;
|
||||
this.DataType = type;
|
||||
this.GetNormalizedPath = getNormalizedPath;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")]
|
||||
public bool AssetNameEquals(string path)
|
||||
{
|
||||
path = this.GetNormalizedPath(path);
|
||||
return this.AssetName.Equals(path, StringComparison.OrdinalIgnoreCase);
|
||||
return this.Name.IsEquivalentTo(path);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
using System;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Content
|
||||
{
|
||||
/// <summary>An asset name that can be loaded through the content pipeline.</summary>
|
||||
internal class AssetName : IAssetName
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>A lowercase version of <see cref="Name"/> used for consistent hash codes and equality checks.</summary>
|
||||
private readonly string ComparableName;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string BaseName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string LocaleCode { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public LocalizedContentManager.LanguageCode? LanguageCode { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="baseName">The base asset name without the locale code.</param>
|
||||
/// <param name="localeCode">The locale code specified in the <see cref="Name"/>, if it's a valid code recognized by the game content.</param>
|
||||
/// <param name="languageCode">The language code matching the <see cref="LocaleCode"/>, if applicable.</param>
|
||||
public AssetName(string baseName, string localeCode, LocalizedContentManager.LanguageCode? languageCode)
|
||||
{
|
||||
// validate
|
||||
if (string.IsNullOrWhiteSpace(baseName))
|
||||
throw new ArgumentException("The asset name can't be null or empty.", nameof(baseName));
|
||||
if (string.IsNullOrWhiteSpace(localeCode))
|
||||
localeCode = null;
|
||||
|
||||
// set base values
|
||||
this.BaseName = PathUtilities.NormalizeAssetName(baseName);
|
||||
this.LocaleCode = localeCode;
|
||||
this.LanguageCode = languageCode;
|
||||
|
||||
// set derived values
|
||||
this.Name = localeCode != null
|
||||
? string.Concat(this.BaseName, '.', this.LocaleCode)
|
||||
: this.BaseName;
|
||||
this.ComparableName = this.Name.ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>Parse a raw asset name into an instance.</summary>
|
||||
/// <param name="rawName">The raw asset name to parse.</param>
|
||||
/// <param name="parseLocale">Get the language code for a given locale, if it's valid.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="rawName"/> is null or empty.</exception>
|
||||
public static AssetName Parse(string rawName, Func<string, LocalizedContentManager.LanguageCode?> parseLocale)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawName))
|
||||
throw new ArgumentException("The asset name can't be null or empty.", nameof(rawName));
|
||||
|
||||
string baseName = rawName;
|
||||
string localeCode = null;
|
||||
LocalizedContentManager.LanguageCode? languageCode = null;
|
||||
|
||||
int lastPeriodIndex = rawName.LastIndexOf('.');
|
||||
if (lastPeriodIndex > 0 && rawName.Length > lastPeriodIndex + 1)
|
||||
{
|
||||
string possibleLocaleCode = rawName[(lastPeriodIndex + 1)..];
|
||||
LocalizedContentManager.LanguageCode? possibleLanguageCode = parseLocale(possibleLocaleCode);
|
||||
|
||||
if (possibleLanguageCode != null)
|
||||
{
|
||||
baseName = rawName[..lastPeriodIndex];
|
||||
localeCode = possibleLocaleCode;
|
||||
languageCode = possibleLanguageCode;
|
||||
}
|
||||
}
|
||||
|
||||
return new AssetName(baseName, localeCode, languageCode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEquivalentTo(string assetName, bool useBaseName = false)
|
||||
{
|
||||
// empty asset key is never equivalent
|
||||
if (string.IsNullOrWhiteSpace(assetName))
|
||||
return false;
|
||||
|
||||
assetName = PathUtilities.NormalizeAssetName(assetName);
|
||||
|
||||
string compareTo = useBaseName ? this.BaseName : this.Name;
|
||||
return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true)
|
||||
{
|
||||
// asset keys never start with null
|
||||
if (prefix is null)
|
||||
return false;
|
||||
|
||||
// asset keys can't have a leading slash, but NormalizeAssetName will trim them
|
||||
{
|
||||
string trimmed = prefix.TrimStart();
|
||||
if (trimmed.StartsWith('/') || trimmed.StartsWith('\\'))
|
||||
return false;
|
||||
}
|
||||
|
||||
// normalize prefix
|
||||
{
|
||||
string normalized = PathUtilities.NormalizeAssetName(prefix);
|
||||
|
||||
string trimmed = prefix.TrimEnd();
|
||||
if (trimmed.EndsWith('/') || trimmed.EndsWith('\\'))
|
||||
normalized += PathUtilities.PreferredAssetSeparator;
|
||||
|
||||
prefix = normalized;
|
||||
}
|
||||
|
||||
// compare
|
||||
return
|
||||
this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
&& (
|
||||
allowPartialWord
|
||||
|| this.Name.Length == prefix.Length
|
||||
|| !char.IsLetterOrDigit(prefix[^1]) // last character in suffix is word separator
|
||||
|| !char.IsLetterOrDigit(this.Name[prefix.Length]) // or first character after it is
|
||||
)
|
||||
&& (
|
||||
allowSubfolder
|
||||
|| this.Name.Length == prefix.Length
|
||||
|| !this.Name[prefix.Length..].Contains(PathUtilities.PreferredAssetSeparator)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public bool IsDirectlyUnderPath(string assetFolder)
|
||||
{
|
||||
return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(IAssetName other)
|
||||
{
|
||||
return other switch
|
||||
{
|
||||
null => false,
|
||||
AssetName otherImpl => this.ComparableName == otherImpl.ComparableName,
|
||||
_ => StringComparer.OrdinalIgnoreCase.Equals(this.Name, other.Name)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.ComparableName.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Name;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -248,6 +248,16 @@ namespace StardewModdingAPI.Framework
|
|||
this.InvalidateCache((contentManager, key, type) => contentManager is GameContentManager);
|
||||
}
|
||||
|
||||
/// <summary>Parse a raw asset name.</summary>
|
||||
/// <param name="rawName">The raw asset name to parse.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="rawName"/> is null or empty.</exception>
|
||||
public AssetName ParseAssetName(string rawName)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(rawName)
|
||||
? AssetName.Parse(rawName, parseLocale: locale => this.LocaleCodes.Value.TryGetValue(locale, out LocalizedContentManager.LanguageCode langCode) ? langCode : null)
|
||||
: throw new ArgumentException("The asset name can't be null or empty.", nameof(rawName));
|
||||
}
|
||||
|
||||
/// <summary>Get whether this asset is mapped to a mod folder.</summary>
|
||||
/// <param name="key">The asset key.</param>
|
||||
public bool IsManagedAssetKey(string key)
|
||||
|
@ -306,11 +316,12 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="predicate">Matches the asset keys to invalidate.</param>
|
||||
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
|
||||
/// <returns>Returns the invalidated asset keys.</returns>
|
||||
public IEnumerable<string> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
|
||||
public IEnumerable<IAssetName> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
|
||||
{
|
||||
string locale = this.GetLocale();
|
||||
return this.InvalidateCache((contentManager, assetName, type) =>
|
||||
return this.InvalidateCache((contentManager, rawName, type) =>
|
||||
{
|
||||
IAssetName assetName = this.ParseAssetName(rawName);
|
||||
IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormalizeAssetName);
|
||||
return predicate(info);
|
||||
}, dispose);
|
||||
|
@ -320,10 +331,10 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="predicate">Matches the asset keys to invalidate.</param>
|
||||
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
|
||||
/// <returns>Returns the invalidated asset names.</returns>
|
||||
public IEnumerable<string> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
|
||||
public IEnumerable<IAssetName> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
|
||||
{
|
||||
// invalidate cache & track removed assets
|
||||
IDictionary<string, Type> removedAssets = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
IDictionary<IAssetName, Type> removedAssets = new Dictionary<IAssetName, Type>();
|
||||
this.ContentManagerLock.InReadLock(() =>
|
||||
{
|
||||
// cached assets
|
||||
|
@ -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<string, bool> propagated,
|
||||
out IDictionary<IAssetName, bool> 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<string> keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase));
|
||||
string FormatKeyList(IEnumerable<IAssetName> 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<TilesheetReference>();
|
||||
}
|
||||
|
||||
/// <summary>Get the language enum which corresponds to a locale code (e.g. <see cref="LocalizedContentManager.LanguageCode.fr"/> given <c>fr-FR</c>).</summary>
|
||||
/// <param name="locale">The locale code to search. This must exactly match the language; no fallback is performed.</param>
|
||||
/// <param name="language">The matched language enum, if any.</param>
|
||||
/// <returns>Returns whether a valid language was found.</returns>
|
||||
public bool TryGetLanguageEnum(string locale, out LocalizedContentManager.LanguageCode language)
|
||||
{
|
||||
return this.LocaleCodes.Value.TryGetValue(locale, out language);
|
||||
}
|
||||
|
||||
/// <summary>Get the locale code which corresponds to a language enum (e.g. <c>fr-FR</c> given <see cref="LocalizedContentManager.LanguageCode.fr"/>).</summary>
|
||||
/// <param name="language">The language enum to search.</param>
|
||||
public string GetLocaleCode(LocalizedContentManager.LanguageCode language)
|
||||
|
|
|
@ -160,14 +160,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
return this.IsNormalizedKeyLoaded(assetName, language);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetAssetKeys()
|
||||
{
|
||||
return this.Cache.Keys
|
||||
.Select(this.GetAssetName)
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
/****
|
||||
** Cache invalidation
|
||||
****/
|
||||
|
@ -177,13 +169,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
IDictionary<string, object> removeAssets = new Dictionary<string, object>(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();
|
||||
}
|
||||
|
||||
/// <summary>Parse a cache key into its component parts.</summary>
|
||||
/// <param name="cacheKey">The input cache key.</param>
|
||||
/// <param name="assetName">The original asset name.</param>
|
||||
/// <param name="localeCode">The asset locale code (or <c>null</c> if not localized).</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Get whether an asset has already been loaded.</summary>
|
||||
/// <param name="normalizedAssetName">The normalized asset name.</param>
|
||||
/// <param name="language">The language to check.</param>
|
||||
protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language);
|
||||
|
||||
/// <summary>Get the asset name from a cache key.</summary>
|
||||
/// <param name="cacheKey">The input cache key.</param>
|
||||
private string GetAssetName(string cacheKey)
|
||||
{
|
||||
this.ParseCacheKey(cacheKey, out string assetName, out string _);
|
||||
return assetName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T>(newAssetName, newLanguage, useCache);
|
||||
IAssetName parsedName = this.Coordinator.ParseAssetName(assetName);
|
||||
if (parsedName.LanguageCode.HasValue)
|
||||
return this.Load<T>(parsedName.BaseName, parsedName.LanguageCode.Value, useCache);
|
||||
|
||||
// get from cache
|
||||
if (useCache && this.IsLoaded(assetName, language))
|
||||
return this.RawLoad<T>(assetName, language, useCache: true);
|
||||
if (useCache && this.IsLoaded(parsedName.Name, language))
|
||||
return this.RawLoad<T>(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<T>(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<T>(assetName, language, useCache);
|
||||
data = this.RawLoad<T>(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<T>(info)
|
||||
?? new AssetDataForObject(info, this.RawLoad<T>(assetName, language, useCache), this.AssertAndNormalizeAssetName);
|
||||
?? new AssetDataForObject(info, this.RawLoad<T>(parsedName.Name, language, useCache), this.AssertAndNormalizeAssetName);
|
||||
asset = this.ApplyEditors<T>(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<string> removeAssetNames = new HashSet<string>(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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Parse an asset key that contains an explicit language into its asset name and language, if applicable.</summary>
|
||||
/// <param name="rawAsset">The asset key to parse.</param>
|
||||
/// <param name="assetName">The asset name without the language code.</param>
|
||||
/// <param name="language">The language code removed from the asset name.</param>
|
||||
/// <returns>Returns whether the asset key contains an explicit language and was successfully parsed.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
||||
|
@ -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<T>(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<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,9 +58,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="language">The language.</param>
|
||||
bool IsLoaded(string assetName, LocalizedContentManager.LanguageCode language);
|
||||
|
||||
/// <summary>Get the cached asset keys.</summary>
|
||||
IEnumerable<string> GetAssetKeys();
|
||||
|
||||
/// <summary>Purge matched assets from the cache.</summary>
|
||||
/// <param name="predicate">Matches the asset keys to invalidate.</param>
|
||||
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
|
||||
|
|
|
@ -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<T>(assetName, useCache: false);
|
||||
asset = this.RawLoad<T>(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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,11 @@ namespace StardewModdingAPI
|
|||
/// <summary>The content's locale code, if the content is localized.</summary>
|
||||
string Locale { get; }
|
||||
|
||||
/// <summary>The asset name being read.</summary>
|
||||
public IAssetName Name { get; }
|
||||
|
||||
/// <summary>The normalized asset name being read. The format may change between platforms; see <see cref="AssetNameEquals"/> to compare with a known path.</summary>
|
||||
[Obsolete($"Use {nameof(Name)} instead.")]
|
||||
string AssetName { get; }
|
||||
|
||||
/// <summary>The content data type.</summary>
|
||||
|
@ -23,6 +27,7 @@ namespace StardewModdingAPI
|
|||
*********/
|
||||
/// <summary>Get whether the asset name being loaded matches a given name after normalization.</summary>
|
||||
/// <param name="path">The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation').</param>
|
||||
[Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")]
|
||||
bool AssetNameEquals(string path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>The name for an asset loaded through the content pipeline.</summary>
|
||||
public interface IAssetName : IEquatable<IAssetName>
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The full normalized asset name, including the locale if applicable (like <c>Data/Achievements.fr-FR</c>).</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>The base asset name without the locale code.</summary>
|
||||
string BaseName { get; }
|
||||
|
||||
/// <summary>The locale code specified in the <see cref="Name"/>, if it's a valid code recognized by the game content.</summary>
|
||||
string LocaleCode { get; }
|
||||
|
||||
/// <summary>The language code matching the <see cref="LocaleCode"/>, if applicable.</summary>
|
||||
LocalizedContentManager.LanguageCode? LanguageCode { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get whether the given asset name is equivalent, ignoring capitalization and formatting.</summary>
|
||||
/// <param name="assetName">The asset name to compare this instance to.</param>
|
||||
/// <param name="useBaseName">Whether to compare the given name with the <see cref="BaseName"/> (if true) or <see cref="Name"/> (if false). This has no effect on any locale included in the given <paramref name="assetName"/>.</param>
|
||||
bool IsEquivalentTo(string assetName, bool useBaseName = false);
|
||||
|
||||
/// <summary>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 <c>Data/</c>.</summary>
|
||||
/// <param name="prefix">The prefix to match.</param>
|
||||
/// <param name="allowPartialWord">Whether to match if the prefix occurs mid-word, so <c>Data/AchievementsToIgnore</c> matches prefix <c>Data/Achievements</c>. If this is false, the prefix only matches if the asset name starts with the prefix followed by a non-alphanumeric character (including <c>.</c>, <c>/</c>, or <c>\\</c>) or the end of string.</param>
|
||||
/// <param name="allowSubfolder">Whether to match the prefix if there's a subfolder path after it, so <c>Data/Achievements/Example</c> matches prefix <c>Data/Achievements</c>. If this is false, the prefix only matches if the asset name has no <c>/</c> or <c>\\</c> characters after the prefix.</param>
|
||||
bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true);
|
||||
|
||||
/// <summary>Get whether the asset is directly within the given asset path.</summary>
|
||||
/// <remarks>For example, <c>Characters/Dialogue/Abigail</c> is directly under <c>Characters/Dialogue</c> but not <c>Characters</c> or <c>Characters/Dialogue/Ab</c>. To allow sub-paths, use <see cref="StartsWith"/> instead.</remarks>
|
||||
/// <param name="assetFolder">The asset path to check. This doesn't need a trailing slash.</param>
|
||||
bool IsDirectlyUnderPath(string assetFolder);
|
||||
}
|
||||
}
|
|
@ -87,22 +87,22 @@ namespace StardewModdingAPI.Metadata
|
|||
/// <param name="ignoreWorld">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.</param>
|
||||
/// <param name="propagatedAssets">A lookup of asset names to whether they've been propagated.</param>
|
||||
/// <param name="updatedNpcWarps">Whether the NPC pathfinding cache was reloaded.</param>
|
||||
public void Propagate(IDictionary<string, Type> assets, bool ignoreWorld, out IDictionary<string, bool> propagatedAssets, out bool updatedNpcWarps)
|
||||
public void Propagate(IDictionary<IAssetName, Type> assets, bool ignoreWorld, out IDictionary<IAssetName, bool> 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
|
||||
*********/
|
||||
/// <summary>Reload one of the game's core assets (if applicable).</summary>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <param name="type">The asset type to reload.</param>
|
||||
/// <param name="ignoreWorld">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.</param>
|
||||
/// <param name="changedWarps">Whether any map warps were changed as part of this propagation.</param>
|
||||
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
|
||||
[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<string> 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<Horse>(content, key);
|
||||
return !ignoreWorld && this.ReloadPetOrHorseSprites<Horse>(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<Dictionary<string, string>>(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<Texture2D>(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<Texture2D>(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<Texture2D>(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<Cat>(content, key);
|
||||
if (this.KeyStartsWith(key, "animals/dog"))
|
||||
return this.ReloadPetOrHorseSprites<Dog>(content, key);
|
||||
if (this.IsInFolder(key, "Animals"))
|
||||
return this.ReloadFarmAnimalSprites(content, key);
|
||||
if (assetName.StartsWith("animals/cat"))
|
||||
return this.ReloadPetOrHorseSprites<Cat>(content, assetName);
|
||||
if (assetName.StartsWith("animals/dog"))
|
||||
return this.ReloadPetOrHorseSprites<Dog>(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
|
|||
****/
|
||||
/// <summary>Reload buttons on the title screen.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
/// <remarks>Derived from the <see cref="TitleMenu"/> constructor and <see cref="TitleMenu.setUpIcons"/>.</remarks>
|
||||
private bool ReloadTitleButtons(LocalizedContentManager content, string key)
|
||||
private bool ReloadTitleButtons(LocalizedContentManager content, IAssetName assetName)
|
||||
{
|
||||
if (Game1.activeClickableMenu is TitleMenu titleMenu)
|
||||
{
|
||||
Texture2D texture = content.Load<Texture2D>(key);
|
||||
Texture2D texture = content.Load<Texture2D>(assetName.Name);
|
||||
|
||||
titleMenu.titleButtonsTexture = texture;
|
||||
titleMenu.backButton.texture = texture;
|
||||
|
@ -645,21 +644,21 @@ namespace StardewModdingAPI.Metadata
|
|||
/// <summary>Reload the sprites for matching pets or horses.</summary>
|
||||
/// <typeparam name="TAnimal">The animal type.</typeparam>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
private bool ReloadPetOrHorseSprites<TAnimal>(LocalizedContentManager content, string key)
|
||||
private bool ReloadPetOrHorseSprites<TAnimal>(LocalizedContentManager content, IAssetName assetName)
|
||||
where TAnimal : NPC
|
||||
{
|
||||
// find matches
|
||||
TAnimal[] animals = this.GetCharacters()
|
||||
.OfType<TAnimal>()
|
||||
.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<Texture2D>(key);
|
||||
Texture2D texture = content.Load<Texture2D>(assetName.Name);
|
||||
foreach (TAnimal animal in animals)
|
||||
animal.Sprite.spriteTexture = texture;
|
||||
return true;
|
||||
|
@ -667,10 +666,10 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
/// <summary>Reload the sprites for matching farm animals.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
/// <remarks>Derived from <see cref="FarmAnimal.reload"/>.</remarks>
|
||||
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<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(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;
|
||||
}
|
||||
|
||||
/// <summary>Reload building textures.</summary>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
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
|
|||
|
||||
/// <summary>Reload map seat textures.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <param name="ignoreWorld">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.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld)
|
||||
private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld)
|
||||
{
|
||||
MapSeat.mapChairTexture = content.Load<Texture2D>(key);
|
||||
MapSeat.mapChairTexture = content.Load<Texture2D>(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
|
|||
|
||||
/// <summary>Reload critter textures.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns the number of reloaded assets.</returns>
|
||||
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<Texture2D>(key);
|
||||
Texture2D texture = content.Load<Texture2D>(assetName.Name);
|
||||
foreach (var entry in critters)
|
||||
entry.sprite.spriteTexture = texture;
|
||||
|
||||
|
@ -785,11 +784,11 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
/// <summary>Reload the sprites for interior doors.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any doors were affected.</returns>
|
||||
private bool ReloadDoorSprites(LocalizedContentManager content, string key)
|
||||
private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName)
|
||||
{
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
|
||||
|
||||
foreach (GameLocation location in this.GetLocations())
|
||||
{
|
||||
|
@ -803,7 +802,7 @@ namespace StardewModdingAPI.Metadata
|
|||
continue;
|
||||
|
||||
string curKey = this.Reflection.GetField<string>(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
|
|||
}
|
||||
|
||||
/// <summary>Reload the sprites for a fence type.</summary>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
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
|
|||
|
||||
/// <summary>Reload tree textures.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
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<Grass>()
|
||||
where this.IsSameAssetKey(grass.textureName(), key)
|
||||
where assetName.IsEquivalentTo(grass.textureName())
|
||||
select grass
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
if (grasses.Any())
|
||||
{
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
|
||||
foreach (Grass grass in grasses)
|
||||
grass.texture = texture;
|
||||
return true;
|
||||
|
@ -932,11 +931,11 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
/// <summary>Reload the disposition data for matching NPCs.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any NPCs were affected.</returns>
|
||||
private bool ReloadNpcDispositions(LocalizedContentManager content, string key)
|
||||
private bool ReloadNpcDispositions(LocalizedContentManager content, IAssetName assetName)
|
||||
{
|
||||
IDictionary<string, string> data = content.Load<Dictionary<string, string>>(key);
|
||||
IDictionary<string, string> data = content.Load<Dictionary<string, string>>(assetName.Name);
|
||||
bool changed = false;
|
||||
foreach (NPC npc in this.GetCharacters())
|
||||
{
|
||||
|
@ -953,16 +952,16 @@ namespace StardewModdingAPI.Metadata
|
|||
/// <summary>Reload the sprites for matching NPCs.</summary>
|
||||
/// <param name="keys">The asset keys to reload.</param>
|
||||
/// <param name="propagated">The asset keys which have been propagated.</param>
|
||||
private void ReloadNpcSprites(IEnumerable<string> keys, IDictionary<string, bool> propagated)
|
||||
private void ReloadNpcSprites(IEnumerable<IAssetName> keys, IDictionary<IAssetName, bool> propagated)
|
||||
{
|
||||
// get NPCs
|
||||
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.OrdinalIgnoreCase);
|
||||
IDictionary<string, IAssetName> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reload the portraits for matching NPCs.</summary>
|
||||
/// <param name="keys">The asset key to reload.</param>
|
||||
/// <param name="propagated">The asset keys which have been propagated.</param>
|
||||
private void ReloadNpcPortraits(IEnumerable<string> keys, IDictionary<string, bool> propagated)
|
||||
private void ReloadNpcPortraits(IEnumerable<IAssetName> keys, IDictionary<IAssetName, bool> propagated)
|
||||
{
|
||||
// get NPCs
|
||||
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.OrdinalIgnoreCase);
|
||||
IDictionary<string, IAssetName> 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<NPC>(adventureGuild, "Gil").GetValue(), Key = gilKey });
|
||||
characters.Add(new { Npc = this.Reflection.GetField<NPC>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reload the sprites for matching players.</summary>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
private bool ReloadPlayerSprites(string key)
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
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
|
|||
|
||||
/// <summary>Reload suspension bridge textures.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
private bool ReloadSuspensionBridges(LocalizedContentManager content, string key)
|
||||
private bool ReloadSuspensionBridges(LocalizedContentManager content, IAssetName assetName)
|
||||
{
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
|
||||
|
||||
foreach (GameLocation location in this.GetLocations(buildingInteriors: false))
|
||||
{
|
||||
|
@ -1059,10 +1058,10 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
/// <summary>Reload tree textures.</summary>
|
||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <param name="type">The type to reload.</param>
|
||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||
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<Tree>())
|
||||
|
@ -1071,7 +1070,7 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
if (trees.Any())
|
||||
{
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
|
||||
foreach (Tree tree in trees)
|
||||
tree.texture = texture;
|
||||
return true;
|
||||
|
@ -1084,12 +1083,12 @@ namespace StardewModdingAPI.Metadata
|
|||
** Reload data methods
|
||||
****/
|
||||
/// <summary>Reload the dialogue data for matching NPCs.</summary>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any assets were reloaded.</returns>
|
||||
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
|
|||
}
|
||||
|
||||
/// <summary>Reload the schedules for matching NPCs.</summary>
|
||||
/// <param name="key">The asset key to reload.</param>
|
||||
/// <param name="assetName">The asset name to reload.</param>
|
||||
/// <returns>Returns whether any assets were reloaded.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Get whether a given asset key is equivalent to a normalized asset key, ignoring unimportant differences like capitalization and formatting.</summary>
|
||||
/// <param name="actualKey">The actual key to check.</param>
|
||||
/// <param name="normalizedKey">The key to match, already normalized via <see cref="AssertAndNormalizeAssetName"/> or <see cref="NormalizeAssetNameIgnoringEmpty"/>.</param>
|
||||
private bool IsSameAssetKey(string actualKey, string normalizedKey)
|
||||
{
|
||||
if (actualKey is null || normalizedKey is null)
|
||||
return false;
|
||||
|
||||
return normalizedKey.Equals(PathUtilities.NormalizeAssetName(actualKey), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Get whether a key starts with a substring after the substring is normalized.</summary>
|
||||
/// <param name="key">The key to check.</param>
|
||||
/// <param name="rawSubstring">The substring to normalize and find.</param>
|
||||
private bool KeyStartsWith(string key, string rawSubstring)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(rawSubstring))
|
||||
return false;
|
||||
|
||||
return key.StartsWith(this.NormalizeAssetNameIgnoringEmpty(rawSubstring), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Get whether a normalized asset key is in the given folder.</summary>
|
||||
/// <param name="key">The normalized asset key (like <c>Animals/cat</c>).</param>
|
||||
/// <param name="folder">The key folder (like <c>Animals</c>); doesn't need to be normalized.</param>
|
||||
/// <param name="allowSubfolders">Whether to return true if the key is inside a subfolder of the <paramref name="folder"/>.</param>
|
||||
private bool IsInFolder(string key, string folder, bool allowSubfolders = false)
|
||||
{
|
||||
return
|
||||
this.KeyStartsWith(key, $"{folder}/")
|
||||
&& (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1);
|
||||
}
|
||||
|
||||
/// <summary>Get the segments in a path (e.g. 'a/b' is 'a' and 'b').</summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
private string[] GetSegments(string path)
|
||||
|
@ -1285,13 +1251,6 @@ namespace StardewModdingAPI.Metadata
|
|||
: Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Count the number of segments in a path (e.g. 'a/b' is 2).</summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
private int CountSegments(string path)
|
||||
{
|
||||
return this.GetSegments(path).Length;
|
||||
}
|
||||
|
||||
/// <summary>Load a texture, and dispose the old one if <see cref="AggressiveMemoryOptimizations"/> is enabled and it's different from the new instance.</summary>
|
||||
/// <param name="oldTexture">The previous texture to dispose.</param>
|
||||
/// <param name="key">The asset key to load.</param>
|
||||
|
@ -1315,8 +1274,8 @@ namespace StardewModdingAPI.Metadata
|
|||
}
|
||||
|
||||
/// <summary>Remove a case-insensitive key from the paint mask cache.</summary>
|
||||
/// <param name="key">The paint mask asset key.</param>
|
||||
private bool RemoveFromPaintMaskCache(string key)
|
||||
/// <param name="assetName">The paint mask asset name.</param>
|
||||
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<string, List<List<int>>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// remove key from cache
|
||||
return BuildingPainter.paintMaskLookup.Remove(key);
|
||||
return BuildingPainter.paintMaskLookup.Remove(assetName.Name);
|
||||
}
|
||||
|
||||
/// <summary>Metadata about a location used in asset propagation.</summary>
|
||||
|
|
Loading…
Reference in New Issue