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);