summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/Content/AssetName.cs108
-rw-r--r--src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs70
2 files changed, 147 insertions, 31 deletions
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 148354a1..99968299 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -1,6 +1,8 @@
using System;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Utilities.AssetPathUtilities;
using StardewValley;
+using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
namespace StardewModdingAPI.Framework.Content
{
@@ -94,10 +96,26 @@ namespace StardewModdingAPI.Framework.Content
if (string.IsNullOrWhiteSpace(assetName))
return false;
- assetName = PathUtilities.NormalizeAssetName(assetName);
+ AssetNamePartEnumerator curParts = new(useBaseName ? this.BaseName : this.Name);
+ AssetNamePartEnumerator otherParts = new(assetName.AsSpan().Trim());
- string compareTo = useBaseName ? this.BaseName : this.Name;
- return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase);
+ while (true)
+ {
+ bool curHasMore = curParts.MoveNext();
+ bool otherHasMore = otherParts.MoveNext();
+
+ // mismatch: lengths differ
+ if (otherHasMore != curHasMore)
+ return false;
+
+ // match: both reached the end without a mismatch
+ if (!curHasMore)
+ return true;
+
+ // mismatch: current segment is different
+ if (!curParts.Current.Equals(otherParts.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
}
/// <inheritdoc />
@@ -119,42 +137,70 @@ namespace StardewModdingAPI.Framework.Content
if (prefix is null)
return false;
- string rawTrimmed = prefix.Trim();
+ // get initial values
+ ReadOnlySpan<char> trimmedPrefix = prefix.AsSpan().Trim();
+ if (trimmedPrefix.Length == 0)
+ return true;
+ ReadOnlySpan<char> pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // just to simplify calling other span APIs
- // asset keys can't have a leading slash, but NormalizeAssetName will trim them
- if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\'))
+ // asset keys can't have a leading slash, but AssetPathYielder will trim them
+ if (pathSeparators.Contains(trimmedPrefix[0]))
return false;
- // normalize prefix
+ // compare segments
+ AssetNamePartEnumerator curParts = new(this.Name);
+ AssetNamePartEnumerator prefixParts = new(trimmedPrefix);
+ while (true)
{
- string normalized = PathUtilities.NormalizeAssetName(prefix);
+ bool curHasMore = curParts.MoveNext();
+ bool prefixHasMore = prefixParts.MoveNext();
- // keep trailing slash
- if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\'))
- normalized += PathUtilities.PreferredAssetSeparator;
+ // reached end for one side
+ if (prefixHasMore != curHasMore)
+ {
+ // mismatch: prefix is longer
+ if (prefixHasMore)
+ return false;
- prefix = normalized;
- }
+ // match if subfolder paths are fine (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
+ return allowSubfolder;
+ }
- // compare
- if (prefix.Length == 0)
- return true;
+ // previous segments matched exactly and both reached the end
+ // match if prefix doesn't end with '/' (which should only match subfolders)
+ if (!prefixHasMore)
+ return !pathSeparators.Contains(trimmedPrefix[^1]);
- 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)
- );
- }
+ // compare segment
+ if (curParts.Current.Length == prefixParts.Current.Length)
+ {
+ // mismatch: segments aren't equivalent
+ if (!curParts.Current.Equals(prefixParts.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+ else
+ {
+ // mismatch: prefix has more beyond this, and this segment isn't an exact match
+ if (prefixParts.Remainder.Length != 0)
+ return false;
+
+ // mismatch: cur segment doesn't start with prefix
+ if (!curParts.Current.StartsWith(prefixParts.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+ // mismatch: something like "Maps/" would need an exact match
+ if (pathSeparators.Contains(trimmedPrefix[^1]))
+ return false;
+
+ // mismatch: partial word match not allowed, and the first or last letter of the suffix isn't a word separator
+ if (!allowPartialWord && char.IsLetterOrDigit(prefixParts.Current[^1]) && char.IsLetterOrDigit(curParts.Current[prefixParts.Current.Length]))
+ return false;
+
+ // possible match
+ return allowSubfolder || (pathSeparators.Contains(trimmedPrefix[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0);
+ }
+ }
+ }
/// <inheritdoc />
public bool IsDirectlyUnderPath(string? assetFolder)
@@ -162,7 +208,7 @@ namespace StardewModdingAPI.Framework.Content
if (assetFolder is null)
return false;
- return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false);
+ return this.StartsWith(assetFolder + ToolkitPathUtilities.PreferredPathSeparator, allowPartialWord: false, allowSubfolder: false);
}
/// <inheritdoc />
diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
new file mode 100644
index 00000000..11987ed6
--- /dev/null
+++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
@@ -0,0 +1,70 @@
+using System;
+using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
+
+namespace StardewModdingAPI.Utilities.AssetPathUtilities
+{
+ /// <summary>Handles enumerating the normalized segments in an asset name.</summary>
+ internal ref struct AssetNamePartEnumerator
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The backing field for <see cref="Remainder"/>.</summary>
+ private ReadOnlySpan<char> RemainderImpl;
+
+
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The remainder of the asset name being enumerated, ignoring segments which have already been yielded.</summary>
+ public ReadOnlySpan<char> Remainder => this.RemainderImpl;
+
+ /// <summary>Get the current segment.</summary>
+ public ReadOnlySpan<char> Current { get; private set; } = default;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="assetName">The asset name to enumerate.</param>
+ public AssetNamePartEnumerator(ReadOnlySpan<char> assetName)
+ {
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName);
+ }
+
+ /// <summary>Move the enumerator to the next segment.</summary>
+ /// <returns>Returns true if a new value was found (accessible via <see cref="Current"/>).</returns>
+ public bool MoveNext()
+ {
+ if (this.RemainderImpl.Length == 0)
+ return false;
+
+ int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators);
+
+ // no more separator characters found, I'm done.
+ if (index < 0)
+ {
+ this.Current = this.RemainderImpl;
+ this.RemainderImpl = ReadOnlySpan<char>.Empty;
+ return true;
+ }
+
+ // Yield the next separate character bit
+ this.Current = this.RemainderImpl[..index];
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]);
+ return true;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Trim path separators at the start of the given path or segment.</summary>
+ /// <param name="span">The path or segment to trim.</param>
+ private static ReadOnlySpan<char> TrimLeadingPathSeparators(ReadOnlySpan<char> span)
+ {
+ return span.TrimStart(new ReadOnlySpan<char>(ToolkitPathUtilities.PossiblePathSeparators));
+ }
+ }
+}