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 ++++++++++++++++++ src/StardewModdingAPI/Program.cs | 77 ++++++++++++++-------- src/StardewModdingAPI/StardewModdingAPI.csproj | 18 +++++ src/StardewModdingAPI/packages.config | 1 + 4 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModAssemblyLoader.cs (limited to 'src/StardewModdingAPI') 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); + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index d31a1d39..12b6cad4 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI /// The full path to the folder containing mods. private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); + /// The full path to the folder containing cached SMAPI data. + private static readonly string CachePath = Path.Combine(Program.ModPath, ".cache"); + /// The log file to which to write messages. private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); @@ -126,6 +129,7 @@ namespace StardewModdingAPI Program.Monitor.Log("Loading SMAPI..."); Console.Title = Constants.ConsoleTitle; Program.VerifyPath(Program.ModPath); + Program.VerifyPath(Program.CachePath); Program.VerifyPath(Constants.LogDir); if (!File.Exists(Program.GameExecutablePath)) { @@ -293,6 +297,8 @@ namespace StardewModdingAPI private static void LoadMods() { Program.Monitor.Log("Loading mods..."); + + ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath); foreach (string directory in Directory.GetDirectories(Program.ModPath)) { foreach (string manifestPath in Directory.GetFiles(directory, "manifest.json")) @@ -382,9 +388,11 @@ namespace StardewModdingAPI } } - // load DLL & hook up mod + // load assembly + Assembly modAssembly; try { + // get assembly path string assemblyPath = Path.Combine(directory, manifest.EntryDll); if (!File.Exists(assemblyPath)) { @@ -392,36 +400,47 @@ namespace StardewModdingAPI continue; } - Assembly modAssembly = Assembly.UnsafeLoadFrom(assemblyPath); - if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) > 0) + // read assembly + modAssembly = modAssemblyLoader.ProcessAssembly(assemblyPath); + if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) { - TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); - Mod modEntry = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); - if (modEntry != null) - { - // track mod - Program.ModRegistry.Add(manifest, modAssembly); - - // hook up mod - modEntry.Manifest = manifest; - modEntry.Helper = helper; - modEntry.Monitor = new Monitor(manifest.Name, Program.LogFile) { ShowTraceInConsole = Program.DeveloperMode }; - modEntry.PathOnDisk = directory; - Program.Monitor.Log($"Loaded mod: {modEntry.Manifest.Name} by {modEntry.Manifest.Author}, v{modEntry.Manifest.Version} | {modEntry.Manifest.Description}", LogLevel.Info); - Program.ModsLoaded += 1; - modEntry.Entry(); // deprecated since 1.0 - modEntry.Entry((ModHelper)modEntry.Helper); // deprecated since 1.1 - modEntry.Entry(modEntry.Helper); // deprecated since 1.1 - - // raise deprecation warning for old Entry() method - if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.0", DeprecationLevel.Notice); - if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) })) - Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.1", DeprecationLevel.Notice); - } - } - else Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); + continue; + } + } + catch (Exception ex) + { + Program.Monitor.Log($"{errorPrefix}: an error occurred while optimising the target DLL.\n{ex}", LogLevel.Error); + continue; + } + + // hook up mod + try + { + TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); + Mod modEntry = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); + if (modEntry != null) + { + // track mod + Program.ModRegistry.Add(manifest, modAssembly); + + // hook up mod + modEntry.Manifest = manifest; + modEntry.Helper = helper; + modEntry.Monitor = new Monitor(manifest.Name, Program.LogFile) { ShowTraceInConsole = Program.DeveloperMode }; + modEntry.PathOnDisk = directory; + Program.Monitor.Log($"Loaded mod: {modEntry.Manifest.Name} by {modEntry.Manifest.Author}, v{modEntry.Manifest.Version} | {modEntry.Manifest.Description}", LogLevel.Info); + Program.ModsLoaded += 1; + modEntry.Entry(); // deprecated since 1.0 + modEntry.Entry((ModHelper)modEntry.Helper); // deprecated since 1.1 + modEntry.Entry(modEntry.Helper); // deprecated since 1.1 + + // raise deprecation warning for old Entry() method + if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) + Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.0", DeprecationLevel.Notice); + if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) })) + Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.1", DeprecationLevel.Notice); + } } catch (Exception ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 0b6a185e..f3dbc45a 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -140,6 +140,22 @@ + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + True + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + True + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + True + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll + True + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll True @@ -200,6 +216,7 @@ + @@ -284,5 +301,6 @@ + \ No newline at end of file diff --git a/src/StardewModdingAPI/packages.config b/src/StardewModdingAPI/packages.config index 0aec11c6..dc07902b 100644 --- a/src/StardewModdingAPI/packages.config +++ b/src/StardewModdingAPI/packages.config @@ -2,5 +2,6 @@ + \ No newline at end of file -- cgit