From 032997650010a9b6cd3378cb1a2b8273fb3f56ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Feb 2018 23:06:44 -0500 Subject: rewrite all mod assemblies to let SMAPI proxy into their internal classes (#435) --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 80 +++++++++++------------- 1 file changed, 38 insertions(+), 42 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 3a7b214a..ac849971 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; @@ -94,23 +95,14 @@ namespace StardewModdingAPI.Framework.ModLoading if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); - if (changed) + this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) { - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } - } - else - { - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); - lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); } } @@ -192,38 +184,48 @@ namespace StardewModdingAPI.Framework.ModLoading /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + private void RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; + // let SMAPI proxy mod internals for mod-provided APIs + { + MethodReference attributeConstructor = module.Import(typeof(InternalsVisibleToAttribute).GetConstructor(new[] { typeof(string) })); + CustomAttribute attribute = new CustomAttribute(attributeConstructor); + attribute.ConstructorArguments.Add(new CustomAttributeArgument(module.TypeSystem.String, "StardewModdingAPI.Proxies")); + assembly.CustomAttributes.Add(attribute); + } + // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + for (int i = 0; i < module.AssemblyReferences.Count; i++) { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); } - } - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); } // find (and optionally rewrite) incompatible instructions - bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { @@ -232,8 +234,6 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; } // check CIL instructions @@ -244,13 +244,9 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; } } } - - return platformChanged || anyRewritten; } /// Process the result from an instruction handler. -- cgit From 024489c33827ce8e1463eac199daa996a8a99216 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 18:50:46 -0500 Subject: overhaul internal mod data format (#439) The new format is much more concise, reduces the memory footprint by only parsing metadata for loaded mods, and adds support for versioning and defaulting most fields. --- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 4 ++-- src/SMAPI/Framework/ModLoading/ModResolver.cs | 24 ++++++++++-------------- 2 files changed, 12 insertions(+), 16 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 30fe211b..1a71920e 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// Metadata about the mod from SMAPI's internal data (if any). - public ModDataRecord DataRecord { get; } + public ParsedModDataRecord DataRecord { get; } /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's full directory path. /// The mod manifest. /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9802d9e9..6671e880 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -53,18 +53,15 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get internal data record (if any) - ModDataRecord dataRecord = null; + // parse internal data record (if any) + ParsedModDataRecord dataRecord = null; if (manifest != null) { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); + ModDataRecord rawDataRecord = dataRecords.FirstOrDefault(p => p.Matches(manifest)); + if (rawDataRecord != null) + dataRecord = rawDataRecord.ParseFieldsFor(manifest); } - // add default update keys - if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) - manifest.UpdateKeys = dataRecord.UpdateKeys; - // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name @@ -93,17 +90,16 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // validate compatibility - ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); - switch (compatibility?.Status) + switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List updateUrls = new List(); @@ -124,10 +120,10 @@ namespace StardewModdingAPI.Framework.ModLoading // build error string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) error += "newer version"; else - error += $"version newer than {compatibility.UpperVersion}"; + error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); -- cgit From 2f101e716adae530d0451b1673a80fd25eced1b6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 22:11:20 -0500 Subject: encapsulate mod DB, add display name, and use in dependency checks (#439) --- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 48 +++++++++++++++------------ 2 files changed, 27 insertions(+), 23 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 1a71920e..29bb6617 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 6671e880..09a9299e 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,6 +3,7 @@ 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; @@ -17,12 +18,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// Get manifest metadata for each folder in the given root path. /// The root path to search for mods. /// The JSON helper with which to read manifests. - /// Metadata about mods from SMAPI's internal data. + /// Handles access to SMAPI's internal mod metadata list. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable dataRecords) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) { - dataRecords = dataRecords.ToArray(); - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file @@ -54,22 +53,19 @@ namespace StardewModdingAPI.Framework.ModLoading } // parse internal data record (if any) - ParsedModDataRecord dataRecord = null; - if (manifest != null) - { - ModDataRecord rawDataRecord = dataRecords.FirstOrDefault(p => p.Matches(manifest)); - if (rawDataRecord != null) - dataRecord = rawDataRecord.ParseFieldsFor(manifest); - } + ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + + // get display name + string displayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = dataRecord?.DisplayName; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); // build metadata - string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) - ? manifest.Name - : modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); ModMetadataStatus status = error == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); } } @@ -193,7 +189,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. - public IEnumerable ProcessDependencies(IEnumerable mods) + /// Handles access to SMAPI's internal mod metadata list. + public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase) { // initialise metadata mods = mods.ToArray(); @@ -209,7 +206,7 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); + this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List()); return sortedMods.Reverse(); } @@ -220,12 +217,13 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. /// The full list of mods being validated. + /// Handles access to SMAPI's internal mod metadata list. /// The mod whose dependencies to process. /// The dependency state for each mod. /// The list in which to save mods sorted by dependency order. /// The current change of mod dependencies. /// Returns the mod dependency status. - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) { // check if already visited switch (states[mod]) @@ -276,11 +274,17 @@ namespace StardewModdingAPI.Framework.ModLoading // missing required dependencies, mark failed { - string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); - if (failedIDs.Any()) + string[] failedModNames = ( + from entry in dependencies + where entry.IsRequired && entry.Mod == null + let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + orderby displayName + select displayName + ).ToArray(); + if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -325,7 +329,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // recursively process each dependency - var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); + var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); switch (substatus) { // sorted successfully -- cgit From 9b3dd42cbf62a8524ac390d9418cf961c502868a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 23:02:04 -0500 Subject: encapsulate update key to URL logic for reuse (#437) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 09a9299e..99d86bf8 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -73,8 +73,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// Validate manifest metadata. /// The mod manifests to validate. /// The current SMAPI version. - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). - public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, IDictionary vendorModUrls) + /// Get an update URL for an update key (if valid). + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, Func getUpdateUrl) { mods = mods.ToArray(); @@ -101,15 +101,9 @@ namespace StardewModdingAPI.Framework.ModLoading List updateUrls = new List(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { - string[] parts = key.Split(new[] { ':' }, 2); - if (parts.Length != 2) - continue; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - updateUrls.Add(string.Format(urlTemplate, modID)); + string url = getUpdateUrl(key); + if (url != null) + updateUrls.Add(url); } if (mod.DataRecord.AlternativeUrl != null) updateUrls.Add(mod.DataRecord.AlternativeUrl); -- cgit From 8a1982326799dfe7f3d078beba32348befa1c9e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 23:12:01 -0500 Subject: add mod page URL to missing-dependency errors (#437) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 99d86bf8..b46ee117 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -272,8 +272,11 @@ namespace StardewModdingAPI.Framework.ModLoading from entry in dependencies where entry.IsRequired && entry.Mod == null let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + let modUrl = modDatabase.GetModPageUrlFor(entry.ID) orderby displayName - select displayName + select modUrl != null + ? $"{displayName}: {modUrl}" + : displayName ).ToArray(); if (failedModNames.Any()) { -- cgit From e64326f9fe5a388e3a0638567bf4bdf8aab4b639 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 16:38:45 -0500 Subject: Revert "rewrite all mod assemblies to let SMAPI proxy into their internal classes (#435)" This reverts commit 032997650010a9b6cd3378cb1a2b8273fb3f56ff. --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 80 +++++++++++++----------- 1 file changed, 42 insertions(+), 38 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index ac849971..3a7b214a 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; @@ -95,14 +94,23 @@ namespace StardewModdingAPI.Framework.ModLoading if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + if (changed) { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + { + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } } @@ -184,48 +192,38 @@ namespace StardewModdingAPI.Framework.ModLoading /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private void RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; - // let SMAPI proxy mod internals for mod-provided APIs - { - MethodReference attributeConstructor = module.Import(typeof(InternalsVisibleToAttribute).GetConstructor(new[] { typeof(string) })); - CustomAttribute attribute = new CustomAttribute(attributeConstructor); - attribute.ConstructorArguments.Add(new CustomAttributeArgument(module.TypeSystem.String, "StardewModdingAPI.Proxies")); - assembly.CustomAttributes.Add(attribute); - } - // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) { - for (int i = 0; i < module.AssemblyReferences.Count; i++) + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) - { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; - } - } - - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; } } + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); + } // find (and optionally rewrite) incompatible instructions + bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { @@ -234,6 +232,8 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; } // check CIL instructions @@ -244,9 +244,13 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; } } } + + return platformChanged || anyRewritten; } /// Process the result from an instruction handler. -- cgit From 4444b590f016ebecfc113a0dd4584723b0250f41 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 16:34:31 -0500 Subject: add content pack feature (#436) --- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 29 ++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 151 +++++++++++++++++++------- 2 files changed, 138 insertions(+), 42 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 29bb6617..1a0f9994 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading @@ -26,12 +27,21 @@ namespace StardewModdingAPI.Framework.ModLoading /// The reason the metadata is invalid, if any. public string Error { get; private set; } - /// The mod instance (if it was loaded). + /// The mod instance (if loaded and is false). public IMod Mod { get; private set; } + /// The content pack instance (if loaded and is true). + public IContentPack ContentPack { get; private set; } + + /// Writes messages to the console and log file as this mod. + public IMonitor Monitor { get; private set; } + /// The mod-provided API (if any). public object Api { get; private set; } + /// Whether the mod is a content pack. + public bool IsContentPack => this.Manifest?.ContentPackFor != null; + /********* ** Public methods @@ -64,7 +74,24 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod instance to set. public IModMetadata SetMod(IMod mod) { + if (this.ContentPack != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + this.Mod = mod; + this.Monitor = mod.Monitor; + return this; + } + + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor) + { + if (this.Mod != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + + this.ContentPack = contentPack; + this.Monitor = monitor; return this; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b46ee117..be73254d 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -30,18 +30,13 @@ namespace StardewModdingAPI.Framework.ModLoading string error = null; try { - // read manifest manifest = jsonHelper.ReadJsonFile(path); - - // validate if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - error = "its manifest doesn't set an entry DLL."; } catch (SParseException ex) { @@ -85,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; - // validate compatibility + // validate compatibility from internal data switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: @@ -128,24 +123,52 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL value - if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll)) - { - mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field."); - continue; - } - if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + // validate DLL / content pack fields { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } + bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); + bool isContentPack = mod.Manifest.ContentPackFor != null; - // validate DLL path - string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; + // validate field presence + if (!hasDll && !isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + continue; + } + if (hasDll && isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + continue; + } + + // validate DLL + if (hasDll) + { + // invalid filename format + if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + continue; + } + + // invalid path + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); + if (!File.Exists(assemblyPath)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + } + + // validate content pack + else + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + continue; + } + } } // validate required fields @@ -243,30 +266,17 @@ namespace StardewModdingAPI.Framework.ModLoading throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } - // no dependencies, mark sorted - if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) + // collect dependencies + ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + + // mark sorted if no dependencies + if (!dependencies.Any()) { sortedMods.Push(mod); return states[mod] = ModDependencyStatus.Sorted; } - // get dependencies - var dependencies = - ( - from entry in mod.Manifest.Dependencies - let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - orderby entry.UniqueID - select new - { - ID = entry.UniqueID, - MinVersion = entry.MinimumVersion, - Mod = dependencyMod, - IsRequired = entry.IsRequired - } - ) - .ToArray(); - - // missing required dependencies, mark failed + // mark failed if missing dependencies { string[] failedModNames = ( from entry in dependencies @@ -371,5 +381,64 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } + + /// Get the dependencies declared in a manifest. + /// The mod manifest. + /// The loaded mods. + private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + { + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + + // yield dependencies + if (manifest.Dependencies != null) + { + foreach (var entry in manifest.Dependencies) + yield return 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); + } + + + /********* + ** Private models + *********/ + /// Represents a dependency from one mod to another. + private struct ModDependency + { + /********* + ** Accessors + *********/ + /// The unique ID of the required mod. + public string ID { get; } + + /// The minimum required version (if any). + public ISemanticVersion MinVersion { get; } + + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public bool IsRequired { get; } + + /// The loaded mod that fulfills the dependency (if available). + public IModMetadata Mod { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the required mod. + /// The minimum required version (if any). + /// The loaded mod that fulfills the dependency (if available). + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired) + { + this.ID = id; + this.MinVersion = minVersion; + this.Mod = mod; + this.IsRequired = isRequired; + } + } } } -- cgit From 258e4c16e3d58256304854f9cd9633f0ff480375 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 13:56:54 -0500 Subject: fix default update keys not being applied (#439) --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index be73254d..8b4a3eb8 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -57,6 +57,13 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + // apply defaults + if (manifest != null && dataRecord != null) + { + if (dataRecord.UpdateKey != null) + manifest.UpdateKeys = new[] { dataRecord.UpdateKey }; + } + // build metadata ModMetadataStatus status = error == null ? ModMetadataStatus.Found -- cgit From 674618664a72679812c1b51065f725fec99aa86d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 19:32:27 -0500 Subject: add unvalidated update tick event for specialised use cases (#446) --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 5 +++++ src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 3a7b214a..ccbd053e 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,6 +285,11 @@ namespace StardewModdingAPI.Framework.ModLoading 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); 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); + 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.", diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index 0ae598fc..6a7e0519 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Events; + namespace StardewModdingAPI.Framework.ModLoading { /// Indicates how an instruction was handled. @@ -19,6 +21,9 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedSaveSerialiser, /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. - DetectedDynamic + DetectedDynamic, + + /// The instruction is compatible, but references which may impact stability. + DetectedUnvalidatedUpdateTick } } -- cgit From 9369232118d0ae08df49bbc30037387cf71dd861 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 20:29:52 -0500 Subject: replace manual relative path logic with new path utilities --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 8b4a3eb8..ba6dab1a 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -6,6 +6,7 @@ using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.ModLoading { @@ -55,7 +56,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); // apply defaults if (manifest != null && dataRecord != null) -- cgit From d7696912e007a2b455a2fd5e1974924d2efe83b3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Feb 2018 16:51:37 -0500 Subject: reimplement log parser with serverside parsing and vue.js frontend --- src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index 6a7e0519..cfa23d08 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. DetectedDynamic, - /// The instruction is compatible, but references which may impact stability. + /// The instruction is compatible, but references which may impact stability. DetectedUnvalidatedUpdateTick } } -- cgit