diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs index 67ac85d7..ad75c62b 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs @@ -59,14 +59,14 @@ public abstract partial class DiagnosticVerifier /// The Diagnostics found by the compiler after running the analyzer on the source code /// The analyzer that was being run on the sources /// Diagnostic Results that should have appeared in the code - private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) + private static void VerifyDiagnosticResults(IReadOnlyList actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) { int expectedCount = expectedResults.Count(); int actualCount = actualResults.Count(); if (expectedCount != actualCount) { - string diagnosticsOutput = actualResults.Any() ? DiagnosticVerifier.FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; + string diagnosticsOutput = actualResults.Any() ? DiagnosticVerifier.FormatDiagnostics(analyzer, actualResults) : " NONE."; Assert.Fail( string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); @@ -89,17 +89,17 @@ public abstract partial class DiagnosticVerifier else { DiagnosticVerifier.VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); - var additionalLocations = actual.AdditionalLocations.ToArray(); + var additionalLocations = actual.AdditionalLocations; - if (additionalLocations.Length != expected.Locations.Length - 1) + if (additionalLocations.Count != expected.Locations.Length - 1) { Assert.Fail( string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", - expected.Locations.Length - 1, additionalLocations.Length, + expected.Locations.Length - 1, additionalLocations.Count, DiagnosticVerifier.FormatDiagnostics(analyzer, actual))); } - for (int j = 0; j < additionalLocations.Length; ++j) + for (int j = 0; j < additionalLocations.Count; ++j) { DiagnosticVerifier.VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); } @@ -176,10 +176,10 @@ public abstract partial class DiagnosticVerifier /// The analyzer that this verifier tests /// The Diagnostics to be formatted /// The Diagnostics formatted as a string - private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) + private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params IReadOnlyList diagnostics) { var builder = new StringBuilder(); - for (int i = 0; i < diagnostics.Length; ++i) + for (int i = 0; i < diagnostics.Count; ++i) { builder.AppendLine("// " + diagnostics[i]); @@ -210,7 +210,7 @@ public abstract partial class DiagnosticVerifier rule.Id); } - if (i != diagnostics.Length - 1) + if (i != diagnostics.Count - 1) { builder.Append(','); } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index db11f16b..e6735814 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -49,7 +49,7 @@ internal class ListItemsCommand : ConsoleCommand // handle SearchableItem[] matches = ( - from item in this.GetItems(args.ToArray()) + from item in this.GetItems(args) orderby item.Type, item.Name select item ) @@ -67,7 +67,7 @@ internal class ListItemsCommand : ConsoleCommand *********/ /// Get all items which can be searched and added to the player's inventory through the console. /// The search string to find. - private IEnumerable GetItems(string[] searchWords) + private IEnumerable GetItems(IReadOnlyList searchWords) { // normalize search term searchWords = searchWords.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 7dab4910..73b0e398 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -86,7 +86,7 @@ internal class ClearCommand : ConsoleCommand case "debris": { int removed = 0; - foreach (var pair in location.terrainFeatures.Pairs.ToArray()) + foreach (var pair in location.terrainFeatures.Pairs) { TerrainFeature feature = pair.Value; if (feature is HoeDirt dirt && dirt.crop?.dead.Value is true) @@ -188,18 +188,7 @@ internal class ClearCommand : ConsoleCommand /// Returns the number of removed entities. private int RemoveTerrainFeatures(GameLocation location, Func shouldRemove) { - int removed = 0; - - foreach ((Vector2 tile, TerrainFeature? feature) in location.terrainFeatures.Pairs.ToArray()) - { - if (shouldRemove(feature)) - { - location.terrainFeatures.Remove(tile); - removed++; - } - } - - return removed; + return location.terrainFeatures.RemoveWhere(pair => shouldRemove(pair.Value)); } /// Remove large terrain features from a location matching a lambda. @@ -208,18 +197,7 @@ internal class ClearCommand : ConsoleCommand /// Returns the number of removed entities. private int RemoveLargeTerrainFeatures(GameLocation location, Func shouldRemove) { - int removed = 0; - - foreach (LargeTerrainFeature feature in location.largeTerrainFeatures.ToArray()) - { - if (shouldRemove(feature)) - { - location.largeTerrainFeatures.Remove(feature); - removed++; - } - } - - return removed; + return location.largeTerrainFeatures.RemoveWhere(shouldRemove); } /// Remove resource clumps from a location matching a lambda. @@ -228,15 +206,7 @@ internal class ClearCommand : ConsoleCommand /// Returns the number of removed entities. private int RemoveResourceClumps(GameLocation location, Func shouldRemove) { - int removed = 0; - - foreach (ResourceClump clump in location.resourceClumps.Where(shouldRemove).ToArray()) - { - location.resourceClumps.Remove(clump); - removed++; - } - - return removed; + return location.resourceClumps.RemoveWhere(shouldRemove); } /// Remove furniture from a location matching a lambda. @@ -245,17 +215,6 @@ internal class ClearCommand : ConsoleCommand /// Returns the number of removed entities. private int RemoveFurniture(GameLocation location, Func shouldRemove) { - int removed = 0; - - foreach (Furniture furniture in location.furniture.ToArray()) - { - if (shouldRemove(furniture)) - { - location.furniture.Remove(furniture); - removed++; - } - } - - return removed; + return location.furniture.RemoveWhere(shouldRemove); } } diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 65075f05..6314c843 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -133,7 +133,7 @@ public class ModResolverTests [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false); + new ModResolver().ValidateManifests([], apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] diff --git a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs index e624cdf9..65283419 100644 --- a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs +++ b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs @@ -35,7 +35,7 @@ internal class ChangeDescriptorTests ]; // act - ChangeDescriptor parsed = ChangeDescriptor.Parse(rawDescriptor, out string[] errors); + ChangeDescriptor parsed = ChangeDescriptor.Parse(rawDescriptor, out IReadOnlyList errors); // assert parsed.Add.Should().BeEquivalentTo(expectedAdd); @@ -65,7 +65,7 @@ internal class ChangeDescriptorTests // act ChangeDescriptor parsed = ChangeDescriptor.Parse( rawDescriptor, - out string[] errors, + out IReadOnlyList errors, formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw @@ -112,7 +112,7 @@ internal class ChangeDescriptorTests [TestCase("Nexus:2400, Nexus:2401, Nexus:B,Chucklefish:14", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:2401, Nexus:B, Nexus:A")] public string Apply_Raw(string input, string? descriptor) { - ChangeDescriptor parsed = ChangeDescriptor.Parse(descriptor, out string[] errors); + ChangeDescriptor parsed = ChangeDescriptor.Parse(descriptor, out IReadOnlyList errors); errors.Should().BeEmpty(); @@ -129,7 +129,7 @@ internal class ChangeDescriptorTests [TestCase("+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "+Nexus:A, +Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A → Nexus:B")] public string ToString(string? descriptor) { - var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors); + var parsed = ChangeDescriptor.Parse(descriptor, out IReadOnlyList errors); errors.Should().BeEmpty(); diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs index 29daaf56..eb093ad6 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs @@ -141,7 +141,7 @@ public class ChangeDescriptor /// The raw change descriptor. /// The human-readable error message describing any invalid values that were ignored. /// Format a raw value into a normalized form if needed. - public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func? formatValue = null) + public static ChangeDescriptor Parse(string? descriptor, out IReadOnlyList errors, Func? formatValue = null) { var parsed = new ChangeDescriptor(formatValue ?? (p => p)); @@ -194,7 +194,7 @@ public class ChangeDescriptor } } - errors = rawErrors.ToArray(); + errors = rawErrors; } else errors = []; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs index 96e0c16d..c43eed24 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; using StardewModdingAPI.Toolkit.Utilities; @@ -55,7 +54,7 @@ public class ModSearchModel /// Whether to include extended metadata for each mod. public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata) { - this.Mods = mods.ToArray(); + this.Mods = mods; this.ApiVersion = apiVersion; this.GameVersion = gameVersion; this.Platform = platform; diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs index ec9749d6..221fa776 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -27,9 +27,9 @@ public class ModDatabase /// Construct an instance. /// The underlying mod data records indexed by default display name. /// Get an update URL for an update key (if valid). - public ModDatabase(IEnumerable records, Func getUpdateUrl) + public ModDatabase(ModDataRecord[] records, Func getUpdateUrl) { - this.Records = records.ToArray(); + this.Records = records; this.GetUpdateUrl = getUpdateUrl; } diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 306ed64c..03bd482e 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -127,7 +127,9 @@ public class ModScanner // set appropriate invalid-mod error if (manifestFile == null) { - FileInfo[] files = this.RecursivelyGetFiles(searchFolder).ToArray(); + List files = []; + this.RecursivelyGetFiles(searchFolder, files); + FileInfo[] relevantFiles = files.Where(this.IsRelevant).ToArray(); // empty Vortex folder @@ -293,7 +295,8 @@ public class ModScanner /// Recursively get all files in a folder. /// The root folder to search. - private IEnumerable RecursivelyGetFiles(DirectoryInfo folder) + /// The list of files to populate. + private void RecursivelyGetFiles(DirectoryInfo folder, List files) { foreach (FileSystemInfo entry in folder.GetFileSystemInfos()) { @@ -301,13 +304,10 @@ public class ModScanner continue; if (entry is FileInfo file) - yield return file; + files.Add(file); if (entry is DirectoryInfo subfolder) - { - foreach (FileInfo subfolderFile in this.RecursivelyGetFiles(subfolder)) - yield return subfolderFile; - } + this.RecursivelyGetFiles(subfolder, files); } } diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index ab62b03c..8e69eb95 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -73,7 +73,7 @@ internal class ChucklefishClient : IChucklefishClient name = name.Substring("[SMAPI]".Length).TrimStart(); // return info - return page.SetInfo(name: name, version: version, url: url, downloads: Array.Empty()); + return page.SetInfo(name: name, version: version, url: url, downloads: []); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index b41413be..e433c2f0 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -173,7 +173,7 @@ internal class CurseForgeClient : ICurseForgeClient } // return info - return this.InitModPage(id).SetInfo(name: mod.Name, version: null, url: mod.Links.WebsiteUrl, downloads: downloads); + return this.InitModPage(id).SetInfo(name: mod.Name, version: null, url: mod.Links.WebsiteUrl, downloads: downloads.ToArray()); } /// Get a raw version string for a mod file, if available. diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 19a42954..36356350 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Clients; @@ -60,12 +58,12 @@ internal class GenericModPage : IModPage /// The mod's semantic version number. /// The mod's web URL. /// The mod downloads. - public IModPage SetInfo(string name, string? version, string url, IEnumerable downloads) + public IModPage SetInfo(string name, string? version, string url, IModDownload[] downloads) { this.Name = name; this.Version = version; this.Url = url; - this.Downloads = downloads.ToArray(); + this.Downloads = downloads; this.Status = RemoteModStatus.Ok; return this; diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs index a1376e24..d0db2b7a 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs @@ -158,7 +158,7 @@ internal class ModDropClient : IModDropClient // return info return this .InitModPage(id) - .SetInfo(name: mod.Mod.Title, version: null, url: this.GetDefaultModPageUrl(id), downloads: downloads); + .SetInfo(name: mod.Mod.Title, version: null, url: this.GetDefaultModPageUrl(id), downloads: downloads.ToArray()); } /// Extract the version number from a ModDrop file's info, if possible. diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index e73744e2..2b193017 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -59,7 +58,7 @@ internal interface IModPage /// The mod's semantic version number. /// The mod's web URL. /// The mod downloads. - IModPage SetInfo(string name, string? version, string url, IEnumerable downloads); + IModPage SetInfo(string name, string? version, string url, IModDownload[] downloads); /// Set a mod fetch error. /// The mod availability status on the remote site. diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 6d8c108c..ad720faa 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -87,7 +87,7 @@ public class ModModel public ModModel(ModCompatibilityEntry entry) { // basic info - this.Id = entry.ID.ToArray(); + this.Id = entry.ID; this.Name = entry.Name.FirstOrDefault(); this.AlternateNames = entry.Name.Length > 1 ? string.Join(", ", entry.Name.Skip(1)) : null; this.Author = entry.Author.FirstOrDefault(); diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 89012a19..a7d1deec 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -325,7 +325,7 @@ public static class Constants removeAssemblyReferences.Add("StardewValley"); targetAssemblies.Add(typeof(StardewValley.Game1).Assembly); - return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray()); + return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 78ae466c..e3394082 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -111,7 +111,7 @@ internal class CommandManager // parse input args = ArgUtility.SplitBySpaceQuoteAware(input); name = this.GetNormalizedName(args[0])!; - args = args.Skip(1).ToArray(); + args = ArgUtility.GetSubsetOf(args, startAt: 1); // get screen ID argument screenId = 0; diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index f318f335..57c3baae 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -92,7 +92,7 @@ internal class HarmonySummaryCommand : IInternalCommand } // matches individual patchers - foreach ((string patcherId, ISet patchTypes) in patch.PatchTypesByOwner.ToArray()) + foreach ((string patcherId, ISet patchTypes) in patch.PatchTypesByOwner) { if (!IsMatch(patcherId) && !patchTypes.Any(type => IsMatch(type.ToString()))) patch.PatchTypesByOwner.Remove(patcherId); @@ -120,13 +120,13 @@ internal class HarmonySummaryCommand : IInternalCommand }; // get patch types by owner - var typesByOwner = new Dictionary>(); + var typesByOwner = new Dictionary>(); foreach ((PatchType type, IReadOnlyCollection patches) in patchGroups) { foreach (Patch patch in patches) { - if (!typesByOwner.TryGetValue(patch.owner, out ISet? patchTypes)) - typesByOwner[patch.owner] = patchTypes = new HashSet(); + if (!typesByOwner.TryGetValue(patch.owner, out HashSet? patchTypes)) + typesByOwner[patch.owner] = patchTypes = []; patchTypes.Add(type); } } @@ -165,7 +165,7 @@ internal class HarmonySummaryCommand : IInternalCommand public string MethodDescription { get; } /// The patch types by the Harmony instance ID that added them. - public IDictionary> PatchTypesByOwner { get; } + public Dictionary> PatchTypesByOwner { get; } /********* @@ -174,7 +174,7 @@ internal class HarmonySummaryCommand : IInternalCommand /// Construct an instance. /// The patched method. /// The patch types by the Harmony instance ID that added them. - public SearchResult(MethodBase method, IDictionary> patchTypesByOwner) + public SearchResult(MethodBase method, Dictionary> patchTypesByOwner) { this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}"; this.MethodDescription = method.FullDescription(); diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs index 59bd34b4..891130cf 100644 --- a/src/SMAPI/Framework/Commands/HelpCommand.cs +++ b/src/SMAPI/Framework/Commands/HelpCommand.cs @@ -83,12 +83,11 @@ internal class HelpCommand : IInternalCommand foreach (var group in groups) { string modName = group.Key; - string[] commandNames = group.ToArray(); message += $""" {modName}: - {string.Join("\n ", commandNames)} + {string.Join("\n ", group)} """; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index cf241c33..de7150dc 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -55,7 +55,7 @@ internal class ContentCoordinator : IDisposable private readonly Action OnAssetLoaded; /// A callback to invoke when any asset names have been invalidated from the cache. - private readonly Action> OnAssetsInvalidated; + private readonly Action> OnAssetsInvalidated; /// Get the load/edit operations to apply to an asset by querying registered event handlers. private readonly Func RequestAssetOperations; @@ -116,7 +116,7 @@ internal class ContentCoordinator : IDisposable /// 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, Multiplayer multiplayer, 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)); @@ -446,7 +446,7 @@ internal class ContentCoordinator : IDisposable this.AssetOperationsByKey.Remove(name); // raise event - this.OnAssetsInvalidated(invalidatedAssets.Keys.ToArray()); + this.OnAssetsInvalidated(invalidatedAssets.Keys); // propagate changes to the game this.CoreAssets.Propagate( @@ -460,12 +460,12 @@ internal class ContentCoordinator : IDisposable // log summary StringBuilder report = new(); { - IAssetName[] invalidatedKeys = invalidatedAssets.Keys.ToArray(); + ICollection invalidatedKeys = invalidatedAssets.Keys; IAssetName[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.Select(p => p.Name).OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); - report.AppendLine($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)})."); + report.AppendLine($"Invalidated {invalidatedKeys.Count} asset names ({FormatKeyList(invalidatedKeys)})."); report.AppendLine(propagated.Count > 0 ? $"Propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})." : "Propagated 0 core assets." diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 94ec24d6..1ec60507 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -228,8 +228,7 @@ internal class GameContentManager : BaseContentManager } // edit asset - AssetEditOperation[] editors = editOperations.OrderBy(p => p.Priority).ToArray(); - foreach (AssetEditOperation editor in editors) + foreach (AssetEditOperation editor in editOperations.OrderBy(p => p.Priority)) { IModMetadata mod = editor.Mod; diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 59edad26..4d47353e 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -281,7 +281,7 @@ internal class LogManager : IDisposable /// Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access. /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. /// Whether Harmony was fixed to work with Stardew Valley. - public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, IModMetadata[] loadedMods, IModMetadata[] skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix) + public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, IModMetadata[] loadedMods, IReadOnlyList skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix) { // report malicious mod foreach (IModMetadata mod in skippedMods) @@ -388,7 +388,7 @@ internal class LogManager : IDisposable /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. /// Whether Harmony was fixed to work with Stardew Valley. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifests aren't guaranteed non-null at this point in the loading process.")] - private void LogModWarnings(IEnumerable mods, IModMetadata[] skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix) + private void LogModWarnings(IEnumerable mods, IReadOnlyList skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix) { // get mods with warnings IModMetadata[] modsWithWarnings = mods @@ -526,7 +526,7 @@ internal class LogManager : IDisposable /// Group failed mods by the priority players should update them, where mods in earlier groups are more likely to fix multiple mods. /// The failed mods to group. - private IEnumerable> GroupFailedModsByPriority(IList failedMods) + private IEnumerable> GroupFailedModsByPriority(IReadOnlyList failedMods) { var failedOthers = failedMods.ToList(); var skippedModIds = new HashSet(from mod in failedMods where mod.HasId() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase); diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs index ae7dd1f8..6a14c576 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs @@ -63,9 +63,25 @@ internal class ReferenceToInvalidMemberFinder : BaseInstructionHandler // wrong return type if (methodDef != null && this.ShouldValidate(methodRef.DeclaringType)) { - MethodDefinition[]? candidateMethods = methodRef.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodRef.Name).ToArray(); - if (candidateMethods is { Length: > 0 } && candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {this.GetMemberDisplayName(methodDef)} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); + if (methodRef.DeclaringType.Resolve() is { } declaringType) + { + bool foundName = false; + bool foundMatch = true; + + foreach (MethodDefinition method in declaringType.Methods) + { + if (method.Name != methodRef.Name) + continue; + + foundName = true; + foundMatch = RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType); + if (foundMatch) + break; + } + + if (foundName && !foundMatch) + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {this.GetMemberDisplayName(methodDef)} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); + } } // missing diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index ed174ca1..39ea3a5c 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Collections.Generic; namespace StardewModdingAPI.Framework.ModLoading.Framework; @@ -141,8 +142,8 @@ internal static class RewriteHelper return false; Type[] defGenerics = type.GetGenericArguments(); - TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); - if (defGenerics.Length != refGenerics.Length) + Collection refGenerics = ((GenericInstanceType)reference).GenericArguments; + if (defGenerics.Length != refGenerics.Count) return false; for (int i = 0; i < defGenerics.Length; i++) { @@ -173,11 +174,11 @@ internal static class RewriteHelper if (!reference.IsGenericInstance) return false; - TypeReference[] defGenerics = ((GenericInstanceType)type).GenericArguments.ToArray(); - TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); - if (defGenerics.Length != refGenerics.Length) + Collection defGenerics = ((GenericInstanceType)type).GenericArguments; + Collection refGenerics = ((GenericInstanceType)reference).GenericArguments; + if (defGenerics.Count != refGenerics.Count) return false; - for (int i = 0; i < defGenerics.Length; i++) + for (int i = 0; i < defGenerics.Count; i++) { if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) return false; @@ -222,10 +223,10 @@ internal static class RewriteHelper // same arguments ParameterInfo[] definitionParameters = definition.GetParameters(); - ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); - if (referenceParameters.Length != definitionParameters.Length) + Collection referenceParameters = reference.Parameters; + if (referenceParameters.Count != definitionParameters.Length) return false; - for (int i = 0; i < referenceParameters.Length; i++) + for (int i = 0; i < referenceParameters.Count; i++) { if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) return false; @@ -247,11 +248,11 @@ internal static class RewriteHelper return false; // same arguments - ParameterDefinition[] definitionParameters = definition.Parameters.ToArray(); - ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); - if (referenceParameters.Length != definitionParameters.Length) + Collection definitionParameters = definition.Parameters; + Collection referenceParameters = reference.Parameters; + if (referenceParameters.Count != definitionParameters.Count) return false; - for (int i = 0; i < referenceParameters.Length; i++) + for (int i = 0; i < referenceParameters.Count; i++) { if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) return false; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9617f40a..e73e735e 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -85,10 +85,8 @@ internal class ModResolver /// Whether to validate that files referenced in the manifest (like ) exist on disk. This can be disabled to only validate the manifest itself. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifest values may be null before they're validated.")] [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "Manifest values may be null before they're validated.")] - public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Func getUpdateUrl, Func getFileLookup, bool validateFilesExist = true) + public void ValidateManifests(IModMetadata[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Func getUpdateUrl, Func getFileLookup, bool validateFilesExist = true) { - mods = mods.ToArray(); - // validate each manifest foreach (IModMetadata mod in mods) { @@ -223,7 +221,6 @@ internal class ModResolver public IEnumerable ProcessDependencies(IReadOnlyList mods, ModDatabase modDatabase) { // initialize metadata - mods = mods.ToArray(); var sortedMods = new Stack(); var states = mods.ToDictionary(mod => mod, _ => ModDependencyStatus.Queued); @@ -280,10 +277,10 @@ internal class ModResolver } // collect dependencies - ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + IReadOnlyList dependencies = this.GetDependenciesFrom(mod.Manifest, mods); // mark sorted if no dependencies - if (dependencies.Length == 0) + if (dependencies.Count == 0) { sortedMods.Push(mod); return states[mod] = ModDependencyStatus.Sorted; @@ -291,41 +288,58 @@ internal class ModResolver // mark failed if missing dependencies { - string[] failedModNames = ( - from entry in dependencies - where entry is { IsRequired: true, Mod: null } + List? failedModNames = null; - let dataEntry = modDatabase.Get(entry.ID) - where dataEntry?.IgnoreDependencies is not true + foreach (ModDependency entry in dependencies) + { + // skip if not a missing dependency + if (entry is not { IsRequired: true, Mod: null }) + continue; - let displayName = dataEntry?.DisplayName ?? entry.ID - let modUrl = modDatabase.GetModPageUrlFor(entry.ID) + // skip if marked ignore + ModDataRecord? dataEntry = modDatabase.Get(entry.ID); + if (dataEntry?.IgnoreDependencies is true) + continue; - orderby displayName - select modUrl != null + // track as failed + string displayName = dataEntry?.DisplayName ?? entry.ID; + string? modUrl = modDatabase.GetModPageUrlFor(entry.ID); + string formattedName = modUrl != null ? $"{displayName}: {modUrl}" - : displayName - ).ToArray(); - if (failedModNames.Length > 0) + : displayName; + + failedModNames ??= []; + failedModNames.Add(formattedName); + } + + if (failedModNames != null) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames.OrderBy(p => p))})."); return states[mod] = ModDependencyStatus.Failed; } } // dependency min version not met, mark failed { - string[] failedLabels = - ( - from entry in dependencies - where - entry is { Mod: not null, MinVersion: not null } - && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) - select $"{entry.Mod!.DisplayName} (needs {entry.MinVersion} or later)" - ) - .ToArray(); - if (failedLabels.Length > 0) + List? failedLabels = null; + + foreach (ModDependency entry in dependencies) + { + // skip if mod isn't loaded with a min version + if (entry is not { Mod: not null, MinVersion: not null }) + continue; + + // skip if version met + if (!entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version)) + continue; + + // track as failed + failedLabels ??= []; + failedLabels.Add($"{entry.Mod!.DisplayName} (needs {entry.MinVersion} or later)"); + } + + if (failedLabels != null) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); @@ -389,17 +403,32 @@ internal class ModResolver /// Get the dependencies declared in a manifest. /// The mod manifest. /// The loaded mods. - private IEnumerable GetDependenciesFrom(IManifest manifest, IReadOnlyList loadedMods) + private IReadOnlyList GetDependenciesFrom(IManifest manifest, IReadOnlyList loadedMods) { - IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasId(id)); + List? dependencies = null; // yield dependencies foreach (IManifestDependency entry in manifest.Dependencies) - yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired); + { + dependencies ??= []; + dependencies.Add(new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired)); + } // yield content pack parent if (manifest.ContentPackFor != null) - yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true); + { + dependencies ??= []; + dependencies.Add(new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true)); + } + + return dependencies != null + ? dependencies + : Array.Empty(); + + IModMetadata? FindMod(string id) + { + return loadedMods.FirstOrDefault(m => m.HasId(id)); + } } /// Get a technical message indicating why a mod's compatibility status was overridden, if applicable. diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index 9e8f71f3..da663ac1 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -20,13 +20,13 @@ internal class PlatformAssemblyMap : IDisposable public readonly Platform TargetPlatform; /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). - public readonly string[] RemoveNames; + public readonly IReadOnlyList RemoveNames; /**** ** Metadata ****/ /// The assemblies to target. Equivalent types should be rewritten to use these assemblies. - public readonly Assembly[] Targets; + public readonly IReadOnlyList Targets; /// An assembly => reference cache. public readonly IDictionary TargetReferences; @@ -42,7 +42,7 @@ internal class PlatformAssemblyMap : IDisposable /// The target game platform. /// The assembly short names to remove (like Stardew Valley). /// The assemblies to target. - public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) + public PlatformAssemblyMap(Platform targetPlatform, IReadOnlyList removeAssemblyNames, IReadOnlyList targetAssemblies) { // save data this.TargetPlatform = targetPlatform; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index fc5f3afa..fb6af4b0 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -349,14 +349,19 @@ internal class ReplaceReferencesRewriter : BaseInstructionHandler // resolve generic method to a specific implementation if (toMethod.DeclaringType?.IsGenericTypeDefinition is true && fromMember.DeclaringType is GenericInstanceType generic) { - Type?[] arguments = generic.GenericArguments.Select(RewriteHelper.GetCSharpType).ToArray(); - foreach (Type? argument in arguments) + // get generic argument types + Type[] genericArgTypes = new Type[generic.GenericArguments.Count]; + for (int i = 0; i < generic.GenericArguments.Count; i++) { - if (argument is null) + Type? type = RewriteHelper.GetCSharpType(generic.GenericArguments[i]); + if (type is null) return false; + + genericArgTypes[i] = type; } - MethodInfo? newMethod = toMethod.DeclaringType.MakeGenericType(arguments!)?.GetMethod(toMethod.Name); + // get method + MethodInfo? newMethod = toMethod.DeclaringType.MakeGenericType(genericArgTypes!)?.GetMethod(toMethod.Name); if (newMethod is null) return false; diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs index e89cd802..28c7a665 100644 --- a/src/SMAPI/Framework/Networking/ModMessageModel.cs +++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs @@ -1,4 +1,3 @@ -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -62,8 +61,8 @@ internal class ModMessageModel { this.FromPlayerId = message.FromPlayerId; this.FromModId = message.FromModId; - this.ToPlayerIds = message.ToPlayerIds?.ToArray(); - this.ToModIds = message.ToModIds?.ToArray(); + this.ToPlayerIds = message.ToPlayerIds; + this.ToModIds = message.ToModIds; this.Type = message.Type; this.Data = message.Data; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6543f17a..eb0ae72f 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -465,15 +465,15 @@ internal class SCore : IDisposable // log loose files { - string[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles().Select(p => p.Name).ToArray(); - if (looseFiles.Any()) + FileInfo[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles(); + if (looseFiles.Length > 0) { - if (looseFiles.Any(name => name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))) + if (looseFiles.Any(file => file.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) || file.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))) { this.Monitor.Log($"Detected mod files directly inside the '{Path.GetFileName(this.ModsPath)}' folder. These will be ignored. Each mod must have its own subfolder instead.", LogLevel.Error); } - this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}"); + this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.Select(p => p.Name).OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}"); } } @@ -481,9 +481,18 @@ internal class SCore : IDisposable IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modBlacklist, modDatabase, useCaseInsensitiveFilePaths: this.Settings.UseCaseInsensitivePaths).ToArray(); // filter out ignored mods - foreach (IModMetadata mod in mods.Where(p => p.IsIgnored)) - this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); - mods = mods.Where(p => !p.IsIgnored).ToArray(); + { + bool anyIgnored = false; + + foreach (IModMetadata mod in mods.Where(p => p.IsIgnored)) + { + anyIgnored = true; + this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); + } + + if (anyIgnored) + mods = mods.Where(p => !p.IsIgnored).ToArray(); + } // validate manifests resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GameVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); @@ -994,8 +1003,8 @@ internal class SCore : IDisposable // location list changes if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners || verbose)) { - var added = state.Locations.LocationList.Added.ToArray(); - var removed = state.Locations.LocationList.Removed.ToArray(); + var added = state.Locations.LocationList.Added; + var removed = state.Locations.LocationList.Removed; if (verbose) { @@ -1400,7 +1409,7 @@ internal class SCore : IDisposable /// A callback invoked after assets have been invalidated from the content cache. /// The invalidated asset names. - private void OnAssetsInvalidated(IList assetNames) + private void OnAssetsInvalidated(ICollection assetNames) { if (this.EventManager.AssetsInvalidated.HasListeners) this.EventManager.AssetsInvalidated.Raise(new AssetsInvalidatedEventArgs(assetNames, assetNames.Select(p => p.GetBaseAssetName()))); @@ -1730,7 +1739,7 @@ internal class SCore : IDisposable .GetUpdateKeys(validOnly: true) .Select(p => p.ToString()) .ToArray(); - searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, mod.Manifest.Version, updateKeys.ToArray(), isBroken: mod.Status == ModMetadataStatus.Failed)); + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, mod.Manifest.Version, updateKeys, isBroken: mod.Status == ModMetadataStatus.Failed)); } // fetch results @@ -1955,7 +1964,7 @@ internal class SCore : IDisposable this.Monitor.Log("Loading mods...", LogLevel.Debug); // load mods - IList skippedMods = new List(); + List skippedMods = []; using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods, this.Settings.LogTechnicalDetailsForBrokenMods)) { // init @@ -1981,7 +1990,7 @@ internal class SCore : IDisposable this.ModRegistry.AreAllModsLoaded = true; // log mod info - this.LogManager.LogModInfo(loaded, loadedContentPacks, loadedMods, skippedMods.ToArray(), this.Settings.ParanoidWarnings, this.Settings.LogTechnicalDetailsForBrokenMods, this.Settings.FixHarmony); + this.LogManager.LogModInfo(loaded, loadedContentPacks, loadedMods, skippedMods, this.Settings.ParanoidWarnings, this.Settings.LogTechnicalDetailsForBrokenMods, this.Settings.FixHarmony); // initialize translations this.ReloadTranslations(loaded); @@ -2458,12 +2467,12 @@ internal class SCore : IDisposable } // validate translations - foreach (string locale in translations.Keys.ToArray()) + foreach (string locale in translations.Keys) { // handle duplicates HashSet keys = new(StringComparer.OrdinalIgnoreCase); HashSet duplicateKeys = new(StringComparer.OrdinalIgnoreCase); - foreach (string key in translations[locale].Keys.ToArray()) + foreach (string key in translations[locale].Keys) { if (!keys.Add(key)) { diff --git a/src/SMAPI/Framework/SnapshotItemListDiff.cs b/src/SMAPI/Framework/SnapshotItemListDiff.cs index 89630d24..8e851b49 100644 --- a/src/SMAPI/Framework/SnapshotItemListDiff.cs +++ b/src/SMAPI/Framework/SnapshotItemListDiff.cs @@ -49,13 +49,24 @@ internal class SnapshotItemListDiff /// Returns whether anything changed. public static bool TryGetChanges(ISet added, ISet removed, IDictionary stackSizes, [NotNullWhen(true)] out SnapshotItemListDiff? changes) { - KeyValuePair[] sizesChanged = stackSizes.Where(p => p.Key.Stack != p.Value).ToArray(); - if (sizesChanged.Length > 0 || added.Count > 0 || removed.Count > 0) + // detect item stack size changes + List? sizesChanges = null; + foreach ((Item item, int oldStack) in stackSizes) + { + if (item.Stack != oldStack) + { + sizesChanges ??= []; + sizesChanges.Add(new ItemStackSizeChange(item, oldStack, item.Stack)); + } + } + + // build diff if needed + if (added.Count > 0 || removed.Count > 0 || sizesChanges != null) { changes = new SnapshotItemListDiff( added: added.ToArray(), removed: removed.ToArray(), - sizesChanged: sizesChanged.Select(p => new ItemStackSizeChange(p.Key, p.Value, p.Key.Stack)).ToArray() + sizesChanged: sizesChanges?.ToArray() ?? [] ); return true; } diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs index 1ffe6984..cda44527 100644 --- a/src/SMAPI/Framework/SnapshotListDiff.cs +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -24,10 +24,10 @@ internal class SnapshotListDiff public bool IsChanged { get; private set; } /// The removed values. - public IEnumerable Removed => this.RemovedImpl; + public IReadOnlyList Removed => this.RemovedImpl; /// The added values. - public IEnumerable Added => this.AddedImpl; + public IReadOnlyList Added => this.AddedImpl; /********* diff --git a/src/SMAPI/Integrations/GenericModConfigMenu/GenericModConfigMenuIntegration.cs b/src/SMAPI/Integrations/GenericModConfigMenu/GenericModConfigMenuIntegration.cs index 1c257d5c..d4b5e988 100644 --- a/src/SMAPI/Integrations/GenericModConfigMenu/GenericModConfigMenuIntegration.cs +++ b/src/SMAPI/Integrations/GenericModConfigMenu/GenericModConfigMenuIntegration.cs @@ -114,7 +114,7 @@ namespace StardewModdingAPI.Integrations.GenericModConfigMenu tooltip: () => this.Translations.Get("config.color-scheme.desc"), getValue: () => getConfig().ConsoleColorScheme.ToString(), setValue: value => this.StageOption(nameof(SConfig.ConsoleColorScheme), value), - allowedValues: Enum.GetValues().Select(p => p.ToString()).ToArray(), + allowedValues: Enum.GetNames(), formatAllowedValue: value => this.Translations.Get( value is nameof(MonitorColorScheme.AutoDetect) && Constants.Platform == Platform.Windows ? $"config.color-scheme.options.{value}.on-windows" diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index d810a955..50c14ab9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -581,29 +581,31 @@ internal class CoreAssetPropagator /// Returns whether any NPCs were updated. private bool UpdateNpcDialogue(IAssetName assetName) { - // get NPCs string name = Path.GetFileName(assetName.BaseName); - NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.IsVillager).ToArray(); - if (!villagers.Any()) - return false; // update dialogue // Note that marriage dialogue isn't reloaded after reset, but it doesn't need to be // propagated anyway since marriage dialogue keys can't be added/removed and the field // doesn't store the text itself. - foreach (NPC villager in villagers) + bool anyChanged = false; + foreach (NPC npc in this.GetCharacters()) { - bool shouldSayMarriageDialogue = villager.shouldSayMarriageDialogue.Value; - MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray(); + if (npc.Name != name || !npc.IsVillager) + continue; - villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue - villager.resetCurrentDialogue(); + bool shouldSayMarriageDialogue = npc.shouldSayMarriageDialogue.Value; + MarriageDialogueReference[] marriageDialogue = npc.currentMarriageDialogue.ToArray(); - villager.shouldSayMarriageDialogue.Set(shouldSayMarriageDialogue); - villager.currentMarriageDialogue.Set(marriageDialogue); + npc.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue + npc.resetCurrentDialogue(); + + npc.shouldSayMarriageDialogue.Set(shouldSayMarriageDialogue); + npc.currentMarriageDialogue.Set(marriageDialogue); + + anyChanged = true; } - return true; + return anyChanged; } /// Update the character data for matching NPCs. @@ -621,33 +623,36 @@ internal class CoreAssetPropagator /// Returns whether any NPCs were updated. private bool UpdateNpcSchedules(IAssetName assetName) { - // get NPCs string name = Path.GetFileName(assetName.BaseName); - NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.IsVillager).ToArray(); - if (!villagers.Any()) - return false; - // update schedule - foreach (NPC villager in villagers) + // update schedules + bool anyChanged = false; + foreach (NPC npc in this.GetCharacters()) { + if (npc.Name != name || !npc.IsVillager) + continue; + // reload schedule - this.Reflection.GetField(villager, "_hasLoadedMasterScheduleData").SetValue(false); - this.Reflection.GetField?>(villager, "_masterScheduleData").SetValue(null); - villager.TryLoadSchedule(); + this.Reflection.GetField(npc, "_hasLoadedMasterScheduleData").SetValue(false); + this.Reflection.GetField?>(npc, "_masterScheduleData").SetValue(null); + npc.TryLoadSchedule(); // switch to new schedule if needed - if (villager.Schedule != null) + if (npc.Schedule != null) { - int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); + int lastScheduleTime = npc.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); if (lastScheduleTime != 0) { - villager.queuedSchedulePaths.Clear(); - villager.lastAttemptedSchedule = 0; - villager.checkSchedule(lastScheduleTime); + npc.queuedSchedulePaths.Clear(); + npc.lastAttemptedSchedule = 0; + npc.checkSchedule(lastScheduleTime); } } + + anyChanged = true; } - return true; + + return anyChanged; } /// Update cached translations from the Strings\StringsFromCSFiles asset. diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs index ad7ad14a..bf3e7597 100644 --- a/src/SMAPI/Utilities/Keybind.cs +++ b/src/SMAPI/Utilities/Keybind.cs @@ -106,28 +106,53 @@ public class Keybind /// Get the keybind state relative to the previous tick. public SButtonState GetState() { -#pragma warning disable CS0618 // Type or member is obsolete: deliberate call to GetButtonState() for unit tests - SButtonState[] states = this.Buttons.Select(this.GetButtonState).Distinct().ToArray(); -#pragma warning restore CS0618 - - // single state - if (states.Length == 1) - return states[0]; - - // if any key has no state, the whole set wasn't enabled last tick - if (states.Contains(SButtonState.None)) + if (!this.IsBound) return SButtonState.None; - // mix of held + pressed => pressed - if (states.All(p => p is SButtonState.Pressed or SButtonState.Held)) - return SButtonState.Pressed; + bool anyPressed = false; + bool anyHeld = false; + bool anyReleased = false; - // mix of held + released => released - if (states.All(p => p is SButtonState.Held or SButtonState.Released)) - return SButtonState.Released; + foreach (SButton button in this.Buttons) + { +#pragma warning disable CS0618 // Type or member is obsolete: deliberate call to GetButtonState() for unit tests + switch (this.GetButtonState(button)) +#pragma warning restore CS0618 + { + case SButtonState.Pressed: + anyPressed = true; + break; - // not down last tick or now - return SButtonState.None; + case SButtonState.Held: + anyHeld = true; + break; + + case SButtonState.Released: + anyReleased = true; + break; + + case SButtonState.None: + default: + return SButtonState.None; // if any key has no state, then the whole set was inactive last + cur tick + } + } + + return (anyPressed, anyHeld, anyReleased) switch + { + // single state + (true, false, false) => SButtonState.Pressed, + (false, true, false) => SButtonState.Held, + (false, false, true) => SButtonState.Released, + + // held + pressed => pressed + (true, true, false) => SButtonState.Pressed, + + // held + released => released + (false, true, true) => SButtonState.Released, + + // any other combination => inactive (i.e. it wasn't fully down last tick or this tick) + _ => SButtonState.None + }; } /// Get a string representation of the keybind. @@ -136,6 +161,6 @@ public class Keybind { return this.Buttons.Length > 0 ? string.Join(" + ", this.Buttons) - : SButton.None.ToString(); + : nameof(SButton.None); } } diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 4fbbd0cd..379f476a 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace StardewModdingAPI.Utilities; @@ -54,7 +53,7 @@ public class PerScreen public IEnumerable> GetActiveValues() { this.RemoveDeadScreens(); - return this.States.ToArray(); + return this.States; } /// Get the value for a given screen ID, creating it if needed. @@ -106,7 +105,7 @@ public class PerScreen /// Returns whether a screen ID should be removed. private void RemoveScreens(Func shouldRemove) { - foreach (var pair in this.States.ToArray()) + foreach (var pair in this.States) { if (shouldRemove(pair.Key)) this.States.Remove(pair.Key);