diff options
| author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-08-01 11:07:29 -0400 |
|---|---|---|
| committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-08-01 11:07:29 -0400 |
| commit | 60b41195778af33fd609eab66d9ae3f1d1165e8f (patch) | |
| tree | 7128b906d40e94c56c34ed6058f27bc31c31a08b /src/SMAPI/Framework/ModLoading | |
| parent | b9bc1a6d17cafa0a97b46ffecda432cfc2f23b51 (diff) | |
| parent | 52cf953f685c65b2b6814e375ec9a5ffa03c440a (diff) | |
| download | SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.tar.gz SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.tar.bz2 SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.zip | |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ModLoading')
16 files changed, 454 insertions, 163 deletions
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index d85a9a28..91c9e192 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading ** Properties *********/ /// <summary>The known assemblies.</summary> - private readonly IDictionary<string, AssemblyDefinition> Loaded = new Dictionary<string, AssemblyDefinition>(); + private readonly IDictionary<string, AssemblyDefinition> Lookup = new Dictionary<string, AssemblyDefinition>(); /********* @@ -22,8 +22,9 @@ namespace StardewModdingAPI.Framework.ModLoading { foreach (AssemblyDefinition assembly in assemblies) { - this.Loaded[assembly.Name.Name] = assembly; - this.Loaded[assembly.Name.FullName] = assembly; + this.RegisterAssembly(assembly); + this.Lookup[assembly.Name.Name] = assembly; + this.Lookup[assembly.Name.FullName] = assembly; } } @@ -36,15 +37,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="parameters">The assembly reader parameters.</param> public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); - /// <summary>Resolve an assembly reference.</summary> - /// <param name="fullName">The assembly full name (including version, etc).</param> - public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); - - /// <summary>Resolve an assembly reference.</summary> - /// <param name="fullName">The assembly full name (including version, etc).</param> - /// <param name="parameters">The assembly reader parameters.</param> - public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); - /********* ** Private methods @@ -53,8 +45,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="name">The assembly's short or full name.</param> private AssemblyDefinition ResolveName(string name) { - return this.Loaded.ContainsKey(name) - ? this.Loaded[name] + return this.Lookup.TryGetValue(name, out AssemblyDefinition match) + ? match : null; } } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index a60f63da..37b1a378 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -6,12 +6,13 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Internal; using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading { /// <summary>Preprocesses and loads mod assemblies.</summary> - internal class AssemblyLoader + internal class AssemblyLoader : IDisposable { /********* ** Properties @@ -19,9 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; - /// <summary>Whether to enable developer mode logging.</summary> - private readonly bool IsDeveloperMode; - /// <summary>Metadata for mapping assemblies to the current platform.</summary> private readonly PlatformAssemblyMap AssemblyMap; @@ -31,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>A minimal assembly definition resolver which resolves references to known loaded assemblies.</summary> private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver; + /// <summary>The objects to dispose as part of this instance.</summary> + private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>(); + /********* ** Public methods @@ -38,13 +39,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Construct an instance.</summary> /// <param name="targetPlatform">The current game platform.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="isDeveloperMode">Whether to enable developer mode logging.</param> - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) + public AssemblyLoader(Platform targetPlatform, IMonitor monitor) { this.Monitor = monitor; - this.IsDeveloperMode = isDeveloperMode; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - this.AssemblyDefinitionResolver = new AssemblyDefinitionResolver(); + this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary<string, Assembly>(); @@ -98,13 +98,26 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + + // detect broken assembly reference + foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) + { + if (!reference.Name.StartsWith("System.") && !this.IsAssemblyLoaded(reference)) + { + this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'."); + if (!assumeCompatible) + throw new IncompatibleInstructionException($"assembly reference to {reference.FullName}", $"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}."); + mod.SetWarning(ModWarning.BrokenCodeLoaded); + break; + } + } // load assembly if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); using (MemoryStream outStream = new MemoryStream()) { assembly.Definition.Write(outStream); @@ -115,7 +128,7 @@ namespace StardewModdingAPI.Framework.ModLoading else { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } @@ -127,6 +140,20 @@ namespace StardewModdingAPI.Framework.ModLoading return lastAssembly; } + /// <summary>Get whether an assembly is loaded.</summary> + /// <param name="reference">The assembly name reference.</param> + public bool IsAssemblyLoaded(AssemblyNameReference reference) + { + try + { + return this.AssemblyDefinitionResolver.Resolve(reference) != null; + } + catch (AssemblyResolutionException) + { + return false; + } + } + /// <summary>Resolve an assembly by its name.</summary> /// <param name="name">The assembly name.</param> /// <remarks> @@ -135,7 +162,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// assemblies (especially with Mono). Since this is meant to be called on <see cref="AppDomain.AssemblyResolve"/>, /// the implicit assumption is that loading the exact assembly failed. /// </remarks> - public Assembly ResolveAssembly(string name) + public static Assembly ResolveAssembly(string name) { string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) return AppDomain.CurrentDomain @@ -143,10 +170,26 @@ namespace StardewModdingAPI.Framework.ModLoading .FirstOrDefault(p => p.GetName().Name == shortName); } + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public void Dispose() + { + foreach (IDisposable instance in this.Disposables) + instance.Dispose(); + } + /********* ** Private methods *********/ + /// <summary>Track an object for disposal as part of the assembly loader.</summary> + /// <typeparam name="T">The instance type.</typeparam> + /// <param name="instance">The disposable instance.</param> + private T TrackForDisposal<T>(T instance) where T : IDisposable + { + this.Disposables.Add(instance); + return instance; + } + /**** ** Assembly parsing ****/ @@ -165,9 +208,8 @@ namespace StardewModdingAPI.Framework.ModLoading // read assembly byte[] assemblyBytes = File.ReadAllBytes(file.FullName); - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); + Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes)); + AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true })); // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) @@ -284,33 +326,27 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); if (!assumeCompatible) throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found broken code ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); + mod.SetWarning(ModWarning.ChangesSaveSerialiser); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); + mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", -#if SMAPI_FOR_WINDOWS - this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug -#else - LogLevel.Warn -#endif - ); + mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.None: diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index b5e45742..cf5a3175 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using Mono.Cecil; using Mono.Cecil.Cil; @@ -16,9 +15,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <summary>The assembly names to which to heuristically detect broken references.</summary> private readonly HashSet<string> ValidateReferencesToAssemblies; - /// <summary>A pattern matching type name substrings to strip for display.</summary> - private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); - /********* ** Accessors @@ -59,21 +55,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - // can't compare generic type parameters between definition and reference - if (fieldRef.FieldType.IsGenericInstance || fieldRef.FieldType.IsGenericParameter) - return InstructionHandleResult.None; - // get target field FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); if (targetField == null) return InstructionHandleResult.None; // validate return type - string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType); - string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType); - if (actualReturnTypeID != expectedReturnTypeID) + if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) { - this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})"; + this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"; return InstructionHandleResult.NotCompatible; } } @@ -82,10 +72,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodReference methodReference = RewriteHelper.AsMethodReference(instruction); if (methodReference != null && this.ShouldValidate(methodReference.DeclaringType)) { - // can't compare generic type parameters between definition and reference - if (methodReference.ReturnType.IsGenericInstance || methodReference.ReturnType.IsGenericParameter) - return InstructionHandleResult.None; - // get potential targets MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray(); if (candidateMethods == null || !candidateMethods.Any()) @@ -99,10 +85,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders return InstructionHandleResult.NotCompatible; } - string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType); - if (candidateMethods.All(method => this.GetComparableTypeID(method.ReturnType) != expectedReturnType)) + if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) { - this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})"; + this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"; return InstructionHandleResult.NotCompatible; } } @@ -121,17 +106,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); } - /// <summary>Get a unique string representation of a type.</summary> - /// <param name="type">The type reference.</param> - private string GetComparableTypeID(TypeReference type) - { - return this.StripTypeNamePattern.Replace(type.FullName, ""); - } - /// <summary>Get a shorter type name for display.</summary> /// <param name="type">The type reference.</param> - /// <param name="typeID">The comparable type ID from <see cref="GetComparableTypeID"/>.</param> - private string GetFriendlyTypeName(TypeReference type, string typeID) + private string GetFriendlyTypeName(TypeReference type) { // most common built-in types switch (type.FullName) @@ -148,10 +125,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" }) { if (type.Namespace == @namespace) - return typeID.Substring(@namespace.Length + 1); + return type.Name; } - return typeID; + return type.FullName; } } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index f5e33313..b95dd79c 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -67,12 +67,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef)) { - MethodDefinition target = methodRef.DeclaringType.Resolve()?.Methods.FirstOrDefault(p => p.Name == methodRef.Name); + MethodDefinition target = methodRef.Resolve(); if (target == null) { - this.NounPhrase = this.IsProperty(methodRef) - ? $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)" - : $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + if (this.IsProperty(methodRef)) + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + else if (methodRef.Name == ".ctor") + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + else + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; return InstructionHandleResult.NotCompatible; } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index 45349def..79045241 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; @@ -16,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <summary>The result to return for matching instructions.</summary> private readonly InstructionHandleResult Result; + /// <summary>A lambda which overrides a matched type.</summary> + protected readonly Func<TypeReference, bool> ShouldIgnore; + /********* ** Accessors @@ -30,11 +34,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> /// <param name="result">The result to return for matching instructions.</param> - public TypeFinder(string fullTypeName, InstructionHandleResult result) + /// <param name="shouldIgnore">A lambda which overrides a matched type.</param> + public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null) { this.FullTypeName = fullTypeName; this.Result = result; this.NounPhrase = $"{fullTypeName} type"; + this.ShouldIgnore = shouldIgnore ?? (p => false); } /// <summary>Perform the predefined logic for a method if applicable.</summary> @@ -113,7 +119,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders protected bool IsMatch(TypeReference type) { // root type - if (type.FullName == this.FullTypeName) + if (type.FullName == this.FullTypeName && !this.ShouldIgnore(type)) return true; // generic arguments diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 1a0f9994..585debb4 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,5 +1,7 @@ using System; -using StardewModdingAPI.Framework.ModData; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { @@ -19,11 +21,14 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary> - public ParsedModDataRecord DataRecord { get; } + public ModDataRecordVersionedFields DataRecord { get; } /// <summary>The metadata resolution status.</summary> public ModMetadataStatus Status { get; private set; } + /// <summary>Indicates non-error issues with the mod.</summary> + public ModWarning Warnings { get; private set; } + /// <summary>The reason the metadata is invalid, if any.</summary> public string Error { get; private set; } @@ -39,6 +44,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The mod-provided API (if any).</summary> public object Api { get; private set; } + /// <summary>The update-check metadata for this mod (if any).</summary> + public ModEntryModel UpdateCheckData { get; private set; } + /// <summary>Whether the mod is a content pack.</summary> public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -51,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="directoryPath">The mod's full directory path.</param> /// <param name="manifest">The mod manifest.</param> /// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param> - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; @@ -70,6 +78,14 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } + /// <summary>Set a warning flag for the mod.</summary> + /// <param name="warning">The warning to set.</param> + public IModMetadata SetWarning(ModWarning warning) + { + this.Warnings |= warning; + return this; + } + /// <summary>Set the mod instance.</summary> /// <param name="mod">The mod instance to set.</param> public IModMetadata SetMod(IMod mod) @@ -102,5 +118,36 @@ namespace StardewModdingAPI.Framework.ModLoading this.Api = api; return this; } + + /// <summary>Set the update-check metadata for this mod.</summary> + /// <param name="data">The update-check metadata.</param> + public IModMetadata SetUpdateData(ModEntryModel data) + { + this.UpdateCheckData = data; + return this; + } + + /// <summary>Whether the mod manifest was loaded (regardless of whether the mod itself was loaded).</summary> + public bool HasManifest() + { + return this.Manifest != null; + } + + /// <summary>Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded).</summary> + public bool HasID() + { + return + this.HasManifest() + && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); + } + + /// <summary>Whether the mod has at least one update key set.</summary> + public bool HasUpdateKeys() + { + return + this.HasManifest() + && this.Manifest.UpdateKeys != null + && this.Manifest.UpdateKeys.Any(key => !string.IsNullOrWhiteSpace(key)); + } } } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index a9896278..9ac95fd4 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using System.Text.RegularExpressions; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading { @@ -17,46 +18,25 @@ namespace StardewModdingAPI.Framework.ModLoading ** Public methods *********/ /// <summary>Get manifest metadata for each folder in the given root path.</summary> + /// <param name="toolkit">The mod toolkit.</param> /// <param name="rootPath">The root path to search for mods.</param> - /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <returns>Returns the manifests by relative folder.</returns> - public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) + public IEnumerable<IModMetadata> ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase) { - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) + foreach (ModFolder folder in toolkit.GetModFolders(rootPath)) { - // read file - Manifest manifest = null; - string path = Path.Combine(modDir.FullName, "manifest.json"); - string error = null; - try - { - manifest = jsonHelper.ReadJsonFile<Manifest>(path); - if (manifest == null) - { - error = File.Exists(path) - ? "its manifest is invalid." - : "it doesn't have a manifest."; - } - } - catch (SParseException ex) - { - error = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; - } + Manifest manifest = folder.Manifest; // parse internal data record (if any) - ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); // get display name string displayName = manifest?.Name; if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); + displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName); // apply defaults if (manifest != null && dataRecord != null) @@ -66,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading } // build metadata - ModMetadataStatus status = error == null + ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); + yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } @@ -98,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModLoading case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's outdated"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List<string> updateUrls = new List<string>(); @@ -194,8 +174,15 @@ namespace StardewModdingAPI.Framework.ModLoading missingFields.Add(nameof(IManifest.UniqueID)); if (missingFields.Any()) + { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + continue; + } } + + // validate ID format + if (Regex.IsMatch(mod.Manifest.UniqueID, "[^a-z0-9_.-]", RegexOptions.IgnoreCase)) + mod.SetStatus(ModMetadataStatus.Failed, "its ma |
