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 01/35] 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 02/35] 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 03/35] 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 04/35] 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 05/35] 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 06/35] 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 07/35] 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 08/35] 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 61d6ec12daee843f758e5f828a713a72a767a94b Mon Sep 17 00:00:00 2001
From: Tyler
Date: Tue, 18 Oct 2022 20:03:28 -0500
Subject: [PATCH 09/35] add detailed manifest validation errors at build time
---
src/SMAPI.ModBuildConfig/DeployModTask.cs | 33 ++++++-
.../Framework/ModFileManager.cs | 12 ---
.../Serialization/Models/Manifest.cs | 96 ++++++++++++++++++
src/SMAPI/Framework/ModLoading/ModResolver.cs | 98 ++-----------------
4 files changed, 138 insertions(+), 101 deletions(-)
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
index 88412d92..357e02b5 100644
--- a/src/SMAPI.ModBuildConfig/DeployModTask.cs
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -7,7 +7,10 @@ using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
+using Newtonsoft.Json;
using StardewModdingAPI.ModBuildConfig.Framework;
+using StardewModdingAPI.Toolkit.Serialization;
+using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.ModBuildConfig
@@ -75,6 +78,34 @@ namespace StardewModdingAPI.ModBuildConfig
this.Log.LogMessage(MessageImportance.High, $"[mod build package] Handling build with options {string.Join(", ", properties)}");
}
+ // check if manifest file exists
+ FileInfo manifestFile = new(Path.Combine(this.ProjectDir, "manifest.json"));
+ if (!manifestFile.Exists)
+ {
+ this.Log.LogError("[mod build package] The mod does not have a manifest.json file.");
+ return false;
+ }
+
+ // check if the json is valid
+ Manifest manifest;
+ try
+ {
+ new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest);
+ } catch (JsonReaderException ex)
+ {
+ // log the inner exception, otherwise the message will be generic
+ Exception exToShow = ex.InnerException ?? ex;
+ this.Log.LogError($"[mod build package] Failed to parse manifest.json: {exToShow.Message}");
+ return false;
+ }
+
+ // validate the manifest's fields
+ if (!manifest.TryValidate(out string error))
+ {
+ this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}");
+ return false;
+ }
+
if (!this.EnableModDeploy && !this.EnableModZip)
return true; // nothing to do
@@ -101,7 +132,7 @@ namespace StardewModdingAPI.ModBuildConfig
// create release zip
if (this.EnableModZip)
{
- string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModFolderName} {package.GetManifestVersion()}.zip");
+ string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModFolderName} {manifest.Version}.zip");
string zipPath = Path.Combine(this.ModZipPath, zipName);
this.Log.LogMessage(MessageImportance.High, $"[mod build package] Generating the release zip at {zipPath}...");
diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
index 80955f67..00f3f439 100644
--- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
+++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
@@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
-using StardewModdingAPI.Toolkit.Serialization;
-using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.ModBuildConfig.Framework
@@ -113,16 +111,6 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
return new Dictionary(this.Files, StringComparer.OrdinalIgnoreCase);
}
- /// Get a semantic version from the mod manifest.
- /// The manifest is missing or invalid.
- public string GetManifestVersion()
- {
- if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile) || !new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest manifest))
- throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor
-
- return manifest.Version.ToString();
- }
-
/*********
** Private methods
diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index 8a449f0a..4f84a60d 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
using System.Text;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization.Converters;
+using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Serialization.Models
{
@@ -103,6 +106,99 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
this.UpdateKeys = updateKeys ?? Array.Empty();
}
+ /// Try to validate a manifest's fields. Fails if any invalid field is found.
+ /// The error message to display to the user.
+ /// Returns whether the manifest was validated successfully.
+ public bool TryValidate(out string error)
+ {
+ // validate DLL / content pack fields
+ bool hasDll = !string.IsNullOrWhiteSpace(this.EntryDll);
+ bool isContentPack = this.ContentPackFor != null;
+
+ // validate field presence
+ if (!hasDll && !isContentPack)
+ {
+ error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.";
+ return false;
+ }
+ if (hasDll && isContentPack)
+ {
+ error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive.";
+ return false;
+ }
+
+ // validate DLL filename format
+ if (hasDll && this.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
+ {
+ error = $"manifest has invalid filename '{this.EntryDll}' for the EntryDLL field.";
+ return false;
+ }
+
+ // validate content pack
+ else if (isContentPack)
+ {
+ // invalid content pack ID
+ if (string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID))
+ {
+ error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.";
+ return false;
+ }
+ }
+
+ // validate required fields
+ {
+ List missingFields = new List(3);
+
+ if (string.IsNullOrWhiteSpace(this.Name))
+ missingFields.Add(nameof(IManifest.Name));
+ if (this.Version == null || this.Version.ToString() == "0.0.0")
+ missingFields.Add(nameof(IManifest.Version));
+ if (string.IsNullOrWhiteSpace(this.UniqueID))
+ missingFields.Add(nameof(IManifest.UniqueID));
+
+ if (missingFields.Any())
+ {
+ error = $"manifest is missing required fields ({string.Join(", ", missingFields)}).";
+ return false;
+ }
+ }
+
+ // validate ID format
+ if (!PathUtilities.IsSlug(this.UniqueID))
+ {
+ error = "manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).";
+ return false;
+ }
+
+ // validate dependencies
+ foreach (IManifestDependency? dependency in this.Dependencies)
+ {
+ // null dependency
+ if (dependency == null)
+ {
+ error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}.";
+ return false;
+ }
+
+ // missing ID
+ if (string.IsNullOrWhiteSpace(dependency.UniqueID))
+ {
+ error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field.";
+ return false;
+ }
+
+ // invalid ID
+ if (!PathUtilities.IsSlug(dependency.UniqueID))
+ {
+ error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens).";
+ return false;
+ }
+ }
+
+ error = "";
+ return true;
+ }
+
/// Override the update keys loaded from the mod info.
/// The new update keys to set.
internal void OverrideUpdateKeys(params string[] updateKeys)
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index fe56f4d2..352c22cc 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -8,7 +8,6 @@ using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Toolkit.Serialization.Models;
-using StardewModdingAPI.Toolkit.Utilities;
using StardewModdingAPI.Toolkit.Utilities.PathLookups;
namespace StardewModdingAPI.Framework.ModLoading
@@ -126,100 +125,23 @@ namespace StardewModdingAPI.Framework.ModLoading
continue;
}
- // validate DLL / content pack fields
+ // check for dll if it's supposed to have one
+ if (!string.IsNullOrEmpty(mod.Manifest.EntryDll) && validateFilesExist)
{
- bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll);
- bool isContentPack = mod.Manifest.ContentPackFor != null;
-
- // validate field presence
- if (!hasDll && !isContentPack)
+ IFileLookup pathLookup = getFileLookup(mod.DirectoryPath);
+ FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!);
+ if (!file.Exists)
{
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.");
- continue;
- }
- if (hasDll && isContentPack)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive.");
- continue;
- }
-
- // validate DLL
- if (hasDll)
- {
- // invalid filename format
- if (mod.Manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field.");
- continue;
- }
-
- // file doesn't exist
- if (validateFilesExist)
- {
- IFileLookup pathLookup = getFileLookup(mod.DirectoryPath);
- FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!);
- if (!file.Exists)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
- continue;
- }
- }
- }
-
- // validate content pack
- else
- {
- // invalid content pack ID
- if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor!.UniqueID))
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.");
- continue;
- }
- }
- }
-
- // validate required fields
- {
- List missingFields = new List(3);
-
- if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
- missingFields.Add(nameof(IManifest.Name));
- if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0.0")
- missingFields.Add(nameof(IManifest.Version));
- if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
- missingFields.Add(nameof(IManifest.UniqueID));
-
- if (missingFields.Any())
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
continue;
}
}
- // validate ID format
- if (!PathUtilities.IsSlug(mod.Manifest.UniqueID))
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
-
- // validate dependencies
- foreach (IManifestDependency? dependency in mod.Manifest.Dependencies)
+ // validate manifest
+ if (mod.Manifest is Manifest manifest && !manifest.TryValidate(out string manifestError))
{
- // null dependency
- if (dependency == null)
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a null entry under {nameof(IManifest.Dependencies)}.");
- continue;
- }
-
- // missing ID
- if (string.IsNullOrWhiteSpace(dependency.UniqueID))
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field.");
- continue;
- }
-
- // invalid ID
- if (!PathUtilities.IsSlug(dependency.UniqueID))
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}");
+ continue;
}
}
From 55eec58eafb9ba07f3e8b0a1c8394cb114de17a0 Mon Sep 17 00:00:00 2001
From: Tyler
Date: Wed, 19 Oct 2022 10:21:19 -0500
Subject: [PATCH 10/35] simplify ContentPackFor validation check
---
src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index 4f84a60d..1607cf3e 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -134,15 +134,11 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
return false;
}
- // validate content pack
- else if (isContentPack)
+ // validate content pack ID
+ else if (isContentPack && string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID))
{
- // invalid content pack ID
- if (string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID))
- {
- error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.";
- return false;
- }
+ error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.";
+ return false;
}
// validate required fields
From bb2fde18292352471501887013ca2b7f60a9dc25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?=
Date: Wed, 9 Nov 2022 17:25:25 +0100
Subject: [PATCH 11/35] Added ModsToLoadFirst/Last to SMAPI config, along with
the implementation
---
src/SMAPI/Framework/ModLoading/ModResolver.cs | 22 ++++++++++++++-----
src/SMAPI/Framework/Models/SConfig.cs | 12 +++++++++-
src/SMAPI/Framework/SCore.cs | 10 ++++++++-
src/SMAPI/SMAPI.config.json | 12 +++++++++-
4 files changed, 48 insertions(+), 8 deletions(-)
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index fe56f4d2..b90f9ba5 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -245,7 +245,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// Sort the given mods by the order they should be loaded.
/// The mods to process.
/// Handles access to SMAPI's internal mod metadata list.
- public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase)
+ /// The mod IDs SMAPI should try to load first, before any other mods not included in this list.
+ /// The mod IDs SMAPI should try to load last, after all other mods not included in this list.
+ public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadFirst, IReadOnlyList modIdsToLoadLast, ModDatabase modDatabase)
{
// initialize metadata
mods = mods.ToArray();
@@ -260,8 +262,18 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// sort mods
- foreach (IModMetadata mod in mods)
- this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List());
+ IModMetadata[] allMods = mods.ToArray();
+ IModMetadata[] modsToLoadFirst = allMods.Where(m => modIdsToLoadFirst.Contains(m.Manifest.UniqueID)).ToArray();
+ IModMetadata[] modsToLoadLast = allMods.Where(m => modIdsToLoadLast.Contains(m.Manifest.UniqueID)).ToArray();
+ IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadFirst.Contains(m) && !modsToLoadLast.Contains(m)).ToArray();
+
+ List orderSortedMods = new();
+ orderSortedMods.AddRange(modsToLoadFirst);
+ orderSortedMods.AddRange(modsToLoadAsUsual);
+ orderSortedMods.AddRange(modsToLoadLast);
+
+ foreach (IModMetadata mod in orderSortedMods)
+ this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List());
return sortedMods.Reverse();
}
@@ -278,7 +290,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// The list in which to save mods sorted by dependency order.
/// The current change of mod dependencies.
/// Returns the mod dependency status.
- private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain)
+ private ModDependencyStatus ProcessDependencies(IReadOnlyList mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain)
{
// check if already visited
switch (states[mod])
@@ -409,7 +421,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// Get the dependencies declared in a manifest.
/// The mod manifest.
/// The loaded mods.
- private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods)
+ private IEnumerable GetDependenciesFrom(IManifest manifest, IReadOnlyList loadedMods)
{
IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id));
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index bceb0940..ddd721d5 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -82,6 +82,12 @@ namespace StardewModdingAPI.Framework.Models
/// The mod IDs SMAPI should ignore when performing update checks or validating update keys.
public HashSet SuppressUpdateChecks { get; set; }
+ /// The mod IDs SMAPI should try to load first, before any other mods not included in this list.
+ public List ModsToLoadFirst { get; set; }
+
+ /// The mod IDs SMAPI should try to load last, after all other mods not included in this list.
+ public List ModsToLoadLast { get; set; }
+
/********
** Public methods
@@ -100,7 +106,9 @@ namespace StardewModdingAPI.Framework.Models
/// The colors to use for text written to the SMAPI console.
/// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod.
/// The mod IDs SMAPI should ignore when performing update checks or validating update keys.
- public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks)
+ /// The mod IDs SMAPI should try to load first, before any other mods not included in this list.
+ /// The mod IDs SMAPI should try to load last, after all other mods not included in this list.
+ public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadFirst, string[]? modsToLoadLast)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
@@ -115,6 +123,8 @@ namespace StardewModdingAPI.Framework.Models
this.ConsoleColors = consoleColors;
this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)];
this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase);
+ this.ModsToLoadFirst = new List(modsToLoadFirst ?? Array.Empty());
+ this.ModsToLoadLast = new List(modsToLoadLast ?? Array.Empty());
}
/// Override the value of .
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 40979b09..7bd60490 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -423,9 +423,17 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot).");
mods = mods.Where(p => !p.IsIgnored).ToArray();
+ // warn about mods that should load first or last which are not found at all
+ foreach (string modId in this.Settings.ModsToLoadFirst)
+ if (!mods.Any(m => m.Manifest.UniqueID == modId))
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load first, but it could not be found.", LogLevel.Warn);
+ foreach (string modId in this.Settings.ModsToLoadLast)
+ if (!mods.Any(m => m.Manifest.UniqueID == modId))
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load last, but it could not be found.", LogLevel.Warn);
+
// load mods
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup);
- mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
+ mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadFirst, this.Settings.ModsToLoadLast, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
// check for software likely to cause issues
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 635e3add..1a342df2 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -141,5 +141,15 @@ copy all the settings, or you may cause bugs due to overridden changes in future
"SMAPI.ConsoleCommands",
"SMAPI.ErrorHandler",
"SMAPI.SaveBackup"
- ]
+ ],
+
+ /**
+ * The mod IDs SMAPI should try to load first, before any other mods not included in this list.
+ */
+ "ModsToLoadFirst": [],
+
+ /**
+ * The mod IDs SMAPI should try to load last, after all other mods not included in this list.
+ */
+ "ModsToLoadLast": []
}
From 42b4b6b6a4ae1bb59182857b383539b24b063215 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?=
Date: Wed, 9 Nov 2022 19:50:32 +0100
Subject: [PATCH 12/35] Renamed first/last to early/late; ignoring mods
declared as both and warning about those
---
src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 ++++++++--------
src/SMAPI/Framework/Models/SConfig.cs | 18 +++++++++---------
src/SMAPI/Framework/SCore.cs | 15 +++++++++------
src/SMAPI/SMAPI.config.json | 8 ++++----
4 files changed, 30 insertions(+), 27 deletions(-)
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index b90f9ba5..f9ba73c4 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -245,9 +245,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// Sort the given mods by the order they should be loaded.
/// The mods to process.
/// Handles access to SMAPI's internal mod metadata list.
- /// The mod IDs SMAPI should try to load first, before any other mods not included in this list.
- /// The mod IDs SMAPI should try to load last, after all other mods not included in this list.
- public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadFirst, IReadOnlyList modIdsToLoadLast, ModDatabase modDatabase)
+ /// The mod IDs SMAPI should try to load early, before any other mods not included in this list.
+ /// The mod IDs SMAPI should try to load late, after all other mods not included in this list.
+ public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadEarly, IReadOnlyList modIdsToLoadLate, ModDatabase modDatabase)
{
// initialize metadata
mods = mods.ToArray();
@@ -263,14 +263,14 @@ namespace StardewModdingAPI.Framework.ModLoading
// sort mods
IModMetadata[] allMods = mods.ToArray();
- IModMetadata[] modsToLoadFirst = allMods.Where(m => modIdsToLoadFirst.Contains(m.Manifest.UniqueID)).ToArray();
- IModMetadata[] modsToLoadLast = allMods.Where(m => modIdsToLoadLast.Contains(m.Manifest.UniqueID)).ToArray();
- IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadFirst.Contains(m) && !modsToLoadLast.Contains(m)).ToArray();
+ IModMetadata[] modsToLoadEarly = allMods.Where(m => modIdsToLoadEarly.Contains(m.Manifest.UniqueID) && !modIdsToLoadLate.Contains(m.Manifest.UniqueID)).ToArray();
+ IModMetadata[] modsToLoadLate = allMods.Where(m => modIdsToLoadLate.Contains(m.Manifest.UniqueID) && !modIdsToLoadEarly.Contains(m.Manifest.UniqueID)).ToArray();
+ IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray();
List orderSortedMods = new();
- orderSortedMods.AddRange(modsToLoadFirst);
+ orderSortedMods.AddRange(modsToLoadEarly);
orderSortedMods.AddRange(modsToLoadAsUsual);
- orderSortedMods.AddRange(modsToLoadLast);
+ orderSortedMods.AddRange(modsToLoadLate);
foreach (IModMetadata mod in orderSortedMods)
this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List());
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index ddd721d5..40d3450f 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Models
/// The mod IDs SMAPI should ignore when performing update checks or validating update keys.
public HashSet SuppressUpdateChecks { get; set; }
- /// The mod IDs SMAPI should try to load first, before any other mods not included in this list.
- public List ModsToLoadFirst { get; set; }
+ /// The mod IDs SMAPI should try to load early, before any other mods not included in this list.
+ public List ModsToLoadEarly { get; set; }
- /// The mod IDs SMAPI should try to load last, after all other mods not included in this list.
- public List ModsToLoadLast { get; set; }
+ /// The mod IDs SMAPI should try to load late, after all other mods not included in this list.
+ public List ModsToLoadLate { get; set; }
/********
@@ -106,9 +106,9 @@ namespace StardewModdingAPI.Framework.Models
/// The colors to use for text written to the SMAPI console.
/// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod.
/// The mod IDs SMAPI should ignore when performing update checks or validating update keys.
- /// The mod IDs SMAPI should try to load first, before any other mods not included in this list.
- /// The mod IDs SMAPI should try to load last, after all other mods not included in this list.
- public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadFirst, string[]? modsToLoadLast)
+ /// The mod IDs SMAPI should try to load early, before any other mods not included in this list.
+ /// The mod IDs SMAPI should try to load late, after all other mods not included in this list.
+ public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
@@ -123,8 +123,8 @@ namespace StardewModdingAPI.Framework.Models
this.ConsoleColors = consoleColors;
this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)];
this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase);
- this.ModsToLoadFirst = new List(modsToLoadFirst ?? Array.Empty());
- this.ModsToLoadLast = new List(modsToLoadLast ?? Array.Empty());
+ this.ModsToLoadEarly = new List(modsToLoadEarly ?? Array.Empty());
+ this.ModsToLoadLate = new List(modsToLoadLate ?? Array.Empty());
}
/// Override the value of .
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 7bd60490..9e91924e 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -423,17 +423,20 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot).");
mods = mods.Where(p => !p.IsIgnored).ToArray();
- // warn about mods that should load first or last which are not found at all
- foreach (string modId in this.Settings.ModsToLoadFirst)
+ // warn about mods that should load early or late which are not found at all, or both
+ foreach (string modId in this.Settings.ModsToLoadEarly)
if (!mods.Any(m => m.Manifest.UniqueID == modId))
- this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load first, but it could not be found.", LogLevel.Warn);
- foreach (string modId in this.Settings.ModsToLoadLast)
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found.", LogLevel.Warn);
+ foreach (string modId in this.Settings.ModsToLoadLate)
if (!mods.Any(m => m.Manifest.UniqueID == modId))
- this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load last, but it could not be found.", LogLevel.Warn);
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found.", LogLevel.Warn);
+ foreach (string modId in this.Settings.ModsToLoadEarly)
+ if (this.Settings.ModsToLoadLate.Contains(modId))
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn);
// load mods
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup);
- mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadFirst, this.Settings.ModsToLoadLast, modDatabase).ToArray();
+ mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
// check for software likely to cause issues
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 1a342df2..68645d24 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -144,12 +144,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
],
/**
- * The mod IDs SMAPI should try to load first, before any other mods not included in this list.
+ * The mod IDs SMAPI should try to load early, before any other mods not included in this list.
*/
- "ModsToLoadFirst": [],
+ "ModsToLoadEarly": [],
/**
- * The mod IDs SMAPI should try to load last, after all other mods not included in this list.
+ * The mod IDs SMAPI should try to load late, after all other mods not included in this list.
*/
- "ModsToLoadLast": []
+ "ModsToLoadLate": []
}
From 9fd8c35b462bc19efb520da21cda66f83559a66e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?=
Date: Wed, 9 Nov 2022 20:26:50 +0100
Subject: [PATCH 13/35] Actually taking order into consideration
---
src/SMAPI/Framework/ModLoading/ModResolver.cs | 14 ++++++++++++--
src/SMAPI/Framework/SCore.cs | 4 ++--
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index f9ba73c4..8ef5e4a8 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -263,8 +263,18 @@ namespace StardewModdingAPI.Framework.ModLoading
// sort mods
IModMetadata[] allMods = mods.ToArray();
- IModMetadata[] modsToLoadEarly = allMods.Where(m => modIdsToLoadEarly.Contains(m.Manifest.UniqueID) && !modIdsToLoadLate.Contains(m.Manifest.UniqueID)).ToArray();
- IModMetadata[] modsToLoadLate = allMods.Where(m => modIdsToLoadLate.Contains(m.Manifest.UniqueID) && !modIdsToLoadEarly.Contains(m.Manifest.UniqueID)).ToArray();
+ IModMetadata[] modsToLoadEarly = modIdsToLoadEarly
+ .Where(modId => !modIdsToLoadLate.Contains(modId))
+ .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId))
+ .Where(m => m != null)
+ .Select(m => m!)
+ .ToArray();
+ IModMetadata[] modsToLoadLate = modIdsToLoadLate
+ .Where(modId => !modIdsToLoadEarly.Contains(modId))
+ .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId))
+ .Where(m => m != null)
+ .Select(m => m!)
+ .ToArray();
IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray();
List orderSortedMods = new();
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 9e91924e..4d1eb959 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -426,10 +426,10 @@ namespace StardewModdingAPI.Framework
// warn about mods that should load early or late which are not found at all, or both
foreach (string modId in this.Settings.ModsToLoadEarly)
if (!mods.Any(m => m.Manifest.UniqueID == modId))
- this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found.", LogLevel.Warn);
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found or was skipped.", LogLevel.Warn);
foreach (string modId in this.Settings.ModsToLoadLate)
if (!mods.Any(m => m.Manifest.UniqueID == modId))
- this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found.", LogLevel.Warn);
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn);
foreach (string modId in this.Settings.ModsToLoadEarly)
if (this.Settings.ModsToLoadLate.Contains(modId))
this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn);
From beb0b0aaf4e349cf911504a72b484d5642f6ac58 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Wed, 9 Nov 2022 20:03:41 -0500
Subject: [PATCH 14/35] fix & improve split-screen column in log parser
---
docs/release-notes.md | 4 ++++
src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 4 ++++
.../Framework/LogParsing/Models/ParsedLog.cs | 3 +++
src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 +++---
src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 11 ++---------
5 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/docs/release-notes.md b/docs/release-notes.md
index a8ddb0a0..e140b13b 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -7,6 +7,10 @@
_If needed, you can update to SMAPI 3.16.0 first and then install the latest version._
-->
+## Upcoming release
+* For the web UI:
+ * Fixed log parser not showing screen IDs in split-screen mode, and improved screen display.
+
## 3.17.2
Released 21 October 2022 for Stardew Valley 1.5.6 or later.
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index 5e0dedf3..c39e612b 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -108,6 +108,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
}
}
+ // detect split-screen mode
+ if (message.ScreenId != 0)
+ log.IsSplitScreen = true;
+
// collect SMAPI metadata
if (message.Mod == "SMAPI")
{
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
index cda0f653..2a2e5f5c 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
@@ -22,6 +22,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
/// The raw log text.
public string? RawText { get; set; }
+ /// Whether there are messages from multiple screens in the log.
+ public bool IsSplitScreen { get; set; }
+
/****
** Log data
****/
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 28127903..c1251c21 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -47,7 +47,7 @@
-
+