From 517a9d82fc1234b6c6ae6f1e45ef7c0f160ee6c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 21 Nov 2016 19:25:53 -0500 Subject: preprocess mods through Mono.Cecil to allow rewriting later (#166) --- .../Framework/ModAssemblyLoader.cs | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/StardewModdingAPI/Framework/ModAssemblyLoader.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs new file mode 100644 index 00000000..2615e1f7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -0,0 +1,65 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework +{ + /// Loads mod assemblies. + internal class ModAssemblyLoader + { + /********* + ** Properties + *********/ + /// The directory in which to cache data. + private readonly string CacheDirPath; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cache directory. + public ModAssemblyLoader(string cacheDirPath) + { + this.CacheDirPath = cacheDirPath; + } + + /// Read an assembly from the given path. + /// The assembly file path. + public Assembly ProcessAssembly(string assemblyPath) + { + // read assembly data + byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); + byte[] hash = MD5.Create().ComputeHash(assemblyBytes); + + // get cache data + string key = Path.GetFileNameWithoutExtension(assemblyPath); + string cachePath = Path.Combine(this.CacheDirPath, $"{key}.dll"); + string cacheHashPath = Path.Combine(this.CacheDirPath, $"{key}-hash.txt"); + bool canUseCache = File.Exists(cachePath) && File.Exists(cacheHashPath) && hash.SequenceEqual(File.ReadAllBytes(cacheHashPath)); + + // process assembly if not cached + if (!canUseCache) + { + // read assembly definition + AssemblyDefinition definition; + using (Stream readStream = new MemoryStream(assemblyBytes)) + definition = AssemblyDefinition.ReadAssembly(readStream); + + // write cache + using (MemoryStream outStream = new MemoryStream()) + { + definition.Write(outStream); + byte[] outBytes = outStream.ToArray(); + File.WriteAllBytes(cachePath, outBytes); + File.WriteAllBytes(cacheHashPath, hash); + } + } + + // load assembly + return Assembly.UnsafeLoadFrom(cachePath); + } + } +} -- cgit From e9fee3f6fe18927192b1dd676cd420507af2e389 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 22 Nov 2016 00:36:48 -0500 Subject: preprocess all mod assemblies for compatibility with multi-assembly mods (#166) --- .../Framework/ModAssemblyLoader.cs | 82 ++++++++++++++++++---- 1 file changed, 69 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 2615e1f7..6c0f0cdf 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; @@ -6,7 +7,7 @@ using Mono.Cecil; namespace StardewModdingAPI.Framework { - /// Loads mod assemblies. + /// Preprocesses and loads mod assemblies. internal class ModAssemblyLoader { /********* @@ -26,19 +27,17 @@ namespace StardewModdingAPI.Framework this.CacheDirPath = cacheDirPath; } - /// Read an assembly from the given path. + /// Preprocess an assembly and cache the modified version. /// The assembly file path. - public Assembly ProcessAssembly(string assemblyPath) + public void ProcessAssembly(string assemblyPath) { // read assembly data byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); byte[] hash = MD5.Create().ComputeHash(assemblyBytes); - // get cache data - string key = Path.GetFileNameWithoutExtension(assemblyPath); - string cachePath = Path.Combine(this.CacheDirPath, $"{key}.dll"); - string cacheHashPath = Path.Combine(this.CacheDirPath, $"{key}-hash.txt"); - bool canUseCache = File.Exists(cachePath) && File.Exists(cacheHashPath) && hash.SequenceEqual(File.ReadAllBytes(cacheHashPath)); + // check cache + CachePaths cachePaths = this.GetCacheInfo(assemblyPath); + bool canUseCache = File.Exists(cachePaths.Assembly) && File.Exists(cachePaths.Hash) && hash.SequenceEqual(File.ReadAllBytes(cachePaths.Hash)); // process assembly if not cached if (!canUseCache) @@ -53,13 +52,70 @@ namespace StardewModdingAPI.Framework { definition.Write(outStream); byte[] outBytes = outStream.ToArray(); - File.WriteAllBytes(cachePath, outBytes); - File.WriteAllBytes(cacheHashPath, hash); + Directory.CreateDirectory(cachePaths.Directory); + File.WriteAllBytes(cachePaths.Assembly, outBytes); + File.WriteAllBytes(cachePaths.Hash, hash); } } + } + + /// Load a preprocessed assembly. + /// The assembly file path. + public Assembly LoadCachedAssembly(string assemblyPath) + { + CachePaths cachePaths = this.GetCacheInfo(assemblyPath); + if (!File.Exists(cachePaths.Assembly)) + throw new InvalidOperationException($"The assembly {assemblyPath} doesn't exist in the preprocessed cache."); + return Assembly.UnsafeLoadFrom(cachePaths.Assembly); + } - // load assembly - return Assembly.UnsafeLoadFrom(cachePath); + + /********* + ** Private methods + *********/ + /// Get the cache details for an assembly. + /// The assembly file path. + private CachePaths GetCacheInfo(string assemblyPath) + { + string key = Path.GetFileNameWithoutExtension(assemblyPath); + string dirPath = Path.Combine(this.CacheDirPath, new DirectoryInfo(Path.GetDirectoryName(assemblyPath)).Name); + string cacheAssemblyPath = Path.Combine(dirPath, $"{key}.dll"); + string cacheHashPath = Path.Combine(dirPath, $"{key}.hash"); + return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath); + } + + /********* + ** Private objects + *********/ + /// Contains the paths for an assembly's cached data. + private struct CachePaths + { + /********* + ** Accessors + *********/ + /// The directory path which contains the assembly. + public string Directory { get; } + + /// The file path of the assembly file. + public string Assembly { get; } + + /// The file path containing the MD5 hash for the assembly. + public string Hash { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The directory path which contains the assembly. + /// The file path of the assembly file. + /// The file path containing the MD5 hash for the assembly. + public CachePaths(string directory, string assembly, string hash) + { + this.Directory = directory; + this.Assembly = assembly; + this.Hash = hash; + } } } } -- cgit From 7bea3c2ba00789a38ae71035548c2c6b5c298d5b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Nov 2016 16:00:02 -0500 Subject: add log entry when preprocessing an assembly (#166) --- src/StardewModdingAPI/Framework/ModAssemblyLoader.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 6c0f0cdf..f367c6a0 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -16,15 +16,20 @@ namespace StardewModdingAPI.Framework /// The directory in which to cache data. private readonly string CacheDirPath; + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + /********* ** Public methods *********/ /// Construct an instance. /// The cache directory. - public ModAssemblyLoader(string cacheDirPath) + /// Encapsulates monitoring and logging for a given module. + public ModAssemblyLoader(string cacheDirPath, IMonitor monitor) { this.CacheDirPath = cacheDirPath; + this.Monitor = monitor; } /// Preprocess an assembly and cache the modified version. @@ -42,6 +47,8 @@ namespace StardewModdingAPI.Framework // process assembly if not cached if (!canUseCache) { + this.Monitor.Log($"Preprocessing new assembly {assemblyPath}..."); + // read assembly definition AssemblyDefinition definition; using (Stream readStream = new MemoryStream(assemblyBytes)) -- cgit From 1de8dc1b0f58991f9d15fa343e849bd6a2023ecc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Nov 2016 16:07:21 -0500 Subject: pass target platform to assembly rewriter for later use (#166) --- src/StardewModdingAPI/Framework/ModAssemblyLoader.cs | 3 ++- src/StardewModdingAPI/Framework/Platform.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/StardewModdingAPI/Framework/Platform.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index f367c6a0..e5a73a8b 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -25,8 +25,9 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The cache directory. + /// The current game platform. /// Encapsulates monitoring and logging for a given module. - public ModAssemblyLoader(string cacheDirPath, IMonitor monitor) + public ModAssemblyLoader(string cacheDirPath, Platform targetPlatform, IMonitor monitor) { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; diff --git a/src/StardewModdingAPI/Framework/Platform.cs b/src/StardewModdingAPI/Framework/Platform.cs new file mode 100644 index 00000000..cab81e06 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Platform.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Framework +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux/Mac version of the game. + Mono, + + /// The Windows version of the game. + Windows + } +} \ No newline at end of file -- cgit From 4df199985558caee3275fdd65762bdc3e1d040c6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Nov 2016 16:10:41 -0500 Subject: move cache struct into its own file (#166) --- .../Framework/AssemblyRewriting/CachePaths.cs | 33 ++++++++++++++++++++ .../Framework/ModAssemblyLoader.cs | 35 +--------------------- 2 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs new file mode 100644 index 00000000..17c4d188 --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs @@ -0,0 +1,33 @@ +namespace StardewModdingAPI.Framework.AssemblyRewriting +{ + /// Contains the paths for an assembly's cached data. + internal struct CachePaths + { + /********* + ** Accessors + *********/ + /// The directory path which contains the assembly. + public string Directory { get; } + + /// The file path of the assembly file. + public string Assembly { get; } + + /// The file path containing the MD5 hash for the assembly. + public string Hash { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The directory path which contains the assembly. + /// The file path of the assembly file. + /// The file path containing the MD5 hash for the assembly. + public CachePaths(string directory, string assembly, string hash) + { + this.Directory = directory; + this.Assembly = assembly; + this.Hash = hash; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index e5a73a8b..bde23e3b 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Security.Cryptography; using Mono.Cecil; +using StardewModdingAPI.Framework.AssemblyRewriting; namespace StardewModdingAPI.Framework { @@ -91,39 +92,5 @@ namespace StardewModdingAPI.Framework string cacheHashPath = Path.Combine(dirPath, $"{key}.hash"); return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath); } - - /********* - ** Private objects - *********/ - /// Contains the paths for an assembly's cached data. - private struct CachePaths - { - /********* - ** Accessors - *********/ - /// The directory path which contains the assembly. - public string Directory { get; } - - /// The file path of the assembly file. - public string Assembly { get; } - - /// The file path containing the MD5 hash for the assembly. - public string Hash { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The directory path which contains the assembly. - /// The file path of the assembly file. - /// The file path containing the MD5 hash for the assembly. - public CachePaths(string directory, string assembly, string hash) - { - this.Directory = directory; - this.Assembly = assembly; - this.Hash = hash; - } - } } } -- cgit From b06aed66c47d093585600ca0f7ee1e247507e6b8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Nov 2016 16:12:21 -0500 Subject: rewrite type references in mod assemblies to match target platform (#166) --- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 318 +++++++++++++++++++++ .../Framework/ModAssemblyLoader.cs | 59 +++- 2 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs new file mode 100644 index 00000000..93003a64 --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -0,0 +1,318 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using CallSite = Mono.Cecil.CallSite; + +namespace StardewModdingAPI.Framework.AssemblyRewriting +{ + /// Rewrites type references. + internal class AssemblyTypeRewriter + { + /********* + ** Properties + *********/ + /// The assemblies to target. Equivalent types will be rewritten to use these assemblies. + private readonly Assembly[] TargetAssemblies; + + /// >The short assembly names to remove as assembly reference, and replace with the . + private readonly string[] RemoveAssemblyNames; + + /// A type => assembly lookup for types which should be rewritten. + private readonly IDictionary TypeAssemblies; + + /// An assembly => reference cache. + private readonly IDictionary AssemblyNameReferences; + + /// An assembly => module cache. + private readonly IDictionary AssemblyModules; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly filenames to target. Equivalent types will be rewritten to use these assemblies. + /// The short assembly names to remove as assembly reference, and replace with the . + public AssemblyTypeRewriter(Assembly[] targetAssemblies, string[] removeAssemblyNames) + { + // save config + this.TargetAssemblies = targetAssemblies; + this.RemoveAssemblyNames = removeAssemblyNames; + + // cache assembly metadata + this.AssemblyNameReferences = targetAssemblies.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); + this.AssemblyModules = targetAssemblies.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); // technically an assembly can contain multiple modules, but none of the build tools (including MSBuild itself) support it + + // collect type => assembly lookup + this.TypeAssemblies = new Dictionary(); + foreach (Assembly assembly in targetAssemblies) + { + ModuleDefinition module = this.AssemblyModules[assembly]; + foreach (TypeDefinition type in module.GetTypes()) + { + if (!type.IsPublic) + continue; // no need to rewrite + if (type.Namespace.Contains("<")) + continue; // ignore C++ stuff + this.TypeAssemblies[type.FullName] = assembly; + } + } + } + + /// Rewrite the types referenced by an assembly. + /// The assembly to rewrite. + public void RewriteAssembly(AssemblyDefinition assembly) + { + foreach (ModuleDefinition module in assembly.Modules) + { + // rewrite assembly references + bool shouldRewriteTypes = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) + { + bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name) || this.TargetAssemblies.Any(a => module.AssemblyReferences[i].Name == a.GetName().Name); + if (shouldRemove) + { + shouldRewriteTypes = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) + { + module.AssemblyReferences.Add(target); + shouldRewriteTypes = true; + } + + // rewrite references + if (shouldRewriteTypes) + { + // rewrite types + foreach (TypeDefinition type in module.GetTypes()) + this.RewriteReferences(type, module); + + // rewrite type references + TypeReference[] refs = (TypeReference[])module.GetTypeReferences(); + for (int i = 0; i < refs.Length; ++i) + refs[i] = this.GetTypeReference(refs[i], module); + } + } + } + + + /********* + ** Private methods + *********/ + /// Rewrite the references for a code object. + /// The type to rewrite. + /// The module being rewritten. + private void RewriteReferences(TypeDefinition type, ModuleDefinition module) + { + // rewrite base type + type.BaseType = this.GetTypeReference(type.BaseType, module); + + // rewrite interfaces + for (int i = 0; i < type.Interfaces.Count; i++) + type.Interfaces[i] = this.GetTypeReference(type.Interfaces[i], module); + + // rewrite events + foreach (EventDefinition @event in type.Events) + { + this.RewriteReferences(@event.AddMethod, module); + this.RewriteReferences(@event.RemoveMethod, module); + this.RewriteReferences(@event.InvokeMethod, module); + } + + // rewrite properties + foreach (PropertyDefinition property in type.Properties) + { + this.RewriteReferences(property.GetMethod, module); + this.RewriteReferences(property.SetMethod, module); + } + + // rewrite methods + foreach (MethodDefinition method in type.Methods) + this.RewriteReferences(method, module); + + // rewrite fields + foreach (FieldDefinition field in type.Fields) + this.RewriteReferences(field, module); + + // rewrite nested types + foreach (TypeDefinition nestedType in type.NestedTypes) + this.RewriteReferences(nestedType, module); + + // rewrite generic parameters + foreach (GenericParameter parameter in type.GenericParameters) + this.RewriteReferences(parameter, module); + + module.Import(type); + } + + /// Rewrite the references for a code object. + /// The method to rewrite. + /// The module being rewritten. + private void RewriteReferences(MethodReference method, ModuleDefinition module) + { + // parameter types + if (method.HasParameters) + { + foreach (ParameterDefinition parameter in method.Parameters) + parameter.ParameterType = this.GetTypeReference(parameter.ParameterType, module); + } + + // return type + method.MethodReturnType.ReturnType = this.GetTypeReference(method.MethodReturnType.ReturnType, module); + + module.Import(method); + } + + /// Rewrite the references for a code object. + /// The method to rewrite. + /// The module being rewritten. + private void RewriteReferences(MethodDefinition method, ModuleDefinition module) + { + if (method == null) + return; + + this.RewriteReferences((MethodReference)method, module); + + // overrides + foreach (MethodReference @override in method.Overrides) + this.RewriteReferences(@override, module); + + // body + if (method.HasBody) + { + // this + if (method.Body.ThisParameter != null) + method.Body.ThisParameter.ParameterType = this.GetTypeReference(method.Body.ThisParameter.ParameterType, module); + + // variables + if (method.Body.HasVariables) + { + foreach (VariableDefinition variable in method.Body.Variables) + variable.VariableType = this.GetTypeReference(variable.VariableType, module); + } + + // instructions + foreach (Instruction instruction in method.Body.Instructions) + { + object operand = instruction.Operand; + + // type + { + TypeReference type = operand as TypeReference; + if (type != null) + { + instruction.Operand = this.GetTypeReference(type, module); + continue; + } + } + + // method + { + MethodReference methodRef = operand as MethodReference; + if (methodRef != null) + { + this.RewriteReferences(methodRef, module); + continue; + } + } + + // field + { + FieldReference field = operand as FieldReference; + if (field != null) + { + this.RewriteReferences(field, module); + continue; + } + } + + // variable + { + VariableDefinition variable = operand as VariableDefinition; + if (variable != null) + { + variable.VariableType = this.GetTypeReference(variable.VariableType, module); + continue; + } + } + + // parameter + { + ParameterDefinition parameter = operand as ParameterDefinition; + if (parameter != null) + { + parameter.ParameterType = this.GetTypeReference(parameter.ParameterType, module); + continue; + } + } + + // call site + { + CallSite call = operand as CallSite; + if (call != null) + { + foreach (ParameterDefinition parameter in call.Parameters) + parameter.ParameterType = this.GetTypeReference(parameter.ParameterType, module); + call.ReturnType = this.GetTypeReference(call.ReturnType, module); + } + } + } + } + + module.Import(method); + } + + /// Rewrite the references for a code object. + /// The generic parameter to rewrite. + /// The module being rewritten. + private void RewriteReferences(GenericParameter parameter, ModuleDefinition module) + { + // constraints + for (int i = 0; i < parameter.Constraints.Count; i++) + parameter.Constraints[i] = this.GetTypeReference(parameter.Constraints[i], module); + + // generic parameters + foreach (GenericParameter genericParam in parameter.GenericParameters) + this.RewriteReferences(genericParam, module); + } + + /// Rewrite the references for a code object. + /// The field to rewrite. + /// The module being rewritten. + private void RewriteReferences(FieldReference field, ModuleDefinition module) + { + field.DeclaringType = this.GetTypeReference(field.DeclaringType, module); + field.FieldType = this.GetTypeReference(field.FieldType, module); + module.Import(field); + } + + /// Get the correct reference to use for compatibility with the current platform. + /// The type reference to rewrite. + /// The module being rewritten. + private TypeReference GetTypeReference(TypeReference type, ModuleDefinition module) + { + // check skip conditions + if (type == null) + return null; + if (type.FullName.StartsWith("System.")) + return type; + + // get assembly + Assembly assembly; + if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) + return type; + + // replace type + AssemblyNameReference newAssembly = this.AssemblyNameReferences[assembly]; + ModuleDefinition newModule = this.AssemblyModules[assembly]; + type = new TypeReference(type.Namespace, type.Name, newModule, newAssembly); + + return module.Import(type); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index bde23e3b..4e59bb08 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework /// The directory in which to cache data. private readonly string CacheDirPath; + /// Rewrites assembly types to match the current platform. + private readonly AssemblyTypeRewriter AssemblyTypeRewriter; + /// Encapsulates monitoring and logging for a given module. private readonly IMonitor Monitor; @@ -32,6 +35,7 @@ namespace StardewModdingAPI.Framework { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; + this.AssemblyTypeRewriter = this.GetAssemblyRewriter(targetPlatform); } /// Preprocess an assembly and cache the modified version. @@ -52,14 +56,17 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Preprocessing new assembly {assemblyPath}..."); // read assembly definition - AssemblyDefinition definition; + AssemblyDefinition assembly; using (Stream readStream = new MemoryStream(assemblyBytes)) - definition = AssemblyDefinition.ReadAssembly(readStream); + assembly = AssemblyDefinition.ReadAssembly(readStream); + + // rewrite assembly to match platform + this.AssemblyTypeRewriter.RewriteAssembly(assembly); // write cache using (MemoryStream outStream = new MemoryStream()) { - definition.Write(outStream); + assembly.Write(outStream); byte[] outBytes = outStream.ToArray(); Directory.CreateDirectory(cachePaths.Directory); File.WriteAllBytes(cachePaths.Assembly, outBytes); @@ -92,5 +99,51 @@ namespace StardewModdingAPI.Framework string cacheHashPath = Path.Combine(dirPath, $"{key}.hash"); return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath); } + + /// Get an assembly rewriter for the target platform. + /// The target game platform. + private AssemblyTypeRewriter GetAssemblyRewriter(Platform targetPlatform) + { + // get assembly changes needed for platform + string[] removeAssemblyReferences; + Assembly[] targetAssemblies; + switch (targetPlatform) + { + case Platform.Mono: + removeAssemblyReferences = new[] + { + "Stardew Valley", + "Microsoft.Xna.Framework", + "Microsoft.Xna.Framework.Game", + "Microsoft.Xna.Framework.Graphics" + }; + targetAssemblies = new[] + { + typeof(StardewValley.Game1).Assembly, + typeof(Microsoft.Xna.Framework.Vector2).Assembly + }; + break; + + case Platform.Windows: + removeAssemblyReferences = new[] + { + "StardewValley", + "MonoGame.Framework" + }; + targetAssemblies = new[] + { + typeof(StardewValley.Game1).Assembly, + typeof(Microsoft.Xna.Framework.Vector2).Assembly, + typeof(Microsoft.Xna.Framework.Game).Assembly, + typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly + }; + break; + + default: + throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); + } + + return new AssemblyTypeRewriter(targetAssemblies, removeAssemblyReferences); + } } } -- cgit From 2154b6de95bc97e3412b0800d3e2809bd2a1e544 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Nov 2016 16:14:10 -0500 Subject: use simpler, non-broken approach for rewriting mod type references (#166) --- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 255 ++------------------- 1 file changed, 24 insertions(+), 231 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 93003a64..7a339266 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -2,8 +2,6 @@ using System.Linq; using System.Reflection; using Mono.Cecil; -using Mono.Cecil.Cil; -using CallSite = Mono.Cecil.CallSite; namespace StardewModdingAPI.Framework.AssemblyRewriting { @@ -25,9 +23,6 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// An assembly => reference cache. private readonly IDictionary AssemblyNameReferences; - /// An assembly => module cache. - private readonly IDictionary AssemblyModules; - /********* ** Public methods @@ -43,20 +38,22 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting // cache assembly metadata this.AssemblyNameReferences = targetAssemblies.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - this.AssemblyModules = targetAssemblies.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); // technically an assembly can contain multiple modules, but none of the build tools (including MSBuild itself) support it // collect type => assembly lookup this.TypeAssemblies = new Dictionary(); foreach (Assembly assembly in targetAssemblies) { - ModuleDefinition module = this.AssemblyModules[assembly]; - foreach (TypeDefinition type in module.GetTypes()) + foreach (Module assemblyModule in assembly.Modules) { - if (!type.IsPublic) - continue; // no need to rewrite - if (type.Namespace.Contains("<")) - continue; // ignore C++ stuff - this.TypeAssemblies[type.FullName] = assembly; + ModuleDefinition module = ModuleDefinition.ReadModule(assemblyModule.FullyQualifiedName); + foreach (TypeDefinition type in module.GetTypes()) + { + if (!type.IsPublic) + continue; // no need to rewrite + if (type.Namespace.Contains("<")) + continue; // ignore assembly metadata + this.TypeAssemblies[type.FullName] = assembly; + } } } } @@ -67,36 +64,25 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting { foreach (ModuleDefinition module in assembly.Modules) { - // rewrite assembly references - bool shouldRewriteTypes = false; + // remove old assembly references for (int i = 0; i < module.AssemblyReferences.Count; i++) { bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name) || this.TargetAssemblies.Any(a => module.AssemblyReferences[i].Name == a.GetName().Name); if (shouldRemove) { - shouldRewriteTypes = true; module.AssemblyReferences.RemoveAt(i); i--; } } + + // add target assembly references foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) - { module.AssemblyReferences.Add(target); - shouldRewriteTypes = true; - } - - // rewrite references - if (shouldRewriteTypes) - { - // rewrite types - foreach (TypeDefinition type in module.GetTypes()) - this.RewriteReferences(type, module); - // rewrite type references - TypeReference[] refs = (TypeReference[])module.GetTypeReferences(); - for (int i = 0; i < refs.Length; ++i) - refs[i] = this.GetTypeReference(refs[i], module); - } + // rewrite type scopes to use target assemblies + TypeReference[] refs = (TypeReference[])module.GetTypeReferences(); + foreach (TypeReference type in refs) + this.ChangeTypeScope(type); } } @@ -104,215 +90,22 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /********* ** Private methods *********/ - /// Rewrite the references for a code object. - /// The type to rewrite. - /// The module being rewritten. - private void RewriteReferences(TypeDefinition type, ModuleDefinition module) - { - // rewrite base type - type.BaseType = this.GetTypeReference(type.BaseType, module); - - // rewrite interfaces - for (int i = 0; i < type.Interfaces.Count; i++) - type.Interfaces[i] = this.GetTypeReference(type.Interfaces[i], module); - - // rewrite events - foreach (EventDefinition @event in type.Events) - { - this.RewriteReferences(@event.AddMethod, module); - this.RewriteReferences(@event.RemoveMethod, module); - this.RewriteReferences(@event.InvokeMethod, module); - } - - // rewrite properties - foreach (PropertyDefinition property in type.Properties) - { - this.RewriteReferences(property.GetMethod, module); - this.RewriteReferences(property.SetMethod, module); - } - - // rewrite methods - foreach (MethodDefinition method in type.Methods) - this.RewriteReferences(method, module); - - // rewrite fields - foreach (FieldDefinition field in type.Fields) - this.RewriteReferences(field, module); - - // rewrite nested types - foreach (TypeDefinition nestedType in type.NestedTypes) - this.RewriteReferences(nestedType, module); - - // rewrite generic parameters - foreach (GenericParameter parameter in type.GenericParameters) - this.RewriteReferences(parameter, module); - - module.Import(type); - } - - /// Rewrite the references for a code object. - /// The method to rewrite. - /// The module being rewritten. - private void RewriteReferences(MethodReference method, ModuleDefinition module) - { - // parameter types - if (method.HasParameters) - { - foreach (ParameterDefinition parameter in method.Parameters) - parameter.ParameterType = this.GetTypeReference(parameter.ParameterType, module); - } - - // return type - method.MethodReturnType.ReturnType = this.GetTypeReference(method.MethodReturnType.ReturnType, module); - - module.Import(method); - } - - /// Rewrite the references for a code object. - /// The method to rewrite. - /// The module being rewritten. - private void RewriteReferences(MethodDefinition method, ModuleDefinition module) - { - if (method == null) - return; - - this.RewriteReferences((MethodReference)method, module); - - // overrides - foreach (MethodReference @override in method.Overrides) - this.RewriteReferences(@override, module); - - // body - if (method.HasBody) - { - // this - if (method.Body.ThisParameter != null) - method.Body.ThisParameter.ParameterType = this.GetTypeReference(method.Body.ThisParameter.ParameterType, module); - - // variables - if (method.Body.HasVariables) - { - foreach (VariableDefinition variable in method.Body.Variables) - variable.VariableType = this.GetTypeReference(variable.VariableType, module); - } - - // instructions - foreach (Instruction instruction in method.Body.Instructions) - { - object operand = instruction.Operand; - - // type - { - TypeReference type = operand as TypeReference; - if (type != null) - { - instruction.Operand = this.GetTypeReference(type, module); - continue; - } - } - - // method - { - MethodReference methodRef = operand as MethodReference; - if (methodRef != null) - { - this.RewriteReferences(methodRef, module); - continue; - } - } - - // field - { - FieldReference field = operand as FieldReference; - if (field != null) - { - this.RewriteReferences(field, module); - continue; - } - } - - // variable - { - VariableDefinition variable = operand as VariableDefinition; - if (variable != null) - { - variable.VariableType = this.GetTypeReference(variable.VariableType, module); - continue; - } - } - - // parameter - { - ParameterDefinition parameter = operand as ParameterDefinition; - if (parameter != null) - { - parameter.ParameterType = this.GetTypeReference(parameter.ParameterType, module); - continue; - } - } - - // call site - { - CallSite call = operand as CallSite; - if (call != null) - { - foreach (ParameterDefinition parameter in call.Parameters) - parameter.ParameterType = this.GetTypeReference(parameter.ParameterType, module); - call.ReturnType = this.GetTypeReference(call.ReturnType, module); - } - } - } - } - - module.Import(method); - } - - /// Rewrite the references for a code object. - /// The generic parameter to rewrite. - /// The module being rewritten. - private void RewriteReferences(GenericParameter parameter, ModuleDefinition module) - { - // constraints - for (int i = 0; i < parameter.Constraints.Count; i++) - parameter.Constraints[i] = this.GetTypeReference(parameter.Constraints[i], module); - - // generic parameters - foreach (GenericParameter genericParam in parameter.GenericParameters) - this.RewriteReferences(genericParam, module); - } - - /// Rewrite the references for a code object. - /// The field to rewrite. - /// The module being rewritten. - private void RewriteReferences(FieldReference field, ModuleDefinition module) - { - field.DeclaringType = this.GetTypeReference(field.DeclaringType, module); - field.FieldType = this.GetTypeReference(field.FieldType, module); - module.Import(field); - } - /// Get the correct reference to use for compatibility with the current platform. /// The type reference to rewrite. - /// The module being rewritten. - private TypeReference GetTypeReference(TypeReference type, ModuleDefinition module) + private void ChangeTypeScope(TypeReference type) { // check skip conditions - if (type == null) - return null; - if (type.FullName.StartsWith("System.")) - return type; + if (type == null || type.FullName.StartsWith("System.")) + return; // get assembly Assembly assembly; if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) - return type; - - // replace type - AssemblyNameReference newAssembly = this.AssemblyNameReferences[assembly]; - ModuleDefinition newModule = this.AssemblyModules[assembly]; - type = new TypeReference(type.Namespace, type.Name, newModule, newAssembly); + return; - return module.Import(type); + // replace scope + AssemblyNameReference assemblyRef = this.AssemblyNameReferences[assembly]; + type.Scope = assemblyRef; } } } -- cgit From 2e40ad7ad3d83970969b160eae87fc9aa4608916 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Nov 2016 16:26:36 -0500 Subject: copy pdb/mdb files to assembly cache (#166) --- src/StardewModdingAPI/Framework/ModAssemblyLoader.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 4e59bb08..264d1ff2 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -43,6 +43,8 @@ namespace StardewModdingAPI.Framework public void ProcessAssembly(string assemblyPath) { // read assembly data + string assemblyFileName = Path.GetFileName(assemblyPath); + string assemblyDir = Path.GetDirectoryName(assemblyPath); byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); byte[] hash = MD5.Create().ComputeHash(assemblyBytes); @@ -53,7 +55,7 @@ namespace StardewModdingAPI.Framework // process assembly if not cached if (!canUseCache) { - this.Monitor.Log($"Preprocessing new assembly {assemblyPath}..."); + this.Monitor.Log($"Preprocessing new assembly {assemblyFileName}..."); // read assembly definition AssemblyDefinition assembly; @@ -66,11 +68,21 @@ namespace StardewModdingAPI.Framework // write cache using (MemoryStream outStream = new MemoryStream()) { + // get assembly bytes assembly.Write(outStream); byte[] outBytes = outStream.ToArray(); + + // write assembly data Directory.CreateDirectory(cachePaths.Directory); File.WriteAllBytes(cachePaths.Assembly, outBytes); File.WriteAllBytes(cachePaths.Hash, hash); + + // copy any mdb/pdb files + foreach (string path in Directory.GetFiles(assemblyDir, "*.mdb").Concat(Directory.GetFiles(assemblyDir, "*.pdb"))) + { + string filename = Path.GetFileName(path); + File.Copy(path, Path.Combine(cachePaths.Directory, filename), overwrite: true); + } } } } -- cgit From 8917fb6697b5eae0c14bcf2437aef7e9a8d51abb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 27 Nov 2016 12:08:00 -0500 Subject: only rewrite assemblies if needed (#166) --- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 21 +++++++++++++-------- .../Framework/ModAssemblyLoader.cs | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 7a339266..2bfb43e4 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -65,24 +65,29 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting foreach (ModuleDefinition module in assembly.Modules) { // remove old assembly references + bool shouldRewrite = false; for (int i = 0; i < module.AssemblyReferences.Count; i++) { - bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name) || this.TargetAssemblies.Any(a => module.AssemblyReferences[i].Name == a.GetName().Name); + bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name); if (shouldRemove) { + shouldRewrite = true; module.AssemblyReferences.RemoveAt(i); i--; } } - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) - module.AssemblyReferences.Add(target); + // replace references + if (shouldRewrite) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) + module.AssemblyReferences.Add(target); - // rewrite type scopes to use target assemblies - TypeReference[] refs = (TypeReference[])module.GetTypeReferences(); - foreach (TypeReference type in refs) - this.ChangeTypeScope(type); + // rewrite type scopes to use target assemblies + foreach (TypeReference type in module.GetTypeReferences()) + this.ChangeTypeScope(type); + } } } diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 264d1ff2..54a111d3 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework CachePaths cachePaths = this.GetCacheInfo(assemblyPath); if (!File.Exists(cachePaths.Assembly)) throw new InvalidOperationException($"The assembly {assemblyPath} doesn't exist in the preprocessed cache."); - return Assembly.UnsafeLoadFrom(cachePaths.Assembly); + return Assembly.UnsafeLoadFrom(cachePaths.Assembly); // unsafe load allows DLLs downloaded from the Internet without the user needing to 'unblock' them } -- cgit From 0d94c628bbc1d1ab098e0a90ee5758ee69694887 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 27 Nov 2016 12:27:44 -0500 Subject: add trace logs when rewriting an assembly (#166) --- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 58 ++++++++++++++-------- .../Framework/ModAssemblyLoader.cs | 8 +-- 2 files changed, 41 insertions(+), 25 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 2bfb43e4..7081df15 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// An assembly => reference cache. private readonly IDictionary AssemblyNameReferences; + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + /********* ** Public methods @@ -30,11 +33,13 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// Construct an instance. /// The assembly filenames to target. Equivalent types will be rewritten to use these assemblies. /// The short assembly names to remove as assembly reference, and replace with the . - public AssemblyTypeRewriter(Assembly[] targetAssemblies, string[] removeAssemblyNames) + /// Encapsulates monitoring and logging. + public AssemblyTypeRewriter(Assembly[] targetAssemblies, string[] removeAssemblyNames, IMonitor monitor) { // save config this.TargetAssemblies = targetAssemblies; this.RemoveAssemblyNames = removeAssemblyNames; + this.Monitor = monitor; // cache assembly metadata this.AssemblyNameReferences = targetAssemblies.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); @@ -62,31 +67,39 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// The assembly to rewrite. public void RewriteAssembly(AssemblyDefinition assembly) { - foreach (ModuleDefinition module in assembly.Modules) + ModuleDefinition module = assembly.Modules.Single(); // technically an assembly can have multiple modules, but none of the build tools (including MSBuild) support it; simplify by assuming one module + bool shouldRewrite = false; + + // remove old assembly references + for (int i = 0; i < module.AssemblyReferences.Count; i++) { - // remove old assembly references - bool shouldRewrite = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) + bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name); + if (shouldRemove) { - bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name); - if (shouldRemove) - { - shouldRewrite = true; - module.AssemblyReferences.RemoveAt(i); - i--; - } + this.Monitor.Log($"removing reference to {module.AssemblyReferences[i]}", LogLevel.Trace); + shouldRewrite = true; + module.AssemblyReferences.RemoveAt(i); + i--; } + } - // replace references - if (shouldRewrite) + // replace references + if (shouldRewrite) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) - module.AssemblyReferences.Add(target); + this.Monitor.Log($" adding reference to {target}", LogLevel.Trace); + module.AssemblyReferences.Add(target); + } - // rewrite type scopes to use target assemblies - foreach (TypeReference type in module.GetTypeReferences()) - this.ChangeTypeScope(type); + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + string lastTypeLogged = null; + foreach (TypeReference type in typeReferences) + { + this.ChangeTypeScope(type, shouldLog: type.FullName != lastTypeLogged); + lastTypeLogged = type.FullName; } } } @@ -97,7 +110,8 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting *********/ /// Get the correct reference to use for compatibility with the current platform. /// The type reference to rewrite. - private void ChangeTypeScope(TypeReference type) + /// Whether to log a message. + private void ChangeTypeScope(TypeReference type, bool shouldLog) { // check skip conditions if (type == null || type.FullName.StartsWith("System.")) @@ -110,6 +124,8 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting // replace scope AssemblyNameReference assemblyRef = this.AssemblyNameReferences[assembly]; + if (shouldLog) + this.Monitor.Log($"redirecting {type.FullName} from {type.Scope.Name} to {assemblyRef.Name}", LogLevel.Trace); type.Scope = assemblyRef; } } diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 54a111d3..7de48649 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework /// Rewrites assembly types to match the current platform. private readonly AssemblyTypeRewriter AssemblyTypeRewriter; - /// Encapsulates monitoring and logging for a given module. + /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// The cache directory. /// The current game platform. - /// Encapsulates monitoring and logging for a given module. + /// Encapsulates monitoring and logging. public ModAssemblyLoader(string cacheDirPath, Platform targetPlatform, IMonitor monitor) { this.CacheDirPath = cacheDirPath; @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Framework // process assembly if not cached if (!canUseCache) { - this.Monitor.Log($"Preprocessing new assembly {assemblyFileName}..."); + this.Monitor.Log($"Loading {assemblyFileName} for the fi