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 IsEquivalentTo(IAssetName? assetName, bool useBaseName = false)
{
if (useBaseName)
return this.BaseName.Equals(assetName?.BaseName, StringComparison.OrdinalIgnoreCase);
if (assetName is AssetName impl)
return this.ComparableName == impl.ComparableName;
return this.Name.Equals(assetName?.Name, StringComparison.OrdinalIgnoreCase);
}
///
public bool StartsWith(string? prefix, bool allowPartialWord = true, bool allowSubfolder = true)
{
// asset keys never start with null
if (prefix is null)
return false;
string rawTrimmed = prefix.Trim();
// asset keys can't have a leading slash, but NormalizeAssetName will trim them
if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\'))
return false;
// normalize prefix
{
string normalized = PathUtilities.NormalizeAssetName(prefix);
// keep trailing slash
if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\'))
normalized += PathUtilities.PreferredAssetSeparator;
prefix = normalized;
}
// compare
if (prefix.Length == 0)
return true;
return
this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
&& (
allowPartialWord
|| this.Name.Length == prefix.Length
|| !char.IsLetterOrDigit(prefix[^1]) // last character in suffix is word separator
|| !char.IsLetterOrDigit(this.Name[prefix.Length]) // or first character after it is
)
&& (
allowSubfolder
|| this.Name.Length == prefix.Length
|| !this.Name[prefix.Length..].Contains(PathUtilities.PreferredAssetSeparator)
);
}
///
public bool IsDirectlyUnderPath(string? assetFolder)
{
if (assetFolder is null)
return false;
return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false);
}
///
IAssetName IAssetName.GetBaseAssetName()
{
return this.LocaleCode == null
? this
: new AssetName(this.BaseName, null, null);
}
///
public bool Equals(IAssetName? other)
{
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;
}
}
}