From db59d384b97716a04b92689efc16f0742d999e64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 Mar 2026 22:42:35 -0400 Subject: [PATCH] avoid `Where()` enumerators when possible --- src/SMAPI.Installer/InteractiveInstaller.cs | 5 +- .../Framework/ModScanning/ModScanner.cs | 5 +- .../Caching/Mods/ModCacheMemoryRepository.cs | 15 ++--- src/SMAPI/Framework/ContentCoordinator.cs | 5 +- src/SMAPI/Framework/Logging/LogManager.cs | 6 +- .../ModLoading/Framework/RecursiveRewriter.cs | 13 +++-- src/SMAPI/Framework/ModLoading/ModResolver.cs | 9 ++- .../Rewriters/HeuristicMethodRewriter.cs | 5 +- src/SMAPI/Framework/SCore.cs | 55 +++++++++++++------ src/SMAPI/Framework/SMultiplayer.cs | 10 +++- .../Framework/StateTracking/ChestTracker.cs | 11 ++-- .../Framework/StateTracking/PlayerTracker.cs | 30 ++++++---- 12 files changed, 110 insertions(+), 59 deletions(-) diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 306a17ad..ed2b6141 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -377,8 +377,11 @@ internal class InteractiveInstaller { // copy SMAPI files to game dir this.PrintDebug("Adding SMAPI files..."); - foreach (FileSystemInfo sourceEntry in paths.BundleDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) + foreach (FileSystemInfo sourceEntry in paths.BundleDir.EnumerateFileSystemInfos()) { + if (!this.ShouldCopy(sourceEntry)) + continue; + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name), allowUserInput); this.RecursiveCopy(sourceEntry, paths.GameDir); } diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 03bd482e..42f4cf34 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -329,8 +329,11 @@ public class ModScanner { bool hasXnbFile = false; - foreach (FileInfo file in files.Where(this.IsRelevant)) + foreach (FileInfo file in files) { + if (!this.IsRelevant(file)) + continue; + if (this.StrictXnbModExtensions.Contains(file.Extension)) { hasXnbFile = true; diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index dab71473..b6029c14 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Caching.Mods; @@ -13,7 +12,7 @@ internal class ModCacheMemoryRepository : BaseCacheRepository, IModCacheReposito ** Fields *********/ /// The cached mod data indexed by {site key}:{ID}. - private readonly IDictionary> Mods = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> Mods = new(StringComparer.OrdinalIgnoreCase); /********* @@ -57,13 +56,11 @@ internal class ModCacheMemoryRepository : BaseCacheRepository, IModCacheReposito { DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); - string[] staleKeys = this.Mods - .Where(p => p.Value.LastRequested < minDate) - .Select(p => p.Key) - .ToArray(); - - foreach (string key in staleKeys) - this.Mods.Remove(key); + foreach ((string key, var modPage) in this.Mods) + { + if (modPage.LastRequested < minDate) + this.Mods.Remove(key); + } } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index de7150dc..48d980d3 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -499,8 +499,11 @@ internal class ContentCoordinator : IDisposable return this.ContentManagerLock.InReadLock(() => { List values = []; - foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName))) + foreach (IContentManager content in this.ContentManagers) { + if (content.IsNamespaced || !content.IsLoaded(assetName)) + continue; + object value = content.LoadExact(assetName, useCache: true); values.Add(value); } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 4d47353e..6480150c 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, IReadOnlyList skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix) + public void LogModInfo(IReadOnlyList loaded, IReadOnlyList loadedContentPacks, IReadOnlyList loadedMods, IReadOnlyList skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix) { // report malicious mod foreach (IModMetadata mod in skippedMods) @@ -300,7 +300,7 @@ internal class LogManager : IDisposable } // log loaded mods - this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + this.Monitor.Log($"Loaded {loadedMods.Count} mods" + (loadedMods.Count > 0 ? ":" : "."), LogLevel.Info); foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; @@ -319,7 +319,7 @@ internal class LogManager : IDisposable { string? GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasId(id))?.DisplayName; - this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); + this.Monitor.Log($"Loaded {loadedContentPacks.Count} content packs:", LogLevel.Info); foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index e2dee40d..bea1d89f 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Mono.Cecil; @@ -69,16 +68,19 @@ internal class RecursiveRewriter /// Returns whether the module was modified. public bool RewriteModule() { - IEnumerable types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like - bool changed = false; try { changed |= this.RewriteModuleImpl(this.Module); - foreach (TypeDefinition type in types) + foreach (TypeDefinition type in this.Module.GetTypes()) + { + if (type.BaseType is null) + continue; // skip special types like + changed |= this.RewriteTypeDefinition(type); + } } catch (Exception ex) { @@ -293,8 +295,7 @@ internal class RecursiveRewriter MethodDefinition? constructor = (newAttrType ?? attribute.AttributeType) .Resolve() ?.Methods - .Where(method => method.IsConstructor) - .FirstOrDefault(ctor => RewriteHelper.HasMatchingSignature(ctor, attribute.Constructor)); + .FirstOrDefault(method => method.IsConstructor && RewriteHelper.HasMatchingSignature(method, attribute.Constructor)); if (constructor == null) throw new InvalidOperationException($"Can't rewrite attribute type '{attribute.AttributeType.FullName}' to '{newAttrType?.FullName}', no equivalent constructor found."); diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index e73e735e..509a0973 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -225,10 +225,13 @@ internal class ModResolver var states = mods.ToDictionary(mod => mod, _ => ModDependencyStatus.Queued); // handle failed mods - foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed)) + foreach (IModMetadata mod in mods) { - states[mod] = ModDependencyStatus.Failed; - sortedMods.Push(mod); + if (mod.Status is ModMetadataStatus.Failed) + { + states[mod] = ModDependencyStatus.Failed; + sortedMods.Push(mod); + } } // sort mods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs index 8eac55c4..4bca259a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs @@ -47,8 +47,11 @@ internal class HeuristicMethodRewriter : BaseInstructionHandler // get method definition MethodDefinition? method = null; - foreach (MethodDefinition match in type.Methods.Where(p => p.Name == methodRef.Name)) + foreach (MethodDefinition match in type.Methods) { + if (match.Name != methodRef.Name) + continue; + // reference matches initial parameters of definition if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match)) continue; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index eb0ae72f..628bc482 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -484,10 +484,13 @@ internal class SCore : IDisposable { bool anyIgnored = false; - foreach (IModMetadata mod in mods.Where(p => p.IsIgnored)) + foreach (IModMetadata mod in mods) { - anyIgnored = true; - this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); + if (mod.IsIgnored) + { + anyIgnored = true; + this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); + } } if (anyIgnored) @@ -1982,9 +1985,18 @@ internal class SCore : IDisposable } } - IModMetadata[] loaded = this.ModRegistry.GetAll().ToArray(); - IModMetadata[] loadedContentPacks = loaded.Where(p => p.IsContentPack).ToArray(); - IModMetadata[] loadedMods = loaded.Where(p => !p.IsContentPack).ToArray(); + List loaded = []; + List loadedContentPacks = []; + List loadedMods = []; + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { + loaded.Add(mod); + + if (mod.IsContentPack) + loadedContentPacks.Add(mod); + else + loadedMods.Add(mod); + } // unlock content packs this.ModRegistry.AreAllModsLoaded = true; @@ -2103,10 +2115,10 @@ internal class SCore : IDisposable // validate dependencies // Although dependencies are validated before mods are loaded, a dependency may have failed to load. - foreach (IManifestDependency dependency in manifest!.Dependencies.Where(p => p.IsRequired)) + foreach (IManifestDependency dependency in manifest!.Dependencies) { // not missing - if (this.ModRegistry.Get(dependency.UniqueID) != null) + if (!dependency.IsRequired || this.ModRegistry.Get(dependency.UniqueID) != null) continue; // ignored in compatibility list (e.g. fully replaced by the game code) @@ -2319,23 +2331,34 @@ internal class SCore : IDisposable mod = null; // find type - TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); - if (modEntries.Length == 0) + TypeInfo? modType = null; + foreach (TypeInfo type in modAssembly.DefinedTypes) + { + // skip: not a Mod type + if (!typeof(Mod).IsAssignableFrom(type) || type.IsAbstract) + continue; + + // fail: multiple Mod types found + if (modType is not null) + { + error = $"its DLL contains multiple '{nameof(Mod)}' subclasses."; + return false; + } + + // else save mod type + modType = type; + } + if (modType is null) { error = $"its DLL has no '{nameof(Mod)}' subclass."; return false; } - if (modEntries.Length > 1) - { - error = $"its DLL contains multiple '{nameof(Mod)}' subclasses."; - return false; - } // get implementation Context.HeuristicModsRunningCode.Push(metadata); try { - mod = (Mod?)modAssembly.CreateInstance(modEntries[0].ToString()); + mod = (Mod?)modAssembly.CreateInstance(modType.ToString()); } finally { diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 10b81730..eebbf8aa 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -192,8 +192,11 @@ internal class SMultiplayer : Multiplayer newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); // reply with other players' context - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + foreach (MultiplayerPeer otherPeer in this.Peers.Values) { + if (otherPeer.PlayerID == newPeer.PlayerID) + continue; + this.Monitor.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); } @@ -202,8 +205,11 @@ internal class SMultiplayer : Multiplayer if (this.Peers.Count > 1) { object[] fields = this.GetContextSyncMessageFields(newPeer); - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + foreach (MultiplayerPeer otherPeer in this.Peers.Values) { + if (otherPeer.PlayerID == newPeer.PlayerID) + continue; + this.Monitor.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index 9b20b6a5..567dac8f 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; @@ -16,7 +15,7 @@ internal class ChestTracker : IDisposable ** Fields *********/ /// The item stack sizes as of the last update. - private readonly IDictionary StackSizes; + private readonly Dictionary StackSizes = []; /// Items added since the last update. private readonly HashSet Added = new(new ObjectReferenceComparer()); @@ -46,9 +45,11 @@ internal class ChestTracker : IDisposable this.Chest = chest; this.InventoryWatcher = WatcherFactory.ForInventory($"{name}.{nameof(chest.Items)}", chest.Items); - this.StackSizes = this.Chest.Items - .Where(n => n != null) - .ToDictionary(n => n, n => n.Stack); + foreach (Item? item in this.Chest.Items) + { + if (item is not null) + this.StackSizes[item] = item.Stack; + } } /// Update the current values if needed. diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 5422be1d..9db1ecd6 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -16,10 +16,10 @@ internal class PlayerTracker : IDisposable ** Fields *********/ /// The player's inventory as of the last reset. - private IDictionary PreviousInventory; + private readonly Dictionary PreviousInventory; /// The player's inventory change as of the last update. - private IDictionary CurrentInventory; + private readonly Dictionary CurrentInventory = []; /// The player's last valid location. private GameLocation? LastValidLocation; @@ -50,7 +50,7 @@ internal class PlayerTracker : IDisposable { // init player data this.Player = player; - this.CurrentInventory = this.GetInventory(); + this.SaveInventoryTo(this.CurrentInventory); this.PreviousInventory = new Dictionary(this.CurrentInventory); // init trackers @@ -81,16 +81,21 @@ internal class PlayerTracker : IDisposable watcher.Update(); // update inventory - this.CurrentInventory = this.GetInventory(); + this.CurrentInventory.Clear(); + this.SaveInventoryTo(this.CurrentInventory); } /// Reset all trackers so their current values are the baseline. public void Reset() { + // reset watchers foreach (IWatcher watcher in this.Watchers) watcher.Reset(); - this.PreviousInventory = this.CurrentInventory; + // reset PreviousInventory to match current + this.PreviousInventory.Clear(); + foreach ((Item item, int stack) in this.CurrentInventory) + this.PreviousInventory.Add(item, stack); } /// Get the player's current location, ignoring temporary null values. @@ -105,8 +110,8 @@ internal class PlayerTracker : IDisposable /// Returns whether anything changed. public bool TryGetInventoryChanges([NotNullWhen(true)] out SnapshotItemListDiff? changes) { - ISet added = new HashSet(new ObjectReferenceComparer()); - ISet removed = new HashSet(new ObjectReferenceComparer()); + HashSet added = new(new ObjectReferenceComparer()); + HashSet removed = new(new ObjectReferenceComparer()); foreach (Item item in this.PreviousInventory.Keys.Union(this.CurrentInventory.Keys)) { if (!this.PreviousInventory.ContainsKey(item)) @@ -133,10 +138,13 @@ internal class PlayerTracker : IDisposable ** Private methods *********/ /// Get the player's current inventory. - private IDictionary GetInventory() + /// The dictionary to fill with the player's inventory. + private void SaveInventoryTo(Dictionary inventory) { - return this.Player.Items - .Where(n => n != null) - .ToDictionary(n => n, n => n.Stack); + foreach (Item? item in this.Player.Items) + { + if (item is not null) + inventory[item] = item.Stack; + } } }