From 0c191eb32c41ffedd321951cda70b521e9b51c96 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Sat, 15 Oct 2022 08:36:24 -0400 Subject: [PATCH 01/35] make asset name comparing lazy. --- src/SMAPI/Framework/Content/AssetName.cs | 114 +++++++++++++----- .../AssetPathUtilities/AssetPartYielder.cs | 67 ++++++++++ 2 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 148354a1..05e1d1c2 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -1,7 +1,11 @@ using System; using StardewModdingAPI.Toolkit.Utilities; +using StardewModdingAPI.Utilities.AssetPathUtilities; + using StardewValley; +using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; + namespace StardewModdingAPI.Framework.Content { /// An asset name that can be loaded through the content pipeline. @@ -94,10 +98,28 @@ namespace StardewModdingAPI.Framework.Content if (string.IsNullOrWhiteSpace(assetName)) return false; - assetName = PathUtilities.NormalizeAssetName(assetName); + AssetPartYielder compareTo = new(useBaseName ? this.BaseName : this.Name); + AssetPartYielder compareFrom = new(assetName); + + while (true) + { + bool otherHasMore = compareFrom.MoveNext(); + bool iHaveMore = compareTo.MoveNext(); - string compareTo = useBaseName ? this.BaseName : this.Name; - return compareTo.Equals(assetName, StringComparison.OrdinalIgnoreCase); + // neither of us have any more to yield, I'm done. + if (!otherHasMore && !iHaveMore) + return true; + + // One of us has more but the other doesn't, this isn't a match. + if (otherHasMore ^ iHaveMore) + return false; + + // My next bit doesn't match their next bit, this isn't a match. + if (!compareTo.Current.Equals(compareFrom.Current, StringComparison.OrdinalIgnoreCase)) + return false; + + // continue checking. + } } /// @@ -119,42 +141,68 @@ namespace StardewModdingAPI.Framework.Content if (prefix is null) return false; - string rawTrimmed = prefix.Trim(); + ReadOnlySpan trimmed = prefix.AsSpan().Trim(); - // asset keys can't have a leading slash, but NormalizeAssetName will trim them - if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\')) + // just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read. + ReadOnlySpan seperators = new(ToolkitPathUtilities.PossiblePathSeparators); + + // asset keys can't have a leading slash, but AssetPathYielder won't yield that. + if (seperators.Contains(trimmed[0])) return false; - // normalize prefix - { - string normalized = PathUtilities.NormalizeAssetName(prefix); - - // keep trailing slash - if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\')) - normalized += PathUtilities.PreferredAssetSeparator; - - prefix = normalized; - } - - // compare - if (prefix.Length == 0) + if (trimmed.Length == 0) return true; - return - this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) - && ( - allowPartialWord - || this.Name.Length == prefix.Length - || !char.IsLetterOrDigit(prefix[^1]) // last character in suffix is word separator - || !char.IsLetterOrDigit(this.Name[prefix.Length]) // or first character after it is - ) - && ( - allowSubfolder - || this.Name.Length == prefix.Length - || !this.Name[prefix.Length..].Contains(PathUtilities.PreferredAssetSeparator) - ); - } + AssetPartYielder compareTo = new(this.Name); + AssetPartYielder compareFrom = new(trimmed); + while (true) + { + bool otherHasMore = compareFrom.MoveNext(); + bool iHaveMore = compareTo.MoveNext(); + + // Neither of us have any more to yield, I'm done. + if (!otherHasMore && !iHaveMore) + return true; + + // the prefix is actually longer than the asset name, this can't be true. + if (otherHasMore && !iHaveMore) + return false; + + // they're done, I have more. (These are going to be word boundaries, I don't need to check that). + if (!otherHasMore && iHaveMore) + { + return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal); + } + + // check my next segment against theirs. + if (otherHasMore && iHaveMore) + { + // my next segment doesn't match theirs. + if (!compareTo.Current.StartsWith(compareFrom.Current, StringComparison.OrdinalIgnoreCase)) + return false; + + // my next segment starts with theirs but isn't an exact match. + if (compareTo.Current.Length != compareFrom.Current.Length) + { + // something like "Maps/" would require an exact match. + if (seperators.Contains(trimmed[^1])) + return false; + + // check for partial word. + if (!allowPartialWord + && char.IsLetterOrDigit(compareFrom.Current[^1]) // last character in suffix is not word separator + && char.IsLetterOrDigit(compareTo.Current[compareFrom.Current.Length]) // and the first character after it isn't either. + ) + return false; + + return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal); + } + + // exact matches should continue checking. + } + } + } /// public bool IsDirectlyUnderPath(string? assetFolder) diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs new file mode 100644 index 00000000..a55a0ab4 --- /dev/null +++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs @@ -0,0 +1,67 @@ +using System; + +using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; + +namespace StardewModdingAPI.Utilities.AssetPathUtilities; + +/// +/// A helper class that yields out each bit of an asset path +/// +internal ref struct AssetPartYielder +{ + private ReadOnlySpan remainder; + + /// + /// Construct an instance. + /// + /// The asset name. + internal AssetPartYielder(ReadOnlySpan assetName) + { + this.remainder = AssetPartYielder.TrimLeadingPathSeperators(assetName); + } + + /// + /// The remainder of the assetName (that hasn't been yielded out yet.) + /// + internal ReadOnlySpan Remainder => this.remainder; + + /// + /// The current segment. + /// + public ReadOnlySpan Current { get; private set; } = default; + + // this is just so it can be used in a foreach loop. + public AssetPartYielder GetEnumerator() => this; + + /// + /// Moves the enumerator to the next element. + /// + /// True if there is a new + public bool MoveNext() + { + if (this.remainder.Length == 0) + { + return false; + } + + int index = this.remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators); + + // no more seperator characters found, I'm done. + if (index < 0) + { + this.Current = this.remainder; + this.remainder = ReadOnlySpan.Empty; + return true; + } + + // Yield the next seperate character bit + this.Current = this.remainder[..index]; + this.remainder = AssetPartYielder.TrimLeadingPathSeperators(this.remainder[(index + 1)..]); + return true; + } + + private static ReadOnlySpan TrimLeadingPathSeperators(ReadOnlySpan span) + { + return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators)); + } +} From 70cde89480e43bb1369c1063c7b19f757784f269 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Oct 2022 14:41:45 -0400 Subject: [PATCH 02/35] tweak naming in new code --- src/SMAPI/Framework/Content/AssetName.cs | 52 +++++++++---------- ...tYielder.cs => AssetNamePartEnumerator.cs} | 31 ++++++----- 2 files changed, 40 insertions(+), 43 deletions(-) rename src/SMAPI/Utilities/AssetPathUtilities/{AssetPartYielder.cs => AssetNamePartEnumerator.cs} (54%) diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 05e1d1c2..d7ee6dba 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -1,9 +1,7 @@ using System; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Utilities.AssetPathUtilities; - using StardewValley; - using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; namespace StardewModdingAPI.Framework.Content @@ -98,24 +96,24 @@ namespace StardewModdingAPI.Framework.Content if (string.IsNullOrWhiteSpace(assetName)) return false; - AssetPartYielder compareTo = new(useBaseName ? this.BaseName : this.Name); - AssetPartYielder compareFrom = new(assetName); - + AssetNamePartEnumerator curParts = new(useBaseName ? this.BaseName : this.Name); + AssetNamePartEnumerator otherParts = new(assetName); + while (true) { - bool otherHasMore = compareFrom.MoveNext(); - bool iHaveMore = compareTo.MoveNext(); + bool otherHasMore = otherParts.MoveNext(); + bool curHasMore = curParts.MoveNext(); // neither of us have any more to yield, I'm done. - if (!otherHasMore && !iHaveMore) + if (!otherHasMore && !curHasMore) return true; // One of us has more but the other doesn't, this isn't a match. - if (otherHasMore ^ iHaveMore) + if (otherHasMore ^ curHasMore) return false; // My next bit doesn't match their next bit, this isn't a match. - if (!compareTo.Current.Equals(compareFrom.Current, StringComparison.OrdinalIgnoreCase)) + if (!curParts.Current.Equals(otherParts.Current, StringComparison.OrdinalIgnoreCase)) return false; // continue checking. @@ -144,59 +142,59 @@ namespace StardewModdingAPI.Framework.Content ReadOnlySpan trimmed = prefix.AsSpan().Trim(); // just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read. - ReadOnlySpan seperators = new(ToolkitPathUtilities.PossiblePathSeparators); + ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // asset keys can't have a leading slash, but AssetPathYielder won't yield that. - if (seperators.Contains(trimmed[0])) + if (pathSeparators.Contains(trimmed[0])) return false; if (trimmed.Length == 0) return true; - AssetPartYielder compareTo = new(this.Name); - AssetPartYielder compareFrom = new(trimmed); + AssetNamePartEnumerator curParts = new(this.Name); + AssetNamePartEnumerator prefixParts = new(trimmed); while (true) { - bool otherHasMore = compareFrom.MoveNext(); - bool iHaveMore = compareTo.MoveNext(); + bool prefixHasMore = prefixParts.MoveNext(); + bool curHasMore = curParts.MoveNext(); // Neither of us have any more to yield, I'm done. - if (!otherHasMore && !iHaveMore) + if (!prefixHasMore && !curHasMore) return true; // the prefix is actually longer than the asset name, this can't be true. - if (otherHasMore && !iHaveMore) + if (prefixHasMore && !curHasMore) return false; // they're done, I have more. (These are going to be word boundaries, I don't need to check that). - if (!otherHasMore && iHaveMore) + if (!prefixHasMore && curHasMore) { - return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal); + return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); } // check my next segment against theirs. - if (otherHasMore && iHaveMore) + if (prefixHasMore && curHasMore) { // my next segment doesn't match theirs. - if (!compareTo.Current.StartsWith(compareFrom.Current, StringComparison.OrdinalIgnoreCase)) + if (!curParts.Current.StartsWith(prefixParts.Current, StringComparison.OrdinalIgnoreCase)) return false; // my next segment starts with theirs but isn't an exact match. - if (compareTo.Current.Length != compareFrom.Current.Length) + if (curParts.Current.Length != prefixParts.Current.Length) { // something like "Maps/" would require an exact match. - if (seperators.Contains(trimmed[^1])) + if (pathSeparators.Contains(trimmed[^1])) return false; // check for partial word. if (!allowPartialWord - && char.IsLetterOrDigit(compareFrom.Current[^1]) // last character in suffix is not word separator - && char.IsLetterOrDigit(compareTo.Current[compareFrom.Current.Length]) // and the first character after it isn't either. + && char.IsLetterOrDigit(prefixParts.Current[^1]) // last character in suffix is not word separator + && char.IsLetterOrDigit(curParts.Current[prefixParts.Current.Length]) // and the first character after it isn't either. ) return false; - return allowSubfolder || !compareTo.Remainder.Contains(seperators, StringComparison.Ordinal); + return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); } // exact matches should continue checking. diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs similarity index 54% rename from src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs rename to src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs index a55a0ab4..0840617a 100644 --- a/src/SMAPI/Utilities/AssetPathUtilities/AssetPartYielder.cs +++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs @@ -1,5 +1,4 @@ using System; - using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; namespace StardewModdingAPI.Utilities.AssetPathUtilities; @@ -7,23 +6,23 @@ namespace StardewModdingAPI.Utilities.AssetPathUtilities; /// /// A helper class that yields out each bit of an asset path /// -internal ref struct AssetPartYielder +internal ref struct AssetNamePartEnumerator { - private ReadOnlySpan remainder; + private ReadOnlySpan RemainderImpl; /// /// Construct an instance. /// /// The asset name. - internal AssetPartYielder(ReadOnlySpan assetName) + internal AssetNamePartEnumerator(ReadOnlySpan assetName) { - this.remainder = AssetPartYielder.TrimLeadingPathSeperators(assetName); + this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName); } /// /// The remainder of the assetName (that hasn't been yielded out yet.) /// - internal ReadOnlySpan Remainder => this.remainder; + internal ReadOnlySpan Remainder => this.RemainderImpl; /// /// The current segment. @@ -31,7 +30,7 @@ internal ref struct AssetPartYielder public ReadOnlySpan Current { get; private set; } = default; // this is just so it can be used in a foreach loop. - public AssetPartYielder GetEnumerator() => this; + public AssetNamePartEnumerator GetEnumerator() => this; /// /// Moves the enumerator to the next element. @@ -39,28 +38,28 @@ internal ref struct AssetPartYielder /// True if there is a new public bool MoveNext() { - if (this.remainder.Length == 0) + if (this.RemainderImpl.Length == 0) { return false; } - int index = this.remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators); + int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators); - // no more seperator characters found, I'm done. + // no more separator characters found, I'm done. if (index < 0) { - this.Current = this.remainder; - this.remainder = ReadOnlySpan.Empty; + this.Current = this.RemainderImpl; + this.RemainderImpl = ReadOnlySpan.Empty; return true; } - // Yield the next seperate character bit - this.Current = this.remainder[..index]; - this.remainder = AssetPartYielder.TrimLeadingPathSeperators(this.remainder[(index + 1)..]); + // Yield the next separate character bit + this.Current = this.RemainderImpl[..index]; + this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]); return true; } - private static ReadOnlySpan TrimLeadingPathSeperators(ReadOnlySpan span) + private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span) { return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators)); } From eed1deb3c75ba2aeea94ea9a57f9fe7ad92a90ce Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Oct 2022 14:41:45 -0400 Subject: [PATCH 03/35] apply conventions to asset part enumerator --- .../AssetNamePartEnumerator.cs | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs index 0840617a..11987ed6 100644 --- a/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs +++ b/src/SMAPI/Utilities/AssetPathUtilities/AssetNamePartEnumerator.cs @@ -1,66 +1,70 @@ using System; using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; -namespace StardewModdingAPI.Utilities.AssetPathUtilities; - -/// -/// A helper class that yields out each bit of an asset path -/// -internal ref struct AssetNamePartEnumerator +namespace StardewModdingAPI.Utilities.AssetPathUtilities { - private ReadOnlySpan RemainderImpl; - - /// - /// Construct an instance. - /// - /// The asset name. - internal AssetNamePartEnumerator(ReadOnlySpan assetName) + /// Handles enumerating the normalized segments in an asset name. + internal ref struct AssetNamePartEnumerator { - this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName); - } + /********* + ** Fields + *********/ + /// The backing field for . + private ReadOnlySpan RemainderImpl; - /// - /// The remainder of the assetName (that hasn't been yielded out yet.) - /// - internal ReadOnlySpan Remainder => this.RemainderImpl; - /// - /// The current segment. - /// - public ReadOnlySpan Current { get; private set; } = default; + /********* + ** Properties + *********/ + /// The remainder of the asset name being enumerated, ignoring segments which have already been yielded. + public ReadOnlySpan Remainder => this.RemainderImpl; - // this is just so it can be used in a foreach loop. - public AssetNamePartEnumerator GetEnumerator() => this; + /// Get the current segment. + public ReadOnlySpan Current { get; private set; } = default; - /// - /// Moves the enumerator to the next element. - /// - /// True if there is a new - public bool MoveNext() - { - if (this.RemainderImpl.Length == 0) + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The asset name to enumerate. + public AssetNamePartEnumerator(ReadOnlySpan assetName) { - return false; + this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(assetName); } - int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators); - - // no more separator characters found, I'm done. - if (index < 0) + /// Move the enumerator to the next segment. + /// Returns true if a new value was found (accessible via ). + public bool MoveNext() { - this.Current = this.RemainderImpl; - this.RemainderImpl = ReadOnlySpan.Empty; + if (this.RemainderImpl.Length == 0) + return false; + + int index = this.RemainderImpl.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators); + + // no more separator characters found, I'm done. + if (index < 0) + { + this.Current = this.RemainderImpl; + this.RemainderImpl = ReadOnlySpan.Empty; + return true; + } + + // Yield the next separate character bit + this.Current = this.RemainderImpl[..index]; + this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]); return true; } - // Yield the next separate character bit - this.Current = this.RemainderImpl[..index]; - this.RemainderImpl = AssetNamePartEnumerator.TrimLeadingPathSeparators(this.RemainderImpl[(index + 1)..]); - return true; - } - private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span) - { - return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators)); + /********* + ** Private methods + *********/ + /// Trim path separators at the start of the given path or segment. + /// The path or segment to trim. + private static ReadOnlySpan TrimLeadingPathSeparators(ReadOnlySpan span) + { + return span.TrimStart(new ReadOnlySpan(ToolkitPathUtilities.PossiblePathSeparators)); + } } } From 4e3b2810e6951b72bdf5c5cbdd23a079d53a4c96 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Oct 2022 14:41:45 -0400 Subject: [PATCH 04/35] fix index-out-of-range error when StartsWith prefix is empty --- src/SMAPI/Framework/Content/AssetName.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index d7ee6dba..c0572105 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -139,21 +139,19 @@ namespace StardewModdingAPI.Framework.Content if (prefix is null) return false; + // get initial values ReadOnlySpan trimmed = prefix.AsSpan().Trim(); + if (trimmed.Length == 0) + return true; + ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); // just to simplify calling other span APIs - // just because most ReadOnlySpan/Span APIs expect a ReadOnlySpan/Span, easier to read. - ReadOnlySpan pathSeparators = new(ToolkitPathUtilities.PossiblePathSeparators); - - // asset keys can't have a leading slash, but AssetPathYielder won't yield that. + // asset keys can't have a leading slash, but AssetPathYielder will trim them if (pathSeparators.Contains(trimmed[0])) return false; - if (trimmed.Length == 0) - return true; - + // compare segments AssetNamePartEnumerator curParts = new(this.Name); AssetNamePartEnumerator prefixParts = new(trimmed); - while (true) { bool prefixHasMore = prefixParts.MoveNext(); From 5d30b47e1e903f7ceb53116528255934c238e5ba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Oct 2022 14:41:46 -0400 Subject: [PATCH 05/35] fix IsEquivalentTo no longer ignoring surrounding whitespace --- src/SMAPI/Framework/Content/AssetName.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index c0572105..6220ea61 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -97,7 +97,7 @@ namespace StardewModdingAPI.Framework.Content return false; AssetNamePartEnumerator curParts = new(useBaseName ? this.BaseName : this.Name); - AssetNamePartEnumerator otherParts = new(assetName); + AssetNamePartEnumerator otherParts = new(assetName.AsSpan().Trim()); while (true) { From 573f732c2a2118d7a4848151764df6bef1a47008 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Oct 2022 14:41:46 -0400 Subject: [PATCH 06/35] reduce sequential bool checks a bit --- src/SMAPI/Framework/Content/AssetName.cs | 77 ++++++++++++------------ 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 6220ea61..bdb79dde 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -101,22 +101,20 @@ namespace StardewModdingAPI.Framework.Content while (true) { - bool otherHasMore = otherParts.MoveNext(); bool curHasMore = curParts.MoveNext(); + bool otherHasMore = otherParts.MoveNext(); - // neither of us have any more to yield, I'm done. - if (!otherHasMore && !curHasMore) + // mismatch: lengths differ + if (otherHasMore != curHasMore) + return false; + + // match: both reached the end without a mismatch + if (!curHasMore) return true; - // One of us has more but the other doesn't, this isn't a match. - if (otherHasMore ^ curHasMore) - return false; - - // My next bit doesn't match their next bit, this isn't a match. + // mismatch: current segment is different if (!curParts.Current.Equals(otherParts.Current, StringComparison.OrdinalIgnoreCase)) return false; - - // continue checking. } } @@ -154,48 +152,47 @@ namespace StardewModdingAPI.Framework.Content AssetNamePartEnumerator prefixParts = new(trimmed); while (true) { - bool prefixHasMore = prefixParts.MoveNext(); bool curHasMore = curParts.MoveNext(); + bool prefixHasMore = prefixParts.MoveNext(); - // Neither of us have any more to yield, I'm done. - if (!prefixHasMore && !curHasMore) - return true; - - // the prefix is actually longer than the asset name, this can't be true. - if (prefixHasMore && !curHasMore) - return false; - - // they're done, I have more. (These are going to be word boundaries, I don't need to check that). - if (!prefixHasMore && curHasMore) + // reached end of prefix or asset name + if (prefixHasMore != curHasMore) { + // mismatch: prefix is longer + if (prefixHasMore) + return false; + + // possible match: all prefix segments matched return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); } - // check my next segment against theirs. - if (prefixHasMore && curHasMore) + // match: previous segments matched exactly and both reached the end + if (!prefixHasMore) + return true; + + // compare segment + if (curParts.Current.Length == prefixParts.Current.Length) { - // my next segment doesn't match theirs. + // mismatch: segments aren't equivalent + if (!curParts.Current.Equals(prefixParts.Current, StringComparison.OrdinalIgnoreCase)) + return false; + } + else + { + // mismatch: cur segment doesn't start with prefix if (!curParts.Current.StartsWith(prefixParts.Current, StringComparison.OrdinalIgnoreCase)) return false; - // my next segment starts with theirs but isn't an exact match. - if (curParts.Current.Length != prefixParts.Current.Length) - { - // something like "Maps/" would require an exact match. - if (pathSeparators.Contains(trimmed[^1])) - return false; + // mismatch: something like "Maps/" would need an exact match + if (pathSeparators.Contains(trimmed[^1])) + return false; - // check for partial word. - if (!allowPartialWord - && char.IsLetterOrDigit(prefixParts.Current[^1]) // last character in suffix is not word separator - && char.IsLetterOrDigit(curParts.Current[prefixParts.Current.Length]) // and the first character after it isn't either. - ) - return false; + // mismatch: partial word match not allowed, and the first or last letter of the suffix isn't a word separator + if (!allowPartialWord && char.IsLetterOrDigit(prefixParts.Current[^1]) && char.IsLetterOrDigit(curParts.Current[prefixParts.Current.Length])) + return false; - return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); - } - - // exact matches should continue checking. + // possible match + return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); } } } From 4dcc6904b9e72ac3567dfafe3824c2de48218b58 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Sun, 16 Oct 2022 18:04:19 -0400 Subject: [PATCH 07/35] fix issues with subfolders --- src/SMAPI.Tests/Core/AssetNameTests.cs | 14 ++++++++++++++ src/SMAPI.Tests/SMAPI.Tests.csproj | 1 + src/SMAPI/Framework/Content/AssetName.cs | 8 ++++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index 655e9bae..fe70e330 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -243,6 +243,20 @@ namespace SMAPI.Tests.Core return result; } + [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", true, ExpectedResult = true)] + [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)] + public bool StartsWith_SubfolderWithPartial(string mainAssetName, string otherAssetName, bool allowSubfolder) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value + return name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder); + } + /**** ** GetHashCode diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 2c32a932..597cd7dd 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index bdb79dde..9d59f222 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -162,8 +162,8 @@ namespace StardewModdingAPI.Framework.Content if (prefixHasMore) return false; - // possible match: all prefix segments matched - return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); + // possible match: all prefix segments matched. + return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.Length == 0 : curParts.Current.Length == 0); } // match: previous segments matched exactly and both reached the end @@ -192,7 +192,7 @@ namespace StardewModdingAPI.Framework.Content return false; // possible match - return allowSubfolder || !curParts.Remainder.Contains(pathSeparators, StringComparison.Ordinal); + return allowSubfolder || (pathSeparators.Contains(trimmed[^1]) ? curParts.Remainder.IndexOfAny(ToolkitPathUtilities.PossiblePathSeparators) < 0 : curParts.Remainder.Length == 0); } } } @@ -203,7 +203,7 @@ namespace StardewModdingAPI.Framework.Content if (assetFolder is null) return false; - return this.StartsWith(assetFolder + "/", allowPartialWord: false, allowSubfolder: false); + return this.StartsWith(assetFolder + ToolkitPathUtilities.PreferredPathSeparator, allowPartialWord: false, allowSubfolder: false); } /// From b99dbf53bda9dc1178a3b6e8cbafea609f3ee6dc Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:58:41 -0400 Subject: [PATCH 08/35] fix this case. --- src/SMAPI.Tests/Core/AssetNameTests.cs | 2 ++ src/SMAPI/Framework/Content/AssetName.cs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index fe70e330..fbc94e95 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -245,6 +245,8 @@ namespace SMAPI.Tests.Core [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", true, ExpectedResult = true)] [TestCase("Mods/SomeMod/SomeSubdirectory", "Mods/Some", false, ExpectedResult = false)] + [TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)] + [TestCase("Mods/Jasper/Data", "Mods/Jas/Image", true, ExpectedResult = false)] public bool StartsWith_SubfolderWithPartial(string mainAssetName, string otherAssetName, bool allowSubfolder) { // arrange diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index 9d59f222..7b87c0c5 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -179,6 +179,10 @@ namespace StardewModdingAPI.Framework.Content } else { + // mismatch: prefix has more beyond this, and this segment isn't an exact match + if (prefixParts.Remainder.Length != 0) + return false; + // mismatch: cur segment doesn't start with prefix if (!curParts.Current.StartsWith(prefixParts.Current, StringComparison.OrdinalIgnoreCase)) return false; From 61d6ec12daee843f758e5f828a713a72a767a94b Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 18 Oct 2022 20:03:28 -0500 Subject: [PATCH 09/35] add detailed manifest validation errors at build time --- src/SMAPI.ModBuildConfig/DeployModTask.cs | 33 ++++++- .../Framework/ModFileManager.cs | 12 --- .../Serialization/Models/Manifest.cs | 96 ++++++++++++++++++ src/SMAPI/Framework/ModLoading/ModResolver.cs | 98 ++----------------- 4 files changed, 138 insertions(+), 101 deletions(-) diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 88412d92..357e02b5 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -7,7 +7,10 @@ using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Newtonsoft.Json; using StardewModdingAPI.ModBuildConfig.Framework; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.ModBuildConfig @@ -75,6 +78,34 @@ namespace StardewModdingAPI.ModBuildConfig this.Log.LogMessage(MessageImportance.High, $"[mod build package] Handling build with options {string.Join(", ", properties)}"); } + // check if manifest file exists + FileInfo manifestFile = new(Path.Combine(this.ProjectDir, "manifest.json")); + if (!manifestFile.Exists) + { + this.Log.LogError("[mod build package] The mod does not have a manifest.json file."); + return false; + } + + // check if the json is valid + Manifest manifest; + try + { + new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest); + } catch (JsonReaderException ex) + { + // log the inner exception, otherwise the message will be generic + Exception exToShow = ex.InnerException ?? ex; + this.Log.LogError($"[mod build package] Failed to parse manifest.json: {exToShow.Message}"); + return false; + } + + // validate the manifest's fields + if (!manifest.TryValidate(out string error)) + { + this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}"); + return false; + } + if (!this.EnableModDeploy && !this.EnableModZip) return true; // nothing to do @@ -101,7 +132,7 @@ namespace StardewModdingAPI.ModBuildConfig // create release zip if (this.EnableModZip) { - string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModFolderName} {package.GetManifestVersion()}.zip"); + string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModFolderName} {manifest.Version}.zip"); string zipPath = Path.Combine(this.ModZipPath, zipName); this.Log.LogMessage(MessageImportance.High, $"[mod build package] Generating the release zip at {zipPath}..."); diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 80955f67..00f3f439 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Toolkit.Serialization; -using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.ModBuildConfig.Framework @@ -113,16 +111,6 @@ namespace StardewModdingAPI.ModBuildConfig.Framework return new Dictionary(this.Files, StringComparer.OrdinalIgnoreCase); } - /// Get a semantic version from the mod manifest. - /// The manifest is missing or invalid. - public string GetManifestVersion() - { - if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile) || !new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest manifest)) - throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor - - return manifest.Version.ToString(); - } - /********* ** Private methods diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 8a449f0a..4f84a60d 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; using System.Text; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Serialization.Models { @@ -103,6 +106,99 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models this.UpdateKeys = updateKeys ?? Array.Empty(); } + /// Try to validate a manifest's fields. Fails if any invalid field is found. + /// The error message to display to the user. + /// Returns whether the manifest was validated successfully. + public bool TryValidate(out string error) + { + // validate DLL / content pack fields + bool hasDll = !string.IsNullOrWhiteSpace(this.EntryDll); + bool isContentPack = this.ContentPackFor != null; + + // validate field presence + if (!hasDll && !isContentPack) + { + error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; + return false; + } + if (hasDll && isContentPack) + { + error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."; + return false; + } + + // validate DLL filename format + if (hasDll && this.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + error = $"manifest has invalid filename '{this.EntryDll}' for the EntryDLL field."; + return false; + } + + // validate content pack + else if (isContentPack) + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID)) + { + error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; + return false; + } + } + + // validate required fields + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(this.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (this.Version == null || this.Version.ToString() == "0.0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(this.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + { + error = $"manifest is missing required fields ({string.Join(", ", missingFields)})."; + return false; + } + } + + // validate ID format + if (!PathUtilities.IsSlug(this.UniqueID)) + { + error = "manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; + return false; + } + + // validate dependencies + foreach (IManifestDependency? dependency in this.Dependencies) + { + // null dependency + if (dependency == null) + { + error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}."; + return false; + } + + // missing ID + if (string.IsNullOrWhiteSpace(dependency.UniqueID)) + { + error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."; + return false; + } + + // invalid ID + if (!PathUtilities.IsSlug(dependency.UniqueID)) + { + error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; + return false; + } + } + + error = ""; + return true; + } + /// Override the update keys loaded from the mod info. /// The new update keys to set. internal void OverrideUpdateKeys(params string[] updateKeys) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fe56f4d2..352c22cc 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -8,7 +8,6 @@ using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Toolkit.Serialization.Models; -using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities.PathLookups; namespace StardewModdingAPI.Framework.ModLoading @@ -126,100 +125,23 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL / content pack fields + // check for dll if it's supposed to have one + if (!string.IsNullOrEmpty(mod.Manifest.EntryDll) && validateFilesExist) { - bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); - bool isContentPack = mod.Manifest.ContentPackFor != null; - - // validate field presence - if (!hasDll && !isContentPack) + IFileLookup pathLookup = getFileLookup(mod.DirectoryPath); + FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!); + if (!file.Exists) { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); - continue; - } - if (hasDll && isContentPack) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); - continue; - } - - // validate DLL - if (hasDll) - { - // invalid filename format - if (mod.Manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } - - // file doesn't exist - if (validateFilesExist) - { - IFileLookup pathLookup = getFileLookup(mod.DirectoryPath); - FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!); - if (!file.Exists) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; - } - } - } - - // validate content pack - else - { - // invalid content pack ID - if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor!.UniqueID)) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); - continue; - } - } - } - - // validate required fields - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } } - // validate ID format - if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); - - // validate dependencies - foreach (IManifestDependency? dependency in mod.Manifest.Dependencies) + // validate manifest + if (mod.Manifest is Manifest manifest && !manifest.TryValidate(out string manifestError)) { - // null dependency - if (dependency == null) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a null entry under {nameof(IManifest.Dependencies)}."); - continue; - } - - // missing ID - if (string.IsNullOrWhiteSpace(dependency.UniqueID)) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."); - continue; - } - - // invalid ID - if (!PathUtilities.IsSlug(dependency.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}"); + continue; } } From 55eec58eafb9ba07f3e8b0a1c8394cb114de17a0 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 19 Oct 2022 10:21:19 -0500 Subject: [PATCH 10/35] simplify ContentPackFor validation check --- src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 4f84a60d..1607cf3e 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -134,15 +134,11 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models return false; } - // validate content pack - else if (isContentPack) + // validate content pack ID + else if (isContentPack && string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID)) { - // invalid content pack ID - if (string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID)) - { - error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; - return false; - } + error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; + return false; } // validate required fields From bb2fde18292352471501887013ca2b7f60a9dc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?= Date: Wed, 9 Nov 2022 17:25:25 +0100 Subject: [PATCH 11/35] Added ModsToLoadFirst/Last to SMAPI config, along with the implementation --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 22 ++++++++++++++----- src/SMAPI/Framework/Models/SConfig.cs | 12 +++++++++- src/SMAPI/Framework/SCore.cs | 10 ++++++++- src/SMAPI/SMAPI.config.json | 12 +++++++++- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fe56f4d2..b90f9ba5 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -245,7 +245,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. /// Handles access to SMAPI's internal mod metadata list. - public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase) + /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. + public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadFirst, IReadOnlyList modIdsToLoadLast, ModDatabase modDatabase) { // initialize metadata mods = mods.ToArray(); @@ -260,8 +262,18 @@ namespace StardewModdingAPI.Framework.ModLoading } // sort mods - foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List()); + IModMetadata[] allMods = mods.ToArray(); + IModMetadata[] modsToLoadFirst = allMods.Where(m => modIdsToLoadFirst.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadLast = allMods.Where(m => modIdsToLoadLast.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadFirst.Contains(m) && !modsToLoadLast.Contains(m)).ToArray(); + + List orderSortedMods = new(); + orderSortedMods.AddRange(modsToLoadFirst); + orderSortedMods.AddRange(modsToLoadAsUsual); + orderSortedMods.AddRange(modsToLoadLast); + + foreach (IModMetadata mod in orderSortedMods) + this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List()); return sortedMods.Reverse(); } @@ -278,7 +290,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The list in which to save mods sorted by dependency order. /// The current change of mod dependencies. /// Returns the mod dependency status. - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) + private ModDependencyStatus ProcessDependencies(IReadOnlyList mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) { // check if already visited switch (states[mod]) @@ -409,7 +421,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// Get the dependencies declared in a manifest. /// The mod manifest. /// The loaded mods. - private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + private IEnumerable GetDependenciesFrom(IManifest manifest, IReadOnlyList loadedMods) { IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id)); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index bceb0940..ddd721d5 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -82,6 +82,12 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public HashSet SuppressUpdateChecks { get; set; } + /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. + public List ModsToLoadFirst { get; set; } + + /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. + public List ModsToLoadLast { get; set; } + /******** ** Public methods @@ -100,7 +106,9 @@ namespace StardewModdingAPI.Framework.Models /// The colors to use for text written to the SMAPI console. /// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks) + /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadFirst, string[]? modsToLoadLast) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -115,6 +123,8 @@ namespace StardewModdingAPI.Framework.Models this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + this.ModsToLoadFirst = new List(modsToLoadFirst ?? Array.Empty()); + this.ModsToLoadLast = new List(modsToLoadLast ?? Array.Empty()); } /// Override the value of . diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 40979b09..7bd60490 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,9 +423,17 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); mods = mods.Where(p => !p.IsIgnored).ToArray(); + // warn about mods that should load first or last which are not found at all + foreach (string modId in this.Settings.ModsToLoadFirst) + if (!mods.Any(m => m.Manifest.UniqueID == modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load first, but it could not be found.", LogLevel.Warn); + foreach (string modId in this.Settings.ModsToLoadLast) + if (!mods.Any(m => m.Manifest.UniqueID == modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load last, but it could not be found.", LogLevel.Warn); + // load mods resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); - mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); + mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadFirst, this.Settings.ModsToLoadLast, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // check for software likely to cause issues diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 635e3add..1a342df2 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -141,5 +141,15 @@ copy all the settings, or you may cause bugs due to overridden changes in future "SMAPI.ConsoleCommands", "SMAPI.ErrorHandler", "SMAPI.SaveBackup" - ] + ], + + /** + * The mod IDs SMAPI should try to load first, before any other mods not included in this list. + */ + "ModsToLoadFirst": [], + + /** + * The mod IDs SMAPI should try to load last, after all other mods not included in this list. + */ + "ModsToLoadLast": [] } From 42b4b6b6a4ae1bb59182857b383539b24b063215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?= Date: Wed, 9 Nov 2022 19:50:32 +0100 Subject: [PATCH 12/35] Renamed first/last to early/late; ignoring mods declared as both and warning about those --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 ++++++++-------- src/SMAPI/Framework/Models/SConfig.cs | 18 +++++++++--------- src/SMAPI/Framework/SCore.cs | 15 +++++++++------ src/SMAPI/SMAPI.config.json | 8 ++++---- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b90f9ba5..f9ba73c4 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -245,9 +245,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. /// Handles access to SMAPI's internal mod metadata list. - /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. - /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. - public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadFirst, IReadOnlyList modIdsToLoadLast, ModDatabase modDatabase) + /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadEarly, IReadOnlyList modIdsToLoadLate, ModDatabase modDatabase) { // initialize metadata mods = mods.ToArray(); @@ -263,14 +263,14 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods IModMetadata[] allMods = mods.ToArray(); - IModMetadata[] modsToLoadFirst = allMods.Where(m => modIdsToLoadFirst.Contains(m.Manifest.UniqueID)).ToArray(); - IModMetadata[] modsToLoadLast = allMods.Where(m => modIdsToLoadLast.Contains(m.Manifest.UniqueID)).ToArray(); - IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadFirst.Contains(m) && !modsToLoadLast.Contains(m)).ToArray(); + IModMetadata[] modsToLoadEarly = allMods.Where(m => modIdsToLoadEarly.Contains(m.Manifest.UniqueID) && !modIdsToLoadLate.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadLate = allMods.Where(m => modIdsToLoadLate.Contains(m.Manifest.UniqueID) && !modIdsToLoadEarly.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray(); List orderSortedMods = new(); - orderSortedMods.AddRange(modsToLoadFirst); + orderSortedMods.AddRange(modsToLoadEarly); orderSortedMods.AddRange(modsToLoadAsUsual); - orderSortedMods.AddRange(modsToLoadLast); + orderSortedMods.AddRange(modsToLoadLate); foreach (IModMetadata mod in orderSortedMods) this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List()); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index ddd721d5..40d3450f 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public HashSet SuppressUpdateChecks { get; set; } - /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. - public List ModsToLoadFirst { get; set; } + /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. + public List ModsToLoadEarly { get; set; } - /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. - public List ModsToLoadLast { get; set; } + /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + public List ModsToLoadLate { get; set; } /******** @@ -106,9 +106,9 @@ namespace StardewModdingAPI.Framework.Models /// The colors to use for text written to the SMAPI console. /// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. - /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadFirst, string[]? modsToLoadLast) + /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -123,8 +123,8 @@ namespace StardewModdingAPI.Framework.Models this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); - this.ModsToLoadFirst = new List(modsToLoadFirst ?? Array.Empty()); - this.ModsToLoadLast = new List(modsToLoadLast ?? Array.Empty()); + this.ModsToLoadEarly = new List(modsToLoadEarly ?? Array.Empty()); + this.ModsToLoadLate = new List(modsToLoadLate ?? Array.Empty()); } /// Override the value of . diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 7bd60490..9e91924e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,17 +423,20 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); mods = mods.Where(p => !p.IsIgnored).ToArray(); - // warn about mods that should load first or last which are not found at all - foreach (string modId in this.Settings.ModsToLoadFirst) + // warn about mods that should load early or late which are not found at all, or both + foreach (string modId in this.Settings.ModsToLoadEarly) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load first, but it could not be found.", LogLevel.Warn); - foreach (string modId in this.Settings.ModsToLoadLast) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found.", LogLevel.Warn); + foreach (string modId in this.Settings.ModsToLoadLate) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load last, but it could not be found.", LogLevel.Warn); + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found.", LogLevel.Warn); + foreach (string modId in this.Settings.ModsToLoadEarly) + if (this.Settings.ModsToLoadLate.Contains(modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn); // load mods resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); - mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadFirst, this.Settings.ModsToLoadLast, modDatabase).ToArray(); + mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // check for software likely to cause issues diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 1a342df2..68645d24 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -144,12 +144,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future ], /** - * The mod IDs SMAPI should try to load first, before any other mods not included in this list. + * The mod IDs SMAPI should try to load early, before any other mods not included in this list. */ - "ModsToLoadFirst": [], + "ModsToLoadEarly": [], /** - * The mod IDs SMAPI should try to load last, after all other mods not included in this list. + * The mod IDs SMAPI should try to load late, after all other mods not included in this list. */ - "ModsToLoadLast": [] + "ModsToLoadLate": [] } From 9fd8c35b462bc19efb520da21cda66f83559a66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?= Date: Wed, 9 Nov 2022 20:26:50 +0100 Subject: [PATCH 13/35] Actually taking order into consideration --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 14 ++++++++++++-- src/SMAPI/Framework/SCore.cs | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index f9ba73c4..8ef5e4a8 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -263,8 +263,18 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods IModMetadata[] allMods = mods.ToArray(); - IModMetadata[] modsToLoadEarly = allMods.Where(m => modIdsToLoadEarly.Contains(m.Manifest.UniqueID) && !modIdsToLoadLate.Contains(m.Manifest.UniqueID)).ToArray(); - IModMetadata[] modsToLoadLate = allMods.Where(m => modIdsToLoadLate.Contains(m.Manifest.UniqueID) && !modIdsToLoadEarly.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadEarly = modIdsToLoadEarly + .Where(modId => !modIdsToLoadLate.Contains(modId)) + .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId)) + .Where(m => m != null) + .Select(m => m!) + .ToArray(); + IModMetadata[] modsToLoadLate = modIdsToLoadLate + .Where(modId => !modIdsToLoadEarly.Contains(modId)) + .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId)) + .Where(m => m != null) + .Select(m => m!) + .ToArray(); IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray(); List orderSortedMods = new(); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9e91924e..4d1eb959 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -426,10 +426,10 @@ namespace StardewModdingAPI.Framework // warn about mods that should load early or late which are not found at all, or both foreach (string modId in this.Settings.ModsToLoadEarly) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found.", LogLevel.Warn); + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found or was skipped.", LogLevel.Warn); foreach (string modId in this.Settings.ModsToLoadLate) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found.", LogLevel.Warn); + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn); foreach (string modId in this.Settings.ModsToLoadEarly) if (this.Settings.ModsToLoadLate.Contains(modId)) this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn); From beb0b0aaf4e349cf911504a72b484d5642f6ac58 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 9 Nov 2022 20:03:41 -0500 Subject: [PATCH 14/35] fix & improve split-screen column in log parser --- docs/release-notes.md | 4 ++++ src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 4 ++++ .../Framework/LogParsing/Models/ParsedLog.cs | 3 +++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 +++--- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 11 ++--------- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index a8ddb0a0..e140b13b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,10 @@ _If needed, you can update to SMAPI 3.16.0 first and then install the latest version._ --> +## Upcoming release +* For the web UI: + * Fixed log parser not showing screen IDs in split-screen mode, and improved screen display. + ## 3.17.2 Released 21 October 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 5e0dedf3..c39e612b 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -108,6 +108,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing } } + // detect split-screen mode + if (message.ScreenId != 0) + log.IsSplitScreen = true; + // collect SMAPI metadata if (message.Mod == "SMAPI") { diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs index cda0f653..2a2e5f5c 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -22,6 +22,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// The raw log text. public string? RawText { get; set; } + /// Whether there are messages from multiple screens in the log. + public bool IsSplitScreen { get; set; } + /**** ** Log data ****/ diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 28127903..c1251c21 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -47,7 +47,7 @@ - +