mirror of
https://github.com/Pathoschild/SMAPI.git
synced 2026-10-12 02:08:51 +00:00
avoid ToArray() copies when possible
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)}
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user