diff options
-rw-r--r-- | src/SMAPI/Framework/Content/AssetName.cs | 108 | ||||
-rw-r--r-- | src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs | 67 |
2 files changed, 145 insertions, 30 deletions
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 148354a1..05e1d1c2 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -1,7 +1,11 @@ using System; using StardewModdingAPI.Toolkit.Utilities; +using StardewModdingAPI.Utilities.AssetPathUtilities; + using StardewValley; +using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; + namespace StardewModdingAPI.Framework.Content { /// <summary>An asset name that can be loaded through the content pipeline.</summary> @@ -94,10 +98,28 @@ namespace StardewModdingAPI.Framework.Content if (string.IsNullOrWhiteSpace(assetName)) return false; - assetName = PathUtilities.NormalizeAssetName(assetName); + AssetPartYielder compareTo = new(useBaseName ? this.BaseName : this.Name); + AssetPartYielder compareFrom = new(assetName); + + while (true) + { + bool otherHasMore = compareFrom.MoveNext(); + bool iHaveMore = compareTo.MoveNext(); + + // neither of us have any more to yield, I'm done. + if (!otherHasMore && !iHaveMore) + return true; + + // One of us has more but the other doesn't, this isn't a match. + if (otherHasMore ^ iHaveMore) + return false; - string compareTo = useBaseName ? this.BaseName : this.Name; - return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase); + // My next bit doesn't match their next bit, this isn't a match. + if (!compareTo.Current.Equals(compareFrom.Current, StringComparison.OrdinalIgnoreCase)) + return false; + + // continue checking. + } } /// <inheritdoc /> @@ -119,43 +141,69 @@ namespace StardewModdingAPI.Framework.Content if (prefix is null) return false; - string rawTrimmed = prefix.Trim(); + ReadOnlySpan<char> trimmed = prefix.AsSpan().Trim(); + + // just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read. + ReadOnlySpan<char> seperators = new(ToolkitPathUtilities.PossiblePathSeparators); - // 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 won't yield that. + if (seperators.Contains(trimmed[0])) return false; - // normalize prefix + if (trimmed.Length == 0) + return true; + + AssetPartYielder compareTo = new(this.Name); + AssetPartYielder compareFrom = new(trimmed); + + while (true) { - string normalized = PathUtilities.NormalizeAssetName(prefix); + bool otherHasMore = compareFrom.MoveNext(); + bool iHaveMore = compareTo.MoveNext(); - // keep trailing slash - if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\')) - normalized += PathUtilities.PreferredAssetSeparator; + // Neither of us have any more to yield, I'm done. + if (!otherHasMore && !iHaveMore) + return true; - prefix = normalized; - } + // the prefix is actually longer than the asset name, this can't be true. + if (otherHasMore && !iHaveMore) + return false; - // compare - if (prefix.Length == 0) - return true; + // they're done, I have more. (These are going to be word boundaries, I don't need to check that). + if (!otherHasMore && iHaveMore) + { + return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal); + } - 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) - ); + // check my next segment against theirs. + if (otherHasMore && iHaveMore) + { + // my next segment doesn't match theirs. + if (!compareTo.Current.StartsWith(compareFrom.Current, StringComparison.OrdinalIgnoreCase)) + return false; + + // my next segment starts with theirs but isn't an exact match. + if (compareTo.Current.Length != compareFrom.Current.Length) + { + // something like "Maps/" would require an exact match. + if (seperators.Contains(trimmed[^1])) + return false; + + // check for partial word. + if (!allowPartialWord + && char.IsLetterOrDigit(compareFrom.Current[^1]) // last character in suffix is not word separator + && char.IsLetterOrDigit(compareTo.Current[compareFrom.Current.Length]) // and the first character after it isn't either. + ) + return false; + + return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal); + } + + // exact matches should continue checking. + } + } } - /// <inheritdoc /> public bool IsDirectlyUnderPath(string? assetFolder) { diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs new file mode 100644 index 00000000..a55a0ab4 --- /dev/null +++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs @@ -0,0 +1,67 @@ +using System; + +using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; + +namespace StardewModdingAPI.Utilities.AssetPathUtilities; + +/// <summary> +/// A helper class that yields out each bit of an asset path +/// </summary> +internal ref struct AssetPartYielder +{ + private ReadOnlySpan<char> remainder; + + /// <summary> + /// Construct an instance. + /// </summary> + /// <param name="assetName">The asset name.</param> + internal AssetPartYielder(ReadOnlySpan<char> assetName) + { + this.remainder = AssetPartYielder.TrimLeadingPathSeperators(assetName); + } + + /// <summary> + /// The remainder of the assetName (that hasn't been yielded out yet.) + /// </summary> + internal ReadOnlySpan<char> Remainder => this.remainder; + + /// <summary> + /// The current segment. + /// </summary> + public ReadOnlySpan<char> Current { get; private set; } = default; + + // this is just so it can be used in a foreach loop. + public AssetPartYielder GetEnumerator() => this; + + /// <summary> + /// Moves the enumerator to the next element. + /// </summary> + /// <returns>True if there is a new</returns> + public bool MoveNext() + { + if (this.remainder.Length == 0) + { + return false; + } + + int index = this.remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators); + + // no more seperator characters found, I'm done. + if (index < 0) + { + this.Current = this.remainder; + this.remainder = ReadOnlySpan<char>.Empty; + return true; + } + + // Yield the next seperate character bit + this.Current = this.remainder[..index]; + this.remainder = AssetPartYielder.TrimLeadingPathSeperators(this.remainder[(index + 1)..]); + return true; + } + + private static ReadOnlySpan<char> TrimLeadingPathSeperators(ReadOnlySpan<char> span) + { + return span.TrimStart(new ReadOnlySpan<char>(ToolkitPathUtilities.PossiblePathSeparators)); + } +} |