From a2190df08cc3f1b4a8dcb394056d65921d10702e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 18 Feb 2022 15:39:49 -0500 Subject: add AssetName to encapsulate asset name handling (#766) --- src/SMAPI/Framework/Content/AssetData.cs | 4 +- .../Framework/Content/AssetDataForDictionary.cs | 4 +- src/SMAPI/Framework/Content/AssetDataForImage.cs | 4 +- src/SMAPI/Framework/Content/AssetDataForMap.cs | 4 +- src/SMAPI/Framework/Content/AssetDataForObject.cs | 12 +- src/SMAPI/Framework/Content/AssetInfo.cs | 16 +- .../Framework/Content/AssetInterceptorChange.cs | 4 +- src/SMAPI/Framework/Content/AssetName.cs | 173 +++++++++++++++++++++ 8 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 src/SMAPI/Framework/Content/AssetName.cs (limited to 'src/SMAPI/Framework/Content') diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs index 5c90d83b..05be8a3b 100644 --- a/src/SMAPI/Framework/Content/AssetData.cs +++ b/src/SMAPI/Framework/Content/AssetData.cs @@ -25,11 +25,11 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetData(string locale, string assetName, TValue data, Func getNormalizedPath, Action onDataReplaced) + public AssetData(string locale, IAssetName assetName, TValue data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data.GetType(), getNormalizedPath) { this.Data = data; diff --git a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs index 26cbff5a..735b651c 100644 --- a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs +++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs @@ -11,11 +11,11 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalizedPath, Action> onDataReplaced) + public AssetDataForDictionary(string locale, IAssetName assetName, IDictionary data, Func getNormalizedPath, Action> onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } } } diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index c75514bc..b0f1b5c7 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -21,11 +21,11 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalizedPath, Action onDataReplaced) + public AssetDataForImage(string locale, IAssetName assetName, Texture2D data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index 0a5fa7e7..26e4986e 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -18,11 +18,11 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForMap(string locale, string assetName, Map data, Func getNormalizedPath, Action onDataReplaced) + public AssetDataForMap(string locale, IAssetName assetName, Map data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs index b7e8dfeb..d91873ae 100644 --- a/src/SMAPI/Framework/Content/AssetDataForObject.cs +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs @@ -13,10 +13,10 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content data being read. /// Normalizes an asset key to match the cache key. - public AssetDataForObject(string locale, string assetName, object data, Func getNormalizedPath) + public AssetDataForObject(string locale, IAssetName assetName, object data, Func getNormalizedPath) : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { } /// Construct an instance. @@ -24,24 +24,24 @@ namespace StardewModdingAPI.Framework.Content /// The content data being read. /// Normalizes an asset key to match the cache key. public AssetDataForObject(IAssetInfo info, object data, Func getNormalizedPath) - : this(info.Locale, info.AssetName, data, getNormalizedPath) { } + : this(info.Locale, info.Name, data, getNormalizedPath) { } /// public IAssetDataForDictionary AsDictionary() { - return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForDictionary(this.Locale, this.Name, this.GetData>(), this.GetNormalizedPath, this.ReplaceWith); } /// public IAssetDataForImage AsImage() { - return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForImage(this.Locale, this.Name, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } /// public IAssetDataForMap AsMap() { - return new AssetDataForMap(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForMap(this.Locale, this.Name, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } /// diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index d8106439..6a5b4f31 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -20,7 +20,11 @@ namespace StardewModdingAPI.Framework.Content public string Locale { get; } /// - public string AssetName { get; } + public IAssetName Name { get; } + + /// + [Obsolete($"Use {nameof(Name)} instead.")] + public string AssetName => this.Name.Name; /// public Type DataType { get; } @@ -31,22 +35,22 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The content's locale code, if the content is localized. - /// The normalized asset name being read. + /// The asset name being read. /// The content type being read. /// Normalizes an asset key to match the cache key. - public AssetInfo(string locale, string assetName, Type type, Func getNormalizedPath) + public AssetInfo(string locale, IAssetName assetName, Type type, Func getNormalizedPath) { this.Locale = locale; - this.AssetName = assetName; + this.Name = assetName; this.DataType = type; this.GetNormalizedPath = getNormalizedPath; } /// + [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} instead.")] public bool AssetNameEquals(string path) { - path = this.GetNormalizedPath(path); - return this.AssetName.Equals(path, StringComparison.OrdinalIgnoreCase); + return this.Name.IsEquivalentTo(path); } diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs index 10488b84..981eed40 100644 --- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs +++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework.Content } catch (Exception ex) { - this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } } @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework.Content } catch (Exception ex) { - this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } } diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs new file mode 100644 index 00000000..992647f8 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -0,0 +1,173 @@ +using System; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.Content +{ + /// An asset name that can be loaded through the content pipeline. + internal class AssetName : IAssetName + { + /********* + ** Fields + *********/ + /// A lowercase version of used for consistent hash codes and equality checks. + private readonly string ComparableName; + + + /********* + ** Accessors + *********/ + /// + public string Name { get; } + + /// + public string BaseName { get; } + + /// + public string LocaleCode { get; } + + /// + public LocalizedContentManager.LanguageCode? LanguageCode { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base asset name without the locale code. + /// The locale code specified in the , if it's a valid code recognized by the game content. + /// The language code matching the , if applicable. + public AssetName(string baseName, string localeCode, LocalizedContentManager.LanguageCode? languageCode) + { + // validate + if (string.IsNullOrWhiteSpace(baseName)) + throw new ArgumentException("The asset name can't be null or empty.", nameof(baseName)); + if (string.IsNullOrWhiteSpace(localeCode)) + localeCode = null; + + // set base values + this.BaseName = PathUtilities.NormalizeAssetName(baseName); + this.LocaleCode = localeCode; + this.LanguageCode = languageCode; + + // set derived values + this.Name = localeCode != null + ? string.Concat(this.BaseName, '.', this.LocaleCode) + : this.BaseName; + this.ComparableName = this.Name.ToLowerInvariant(); + } + + /// Parse a raw asset name into an instance. + /// The raw asset name to parse. + /// Get the language code for a given locale, if it's valid. + /// The is null or empty. + public static AssetName Parse(string rawName, Func parseLocale) + { + if (string.IsNullOrWhiteSpace(rawName)) + throw new ArgumentException("The asset name can't be null or empty.", nameof(rawName)); + + string baseName = rawName; + string localeCode = null; + LocalizedContentManager.LanguageCode? languageCode = null; + + int lastPeriodIndex = rawName.LastIndexOf('.'); + if (lastPeriodIndex > 0 && rawName.Length > lastPeriodIndex + 1) + { + string possibleLocaleCode = rawName[(lastPeriodIndex + 1)..]; + LocalizedContentManager.LanguageCode? possibleLanguageCode = parseLocale(possibleLocaleCode); + + if (possibleLanguageCode != null) + { + baseName = rawName[..lastPeriodIndex]; + localeCode = possibleLocaleCode; + languageCode = possibleLanguageCode; + } + } + + return new AssetName(baseName, localeCode, languageCode); + } + + /// + public bool IsEquivalentTo(string assetName, bool useBaseName = false) + { + // empty asset key is never equivalent + if (string.IsNullOrWhiteSpace(assetName)) + return false; + + assetName = PathUtilities.NormalizeAssetName(assetName); + + string compareTo = useBaseName ? this.BaseName : this.Name; + return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase); + } + + /// + public bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true) + { + // asset keys never start with null + if (prefix is null) + return false; + + // asset keys can't have a leading slash, but NormalizeAssetName will trim them + { + string trimmed = prefix.TrimStart(); + if (trimmed.StartsWith('/') || trimmed.StartsWith('\\')) + return false; + } + + // normalize prefix + { + string normalized = PathUtilities.NormalizeAssetName(prefix); + + string trimmed = prefix.TrimEnd(); + if (trimmed.EndsWith('/') || trimmed.EndsWith('\\')) + normalized += PathUtilities.PreferredAssetSeparator; + + prefix = normalized; + } + + // compare + return + this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) + && ( + allowPartialWord + || this.Name.Length == prefix.Length + || !char.IsLetterOrDigit(prefix[^1]) // last character in suffix is word separator + || !char.IsLetterOrDigit(this.Name[prefix.Length]) // or first character after it is + ) + && ( + allowSubfolder + || this.Name.Length == prefix.Length + || !this.Name[prefix.Length..].Contains(PathUtilities.PreferredAssetSeparator) + ); + } + + + public bool IsDirectlyUnderPath(string assetFolder) + { + return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false); + } + + /// + public bool Equals(IAssetName other) + { + return other switch + { + null => false, + AssetName otherImpl => this.ComparableName == otherImpl.ComparableName, + _ => StringComparer.OrdinalIgnoreCase.Equals(this.Name, other.Name) + }; + } + + /// + public override int GetHashCode() + { + return this.ComparableName.GetHashCode(); + } + + /// + public override string ToString() + { + return this.Name; + } + } +} -- cgit