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: make asset name comparing lazy.
---
src/SMAPI/Framework/Content/AssetName.cs | 108 +++++++++++++++------
.../AssetPathUtilities/AssetPartYielder.cs | 67 +++++++++++++
2 files changed, 145 insertions(+), 30 deletions(-)
create mode 100644 src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
(limited to 'src/SMAPI')
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();
+
+ // neither of us have any more to yield, I'm done.
+ if (!otherHasMore && !iHaveMore)
+ return true;
+
+ // One of us has more but the other doesn't, this isn't a match.
+ if (otherHasMore ^ iHaveMore)
+ return false;
- string compareTo = useBaseName ? this.BaseName : this.Name;
- return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase);
+ // My next bit doesn't match their next bit, this isn't a match.
+ if (!compareTo.Current.Equals(compareFrom.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ // continue checking.
+ }
}
///
@@ -119,43 +141,69 @@ namespace StardewModdingAPI.Framework.Content
if (prefix is null)
return false;
- string rawTrimmed = prefix.Trim();
+ ReadOnlySpan trimmed = prefix.AsSpan().Trim();
+
+ // 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 NormalizeAssetName will trim them
- if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\'))
+ // asset keys can't have a leading slash, but AssetPathYielder won't yield that.
+ if (seperators.Contains(trimmed[0]))
return false;
- // normalize prefix
+ if (trimmed.Length == 0)
+ return true;
+
+ AssetPartYielder compareTo = new(this.Name);
+ AssetPartYielder compareFrom = new(trimmed);
+
+ while (true)
{
- string normalized = PathUtilities.NormalizeAssetName(prefix);
+ bool otherHasMore = compareFrom.MoveNext();
+ bool iHaveMore = compareTo.MoveNext();
- // keep trailing slash
- if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\'))
- normalized += PathUtilities.PreferredAssetSeparator;
+ // Neither of us have any more to yield, I'm done.
+ if (!otherHasMore && !iHaveMore)
+ return true;
- prefix = normalized;
- }
+ // the prefix is actually longer than the asset name, this can't be true.
+ if (otherHasMore && !iHaveMore)
+ return false;
- // compare
- if (prefix.Length == 0)
- return true;
+ // they're done, I have more. (These are going to be word boundaries, I don't need to check that).
+ if (!otherHasMore && iHaveMore)
+ {
+ return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal);
+ }
- return
- this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
- && (
- allowPartialWord
- || this.Name.Length == prefix.Length
- || !char.IsLetterOrDigit(prefix[^1]) // last character in suffix is word separator
- || !char.IsLetterOrDigit(this.Name[prefix.Length]) // or first character after it is
- )
- && (
- allowSubfolder
- || this.Name.Length == prefix.Length
- || !this.Name[prefix.Length..].Contains(PathUtilities.PreferredAssetSeparator)
- );
+ // check my next segment against theirs.
+ if (otherHasMore && iHaveMore)
+ {
+ // my next segment doesn't match theirs.
+ if (!compareTo.Current.StartsWith(compareFrom.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ // my next segment starts with theirs but isn't an exact match.
+ if (compareTo.Current.Length != compareFrom.Current.Length)
+ {
+ // something like "Maps/" would require an exact match.
+ if (seperators.Contains(trimmed[^1]))
+ return false;
+
+ // check for partial word.
+ if (!allowPartialWord
+ && char.IsLetterOrDigit(compareFrom.Current[^1]) // last character in suffix is not word separator
+ && char.IsLetterOrDigit(compareTo.Current[compareFrom.Current.Length]) // and the first character after it isn't either.
+ )
+ return false;
+
+ return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal);
+ }
+
+ // exact matches should continue checking.
+ }
+ }
}
-
///
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));
+ }
+}
--
cgit
From 70cde89480e43bb1369c1063c7b19f757784f269 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:45 -0400
Subject: tweak naming in new code
---
src/SMAPI/Framework/Content/AssetName.cs | 52 ++++++++---------
.../AssetPathUtilities/AssetNamePartEnumerator.cs | 66 +++++++++++++++++++++
.../AssetPathUtilities/AssetPartYielder.cs | 67 ----------------------
3 files changed, 91 insertions(+), 94 deletions(-)
create mode 100644 src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
delete mode 100644 src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
(limited to 'src/SMAPI')
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/AssetNamePartEnumerator.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
new file mode 100644
index 00000000..0840617a
--- /dev/null
+++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs
@@ -0,0 +1,66 @@
+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
+{
+ private ReadOnlySpan RemainderImpl;
+
+ ///
+ /// Construct an instance.
+ ///
+ /// The asset name.
+ internal AssetNamePartEnumerator(ReadOnlySpan assetName)
+ {
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName);
+ }
+
+ ///
+ /// 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;
+
+ // this is just so it can be used in a foreach loop.
+ public AssetNamePartEnumerator GetEnumerator() => this;
+
+ ///
+ /// Moves the enumerator to the next element.
+ ///
+ /// True if there is a new
+ public bool MoveNext()
+ {
+ if (this.RemainderImpl.Length == 0)
+ {
+ return false;
+ }
+
+ int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators);
+
+ // no more separator characters found, I'm done.
+ if (index < 0)
+ {
+ this.Current = this.RemainderImpl;
+ this.RemainderImpl = ReadOnlySpan.Empty;
+ return true;
+ }
+
+ // Yield the next separate character bit
+ this.Current = this.RemainderImpl[..index];
+ this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]);
+ return true;
+ }
+
+ private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span)
+ {
+ return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators));
+ }
+}
diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
deleted file mode 100644
index a55a0ab4..00000000
--- a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-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));
- }
-}
--
cgit
From eed1deb3c75ba2aeea94ea9a57f9fe7ad92a90ce Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:45 -0400
Subject: apply conventions to asset part enumerator
---
.../AssetPathUtilities/AssetNamePartEnumerator.cs | 98 +++++++++++-----------
1 file changed, 51 insertions(+), 47 deletions(-)
(limited to 'src/SMAPI')
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));
+ }
}
}
--
cgit
From 4e3b2810e6951b72bdf5c5cbdd23a079d53a4c96 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:45 -0400
Subject: 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(-)
(limited to 'src/SMAPI')
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();
--
cgit
From 5d30b47e1e903f7ceb53116528255934c238e5ba Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:46 -0400
Subject: fix IsEquivalentTo no longer ignoring surrounding whitespace
---
src/SMAPI/Framework/Content/AssetName.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'src/SMAPI')
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)
{
--
cgit
From 573f732c2a2118d7a4848151764df6bef1a47008 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sun, 16 Oct 2022 14:41:46 -0400
Subject: reduce sequential bool checks a bit
---
src/SMAPI/Framework/Content/AssetName.cs | 77 +++++++++++++++-----------------
1 file changed, 37 insertions(+), 40 deletions(-)
(limited to 'src/SMAPI')
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)
- return true;
-
- // One of us has more but the other doesn't, this isn't a match.
- if (otherHasMore ^ curHasMore)
+ // mismatch: lengths differ
+ if (otherHasMore != curHasMore)
return false;
- // My next bit doesn't match their next bit, this isn't a match.
+ // match: both reached the end without a mismatch
+ if (!curHasMore)
+ return true;
+
+ // mismatch: current segment is different
if (!curParts.Current.Equals(otherParts.Current, StringComparison.OrdinalIgnoreCase))
return false;
-
- // 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)
+ {
+ // mismatch: segments aren't equivalent
+ if (!curParts.Current.Equals(prefixParts.Current, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+ else
{
- // my next segment doesn't match theirs.
+ // 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;
-
- // 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: something like "Maps/" would need an exact match
+ if (pathSeparators.Contains(trimmed[^1]))
+ return false;
- return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
- }
+ // 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;
- // exact matches should continue checking.
+ // possible match
+ return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal);
}
}
}
--
cgit
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: 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(-)
(limited to 'src/SMAPI')
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);
}
///
--
cgit
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: fix this case.
---
src/SMAPI.Tests/Core/AssetNameTests.cs | 2 ++
src/SMAPI/Framework/Content/AssetName.cs | 4 ++++
2 files changed, 6 insertions(+)
(limited to 'src/SMAPI')
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;
--
cgit
From 61d6ec12daee843f758e5f828a713a72a767a94b Mon Sep 17 00:00:00 2001
From: Tyler
Date: Tue, 18 Oct 2022 20:03:28 -0500
Subject: add detailed manifest validation errors at build time
---
src/SMAPI.ModBuildConfig/DeployModTask.cs | 33 +++++++-
.../Framework/ModFileManager.cs | 12 ---
src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 96 +++++++++++++++++++++
src/SMAPI/Framework/ModLoading/ModResolver.cs | 98 +++-------------------
4 files changed, 138 insertions(+), 101 deletions(-)
(limited to 'src/SMAPI')
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;
}
}
--
cgit
From bb2fde18292352471501887013ca2b7f60a9dc25 Mon Sep 17 00:00:00 2001
From: Michał Dolaś
Date: Wed, 9 Nov 2022 17:25:25 +0100
Subject: 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(-)
(limited to 'src/SMAPI')
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": []
}
--
cgit
From 42b4b6b6a4ae1bb59182857b383539b24b063215 Mon Sep 17 00:00:00 2001
From: Michał Dolaś
Date: Wed, 9 Nov 2022 19:50:32 +0100
Subject: 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(-)
(limited to 'src/SMAPI')
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": []
}
--
cgit
From 9fd8c35b462bc19efb520da21cda66f83559a66e Mon Sep 17 00:00:00 2001
From: Michał Dolaś
Date: Wed, 9 Nov 2022 20:26:50 +0100
Subject: 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(-)
(limited to 'src/SMAPI')
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);
--
cgit
From 76e5588f02b247204969efb488c3fb293601faeb Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Wed, 9 Nov 2022 21:41:04 -0500
Subject: add option to disable console input
---
docs/release-notes.md | 3 +++
src/SMAPI/Framework/Models/SConfig.cs | 34 ++++++++++++++++++++--------------
src/SMAPI/Framework/SCore.cs | 19 +++++++++++--------
src/SMAPI/SMAPI.config.json | 6 ++++++
4 files changed, 40 insertions(+), 22 deletions(-)
(limited to 'src/SMAPI')
diff --git a/docs/release-notes.md b/docs/release-notes.md
index e140b13b..0843f59a 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -8,6 +8,9 @@
-->
## Upcoming release
+* For players:
+ * Added config option to disable console input. This may reduce CPU usage on some Linux systems.
+
* For the web UI:
* Fixed log parser not showing screen IDs in split-screen mode, and improved screen display.
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index bceb0940..825ebb44 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -16,6 +16,7 @@ namespace StardewModdingAPI.Framework.Models
private static readonly IDictionary DefaultValues = new Dictionary
{
[nameof(CheckForUpdates)] = true,
+ [nameof(ListenForConsoleInput)] = true,
[nameof(ParanoidWarnings)] = Constants.IsDebugBuild,
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
@@ -48,6 +49,9 @@ namespace StardewModdingAPI.Framework.Models
/// Whether to check for newer versions of SMAPI and mods on startup.
public bool CheckForUpdates { get; set; }
+ /// Whether SMAPI should listen for console input to support console commands.
+ public bool ListenForConsoleInput { get; set; }
+
/// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.
public bool ParanoidWarnings { get; set; }
@@ -87,23 +91,25 @@ namespace StardewModdingAPI.Framework.Models
** Public methods
********/
/// Construct an instance.
- /// Whether to enable development features.
- /// Whether to check for newer versions of SMAPI and mods on startup.
- /// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.
- /// Whether to show beta versions as valid updates.
- /// SMAPI's GitHub project name, used to perform update checks.
- /// The base URL for SMAPI's web API, used to perform update checks.
- /// The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.
- /// Whether SMAPI should rewrite mods for compatibility.
- /// >Whether to make SMAPI file APIs case-insensitive, even on Linux.
- /// Whether SMAPI should log network traffic.
- /// 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)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
+ this.ListenForConsoleInput = listenForConsoleInput ?? (bool)SConfig.DefaultValues[nameof(this.ListenForConsoleInput)];
this.ParanoidWarnings = paranoidWarnings ?? (bool)SConfig.DefaultValues[nameof(this.ParanoidWarnings)];
this.UseBetaChannel = useBetaChannel ?? (bool)SConfig.DefaultValues[nameof(this.UseBetaChannel)];
this.GitHubProjectName = gitHubProjectName;
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 40979b09..7bb54653 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -447,14 +447,17 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error);
// start SMAPI console
- new Thread(
- () => this.LogManager.RunConsoleInputLoop(
- commandManager: this.CommandManager,
- reloadTranslations: this.ReloadTranslations,
- handleInput: input => this.RawCommandQueue.Add(input),
- continueWhile: () => this.IsGameRunning && !this.IsExiting
- )
- ).Start();
+ if (this.Settings.ListenForConsoleInput)
+ {
+ new Thread(
+ () => this.LogManager.RunConsoleInputLoop(
+ commandManager: this.CommandManager,
+ reloadTranslations: this.ReloadTranslations,
+ handleInput: input => this.RawCommandQueue.Add(input),
+ continueWhile: () => this.IsGameRunning && !this.IsExiting
+ )
+ ).Start();
+ }
}
/// Raised after an instance finishes loading its initial content.
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 635e3add..52f25fdd 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -41,6 +41,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
*/
"DeveloperMode": true,
+ /**
+ * Whether SMAPI should listen for console input. Disabling this will prevent you from using
+ * console commands. On some specific Linux systems, disabling this may reduce CPU usage.
+ */
+ "ListenForConsoleInput": true,
+
/**
* Whether SMAPI should rewrite mods for compatibility. This may prevent older mods from
* loading, but bypasses a Visual Studio crash when debugging.
--
cgit
From 303b3924ae3ef905d77b9d7ef0f9efc70e58c2b8 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Thu, 10 Nov 2022 21:50:01 -0500
Subject: 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(-)
(limited to 'src/SMAPI')
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);
}
}
}
--
cgit
From 346fddda670704c1458e42104ee7405fc1de7ccc Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Thu, 10 Nov 2022 23:27:38 -0500
Subject: move validation logic out of Manifest model
This avoids tightly coupling higher logic to the implementation class, since we can validate the interface.
---
src/SMAPI.ModBuildConfig/DeployModTask.cs | 6 +-
src/SMAPI.Toolkit/Framework/ManifestValidator.cs | 101 +++++++++++++++++++++
src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 92 -------------------
src/SMAPI/Framework/ModLoading/ModResolver.cs | 3 +-
4 files changed, 107 insertions(+), 95 deletions(-)
create mode 100644 src/SMAPI.Toolkit/Framework/ManifestValidator.cs
(limited to 'src/SMAPI')
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
index 357e02b5..70761a2f 100644
--- a/src/SMAPI.ModBuildConfig/DeployModTask.cs
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -9,6 +9,7 @@ using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Newtonsoft.Json;
using StardewModdingAPI.ModBuildConfig.Framework;
+using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
@@ -91,7 +92,8 @@ namespace StardewModdingAPI.ModBuildConfig
try
{
new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest);
- } catch (JsonReaderException ex)
+ }
+ catch (JsonReaderException ex)
{
// log the inner exception, otherwise the message will be generic
Exception exToShow = ex.InnerException ?? ex;
@@ -100,7 +102,7 @@ namespace StardewModdingAPI.ModBuildConfig
}
// validate the manifest's fields
- if (!manifest.TryValidate(out string error))
+ if (!ManifestValidator.TryValidate(manifest, out string error))
{
this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}");
return false;
diff --git a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs
new file mode 100644
index 00000000..62cfd8e9
--- /dev/null
+++ b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs
@@ -0,0 +1,101 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Utilities;
+
+namespace StardewModdingAPI.Toolkit.Framework
+{
+ /// Validates manifest fields.
+ public static class ManifestValidator
+ {
+ /// Try to validate a manifest's fields. Fails if any invalid field is found.
+ /// The manifest to validate.
+ /// The error message to display to the user.
+ /// Returns whether the manifest was validated successfully.
+ public static bool TryValidate(IManifest manifest, out string error)
+ {
+ // validate DLL / content pack fields
+ bool hasDll = !string.IsNullOrWhiteSpace(manifest.EntryDll);
+ bool isContentPack = manifest.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 && manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
+ {
+ error = $"manifest has invalid filename '{manifest.EntryDll}' for the EntryDLL field.";
+ return false;
+ }
+
+ // validate content pack ID
+ else if (isContentPack && string.IsNullOrWhiteSpace(manifest.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(manifest.Name))
+ missingFields.Add(nameof(IManifest.Name));
+ if (manifest.Version == null || manifest.Version.ToString() == "0.0.0")
+ missingFields.Add(nameof(IManifest.Version));
+ if (string.IsNullOrWhiteSpace(manifest.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(manifest.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 manifest.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;
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index 1607cf3e..8a449f0a 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -1,12 +1,9 @@
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
{
@@ -106,95 +103,6 @@ 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 ID
- else if (isContentPack && 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 352c22cc..0b4fe3e9 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -138,7 +139,7 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// validate manifest
- if (mod.Manifest is Manifest manifest && !manifest.TryValidate(out string manifestError))
+ if (!ManifestValidator.TryValidate(mod.Manifest, out string manifestError))
{
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}");
continue;
--
cgit
From 867afdd96ff8896dc81fdab204cf045713d32d91 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Thu, 10 Nov 2022 23:27:38 -0500
Subject: tweak new code
---
src/SMAPI.ModBuildConfig/DeployModTask.cs | 27 +++++------
src/SMAPI.Toolkit/Framework/ManifestValidator.cs | 57 +++++++++++++-----------
src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 +++----
3 files changed, 51 insertions(+), 49 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
index 1581b282..3508a6db 100644
--- a/src/SMAPI.ModBuildConfig/DeployModTask.cs
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -85,33 +85,30 @@ namespace StardewModdingAPI.ModBuildConfig
return true;
// validate the manifest file
- Manifest manifest;
+ IManifest manifest;
{
- // 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
try
{
- new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest);
+ string manifestPath = Path.Combine(this.ProjectDir, "manifest.json");
+ if (!new JsonHelper().ReadJsonFileIfExists(manifestPath, out Manifest rawManifest))
+ {
+ this.Log.LogError("[mod build package] The mod's manifest.json file doesn't exist.");
+ return false;
+ }
+ manifest = rawManifest;
}
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}");
+ this.Log.LogError($"[mod build package] The mod's manifest.json file isn't valid JSON: {exToShow.Message}");
return false;
}
- // validate the manifest's fields
- if (!ManifestValidator.TryValidate(manifest, out string error))
+ // validate manifest fields
+ if (!ManifestValidator.TryValidateFields(manifest, out string error))
{
- this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}");
+ this.Log.LogError($"[mod build package] The mod's manifest.json file is invalid: {error}");
return false;
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs
index 62cfd8e9..461dc325 100644
--- a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs
+++ b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using StardewModdingAPI.Toolkit.Utilities;
@@ -8,40 +9,47 @@ namespace StardewModdingAPI.Toolkit.Framework
/// Validates manifest fields.
public static class ManifestValidator
{
- /// Try to validate a manifest's fields. Fails if any invalid field is found.
+ /// Validate a manifest's fields.
/// The manifest to validate.
- /// The error message to display to the user.
- /// Returns whether the manifest was validated successfully.
- public static bool TryValidate(IManifest manifest, out string error)
+ /// The error message indicating why validation failed, if applicable.
+ /// Returns whether all manifest fields validated successfully.
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method that ensures those annotations are respected.")]
+ public static bool TryValidateFields(IManifest manifest, out string error)
{
- // validate DLL / content pack fields
+ //
+ // Note: SMAPI assumes that it can grammatically append the returned sentence in the
+ // form "failed loading because its ". Any errors returned should be valid
+ // in that format, unless the SMAPI call is adjusted accordingly.
+ //
+
bool hasDll = !string.IsNullOrWhiteSpace(manifest.EntryDll);
bool isContentPack = manifest.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)
+ // validate use of EntryDll vs ContentPackFor fields
+ if (hasDll == isContentPack)
{
- error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive.";
+ error = hasDll
+ ? $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."
+ : $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.";
return false;
}
- // validate DLL filename format
- if (hasDll && manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
+ // validate EntryDll/ContentPackFor format
+ if (hasDll)
{
- error = $"manifest has invalid filename '{manifest.EntryDll}' for the EntryDLL field.";
- return false;
+ if (manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
+ {
+ error = $"manifest has invalid filename '{manifest.EntryDll}' for the {nameof(IManifest.EntryDll)} field.";
+ return false;
+ }
}
-
- // validate content pack ID
- else if (isContentPack && string.IsNullOrWhiteSpace(manifest.ContentPackFor!.UniqueID))
+ else
{
- error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.";
- return false;
+ if (string.IsNullOrWhiteSpace(manifest.ContentPackFor!.UniqueID))
+ {
+ error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.";
+ return false;
+ }
}
// validate required fields
@@ -69,24 +77,21 @@ namespace StardewModdingAPI.Toolkit.Framework
return false;
}
- // validate dependencies
+ // validate dependency format
foreach (IManifestDependency? dependency in manifest.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).";
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 0b4fe3e9..9db9db99 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -126,7 +126,14 @@ namespace StardewModdingAPI.Framework.ModLoading
continue;
}
- // check for dll if it's supposed to have one
+ // validate manifest format
+ if (!ManifestValidator.TryValidateFields(mod.Manifest, out string manifestError))
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}");
+ continue;
+ }
+
+ // check that DLL exists if applicable
if (!string.IsNullOrEmpty(mod.Manifest.EntryDll) && validateFilesExist)
{
IFileLookup pathLookup = getFileLookup(mod.DirectoryPath);
@@ -137,13 +144,6 @@ namespace StardewModdingAPI.Framework.ModLoading
continue;
}
}
-
- // validate manifest
- if (!ManifestValidator.TryValidate(mod.Manifest, out string manifestError))
- {
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}");
- continue;
- }
}
// validate IDs are unique
--
cgit
From 0629f19698c9920e2988d96f316d227f97932df8 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 01:22:45 -0500
Subject: change new fields to hash sets & simplify sorting
This makes the mod IDs case-insensitive (like the 'SuppressUpdateChecks' field), fixes a build error in unit tests, and avoids re-scanning the mod list multiple times.
---
src/SMAPI/Framework/ModLoading/ModResolver.cs | 50 +++++++++++++--------------
src/SMAPI/Framework/Models/SConfig.cs | 16 ++++-----
src/SMAPI/Framework/SCore.cs | 28 +++++++++------
3 files changed, 51 insertions(+), 43 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 8ef5e4a8..1080a888 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -242,12 +242,32 @@ namespace StardewModdingAPI.Framework.ModLoading
}
}
+ /// Apply preliminary overrides to the load order based on the SMAPI configuration.
+ /// The mods to process.
+ /// The mod IDs SMAPI should load before any other mods (except those needed to load them).
+ /// The mod IDs SMAPI should load after any other mods.
+ public IModMetadata[] ApplyLoadOrderOverrides(IModMetadata[] mods, HashSet modIdsToLoadEarly, HashSet modIdsToLoadLate)
+ {
+ if (!modIdsToLoadEarly.Any() && !modIdsToLoadLate.Any())
+ return mods;
+
+ return mods
+ .OrderBy(mod =>
+ {
+ string id = mod.Manifest.UniqueID;
+ if (modIdsToLoadEarly.Contains(id))
+ return -1;
+ if (modIdsToLoadLate.Contains(id))
+ return 1;
+ return 0;
+ })
+ .ToArray();
+ }
+
/// 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 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)
+ public IEnumerable ProcessDependencies(IReadOnlyList mods, ModDatabase modDatabase)
{
// initialize metadata
mods = mods.ToArray();
@@ -262,28 +282,8 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// sort mods
- IModMetadata[] allMods = mods.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();
- orderSortedMods.AddRange(modsToLoadEarly);
- orderSortedMods.AddRange(modsToLoadAsUsual);
- orderSortedMods.AddRange(modsToLoadLate);
-
- foreach (IModMetadata mod in orderSortedMods)
- this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List());
+ foreach (IModMetadata mod in mods)
+ this.ProcessDependencies(mods, modDatabase, mod, states, sortedMods, new List());
return sortedMods.Reverse();
}
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index 40d3450f..ee2dc18d 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 early, before any other mods not included in this list.
- public List ModsToLoadEarly { get; set; }
+ /// The mod IDs SMAPI should load before any other mods (except those needed to load them).
+ public HashSet ModsToLoadEarly { 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; }
+ /// The mod IDs SMAPI should load after any other mods.
+ public HashSet ModsToLoadLate { get; set; }
/********
@@ -106,8 +106,8 @@ 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 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.
+ /// The mod IDs SMAPI should load before any other mods (except those needed to load them).
+ /// The mod IDs SMAPI should load after any other mods.
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;
@@ -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.ModsToLoadEarly = new List(modsToLoadEarly ?? Array.Empty());
- this.ModsToLoadLate = new List(modsToLoadLate ?? Array.Empty());
+ this.ModsToLoadEarly = new HashSet(modsToLoadEarly ?? Array.Empty(), StringComparer.OrdinalIgnoreCase);
+ this.ModsToLoadLate = new HashSet(modsToLoadLate ?? Array.Empty(), StringComparer.OrdinalIgnoreCase);
}
/// Override the value of .
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 4d1eb959..fb835002 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -424,19 +424,27 @@ namespace StardewModdingAPI.Framework
mods = mods.Where(p => !p.IsIgnored).ToArray();
// 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 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 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);
+ {
+ HashSet installedIds = new HashSet(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
+
+ foreach (string modId in this.Settings.ModsToLoadEarly)
+ {
+ if (!installedIds.Contains(modId))
+ 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 (this.Settings.ModsToLoadEarly.Contains(modId))
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn);
+ else if (!installedIds.Contains(modId))
+ this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn);
+ }
+ }
// load mods
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup);
- mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate, modDatabase).ToArray();
+ mods = resolver.ApplyLoadOrderOverrides(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate);
+ mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
// check for software likely to cause issues
--
cgit
From 3059794622e4442bf289dde7b4f533d8021d3bf7 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 01:22:46 -0500
Subject: adjust warning messages & log header when load order is edited
---
src/SMAPI/Framework/Models/SConfig.cs | 6 ++++++
src/SMAPI/Framework/SCore.cs | 22 ++++++++++------------
src/SMAPI/SMAPI.config.json | 12 +++++++-----
3 files changed, 23 insertions(+), 17 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index ee2dc18d..158831a9 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -146,6 +146,12 @@ namespace StardewModdingAPI.Framework.Models
custom[name] = value;
}
+ if (this.ModsToLoadEarly.Any())
+ custom[nameof(this.ModsToLoadEarly)] = $"[{string.Join(", ", this.ModsToLoadEarly)}]";
+
+ if (this.ModsToLoadLate.Any())
+ custom[nameof(this.ModsToLoadLate)] = $"[{string.Join(", ", this.ModsToLoadLate)}]";
+
if (!this.SuppressUpdateChecks.SetEquals(SConfig.DefaultSuppressUpdateChecks))
custom[nameof(this.SuppressUpdateChecks)] = $"[{string.Join(", ", this.SuppressUpdateChecks)}]";
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index fb835002..7f564a28 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -427,18 +427,16 @@ namespace StardewModdingAPI.Framework
{
HashSet installedIds = new HashSet(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
- foreach (string modId in this.Settings.ModsToLoadEarly)
- {
- if (!installedIds.Contains(modId))
- 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 (this.Settings.ModsToLoadEarly.Contains(modId))
- this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn);
- else if (!installedIds.Contains(modId))
- this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn);
- }
+ string[] missingEarlyMods = this.Settings.ModsToLoadEarly.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
+ string[] missingLateMods = this.Settings.ModsToLoadLate.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
+ string[] duplicateMods = this.Settings.ModsToLoadLate.Where(id => this.Settings.ModsToLoadEarly.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
+
+ if (missingEarlyMods.Any())
+ this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadEarly)} which aren't installed: '{string.Join("', '", missingEarlyMods)}'.", LogLevel.Warn);
+ if (missingLateMods.Any())
+ this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadLate)} which aren't installed: '{string.Join("', '", missingLateMods)}'.", LogLevel.Warn);
+ if (duplicateMods.Any())
+ this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs which are in both {nameof(this.Settings.ModsToLoadEarly)} and {nameof(this.Settings.ModsToLoadLate)}: '{string.Join("', '", duplicateMods)}'. These will be loaded early.", LogLevel.Warn);
}
// load mods
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 68645d24..0ab68a7d 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -144,12 +144,14 @@ copy all the settings, or you may cause bugs due to overridden changes in future
],
/**
- * The mod IDs SMAPI should try to load early, before any other mods not included in this list.
+ * The mod IDs SMAPI should load before any other mods (except those needed to load them)
+ * or after any other mods.
+ *
+ * This lets you manually fix the load order if needed, but this is a last resort — SMAPI
+ * automatically adjusts the load order based on mods' dependencies, so needing to manually
+ * edit the order is usually a problem with one or both mods' metadata that can be reported to
+ * the mod author.
*/
"ModsToLoadEarly": [],
-
- /**
- * The mod IDs SMAPI should try to load late, after all other mods not included in this list.
- */
"ModsToLoadLate": []
}
--
cgit
From dbf7750f3e27cf7c50e2f06005fd14da95627dc3 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 01:22:46 -0500
Subject: only validate & apply custom load order if there is one
---
src/SMAPI/Framework/SCore.cs | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 7f564a28..be5bc40f 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -423,7 +423,11 @@ 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 early or late which are not found at all, or both
+ // validate manifests
+ resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup);
+
+ // apply load order customizations
+ if (this.Settings.ModsToLoadEarly.Any() || this.Settings.ModsToLoadLate.Any())
{
HashSet installedIds = new HashSet(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
@@ -437,11 +441,11 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadLate)} which aren't installed: '{string.Join("', '", missingLateMods)}'.", LogLevel.Warn);
if (duplicateMods.Any())
this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs which are in both {nameof(this.Settings.ModsToLoadEarly)} and {nameof(this.Settings.ModsToLoadLate)}: '{string.Join("', '", duplicateMods)}'. These will be loaded early.", LogLevel.Warn);
+
+ mods = resolver.ApplyLoadOrderOverrides(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate);
}
// load mods
- resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup);
- mods = resolver.ApplyLoadOrderOverrides(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate);
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
--
cgit
From cefd9e23b06599aca3fefba7a78c6c785f4e5d57 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 01:38:45 -0500
Subject: set max game version
---
docs/release-notes.md | 1 +
src/SMAPI/Constants.cs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
(limited to 'src/SMAPI')
diff --git a/docs/release-notes.md b/docs/release-notes.md
index c247a9d5..014e8814 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -11,6 +11,7 @@
* For players:
* Added config options to override the mod load order, for the rare cases where that's needed (thanks to Shockah!).
* Added config option to disable console input, which may reduce CPU usage on some Linux systems.
+ * Set the max game version to 1.5.6 (since 1.6 will need a SMAPI update).
* For mod authors:
* Optimized asset name comparisons (thanks to atravita!).
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 77900ca6..222e5276 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -71,7 +71,7 @@ namespace StardewModdingAPI
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6");
/// The maximum supported version of Stardew Valley, if any.
- public static ISemanticVersion? MaximumGameVersion { get; } = null;
+ public static ISemanticVersion? MaximumGameVersion { get; } = new GameVersion("1.5.6");
/// The target game platform.
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
--
cgit
From 28ba3408bc84dd9d33f0aed126080be4dceb17f6 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 01:47:12 -0500
Subject: raise all deprecation messages to the final level
---
docs/release-notes.md | 1 +
src/SMAPI/Constants.cs | 2 +-
src/SMAPI/Framework/Content/AssetInfo.cs | 4 ++--
src/SMAPI/Framework/Deprecations/DeprecationManager.cs | 2 +-
src/SMAPI/Framework/ModHelpers/CommandHelper.cs | 2 +-
src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 4 ++--
src/SMAPI/Framework/ModHelpers/ModHelper.cs | 2 +-
src/SMAPI/Framework/SCore.cs | 6 +++---
src/SMAPI/Utilities/PerScreen.cs | 2 +-
9 files changed, 13 insertions(+), 12 deletions(-)
(limited to 'src/SMAPI')
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 014e8814..972360bc 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -15,6 +15,7 @@
* For mod authors:
* Optimized asset name comparisons (thanks to atravita!).
+ * Raised all deprecation messages to the final 'pending removal' level.
* For the web UI:
* Fixed log parser not showing screen IDs in split-screen mode, and improved screen display.
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 222e5276..4cb30dc9 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -90,7 +90,7 @@ namespace StardewModdingAPI
source: null,
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return Constants.GamePath;
diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs
index af000300..52ef02e6 100644
--- a/src/SMAPI/Framework/Content/AssetInfo.cs
+++ b/src/SMAPI/Framework/Content/AssetInfo.cs
@@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.Content
source: null,
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
unlessStackIncludes: new[]
{
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
@@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework.Content
source: null,
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
unlessStackIncludes: new[]
{
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs
index 5a5850d1..f44b5d7d 100644
--- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs
+++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs
@@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework.Deprecations
foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase))
{
// build message
- string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase}) and will break in the upcoming SMAPI 4.0.0.";
+ string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase}) and will break in the next major SMAPI update.";
// get log level
LogLevel level;
diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
index b7d4861f..90edc137 100644
--- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
@@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
version: "3.8.1",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.CommandManager.Trigger(name, arguments);
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 0a1633bf..152b264c 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.ObservableAssetLoaders;
@@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.ObservableAssetEditors;
diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
index 1cdd8536..531289d0 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}",
version: "3.14.0",
- severity: DeprecationLevel.Info
+ severity: DeprecationLevel.PendingRemoval
);
return this.ContentImpl;
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 8e21e7dc..90621ee8 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1736,7 +1736,7 @@ namespace StardewModdingAPI.Framework
source: metadata,
nounPhrase: $"{nameof(IAssetEditor)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
logStackTrace: false
);
@@ -1749,7 +1749,7 @@ namespace StardewModdingAPI.Framework
source: metadata,
nounPhrase: $"{nameof(IAssetLoader)}",
version: "3.14.0",
- severity: DeprecationLevel.Info,
+ severity: DeprecationLevel.PendingRemoval,
logStackTrace: false
);
@@ -1781,7 +1781,7 @@ namespace StardewModdingAPI.Framework
metadata,
$"using {name} without bundling it",
"3.14.7",
- DeprecationLevel.Info,
+ DeprecationLevel.PendingRemoval,
logStackTrace: false
);
}
diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs
index 468df0bd..87bf2027 100644
--- a/src/SMAPI/Utilities/PerScreen.cs
+++ b/src/SMAPI/Utilities/PerScreen.cs
@@ -59,7 +59,7 @@ namespace StardewModdingAPI.Utilities
null,
$"calling the {nameof(PerScreen)} constructor with null",
"3.14.0",
- DeprecationLevel.Info
+ DeprecationLevel.PendingRemoval
);
#else
throw new ArgumentNullException(nameof(createNewState));
--
cgit
From be84248a9a0a57ee1b2384e63b25fc9bb694fa4d Mon Sep 17 00:00:00 2001
From: SinZ
Date: Fri, 11 Nov 2022 22:01:48 +1100
Subject: Add logic to remove from the multiplayer map cache for asset
propagation
---
src/SMAPI/Metadata/CoreAssetPropagator.cs | 7 +++++++
1 file changed, 7 insertions(+)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 1ef9a8f2..d94fe2ae 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -1166,6 +1166,13 @@ namespace StardewModdingAPI.Metadata
GameLocation location = locationInfo.Location;
Vector2? playerPos = Game1.player?.Position;
+ // clear cachedMultiplayerMaps so Asset Propegation works on farmhands and Map edits can be applied after an initial load
+ if (!Game1.IsMasterGame)
+ {
+ var multiplayer = this.Reflection.GetField(typeof(Game1), "multiplayer").GetValue();
+ multiplayer.cachedMultiplayerMaps.Remove(locationInfo.Location.NameOrUniqueName);
+ }
+
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
location.reloadMap();
--
cgit
From 4ca546a7a8d5e0f6c9e0df7370f4e6c858d5b69b Mon Sep 17 00:00:00 2001
From: atravita-mods <94934860+atravita-mods@users.noreply.github.com>
Date: Fri, 11 Nov 2022 06:38:35 -0500
Subject: directly add tests over the trailing slash.
---
src/SMAPI.Tests/Core/AssetNameTests.cs | 20 ++++++++++++++++++++
src/SMAPI/Framework/Content/AssetName.cs | 2 +-
2 files changed, 21 insertions(+), 1 deletion(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
index fdaa2c01..cd4d0473 100644
--- a/src/SMAPI.Tests/Core/AssetNameTests.cs
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -265,6 +265,26 @@ namespace SMAPI.Tests.Core
return name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder);
}
+ // the enumerator strips the trailing path seperator
+ // so each of these cases has to be handled on each branch.
+ [TestCase("Mods/SomeMod", "Mods/", false, ExpectedResult = true)]
+ [TestCase("Mods/SomeMod", "Mods", false, ExpectedResult = false)]
+ [TestCase("Mods/Jasper/Data", "Mods/Jas/", false, ExpectedResult = false)]
+ [TestCase("Mods/Jasper/Data", "Mods/Jas", false, ExpectedResult = false)]
+ [TestCase("Mods/Jas", "Mods/Jas/", false, ExpectedResult = false)]
+ [TestCase("Mods/Jas", "Mods/Jas", false, ExpectedResult = true)]
+ public bool StartsWith_PrefixHasSeperator(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/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 99968299..83593de8 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -163,7 +163,7 @@ namespace StardewModdingAPI.Framework.Content
return false;
// match if subfolder paths are fine (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
- return allowSubfolder;
+ return allowSubfolder || (pathSeparators.Contains(trimmedPrefix[^1]) && curParts.Remainder.Length == 0);
}
// previous segments matched exactly and both reached the end
--
cgit
From 2bccdd97370b958187b0ad99437e4ca85ec5ae1f Mon Sep 17 00:00:00 2001
From: atravita-mods <94934860+atravita-mods@users.noreply.github.com>
Date: Fri, 11 Nov 2022 06:52:46 -0500
Subject: edit comment.
---
src/SMAPI/Framework/Content/AssetName.cs | 2 ++
1 file changed, 2 insertions(+)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs
index 83593de8..a1c6a5da 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -163,6 +163,8 @@ namespace StardewModdingAPI.Framework.Content
return false;
// match if subfolder paths are fine (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
+ // special case if the original prefix ended with a '/' - subfolder checking pushes forward one, to check the Remainder instead of Current
+ // which is necessarily nonzero in this block.
return allowSubfolder || (pathSeparators.Contains(trimmedPrefix[^1]) && curParts.Remainder.Length == 0);
}
--
cgit
From 286c2d244949f4eee62e2de440938d2d8ae3ce76 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 20:55:24 -0500
Subject: pass multiplayer into asset propagator to avoid reflection
---
src/SMAPI/Framework/ContentCoordinator.cs | 5 +++--
src/SMAPI/Framework/SCore.cs | 1 +
src/SMAPI/Metadata/CoreAssetPropagator.cs | 16 +++++++++-------
3 files changed, 13 insertions(+), 9 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index cf26307f..86415a5f 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -129,6 +129,7 @@ namespace StardewModdingAPI.Framework
/// The root directory to search for content.
/// The current culture for which to localize content.
/// Encapsulates monitoring and logging.
+ /// The multiplayer instance whose map cache to update during asset propagation.
/// Simplifies access to private code.
/// Encapsulates SMAPI's JSON file parsing.
/// A callback to invoke the first time *any* game content manager loads an asset.
@@ -136,7 +137,7 @@ namespace StardewModdingAPI.Framework
/// Get a file lookup for the given directory.
/// A callback to invoke when any asset names have been invalidated from the cache.
/// Get the load/edit operations to apply to an asset by querying registered event handlers.
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations)
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Multiplayer multiplayer, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations)
{
this.GetFileLookup = getFileLookup;
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
@@ -177,7 +178,7 @@ namespace StardewModdingAPI.Framework
this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
- this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true));
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, multiplayer, reflection, name => this.ParseAssetName(name, allowLocales: true));
this.LocaleCodes = new Lazy>(() => this.GetLocaleCodes(customLanguages: Enumerable.Empty()));
}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 40979b09..cf1e73fc 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1327,6 +1327,7 @@ namespace StardewModdingAPI.Framework
rootDirectory: rootDirectory,
currentCulture: Thread.CurrentThread.CurrentUICulture,
monitor: this.Monitor,
+ multiplayer: this.Multiplayer,
reflection: this.Reflection,
jsonHelper: this.Toolkit.JsonHelper,
onLoadingFirstAsset: this.InitializeBeforeFirstAssetLoaded,
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index d94fe2ae..037e4573 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -40,6 +40,9 @@ namespace StardewModdingAPI.Metadata
/// Writes messages to the console.
private readonly IMonitor Monitor;
+ /// The multiplayer instance whose map cache to update.
+ private readonly Multiplayer Multiplayer;
+
/// Simplifies access to private game code.
private readonly Reflector Reflection;
@@ -70,13 +73,15 @@ namespace StardewModdingAPI.Metadata
/// The main content manager through which to reload assets.
/// An internal content manager used only for asset propagation.
/// Writes messages to the console.
+ /// The multiplayer instance whose map cache to update.
/// Simplifies access to private code.
/// Parse a raw asset name.
- public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, Func parseAssetName)
+ public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Multiplayer multiplayer, Reflector reflection, Func parseAssetName)
{
this.MainContentManager = mainContent;
this.DisposableContentManager = disposableContent;
this.Monitor = monitor;
+ this.Multiplayer = multiplayer;
this.Reflection = reflection;
this.ParseAssetName = parseAssetName;
}
@@ -1166,12 +1171,9 @@ namespace StardewModdingAPI.Metadata
GameLocation location = locationInfo.Location;
Vector2? playerPos = Game1.player?.Position;
- // clear cachedMultiplayerMaps so Asset Propegation works on farmhands and Map edits can be applied after an initial load
- if (!Game1.IsMasterGame)
- {
- var multiplayer = this.Reflection.GetField(typeof(Game1), "multiplayer").GetValue();
- multiplayer.cachedMultiplayerMaps.Remove(locationInfo.Location.NameOrUniqueName);
- }
+ // clear multiplayer cache for farmhands
+ if (!Context.IsMainPlayer)
+ this.Multiplayer.cachedMultiplayerMaps.Remove(location.NameOrUniqueName);
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
--
cgit
From ad2dcc28791eb8026335646474fb2e0766fcc51b Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 21:22:52 -0500
Subject: expand code comments for clarity
---
src/SMAPI.Tests/Core/AssetNameTests.cs | 5 ++---
src/SMAPI/Framework/Content/AssetName.cs | 12 ++++++++----
2 files changed, 10 insertions(+), 7 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
index cd4d0473..2d546ec7 100644
--- a/src/SMAPI.Tests/Core/AssetNameTests.cs
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -265,15 +265,14 @@ namespace SMAPI.Tests.Core
return name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder);
}
- // the enumerator strips the trailing path seperator
- // so each of these cases has to be handled on each branch.
+ // The enumerator strips the trailing path separator, so each of these cases has to be handled on each branch.
[TestCase("Mods/SomeMod", "Mods/", false, ExpectedResult = true)]
[TestCase("Mods/SomeMod", "Mods", false, ExpectedResult = false)]
[TestCase("Mods/Jasper/Data", "Mods/Jas/", false, ExpectedResult = false)]
[TestCase("Mods/Jasper/Data", "Mods/Jas", false, ExpectedResult = false)]
[TestCase("Mods/Jas", "Mods/Jas/", false, ExpectedResult = false)]
[TestCase("Mods/Jas", "Mods/Jas", false, ExpectedResult = true)]
- public bool StartsWith_PrefixHasSeperator(string mainAssetName, string otherAssetName, bool allowSubfolder)
+ public bool StartsWith_PrefixHasSeparator(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 a1c6a5da..8355f9ec 100644
--- a/src/SMAPI/Framework/Content/AssetName.cs
+++ b/src/SMAPI/Framework/Content/AssetName.cs
@@ -162,10 +162,14 @@ namespace StardewModdingAPI.Framework.Content
if (prefixHasMore)
return false;
- // match if subfolder paths are fine (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
- // special case if the original prefix ended with a '/' - subfolder checking pushes forward one, to check the Remainder instead of Current
- // which is necessarily nonzero in this block.
- return allowSubfolder || (pathSeparators.Contains(trimmedPrefix[^1]) && curParts.Remainder.Length == 0);
+ // match: every segment in the prefix matched and subfolders are allowed (e.g. prefix 'Data/Events' with target 'Data/Events/Beach')
+ if (allowSubfolder)
+ return true;
+
+ // Special case: the prefix ends with a path separator, but subfolders aren't allowed. This case
+ // matches if there's no further path separator in the asset name *after* the current separator.
+ // For example, the prefix 'A/B/' matches 'A/B/C' but not 'A/B/C/D'.
+ return pathSeparators.Contains(trimmedPrefix[^1]) && curParts.Remainder.Length == 0;
}
// previous segments matched exactly and both reached the end
--
cgit
From c1e3b25dcaff2406fa1e3a457c1ba9c0f8ecda7f Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Fri, 11 Nov 2022 21:43:42 -0500
Subject: fix load-early/late mods not correctly sorted relative to others in
the same list
---
src/SMAPI/Framework/ModLoading/ModResolver.cs | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 96975e05..cb62e16f 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -174,14 +174,20 @@ namespace StardewModdingAPI.Framework.ModLoading
if (!modIdsToLoadEarly.Any() && !modIdsToLoadLate.Any())
return mods;
+ string[] earlyArray = modIdsToLoadEarly.ToArray();
+ string[] lateArray = modIdsToLoadLate.ToArray();
+
return mods
.OrderBy(mod =>
{
string id = mod.Manifest.UniqueID;
- if (modIdsToLoadEarly.Contains(id))
- return -1;
- if (modIdsToLoadLate.Contains(id))
- return 1;
+
+ if (modIdsToLoadEarly.TryGetValue(id, out string? actualId))
+ return -int.MaxValue + Array.IndexOf(earlyArray, actualId);
+
+ if (modIdsToLoadLate.TryGetValue(id, out actualId))
+ return int.MaxValue - Array.IndexOf(lateArray, actualId);
+
return 0;
})
.ToArray();
--
cgit
From 57d3e2b98ed5429e4bd61cd1cd33b84ec3ad7b8c Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sat, 12 Nov 2022 13:50:02 -0500
Subject: also update multiplayer map cache for host player
---
src/SMAPI/Metadata/CoreAssetPropagator.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
(limited to 'src/SMAPI')
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 037e4573..97ae32e4 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -1171,9 +1171,8 @@ namespace StardewModdingAPI.Metadata
GameLocation location = locationInfo.Location;
Vector2? playerPos = Game1.player?.Position;
- // clear multiplayer cache for farmhands
- if (!Context.IsMainPlayer)
- this.Multiplayer.cachedMultiplayerMaps.Remove(location.NameOrUniqueName);
+ // remove from multiplayer cache
+ this.Multiplayer.cachedMultiplayerMaps.Remove(location.NameOrUniqueName);
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
--
cgit
From 613946003d5a2a6ea7c13a4dca04bda4f2387957 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard
Date: Sat, 12 Nov 2022 15:14:59 -0500
Subject: prepare for release
---
build/common.targets | 2 +-
docs/release-notes.md | 14 ++++++++------
src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++--
src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++--
src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++--
src/SMAPI/Constants.cs | 2 +-
6 files changed, 16 insertions(+), 14 deletions(-)
(limited to 'src/SMAPI')
diff --git a/build/common.targets b/build/common.targets
index 3c22b913..4b92ecc2 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed.
- 3.17.2
+ 3.18.0
SMAPI
latest
$(AssemblySearchPaths);{GAC}
diff --git a/docs/release-notes.md b/docs/release-notes.md
index c299cfa1..9b2dea77 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -7,17 +7,19 @@
_If needed, you can update to SMAPI 3.16.0 first and then install the latest version._
-->
-## Upcoming release
+## 3.18.0
+Released 12 November 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/74565278).
+
* For players:
- * Added config options to override the mod load order for specific mods (thanks to Shockah!).
- * Added config option to disable console input, which may reduce CPU usage on some Linux systems.
- * Set the max game version to 1.5.6 (since 1.6 will need a SMAPI update).
+ * You can now override the mod load order in `smapi-internal/config.json` (thanks to Shockah!).
+ * You can now disable console input in `smapi-internal/config.json`, which may reduce CPU usage on some Linux systems.
* Fixed map edits not always applied for farmhands in multiplayer (thanks to SinZ163!).
+ * Internal changes to prepare for the upcoming Stardew Valley 1.6 and SMAPI 4.0.
* For mod authors:
* Optimized asset name comparisons (thanks to atravita!).
- * Raised all deprecation messages to the final 'pending removal' level.
- * **This is the last non-bugfix update before SMAPI 4.0.0, which will drop all deprecated APIs.** If you haven't [fixed deprecation warnings in your mod code](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) (if any), you should do it soon. SMAPI 4.0.0 will release alongside the upcoming Stardew Valley 1.6.
+ * Raised all deprecation messages to the 'pending removal' level.
+ * **This is the last major update before SMAPI 4.0.0, which will drop all deprecated APIs.** If you haven't [fixed deprecation warnings in your mod code](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) (if any), you should do it soon. SMAPI 4.0.0 will release alongside the upcoming Stardew Valley 1.6.
* For the web UI:
* The log parser now detects split-screen mode and shows which screen logged each message.
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index d1296dbe..ddb0e20d 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.17.2",
+ "Version": "3.18.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.17.2"
+ "MinimumApiVersion": "3.18.0"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index c3757e8f..eeea1d28 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.17.2",
+ "Version": "3.18.0",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.17.2"
+ "MinimumApiVersion": "3.18.0"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 78821a3e..0acf066d 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.17.2",
+ "Version": "3.18.0",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.17.2"
+ "MinimumApiVersion": "3.18.0"
}
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 4cb30dc9..02e75e05 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -52,7 +52,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// SMAPI's current raw semantic version.
- internal static string RawApiVersion = "3.17.2";
+ internal static string RawApiVersion = "3.18.0";
}
/// Contains SMAPI's constants and assumptions.
--
cgit