avoid ToArray() copies when possible

This commit is contained in:
Jesse Plamondon-Willard
2026-03-26 22:42:35 -04:00
parent 42238538bd
commit da888ba9b0
35 changed files with 286 additions and 234 deletions
@@ -59,14 +59,14 @@ public abstract partial class DiagnosticVerifier
/// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
/// <param name="analyzer">The analyzer that was being run on the sources</param>
/// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
private static void VerifyDiagnosticResults(IReadOnlyList<Diagnostic> 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
/// <param name="analyzer">The analyzer that this verifier tests</param>
/// <param name="diagnostics">The Diagnostics to be formatted</param>
/// <returns>The Diagnostics formatted as a string</returns>
private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params IReadOnlyList<Diagnostic> 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(',');
}
@@ -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
*********/
/// <summary>Get all items which can be searched and added to the player's inventory through the console.</summary>
/// <param name="searchWords">The search string to find.</param>
private IEnumerable<SearchableItem> GetItems(string[] searchWords)
private IEnumerable<SearchableItem> GetItems(IReadOnlyList<string> searchWords)
{
// normalize search term
searchWords = searchWords.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray();
@@ -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>Returns the number of removed entities.</returns>
private int RemoveTerrainFeatures(GameLocation location, Func<TerrainFeature, bool> 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));
}
/// <summary>Remove large terrain features from a location matching a lambda.</summary>
@@ -208,18 +197,7 @@ internal class ClearCommand : ConsoleCommand
/// <returns>Returns the number of removed entities.</returns>
private int RemoveLargeTerrainFeatures(GameLocation location, Func<LargeTerrainFeature, bool> 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);
}
/// <summary>Remove resource clumps from a location matching a lambda.</summary>
@@ -228,15 +206,7 @@ internal class ClearCommand : ConsoleCommand
/// <returns>Returns the number of removed entities.</returns>
private int RemoveResourceClumps(GameLocation location, Func<ResourceClump, bool> 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);
}
/// <summary>Remove furniture from a location matching a lambda.</summary>
@@ -245,17 +215,6 @@ internal class ClearCommand : ConsoleCommand
/// <returns>Returns the number of removed entities.</returns>
private int RemoveFurniture(GameLocation location, Func<Furniture, bool> 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);
}
}
+1 -1
View File
@@ -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<ModMetadata>(), 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.")]
@@ -35,7 +35,7 @@ internal class ChangeDescriptorTests
];
// act
ChangeDescriptor parsed = ChangeDescriptor.Parse(rawDescriptor, out string[] errors);
ChangeDescriptor parsed = ChangeDescriptor.Parse(rawDescriptor, out IReadOnlyList<string> 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<string> 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<string> 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<string> errors);
errors.Should().BeEmpty();
@@ -141,7 +141,7 @@ public class ChangeDescriptor
/// <param name="descriptor">The raw change descriptor.</param>
/// <param name="errors">The human-readable error message describing any invalid values that were ignored.</param>
/// <param name="formatValue">Format a raw value into a normalized form if needed.</param>
public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func<string, string>? formatValue = null)
public static ChangeDescriptor Parse(string? descriptor, out IReadOnlyList<string> errors, Func<string, string>? formatValue = null)
{
var parsed = new ChangeDescriptor(formatValue ?? (p => p));
@@ -194,7 +194,7 @@ public class ChangeDescriptor
}
}
errors = rawErrors.ToArray();
errors = rawErrors;
}
else
errors = [];
@@ -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
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
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;
@@ -27,9 +27,9 @@ public class ModDatabase
/// <summary>Construct an instance.</summary>
/// <param name="records">The underlying mod data records indexed by default display name.</param>
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string?> getUpdateUrl)
public ModDatabase(ModDataRecord[] records, Func<string, string?> getUpdateUrl)
{
this.Records = records.ToArray();
this.Records = records;
this.GetUpdateUrl = getUpdateUrl;
}
@@ -127,7 +127,9 @@ public class ModScanner
// set appropriate invalid-mod error
if (manifestFile == null)
{
FileInfo[] files = this.RecursivelyGetFiles(searchFolder).ToArray();
List<FileInfo> files = [];
this.RecursivelyGetFiles(searchFolder, files);
FileInfo[] relevantFiles = files.Where(this.IsRelevant).ToArray();
// empty Vortex folder
@@ -293,7 +295,8 @@ public class ModScanner
/// <summary>Recursively get all files in a folder.</summary>
/// <param name="folder">The root folder to search.</param>
private IEnumerable<FileInfo> RecursivelyGetFiles(DirectoryInfo folder)
/// <param name="files">The list of files to populate.</param>
private void RecursivelyGetFiles(DirectoryInfo folder, List<FileInfo> 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);
}
}
@@ -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<IModDownload>());
return page.SetInfo(name: name, version: version, url: url, downloads: []);
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
@@ -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());
}
/// <summary>Get a raw version string for a mod file, if available.</summary>
@@ -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
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="downloads">The mod downloads.</param>
public IModPage SetInfo(string name, string? version, string url, IEnumerable<IModDownload> 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;
@@ -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());
}
/// <summary>Extract the version number from a ModDrop file's info, if possible.</summary>
+1 -2
View File
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -59,7 +58,7 @@ internal interface IModPage
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="downloads">The mod downloads.</param>
IModPage SetInfo(string name, string? version, string url, IEnumerable<IModDownload> downloads);
IModPage SetInfo(string name, string? version, string url, IModDownload[] downloads);
/// <summary>Set a mod fetch error.</summary>
/// <param name="status">The mod availability status on the remote site.</param>
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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;
@@ -92,7 +92,7 @@ internal class HarmonySummaryCommand : IInternalCommand
}
// matches individual patchers
foreach ((string patcherId, ISet<PatchType> patchTypes) in patch.PatchTypesByOwner.ToArray())
foreach ((string patcherId, ISet<PatchType> 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<string, ISet<PatchType>>();
var typesByOwner = new Dictionary<string, HashSet<PatchType>>();
foreach ((PatchType type, IReadOnlyCollection<Patch> patches) in patchGroups)
{
foreach (Patch patch in patches)
{
if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType>? patchTypes))
typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>();
if (!typesByOwner.TryGetValue(patch.owner, out HashSet<PatchType>? patchTypes))
typesByOwner[patch.owner] = patchTypes = [];
patchTypes.Add(type);
}
}
@@ -165,7 +165,7 @@ internal class HarmonySummaryCommand : IInternalCommand
public string MethodDescription { get; }
/// <summary>The patch types by the Harmony instance ID that added them.</summary>
public IDictionary<string, ISet<PatchType>> PatchTypesByOwner { get; }
public Dictionary<string, HashSet<PatchType>> PatchTypesByOwner { get; }
/*********
@@ -174,7 +174,7 @@ internal class HarmonySummaryCommand : IInternalCommand
/// <summary>Construct an instance.</summary>
/// <param name="method">The patched method.</param>
/// <param name="patchTypesByOwner">The patch types by the Harmony instance ID that added them.</param>
public SearchResult(MethodBase method, IDictionary<string, ISet<PatchType>> patchTypesByOwner)
public SearchResult(MethodBase method, Dictionary<string, HashSet<PatchType>> patchTypesByOwner)
{
this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}";
this.MethodDescription = method.FullDescription();
+1 -2
View File
@@ -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)}
""";
}
+5 -5
View File
@@ -55,7 +55,7 @@ internal class ContentCoordinator : IDisposable
private readonly Action<BaseContentManager, IAssetName> OnAssetLoaded;
/// <summary>A callback to invoke when any asset names have been invalidated from the cache.</summary>
private readonly Action<IList<IAssetName>> OnAssetsInvalidated;
private readonly Action<ICollection<IAssetName>> OnAssetsInvalidated;
/// <summary>Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</summary>
private readonly Func<IAssetInfo, AssetOperationGroup?> RequestAssetOperations;
@@ -116,7 +116,7 @@ internal class ContentCoordinator : IDisposable
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Multiplayer multiplayer, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Multiplayer multiplayer, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<ICollection<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> 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<IAssetName> invalidatedKeys = invalidatedAssets.Keys;
IAssetName[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray();
string FormatKeyList(IEnumerable<IAssetName> 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."
@@ -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;
+3 -3
View File
@@ -281,7 +281,7 @@ internal class LogManager : IDisposable
/// <param name="logParanoidWarnings">Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access.</param>
/// <param name="logTechnicalDetailsForBrokenMods">Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters.</param>
/// <param name="hasHarmonyFix">Whether Harmony was fixed to work with Stardew Valley.</param>
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<IModMetadata> skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix)
{
// report malicious mod
foreach (IModMetadata mod in skippedMods)
@@ -388,7 +388,7 @@ internal class LogManager : IDisposable
/// <param name="logTechnicalDetailsForBrokenMods">Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters.</param>
/// <param name="hasHarmonyFix">Whether Harmony was fixed to work with Stardew Valley.</param>
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifests aren't guaranteed non-null at this point in the loading process.")]
private void LogModWarnings(IEnumerable<IModMetadata> mods, IModMetadata[] skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix)
private void LogModWarnings(IEnumerable<IModMetadata> mods, IReadOnlyList<IModMetadata> skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods, bool hasHarmonyFix)
{
// get mods with warnings
IModMetadata[] modsWithWarnings = mods
@@ -526,7 +526,7 @@ internal class LogManager : IDisposable
/// <summary>Group failed mods by the priority players should update them, where mods in earlier groups are more likely to fix multiple mods.</summary>
/// <param name="failedMods">The failed mods to group.</param>
private IEnumerable<IList<IModMetadata>> GroupFailedModsByPriority(IList<IModMetadata> failedMods)
private IEnumerable<IList<IModMetadata>> GroupFailedModsByPriority(IReadOnlyList<IModMetadata> failedMods)
{
var failedOthers = failedMods.ToList();
var skippedModIds = new HashSet<string>(from mod in failedMods where mod.HasId() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase);
@@ -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
@@ -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<TypeReference> 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<TypeReference> defGenerics = ((GenericInstanceType)type).GenericArguments;
Collection<TypeReference> 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<ParameterDefinition> 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<ParameterDefinition> definitionParameters = definition.Parameters;
Collection<ParameterDefinition> 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;
+62 -33
View File
@@ -85,10 +85,8 @@ internal class ModResolver
/// <param name="validateFilesExist">Whether to validate that files referenced in the manifest (like <see cref="IManifest.EntryDll"/>) exist on disk. This can be disabled to only validate the manifest itself.</param>
[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<IModMetadata> mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Func<string, string?> getUpdateUrl, Func<string, IFileLookup> getFileLookup, bool validateFilesExist = true)
public void ValidateManifests(IModMetadata[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Func<string, string?> getUpdateUrl, Func<string, IFileLookup> getFileLookup, bool validateFilesExist = true)
{
mods = mods.ToArray();
// validate each manifest
foreach (IModMetadata mod in mods)
{
@@ -223,7 +221,6 @@ internal class ModResolver
public IEnumerable<IModMetadata> ProcessDependencies(IReadOnlyList<IModMetadata> mods, ModDatabase modDatabase)
{
// initialize metadata
mods = mods.ToArray();
var sortedMods = new Stack<IModMetadata>();
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<ModDependency> 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<string>? 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<string>? 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
/// <summary>Get the dependencies declared in a manifest.</summary>
/// <param name="manifest">The mod manifest.</param>
/// <param name="loadedMods">The loaded mods.</param>
private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IReadOnlyList<IModMetadata> loadedMods)
private IReadOnlyList<ModDependency> GetDependenciesFrom(IManifest manifest, IReadOnlyList<IModMetadata> loadedMods)
{
IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasId(id));
List<ModDependency>? 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<ModDependency>();
IModMetadata? FindMod(string id)
{
return loadedMods.FirstOrDefault(m => m.HasId(id));
}
}
/// <summary>Get a technical message indicating why a mod's compatibility status was overridden, if applicable.</summary>
@@ -20,13 +20,13 @@ internal class PlatformAssemblyMap : IDisposable
public readonly Platform TargetPlatform;
/// <summary>The short assembly names to remove as assembly reference, and replace with the <see cref="Targets"/>. These should be short names (like "Stardew Valley").</summary>
public readonly string[] RemoveNames;
public readonly IReadOnlyList<string> RemoveNames;
/****
** Metadata
****/
/// <summary>The assemblies to target. Equivalent types should be rewritten to use these assemblies.</summary>
public readonly Assembly[] Targets;
public readonly IReadOnlyList<Assembly> Targets;
/// <summary>An assembly => reference cache.</summary>
public readonly IDictionary<Assembly, AssemblyNameReference> TargetReferences;
@@ -42,7 +42,7 @@ internal class PlatformAssemblyMap : IDisposable
/// <param name="targetPlatform">The target game platform.</param>
/// <param name="removeAssemblyNames">The assembly short names to remove (like <c>Stardew Valley</c>).</param>
/// <param name="targetAssemblies">The assemblies to target.</param>
public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies)
public PlatformAssemblyMap(Platform targetPlatform, IReadOnlyList<string> removeAssemblyNames, IReadOnlyList<Assembly> targetAssemblies)
{
// save data
this.TargetPlatform = targetPlatform;
@@ -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;
@@ -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;
}
+24 -15
View File
@@ -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
/// <summary>A callback invoked after assets have been invalidated from the content cache.</summary>
/// <param name="assetNames">The invalidated asset names.</param>
private void OnAssetsInvalidated(IList<IAssetName> assetNames)
private void OnAssetsInvalidated(ICollection<IAssetName> 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<IModMetadata> skippedMods = new List<IModMetadata>();
List<IModMetadata> 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<string> keys = new(StringComparer.OrdinalIgnoreCase);
HashSet<string> duplicateKeys = new(StringComparer.OrdinalIgnoreCase);
foreach (string key in translations[locale].Keys.ToArray())
foreach (string key in translations[locale].Keys)
{
if (!keys.Add(key))
{
+14 -3
View File
@@ -49,13 +49,24 @@ internal class SnapshotItemListDiff
/// <returns>Returns whether anything changed.</returns>
public static bool TryGetChanges(ISet<Item> added, ISet<Item> removed, IDictionary<Item, int> stackSizes, [NotNullWhen(true)] out SnapshotItemListDiff? changes)
{
KeyValuePair<Item, int>[] 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<ItemStackSizeChange>? 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;
}
+2 -2
View File
@@ -24,10 +24,10 @@ internal class SnapshotListDiff<T>
public bool IsChanged { get; private set; }
/// <summary>The removed values.</summary>
public IEnumerable<T> Removed => this.RemovedImpl;
public IReadOnlyList<T> Removed => this.RemovedImpl;
/// <summary>The added values.</summary>
public IEnumerable<T> Added => this.AddedImpl;
public IReadOnlyList<T> Added => this.AddedImpl;
/*********
@@ -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<MonitorColorScheme>().Select(p => p.ToString()).ToArray(),
allowedValues: Enum.GetNames<MonitorColorScheme>(),
formatAllowedValue: value => this.Translations.Get(
value is nameof(MonitorColorScheme.AutoDetect) && Constants.Platform == Platform.Windows
? $"config.color-scheme.options.{value}.on-windows"
+32 -27
View File
@@ -581,29 +581,31 @@ internal class CoreAssetPropagator
/// <returns>Returns whether any NPCs were updated.</returns>
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;
}
/// <summary>Update the character data for matching NPCs.</summary>
@@ -621,33 +623,36 @@ internal class CoreAssetPropagator
/// <returns>Returns whether any NPCs were updated.</returns>
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<bool>(villager, "_hasLoadedMasterScheduleData").SetValue(false);
this.Reflection.GetField<Dictionary<string, string>?>(villager, "_masterScheduleData").SetValue(null);
villager.TryLoadSchedule();
this.Reflection.GetField<bool>(npc, "_hasLoadedMasterScheduleData").SetValue(false);
this.Reflection.GetField<Dictionary<string, string>?>(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;
}
/// <summary>Update cached translations from the <c>Strings\StringsFromCSFiles</c> asset.</summary>
+44 -19
View File
@@ -106,28 +106,53 @@ public class Keybind
/// <summary>Get the keybind state relative to the previous tick.</summary>
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
};
}
/// <summary>Get a string representation of the keybind.</summary>
@@ -136,6 +161,6 @@ public class Keybind
{
return this.Buttons.Length > 0
? string.Join(" + ", this.Buttons)
: SButton.None.ToString();
: nameof(SButton.None);
}
}
+2 -3
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace StardewModdingAPI.Utilities;
@@ -54,7 +53,7 @@ public class PerScreen<T>
public IEnumerable<KeyValuePair<int, T>> GetActiveValues()
{
this.RemoveDeadScreens();
return this.States.ToArray();
return this.States;
}
/// <summary>Get the value for a given screen ID, creating it if needed.</summary>
@@ -106,7 +105,7 @@ public class PerScreen<T>
/// <param name="shouldRemove">Returns whether a screen ID should be removed.</param>
private void RemoveScreens(Func<int, bool> shouldRemove)
{
foreach (var pair in this.States.ToArray())
foreach (var pair in this.States)
{
if (shouldRemove(pair.Key))
this.States.Remove(pair.Key);