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