From 0c191eb32c41ffedd321951cda70b521e9b51c96 Mon Sep 17 00:00:00 2001
From: atravita-mods <94934860+atravita-mods@users.noreply.github.com>
Date: Sat, 15 Oct 2022 08:36:24 -0400
Subject: [PATCH 1/9] make asset name comparing lazy.
---
src/SMAPI/Framework/Content/AssetName.cs | 114 +++++++++++++-----
.../AssetPathUtilities/AssetPartYielder.cs | 67 ++++++++++
2 files changed, 148 insertions(+), 33 deletions(-)
create mode 100644 src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
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
{
/// An asset name that can be loaded through the content pipeline.
@@ -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();
- string compareTo = useBaseName ? this.BaseName : this.Name;
- return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase);
+ // 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;
+
+ // 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.
+ }
}
///
@@ -119,42 +141,68 @@ namespace StardewModdingAPI.Framework.Content
if (prefix is null)
return false;
- string rawTrimmed = prefix.Trim();
+ ReadOnlySpan trimmed = prefix.AsSpan().Trim();
- // asset keys can't have a leading slash, but NormalizeAssetName will trim them
- if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\'))
+ // just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read.
+ ReadOnlySpan seperators = new(ToolkitPathUtilities.PossiblePathSeparators);
+
+ // asset keys can't have a leading slash, but AssetPathYielder won't yield that.
+ if (seperators.Contains(trimmed[0]))
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)
+ if (trimmed.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)
- );
- }
+ AssetPartYielder compareTo = new(this.Name);
+ AssetPartYielder compareFrom = new(trimmed);
+ 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;
+
+ // the prefix is actually longer than the asset name, this can't be true.
+ if (otherHasMore && !iHaveMore)
+ return false;
+
+ // 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);
+ }
+
+ // 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.
+ }
+ }
+ }
///
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;
+
+///
+/// A helper class that yields out each bit of an asset path
+///
+internal ref struct AssetPartYielder
+{
+ private ReadOnlySpan remainder;
+
+ ///
+ /// Construct an instance.
+ ///
+ /// The asset name.
+ internal AssetPartYielder(ReadOnlySpan assetName)
+ {
+ this.remainder = AssetPartYielder.TrimLeadingPathSeperators(assetName);
+ }
+
+ ///
+ /// The remainder of the assetName (that hasn't been yielded out yet.)
+ ///
+ internal ReadOnlySpan Remainder => this.remainder;
+
+ ///
+ /// The current segment.
+ ///
+ public ReadOnlySpan Current { get; private set; } = default;
+
+ // this is just so it can be used in a foreach loop.
+ public AssetPartYielder GetEnumerator() => this;
+
+ ///
+ /// Moves the enumerator to the next element.
+ ///
+ /// True if there is a new
+ 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.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 TrimLeadingPathSeperators(ReadOnlySpan span)
+ {
+ return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators));
+ }
+}
From 70cde89480e43bb1369c1063c7b19f757784f269 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:45 -0400
Subject: [PATCH 2/9] tweak naming in new code
---
src/SMAPI/Framework/Content/AssetName.cs | 52 +++++++++----------
...tYielder.cs => AssetNamePartEnumerator.cs} | 31 ++++++-----
2 files changed, 40 insertions(+), 43 deletions(-)
rename src/SMAPI/Utilities/AssetPathUtilities/{AssetPartYielder.cs => AssetNamePartEnumerator.cs} (54%)
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 05e1d1c2..d7ee6dba 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -1,9 +1,7 @@
using System;
using StardewModdingAPI.Toolkit.Utilities;
using StardewModdingAPI.Utilities.AssetPathUtilities;
-
using StardewValley;
-
using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
namespace StardewModdingAPI.Framework.Content
@@ -98,24 +96,24 @@ namespace StardewModdingAPI.Framework.Content
if (string.IsNullOrWhiteSpace(assetName))
return false;
- AssetPartYielder compareTo = new(useBaseName ? this.BaseName : this.Name);
- AssetPartYielder compareFrom = new(assetName);
-
+ AssetNamePartEnumerator curParts = new(useBaseName ? this.BaseName : this.Name);
+ AssetNamePartEnumerator otherParts = new(assetName);
+
while (true)
{
- bool otherHasMore = compareFrom.MoveNext();
- bool iHaveMore = compareTo.MoveNext();
+ bool otherHasMore = otherParts.MoveNext();
+ bool curHasMore = curParts.MoveNext();
// neither of us have any more to yield, I'm done.
- if (!otherHasMore && !iHaveMore)
+ if (!otherHasMore && !curHasMore)
return true;
// One of us has more but the other doesn't, this isn't a match.
- if (otherHasMore ^ iHaveMore)
+ if (otherHasMore ^ curHasMore)
return false;
// My next bit doesn't match their next bit, this isn't a match.
- if (!compareTo.Current.Equals(compareFrom.Current, StringComparison.OrdinalIgnoreCase))
+ if (!curParts.Current.Equals(otherParts.Current, StringComparison.OrdinalIgnoreCase))
return false;
// continue checking.
@@ -144,59 +142,59 @@ namespace StardewModdingAPI.Framework.Content
ReadOnlySpan trimmed = prefix.AsSpan().Trim();
// just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read.
- ReadOnlySpan seperators = new(ToolkitPathUtilities.PossiblePathSeparators);
+ ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators);
// asset keys can't have a leading slash, but AssetPathYielder won't yield that.
- if (seperators.Contains(trimmed[0]))
+ if (pathSeparators.Contains(trimmed[0]))
return false;
if (trimmed.Length == 0)
return true;
- AssetPartYielder compareTo = new(this.Name);
- AssetPartYielder compareFrom = new(trimmed);
+ AssetNamePartEnumerator curParts = new(this.Name);
+ AssetNamePartEnumerator prefixParts = new(trimmed);
while (true)
{
- bool otherHasMore = compareFrom.MoveNext();
- bool iHaveMore = compareTo.MoveNext();
+ bool prefixHasMore = prefixParts.MoveNext();
+ bool curHasMore = curParts.MoveNext();
// Neither of us have any more to yield, I'm done.
- if (!otherHasMore && !iHaveMore)
+ if (!prefixHasMore && !curHasMore)
return true;
// the prefix is actually longer than the asset name, this can't be true.
- if (otherHasMore && !iHaveMore)
+ if (prefixHasMore && !curHasMore)
return false;
// they're done, I have more. (These are going to be word boundaries, I don't need to check that).
- if (!otherHasMore && iHaveMore)
+ if (!prefixHasMore && curHasMore)
{
- return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal);
+ return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
}
// check my next segment against theirs.
- if (otherHasMore && iHaveMore)
+ if (prefixHasMore && curHasMore)
{
// my next segment doesn't match theirs.
- if (!compareTo.Current.StartsWith(compareFrom.Current, StringComparison.OrdinalIgnoreCase))
+ if (!curParts.Current.StartsWith(prefixParts.Current, StringComparison.OrdinalIgnoreCase))
return false;
// my next segment starts with theirs but isn't an exact match.
- if (compareTo.Current.Length != compareFrom.Current.Length)
+ if (curParts.Current.Length != prefixParts.Current.Length)
{
// something like "Maps/" would require an exact match.
- if (seperators.Contains(trimmed[^1]))
+ if (pathSeparators.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.
+ && char.IsLetterOrDigit(prefixParts.Current[^1]) // last character in suffix is not word separator
+ && char.IsLetterOrDigit(curParts.Current[prefixParts.Current.Length]) // and the first character after it isn't either.
)
return false;
- return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal);
+ return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
}
// exact matches should continue checking.
diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
similarity index 54%
rename from src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
rename to src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
index a55a0ab4..0840617a 100644
--- a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
+++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
@@ -1,5 +1,4 @@
using System;
-
using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
namespace StardewModdingAPI.Utilities.AssetPathUtilities;
@@ -7,23 +6,23 @@ namespace StardewModdingAPI.Utilities.AssetPathUtilities;
///
/// A helper class that yields out each bit of an asset path
///
-internal ref struct AssetPartYielder
+internal ref struct AssetNamePartEnumerator
{
- private ReadOnlySpan remainder;
+ private ReadOnlySpan RemainderImpl;
///
/// Construct an instance.
///
/// The asset name.
- internal AssetPartYielder(ReadOnlySpan assetName)
+ internal AssetNamePartEnumerator(ReadOnlySpan assetName)
{
- this.remainder = AssetPartYielder.TrimLeadingPathSeperators(assetName);
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName);
}
///
/// The remainder of the assetName (that hasn't been yielded out yet.)
///
- internal ReadOnlySpan Remainder => this.remainder;
+ internal ReadOnlySpan Remainder => this.RemainderImpl;
///
/// The current segment.
@@ -31,7 +30,7 @@ internal ref struct AssetPartYielder
public ReadOnlySpan Current { get; private set; } = default;
// this is just so it can be used in a foreach loop.
- public AssetPartYielder GetEnumerator() => this;
+ public AssetNamePartEnumerator GetEnumerator() => this;
///
/// Moves the enumerator to the next element.
@@ -39,28 +38,28 @@ internal ref struct AssetPartYielder
/// True if there is a new
public bool MoveNext()
{
- if (this.remainder.Length == 0)
+ if (this.RemainderImpl.Length == 0)
{
return false;
}
- int index = this.remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators);
+ int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators);
- // no more seperator characters found, I'm done.
+ // no more separator characters found, I'm done.
if (index < 0)
{
- this.Current = this.remainder;
- this.remainder = ReadOnlySpan.Empty;
+ this.Current = this.RemainderImpl;
+ this.RemainderImpl = ReadOnlySpan.Empty;
return true;
}
- // Yield the next seperate character bit
- this.Current = this.remainder[..index];
- this.remainder = AssetPartYielder.TrimLeadingPathSeperators(this.remainder[(index + 1)..]);
+ // Yield the next separate character bit
+ this.Current = this.RemainderImpl[..index];
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]);
return true;
}
- private static ReadOnlySpan TrimLeadingPathSeperators(ReadOnlySpan span)
+ private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span)
{
return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators));
}
From eed1deb3c75ba2aeea94ea9a57f9fe7ad92a90ce Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:45 -0400
Subject: [PATCH 3/9] apply conventions to asset part enumerator
---
.../AssetNamePartEnumerator.cs | 98 ++++++++++---------
1 file changed, 51 insertions(+), 47 deletions(-)
diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
index 0840617a..11987ed6 100644
--- a/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
+++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
@@ -1,66 +1,70 @@
using System;
using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
-namespace StardewModdingAPI.Utilities.AssetPathUtilities;
-
-///
-/// A helper class that yields out each bit of an asset path
-///
-internal ref struct AssetNamePartEnumerator
+namespace StardewModdingAPI.Utilities.AssetPathUtilities
{
- private ReadOnlySpan RemainderImpl;
-
- ///
- /// Construct an instance.
- ///
- /// The asset name.
- internal AssetNamePartEnumerator(ReadOnlySpan assetName)
+ /// Handles enumerating the normalized segments in an asset name.
+ internal ref struct AssetNamePartEnumerator
{
- this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName);
- }
+ /*********
+ ** Fields
+ *********/
+ /// The backing field for .
+ private ReadOnlySpan RemainderImpl;
- ///
- /// The remainder of the assetName (that hasn't been yielded out yet.)
- ///
- internal ReadOnlySpan Remainder => this.RemainderImpl;
- ///
- /// The current segment.
- ///
- public ReadOnlySpan Current { get; private set; } = default;
+ /*********
+ ** Properties
+ *********/
+ /// The remainder of the asset name being enumerated, ignoring segments which have already been yielded.
+ public ReadOnlySpan Remainder => this.RemainderImpl;
- // this is just so it can be used in a foreach loop.
- public AssetNamePartEnumerator GetEnumerator() => this;
+ /// Get the current segment.
+ public ReadOnlySpan Current { get; private set; } = default;
- ///
- /// Moves the enumerator to the next element.
- ///
- /// True if there is a new
- public bool MoveNext()
- {
- if (this.RemainderImpl.Length == 0)
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The asset name to enumerate.
+ public AssetNamePartEnumerator(ReadOnlySpan assetName)
{
- return false;
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName);
}
- int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators);
-
- // no more separator characters found, I'm done.
- if (index < 0)
+ /// Move the enumerator to the next segment.
+ /// Returns true if a new value was found (accessible via ).
+ public bool MoveNext()
{
- this.Current = this.RemainderImpl;
- this.RemainderImpl = ReadOnlySpan.Empty;
+ 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.Empty;
+ return true;
+ }
+
+ // Yield the next separate character bit
+ this.Current = this.RemainderImpl[..index];
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]);
return true;
}
- // Yield the next separate character bit
- this.Current = this.RemainderImpl[..index];
- this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]);
- return true;
- }
- private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span)
- {
- return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators));
+ /*********
+ ** Private methods
+ *********/
+ /// Trim path separators at the start of the given path or segment.
+ /// The path or segment to trim.
+ private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span)
+ {
+ return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators));
+ }
}
}
From 4e3b2810e6951b72bdf5c5cbdd23a079d53a4c96 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:45 -0400
Subject: [PATCH 4/9] fix index-out-of-range error when StartsWith prefix is
empty
---
src/SMAPI/Framework/Content/AssetName.cs | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index d7ee6dba..c0572105 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -139,21 +139,19 @@ namespace StardewModdingAPI.Framework.Content
if (prefix is null)
return false;
+ // get initial values
ReadOnlySpan trimmed = prefix.AsSpan().Trim();
+ if (trimmed.Length == 0)
+ return true;
+ ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // just to simplify calling other span APIs
- // just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read.
- ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators);
-
- // asset keys can't have a leading slash, but AssetPathYielder won't yield that.
+ // asset keys can't have a leading slash, but AssetPathYielder will trim them
if (pathSeparators.Contains(trimmed[0]))
return false;
- if (trimmed.Length == 0)
- return true;
-
+ // compare segments
AssetNamePartEnumerator curParts = new(this.Name);
AssetNamePartEnumerator prefixParts = new(trimmed);
-
while (true)
{
bool prefixHasMore = prefixParts.MoveNext();
From 5d30b47e1e903f7ceb53116528255934c238e5ba Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:46 -0400
Subject: [PATCH 5/9] fix IsEquivalentTo no longer ignoring surrounding
whitespace
---
src/SMAPI/Framework/Content/AssetName.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index c0572105..6220ea61 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -97,7 +97,7 @@ namespace StardewModdingAPI.Framework.Content
return false;
AssetNamePartEnumerator curParts = new(useBaseName ? this.BaseName : this.Name);
- AssetNamePartEnumerator otherParts = new(assetName);
+ AssetNamePartEnumerator otherParts = new(assetName.AsSpan().Trim());
while (true)
{
From 573f732c2a2118d7a4848151764df6bef1a47008 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:46 -0400
Subject: [PATCH 6/9] reduce sequential bool checks a bit
---
src/SMAPI/Framework/Content/AssetName.cs | 77 ++++++++++++------------
1 file changed, 37 insertions(+), 40 deletions(-)
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 6220ea61..bdb79dde 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -101,22 +101,20 @@ namespace StardewModdingAPI.Framework.Content
while (true)
{
- bool otherHasMore = otherParts.MoveNext();
bool curHasMore = curParts.MoveNext();
+ bool otherHasMore = otherParts.MoveNext();
- // neither of us have any more to yield, I'm done.
- if (!otherHasMore && !curHasMore)
+ // mismatch: lengths differ
+ if (otherHasMore != curHasMore)
+ return false;
+
+ // match: both reached the end without a mismatch
+ if (!curHasMore)
return true;
- // One of us has more but the other doesn't, this isn't a match.
- if (otherHasMore ^ curHasMore)
- return false;
-
- // My next bit doesn't match their next bit, this isn't a match.
+ // mismatch: current segment is different
if (!curParts.Current.Equals(otherParts.Current, StringComparison.OrdinalIgnoreCase))
return false;
-
- // continue checking.
}
}
@@ -154,48 +152,47 @@ namespace StardewModdingAPI.Framework.Content
AssetNamePartEnumerator prefixParts = new(trimmed);
while (true)
{
- bool prefixHasMore = prefixParts.MoveNext();
bool curHasMore = curParts.MoveNext();
+ bool prefixHasMore = prefixParts.MoveNext();
- // Neither of us have any more to yield, I'm done.
- if (!prefixHasMore && !curHasMore)
- return true;
-
- // the prefix is actually longer than the asset name, this can't be true.
- if (prefixHasMore && !curHasMore)
- return false;
-
- // they're done, I have more. (These are going to be word boundaries, I don't need to check that).
- if (!prefixHasMore && curHasMore)
+ // reached end of prefix or asset name
+ if (prefixHasMore != curHasMore)
{
+ // mismatch: prefix is longer
+ if (prefixHasMore)
+ return false;
+
+ // possible match: all prefix segments matched
return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
}
- // check my next segment against theirs.
- if (prefixHasMore && curHasMore)
+ // match: previous segments matched exactly and both reached the end
+ if (!prefixHasMore)
+ return true;
+
+ // compare segment
+ if (curParts.Current.Length == prefixParts.Current.Length)
{
- // my next segment doesn't match theirs.
+ // mismatch: segments aren't equivalent
+ if (!curParts.Current.Equals(prefixParts.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+ else
+ {
+ // mismatch: cur segment doesn't start with prefix
if (!curParts.Current.StartsWith(prefixParts.Current, StringComparison.OrdinalIgnoreCase))
return false;
- // my next segment starts with theirs but isn't an exact match.
- if (curParts.Current.Length != prefixParts.Current.Length)
- {
- // something like "Maps/" would require an exact match.
- if (pathSeparators.Contains(trimmed[^1]))
- return false;
+ // mismatch: something like "Maps/" would need an exact match
+ if (pathSeparators.Contains(trimmed[^1]))
+ return false;
- // check for partial word.
- if (!allowPartialWord
- && char.IsLetterOrDigit(prefixParts.Current[^1]) // last character in suffix is not word separator
- && char.IsLetterOrDigit(curParts.Current[prefixParts.Current.Length]) // and the first character after it isn't either.
- )
- 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;
- return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
- }
-
- // exact matches should continue checking.
+ // possible match
+ return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
}
}
}
From 4dcc6904b9e72ac3567dfafe3824c2de48218b58 Mon Sep 17 00:00:00 2001
From: atravita-mods <94934860+atravita-mods@users.noreply.github.com>
Date: Sun, 16 Oct 2022 18:04:19 -0400
Subject: [PATCH 7/9] fix issues with subfolders
---
src/SMAPI.Tests/Core/AssetNameTests.cs | 14 ++++++++++++++
src/SMAPI.Tests/SMAPI.Tests.csproj | 1 +
src/SMAPI/Framework/Content/AssetName.cs | 8 ++++----
3 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
index 655e9bae..fe70e330 100644
--- a/src/SMAPI.Tests/Core/AssetNameTests.cs
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -243,6 +243,20 @@ namespace SMAPI.Tests.Core
return result;
}
+ [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", true, ExpectedResult = true)]
+ [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)]
+ public bool StartsWith_SubfolderWithPartial(string mainAssetName, string otherAssetName, bool allowSubfolder)
+ {
+ // arrange
+ mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
+
+ // act
+ AssetName name = AssetName.Parse(mainAssetName, _ => null);
+
+ // assert value
+ return name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder);
+ }
+
/****
** GetHashCode
diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj
index 2c32a932..597cd7dd 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -19,6 +19,7 @@
+
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index bdb79dde..9d59f222 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -162,8 +162,8 @@ namespace StardewModdingAPI.Framework.Content
if (prefixHasMore)
return false;
- // possible match: all prefix segments matched
- return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
+ // possible match: all prefix segments matched.
+ return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.Length == 0 : curParts.Current.Length == 0);
}
// match: previous segments matched exactly and both reached the end
@@ -192,7 +192,7 @@ namespace StardewModdingAPI.Framework.Content
return false;
// possible match
- return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
+ return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0);
}
}
}
@@ -203,7 +203,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);
}
///
From b99dbf53bda9dc1178a3b6e8cbafea609f3ee6dc Mon Sep 17 00:00:00 2001
From: atravita-mods <94934860+atravita-mods@users.noreply.github.com>
Date: Tue, 18 Oct 2022 18:58:41 -0400
Subject: [PATCH 8/9] fix this case.
---
src/SMAPI.Tests/Core/AssetNameTests.cs | 2 ++
src/SMAPI/Framework/Content/AssetName.cs | 4 ++++
2 files changed, 6 insertions(+)
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
index fe70e330..fbc94e95 100644
--- a/src/SMAPI.Tests/Core/AssetNameTests.cs
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -245,6 +245,8 @@ namespace SMAPI.Tests.Core
[TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", true, ExpectedResult = true)]
[TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)]
+ [TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)]
+ [TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)]
public bool StartsWith_SubfolderWithPartial(string mainAssetName, string otherAssetName, bool allowSubfolder)
{
// arrange
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 9d59f222..7b87c0c5 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -179,6 +179,10 @@ namespace StardewModdingAPI.Framework.Content
}
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;
From 303b3924ae3ef905d77b9d7ef0f9efc70e58c2b8 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Thu, 10 Nov 2022 21:50:01 -0500
Subject: [PATCH 9/9] fix case where prefix ends with a path separator
---
src/SMAPI.Tests/Core/AssetNameTests.cs | 8 +++++++-
src/SMAPI/Framework/Content/AssetName.cs | 23 ++++++++++++-----------
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
index fbc94e95..fdaa2c01 100644
--- a/src/SMAPI.Tests/Core/AssetNameTests.cs
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -151,6 +151,12 @@ namespace SMAPI.Tests.Core
// with locale codes
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)]
+
+ // prefix ends with path separator
+ [TestCase("Data/Events/Boop", "Data/Events/", ExpectedResult = true)]
+ [TestCase("Data/Events/Boop", "Data/Events\\", ExpectedResult = true)]
+ [TestCase("Data/Events", "Data/Events/", ExpectedResult = false)]
+ [TestCase("Data/Events", "Data/Events\\", ExpectedResult = false)]
public bool StartsWith_SimpleCases(string mainAssetName, string prefix)
{
// arrange
@@ -247,7 +253,7 @@ namespace SMAPI.Tests.Core
[TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)]
[TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)]
[TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)]
- public bool StartsWith_SubfolderWithPartial(string mainAssetName, string otherAssetName, bool allowSubfolder)
+ public bool StartsWith_PartialMatchInPathSegment(string mainAssetName, string otherAssetName, bool allowSubfolder)
{
// arrange
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 7b87c0c5..99968299 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -138,37 +138,38 @@ namespace StardewModdingAPI.Framework.Content
return false;
// get initial values
- ReadOnlySpan trimmed = prefix.AsSpan().Trim();
- if (trimmed.Length == 0)
+ ReadOnlySpan trimmedPrefix = prefix.AsSpan().Trim();
+ if (trimmedPrefix.Length == 0)
return true;
ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // just to simplify calling other span APIs
// asset keys can't have a leading slash, but AssetPathYielder will trim them
- if (pathSeparators.Contains(trimmed[0]))
+ if (pathSeparators.Contains(trimmedPrefix[0]))
return false;
// compare segments
AssetNamePartEnumerator curParts = new(this.Name);
- AssetNamePartEnumerator prefixParts = new(trimmed);
+ AssetNamePartEnumerator prefixParts = new(trimmedPrefix);
while (true)
{
bool curHasMore = curParts.MoveNext();
bool prefixHasMore = prefixParts.MoveNext();
- // reached end of prefix or asset name
+ // reached end for one side
if (prefixHasMore != curHasMore)
{
// mismatch: prefix is longer
if (prefixHasMore)
return false;
- // possible match: all prefix segments matched.
- return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.Length == 0 : curParts.Current.Length == 0);
+ // match if subfolder paths are fine (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
+ return allowSubfolder;
}
- // match: previous segments matched exactly and both reached the end
+ // previous segments matched exactly and both reached the end
+ // match if prefix doesn't end with '/' (which should only match subfolders)
if (!prefixHasMore)
- return true;
+ return !pathSeparators.Contains(trimmedPrefix[^1]);
// compare segment
if (curParts.Current.Length == prefixParts.Current.Length)
@@ -188,7 +189,7 @@ namespace StardewModdingAPI.Framework.Content
return false;
// mismatch: something like "Maps/" would need an exact match
- if (pathSeparators.Contains(trimmed[^1]))
+ 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
@@ -196,7 +197,7 @@ namespace StardewModdingAPI.Framework.Content
return false;
// possible match
- return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0);
+ return allowSubfolder || (pathSeparators.Contains(trimmedPrefix[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0);
}
}
}