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) --- .../StardewModdingAPI.Installer.csproj | 1 + .../Framework/ModAssemblyLoader.cs | 65 ++++++++++++++++++ src/StardewModdingAPI/Program.cs | 77 ++++++++++++++-------- src/StardewModdingAPI/StardewModdingAPI.csproj | 18 +++++ src/StardewModdingAPI/packages.config | 1 + 5 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModAssemblyLoader.cs (limited to 'src') diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index 6f9ed2eb..23e2d278 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -67,6 +67,7 @@ + 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 From 08d5ee293ffc001dff3e866bd45954ef306e81ac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 22 Nov 2016 00:03:14 -0500 Subject: simplify manifest.json path check --- src/StardewModdingAPI/Program.cs | 223 +++++++++++++++++++-------------------- 1 file changed, 111 insertions(+), 112 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 12b6cad4..255ad365 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -301,152 +301,151 @@ namespace StardewModdingAPI ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath); foreach (string directory in Directory.GetDirectories(Program.ModPath)) { - foreach (string manifestPath in Directory.GetFiles(directory, "manifest.json")) + // check for cancellation + if (Program.CancellationTokenSource.IsCancellationRequested) { - // check for cancellation - if (Program.CancellationTokenSource.IsCancellationRequested) - { - Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); - return; - } + Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); + return; + } - IModHelper helper = new ModHelper(directory); - string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; + // get helper + IModHelper helper = new ModHelper(directory); - // read manifest - Manifest manifest; - try + // get manifest path + string manifestPath = Path.Combine(directory, "manifest.json"); + string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; + Manifest manifest; + try + { + // read manifest text + string json = File.ReadAllText(manifestPath); + if (string.IsNullOrEmpty(json)) { - // read manifest text - string json = File.ReadAllText(manifestPath); - if (string.IsNullOrEmpty(json)) - { - Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); - continue; - } - - // deserialise manifest - manifest = helper.ReadJsonFile("manifest.json"); - if (manifest == null) - { - Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); - continue; - } - if (string.IsNullOrEmpty(manifest.EntryDll)) - { - Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); - continue; - } + Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); + continue; + } - // log deprecated fields - if (manifest.UsedAuthourField) - Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0", DeprecationLevel.Notice); + // deserialise manifest + manifest = helper.ReadJsonFile("manifest.json"); + if (manifest == null) + { + Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); + continue; } - catch (Exception ex) + if (string.IsNullOrEmpty(manifest.EntryDll)) { - Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } - // validate version - if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) + // log deprecated fields + if (manifest.UsedAuthourField) + Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0", DeprecationLevel.Notice); + } + catch (Exception ex) + { + Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // validate version + if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) + { + try { - try + Version minVersion = new Version(manifest.MinimumApiVersion); + if (minVersion.IsNewerThan(Constants.Version)) { - Version minVersion = new Version(manifest.MinimumApiVersion); - if (minVersion.IsNewerThan(Constants.Version)) - { - Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); - continue; - } - } - catch (FormatException ex) when (ex.Message.Contains("not a semantic version")) - { - Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); continue; } } - - // create per-save directory - if (manifest.PerSaveConfigs) + catch (FormatException ex) when (ex.Message.Contains("not a semantic version")) { - Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Notice); - try - { - string psDir = Path.Combine(directory, "psconfigs"); - Directory.CreateDirectory(psDir); - if (!Directory.Exists(psDir)) - { - Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); - continue; - } - } - catch (Exception ex) - { - Program.Monitor.Log($"{errorPrefix}: couldm't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } + Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error); + continue; } + } - // load assembly - Assembly modAssembly; + // create per-save directory + if (manifest.PerSaveConfigs) + { + Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Notice); try { - // get assembly path - string assemblyPath = Path.Combine(directory, manifest.EntryDll); - if (!File.Exists(assemblyPath)) + string psDir = Path.Combine(directory, "psconfigs"); + Directory.CreateDirectory(psDir); + if (!Directory.Exists(psDir)) { - Program.Monitor.Log($"{errorPrefix}: target DLL '{assemblyPath}' does not exist.", LogLevel.Error); - continue; - } - - // read assembly - modAssembly = modAssemblyLoader.ProcessAssembly(assemblyPath); - if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) - { - Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while optimising the target DLL.\n{ex}", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: couldm't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } + } - // hook up mod - try + // load assembly + Assembly modAssembly; + try + { + // get assembly path + string assemblyPath = Path.Combine(directory, manifest.EntryDll); + if (!File.Exists(assemblyPath)) { - 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); - } + Program.Monitor.Log($"{errorPrefix}: target DLL '{assemblyPath}' does not exist.", LogLevel.Error); + continue; } - catch (Exception ex) + + // read assembly + modAssembly = modAssemblyLoader.ProcessAssembly(assemblyPath); + if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) + { + 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) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + // 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) + { + Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + } } // print result -- 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 ++++++++++++++++++---- src/StardewModdingAPI/Program.cs | 34 ++++++--- 2 files changed, 94 insertions(+), 22 deletions(-) (limited to 'src') 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; + } } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 255ad365..f8920896 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -301,6 +301,10 @@ namespace StardewModdingAPI ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath); foreach (string directory in Directory.GetDirectories(Program.ModPath)) { + // ignore internal directory + if (new DirectoryInfo(directory).Name == ".cache") + continue; + // check for cancellation if (Program.CancellationTokenSource.IsCancellationRequested) { @@ -388,20 +392,32 @@ namespace StardewModdingAPI } } + // preprocess mod assemblies + { + bool succeeded = true; + foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll")) + { + try + { + modAssemblyLoader.ProcessAssembly(assemblyPath); + } + catch (Exception ex) + { + Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{assemblyPath}'.\n{ex}", LogLevel.Error); + succeeded = false; + break; + } + } + if (!succeeded) + continue; + } + // load assembly Assembly modAssembly; try { - // get assembly path string assemblyPath = Path.Combine(directory, manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - Program.Monitor.Log($"{errorPrefix}: target DLL '{assemblyPath}' does not exist.", LogLevel.Error); - continue; - } - - // read assembly - modAssembly = modAssemblyLoader.ProcessAssembly(assemblyPath); + modAssembly = modAssemblyLoader.LoadCachedAssembly(assemblyPath); if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) { Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); -- 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 ++++++++- src/StardewModdingAPI/Program.cs | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) (limited to 'src') 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)) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index f8920896..e364ef03 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -298,7 +298,7 @@ namespace StardewModdingAPI { Program.Monitor.Log("Loading mods..."); - ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath); + ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath, Program.Monitor); foreach (string directory in Directory.GetDirectories(Program.ModPath)) { // ignore internal directory @@ -403,7 +403,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{assemblyPath}'.\n{ex}", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{Path.GetFileName(assemblyPath)}'.\n{ex.GetLogSummary()}", LogLevel.Error); succeeded = false; break; } @@ -426,7 +426,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while optimising the target DLL.\n{ex}", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: an error occurred while optimising the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } -- 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 ++++++++++++ src/StardewModdingAPI/Program.cs | 12 ++++++++---- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Platform.cs (limited to 'src') 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 diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index e364ef03..eba89981 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -23,13 +23,17 @@ namespace StardewModdingAPI /********* ** Properties *********/ - /// The full path to the Stardew Valley executable. + /// The target game platform. + private static readonly Platform TargetPlatform = #if SMAPI_FOR_WINDOWS - private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, "Stardew Valley.exe"); + Platform.Windows; #else - private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, "StardewValley.exe"); + Platform.Mono; #endif + /// The full path to the Stardew Valley executable. + private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, Program.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + /// The full path to the folder containing mods. private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); @@ -298,7 +302,7 @@ namespace StardewModdingAPI { Program.Monitor.Log("Loading mods..."); - ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath, Program.Monitor); + ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath, Program.TargetPlatform, Program.Monitor); foreach (string directory in Directory.GetDirectories(Program.ModPath)) { // ignore internal directory diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index f3dbc45a..01be9a68 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -217,6 +217,7 @@ + -- 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 +--------------------- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 35 insertions(+), 34 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs (limited to 'src') 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; - } - } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 01be9a68..c835df42 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -213,6 +213,7 @@ + -- 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 +++- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs (limited to 'src') 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); + } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index c835df42..2abcdc23 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -214,6 +214,7 @@ + -- 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') 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') 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') 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') 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 first time; preprocessing..."); // read assembly definition AssemblyDefinition assembly; @@ -155,7 +155,7 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); } - return new AssemblyTypeRewriter(targetAssemblies, removeAssemblyReferences); + return new AssemblyTypeRewriter(targetAssemblies, removeAssemblyReferences, this.Monitor); } } } -- cgit From f7b8879011873fa8f7a3d5dd7db27254bfc90469 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 27 Nov 2016 15:56:47 -0500 Subject: supplement assembly resolution for Mono (#166) --- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 21 +++++-------- .../AssemblyRewriting/PlatformAssemblyMap.cs | 35 ++++++++++++++++++++++ .../Framework/ModAssemblyLoader.cs | 20 ++++++++++--- src/StardewModdingAPI/Program.cs | 4 ++- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 7081df15..66c36c03 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -11,11 +11,8 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /********* ** 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; + /// Metadata for mapping assemblies to the current . + private readonly PlatformAssemblyMap AssemblyMap; /// A type => assembly lookup for types which should be rewritten. private readonly IDictionary TypeAssemblies; @@ -31,22 +28,20 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting ** 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 . + /// Metadata for mapping assemblies to the current . /// Encapsulates monitoring and logging. - public AssemblyTypeRewriter(Assembly[] targetAssemblies, string[] removeAssemblyNames, IMonitor monitor) + public AssemblyTypeRewriter(PlatformAssemblyMap assemblyMap, IMonitor monitor) { // save config - this.TargetAssemblies = targetAssemblies; - this.RemoveAssemblyNames = removeAssemblyNames; + this.AssemblyMap = assemblyMap; this.Monitor = monitor; // cache assembly metadata - this.AssemblyNameReferences = targetAssemblies.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); + this.AssemblyNameReferences = assemblyMap.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); // collect type => assembly lookup this.TypeAssemblies = new Dictionary(); - foreach (Assembly assembly in targetAssemblies) + foreach (Assembly assembly in assemblyMap.Targets) { foreach (Module assemblyModule in assembly.Modules) { @@ -73,7 +68,7 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting // remove old assembly references for (int i = 0; i < module.AssemblyReferences.Count; i++) { - bool shouldRemove = this.RemoveAssemblyNames.Any(name => module.AssemblyReferences[i].Name == name); + bool shouldRemove = this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name); if (shouldRemove) { this.Monitor.Log($"removing reference to {module.AssemblyReferences[i]}", LogLevel.Trace); diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs new file mode 100644 index 00000000..3e6cec8a --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs @@ -0,0 +1,35 @@ +using System.Reflection; + +namespace StardewModdingAPI.Framework.AssemblyRewriting +{ + /// Metadata for mapping assemblies to the current . + internal class PlatformAssemblyMap + { + /********* + ** Accessors + *********/ + /// The target game platform. + public readonly Platform TargetPlatform; + + /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). + public readonly string[] RemoveNames; + + /// The assembly filenames to target. Equivalent types should be rewritten to use these assemblies. + public readonly Assembly[] Targets; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The target game platform. + /// The assembly short names to remove (like Stardew Valley). + /// The assemblies to target. + public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) + { + this.TargetPlatform = targetPlatform; + this.RemoveNames = removeAssemblyNames; + this.Targets = targetAssemblies; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 7de48649..3d08ec64 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; + /// Metadata for mapping assemblies to the current . + private readonly PlatformAssemblyMap AssemblyMap; + /// Rewrites assembly types to match the current platform. private readonly AssemblyTypeRewriter AssemblyTypeRewriter; @@ -35,7 +38,8 @@ namespace StardewModdingAPI.Framework { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; - this.AssemblyTypeRewriter = this.GetAssemblyRewriter(targetPlatform); + this.AssemblyMap = this.GetAssemblyMap(targetPlatform); + this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor); } /// Preprocess an assembly and cache the modified version. @@ -97,6 +101,14 @@ namespace StardewModdingAPI.Framework return Assembly.UnsafeLoadFrom(cachePaths.Assembly); // unsafe load allows DLLs downloaded from the Internet without the user needing to 'unblock' them } + /// Resolve an assembly from its name. + /// The assembly name. + public Assembly ResolveAssembly(string name) + { + string shortName = name.Split(new[] { ',' }, 2).First(); + return this.AssemblyMap.Targets.FirstOrDefault(p => p.GetName().Name == shortName); + } + /********* ** Private methods @@ -112,9 +124,9 @@ namespace StardewModdingAPI.Framework return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath); } - /// Get an assembly rewriter for the target platform. + /// Get metadata for mapping assemblies to the current platform. /// The target game platform. - private AssemblyTypeRewriter GetAssemblyRewriter(Platform targetPlatform) + private PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) { // get assembly changes needed for platform string[] removeAssemblyReferences; @@ -155,7 +167,7 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); } - return new AssemblyTypeRewriter(targetAssemblies, removeAssemblyReferences, this.Monitor); + return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index eba89981..bed4cafc 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -303,6 +303,8 @@ namespace StardewModdingAPI Program.Monitor.Log("Loading mods..."); ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath, Program.TargetPlatform, Program.Monitor); + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // supplement Mono's assembly resolution which doesn't handle assembly rewrites very well + foreach (string directory in Directory.GetDirectories(Program.ModPath)) { // ignore internal directory @@ -391,7 +393,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: couldm't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 2abcdc23..fd02f802 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -215,6 +215,7 @@ + -- cgit From f3675aa466e429810fd4254a0413403e203c942e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 28 Nov 2016 20:51:57 -0500 Subject: move assembly map into constants (#166) --- src/StardewModdingAPI/Constants.cs | 52 ++++++++++++++++++++++ .../Framework/ModAssemblyLoader.cs | 48 +------------------- 2 files changed, 53 insertions(+), 47 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 05e410ab..66eb1336 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -2,6 +2,8 @@ using System.IO; using System.Linq; using System.Reflection; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.AssemblyRewriting; using StardewValley; namespace StardewModdingAPI @@ -62,6 +64,56 @@ namespace StardewModdingAPI public static string LogPath => Path.Combine(Constants.LogDir, "MODDED_ProgramLog.Log_LATEST.txt"); + /********* + ** Internal field + *********/ + /// Get metadata for mapping assemblies to the current platform. + /// The target game platform. + internal static PlatformAssemblyMap GetAssemblyMap(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 PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); + } + + /********* ** Private field *********/ diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index 3d08ec64..b6d98bde 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; - this.AssemblyMap = this.GetAssemblyMap(targetPlatform); + this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor); } @@ -123,51 +123,5 @@ namespace StardewModdingAPI.Framework string cacheHashPath = Path.Combine(dirPath, $"{key}.hash"); return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath); } - - /// Get metadata for mapping assemblies to the current platform. - /// The target game platform. - private PlatformAssemblyMap GetAssemblyMap(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 PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); - } } } -- cgit From b425e320558299bf6dd0c9252efec24e63ad3605 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 29 Nov 2016 12:56:03 -0500 Subject: move dependencies into targets file for reuse (#166) --- src/StardewModdingAPI/StardewModdingAPI.csproj | 61 +------------------------ src/dependencies.targets | 62 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 60 deletions(-) create mode 100644 src/dependencies.targets (limited to 'src') diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index fd02f802..efd760f2 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -79,66 +79,7 @@ icon.ico - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - - - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - - - False - - - False - - - False - - - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - $(DefineConstants);SMAPI_FOR_UNIX - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll diff --git a/src/dependencies.targets b/src/dependencies.targets new file mode 100644 index 00000000..d5428967 --- /dev/null +++ b/src/dependencies.targets @@ -0,0 +1,62 @@ + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + + + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + + + False + + + False + + + False + + + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + $(DefineConstants);SMAPI_FOR_UNIX + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + \ No newline at end of file -- cgit From 5470e95bf59e5e3bae249ead6909163390fb2af3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 29 Nov 2016 14:02:59 -0500 Subject: add separate project to support upcoming IL rewriting (#166) --- .../Platform.cs | 12 +++ .../PlatformAssemblyMap.cs | 35 +++++++++ .../Properties/AssemblyInfo.cs | 7 ++ .../StardewModdingAPI.AssemblyRewriters.csproj | 90 ++++++++++++++++++++++ .../packages.config | 4 + .../StardewModdingAPI.Installer.csproj | 1 + src/StardewModdingAPI.sln | 14 ++++ src/StardewModdingAPI/Constants.cs | 3 +- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 1 + .../AssemblyRewriting/PlatformAssemblyMap.cs | 35 --------- .../Framework/ModAssemblyLoader.cs | 1 + src/StardewModdingAPI/Framework/Platform.cs | 12 --- src/StardewModdingAPI/Program.cs | 1 + src/StardewModdingAPI/StardewModdingAPI.csproj | 9 ++- 14 files changed, 174 insertions(+), 51 deletions(-) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Platform.cs create mode 100644 src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj create mode 100644 src/StardewModdingAPI.AssemblyRewriters/packages.config delete mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs delete mode 100644 src/StardewModdingAPI/Framework/Platform.cs (limited to 'src') diff --git a/src/StardewModdingAPI.AssemblyRewriters/Platform.cs b/src/StardewModdingAPI.AssemblyRewriters/Platform.cs new file mode 100644 index 00000000..8888a9a8 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Platform.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.AssemblyRewriters +{ + /// The game's platform version. + public enum Platform + { + /// The Linux/Mac version of the game. + Mono, + + /// The Windows version of the game. + Windows + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs new file mode 100644 index 00000000..c0855719 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs @@ -0,0 +1,35 @@ +using System.Reflection; + +namespace StardewModdingAPI.AssemblyRewriters +{ + /// Metadata for mapping assemblies to the current . + public class PlatformAssemblyMap + { + /********* + ** Accessors + *********/ + /// The target game platform. + public readonly Platform TargetPlatform; + + /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). + public readonly string[] RemoveNames; + + /// The assembly filenames to target. Equivalent types should be rewritten to use these assemblies. + public readonly Assembly[] Targets; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The target game platform. + /// The assembly short names to remove (like Stardew Valley). + /// The assemblies to target. + public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) + { + this.TargetPlatform = targetPlatform; + this.RemoveNames = removeAssemblyNames; + this.Targets = targetAssemblies; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..25fbfef9 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")] +[assembly: AssemblyDescription("Contains internal SMAPI code for converting mods between Linux/Mac and Windows. This assembly is used by SMAPI internally and shouldn't be referenced directly by mods.")] +[assembly: AssemblyProduct("StardewModdingAPI.CrossplatformRewriters")] +[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")] diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj new file mode 100644 index 00000000..d87a48bc --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49} + Library + Properties + StardewModdingAPI.AssemblyRewriters + StardewModdingAPI.AssemblyRewriters + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x86\Debug\ + DEBUG;TRACE;SMAPI_FOR_WINDOWS + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE;SMAPI_FOR_WINDOWS + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + + ..\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 + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + \ No newline at end of file diff --git a/src/StardewModdingAPI.AssemblyRewriters/packages.config b/src/StardewModdingAPI.AssemblyRewriters/packages.config new file mode 100644 index 00000000..88fbc79d --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index 23e2d278..c19e5f55 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -70,6 +70,7 @@ + diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 31bf5f2f..d97e4645 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,18 @@ Global {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|Any CPU {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|Any CPU {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.Build.0 = Release|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|Any CPU + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 66eb1336..b9074b6e 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -2,8 +2,7 @@ using System.IO; using System.Linq; using System.Reflection; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.AssemblyRewriting; +using StardewModdingAPI.AssemblyRewriters; using StardewValley; namespace StardewModdingAPI diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 66c36c03..43f6aa11 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using Mono.Cecil; +using StardewModdingAPI.AssemblyRewriters; namespace StardewModdingAPI.Framework.AssemblyRewriting { diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs deleted file mode 100644 index 3e6cec8a..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/PlatformAssemblyMap.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI.Framework.AssemblyRewriting -{ - /// Metadata for mapping assemblies to the current . - internal class PlatformAssemblyMap - { - /********* - ** Accessors - *********/ - /// The target game platform. - public readonly Platform TargetPlatform; - - /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). - public readonly string[] RemoveNames; - - /// The assembly filenames to target. Equivalent types should be rewritten to use these assemblies. - public readonly Assembly[] Targets; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The target game platform. - /// The assembly short names to remove (like Stardew Valley). - /// The assemblies to target. - public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) - { - this.TargetPlatform = targetPlatform; - this.RemoveNames = removeAssemblyNames; - this.Targets = targetAssemblies; - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs index b6d98bde..e095cde8 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.AssemblyRewriters; using StardewModdingAPI.Framework.AssemblyRewriting; namespace StardewModdingAPI.Framework diff --git a/src/StardewModdingAPI/Framework/Platform.cs b/src/StardewModdingAPI/Framework/Platform.cs deleted file mode 100644 index cab81e06..00000000 --- a/src/StardewModdingAPI/Framework/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index bed4cafc..f0686039 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -9,6 +9,7 @@ using System.Windows.Forms; #endif using Microsoft.Xna.Framework.Graphics; using Newtonsoft.Json; +using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Inheritance; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index efd760f2..6f3bcb4b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -156,12 +156,10 @@ - - @@ -218,6 +216,12 @@ false + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.AssemblyRewriters + + @@ -243,6 +247,7 @@ + -- cgit From cc4d3c1cf8755467acc4c72d25bf1e03fd8c4cba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 29 Nov 2016 14:03:14 -0500 Subject: add framework for rewriting incompatible methods (#166) --- .../IMethodRewriter.cs | 21 +++++ .../PlatformAssemblyMap.cs | 24 ++++- .../StardewModdingAPI.AssemblyRewriters.csproj | 1 + .../StardewModdingAPI.Installer.csproj | 1 + src/StardewModdingAPI/Constants.cs | 9 +- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 100 ++++++++++++++------- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs (limited to 'src') diff --git a/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs new file mode 100644 index 00000000..5cbb7e0d --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs @@ -0,0 +1,21 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.AssemblyRewriters +{ + /// Rewrites a method for compatibility. + public interface IMethodRewriter + { + /// Get whether the given method reference can be rewritten. + /// The method reference. + bool ShouldRewrite(MethodReference methodRef); + + /// Rewrite a method for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction which calls the method. + /// The method reference invoked by the . + /// Metadata for mapping assemblies to the current platform. + void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap); + } +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs index c0855719..f2826080 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using Mono.Cecil; namespace StardewModdingAPI.AssemblyRewriters { @@ -8,6 +11,9 @@ namespace StardewModdingAPI.AssemblyRewriters /********* ** Accessors *********/ + /**** + ** Data + ****/ /// The target game platform. public readonly Platform TargetPlatform; @@ -15,8 +21,19 @@ namespace StardewModdingAPI.AssemblyRewriters public readonly string[] RemoveNames; /// The assembly filenames to target. Equivalent types should be rewritten to use these assemblies. + + /**** + ** Metadata + ****/ + /// The assemblies to target. Equivalent types should be rewritten to use these assemblies. public readonly Assembly[] Targets; + /// An assembly => reference cache. + public readonly IDictionary TargetReferences; + + /// An assembly => module cache. + public readonly IDictionary TargetModules; + /********* ** Public methods @@ -27,9 +44,14 @@ namespace StardewModdingAPI.AssemblyRewriters /// The assemblies to target. public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) { + // save data this.TargetPlatform = targetPlatform; this.RemoveNames = removeAssemblyNames; + + // cache assembly metadata this.Targets = targetAssemblies; + this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index d87a48bc..b2533566 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -71,6 +71,7 @@ Properties\GlobalAssemblyInfo.cs + diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index c19e5f55..8e4d38b3 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -68,6 +68,7 @@ + diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index b9074b6e..4f2f00a1 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -112,6 +113,12 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } + /// Get method rewriters which fix incompatible method calls in mod assemblies. + internal static IEnumerable GetMethodRewriters() + { + yield break; + } + /********* ** Private field @@ -123,4 +130,4 @@ namespace StardewModdingAPI return $"{prefix}_{Game1.uniqueIDForThisGame}"; } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 43f6aa11..1e97bdcb 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Reflection; using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; using StardewModdingAPI.AssemblyRewriters; namespace StardewModdingAPI.Framework.AssemblyRewriting @@ -18,9 +20,6 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// A type => assembly lookup for types which should be rewritten. private readonly IDictionary TypeAssemblies; - /// An assembly => reference cache. - private readonly IDictionary AssemblyNameReferences; - /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; @@ -37,24 +36,18 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting this.AssemblyMap = assemblyMap; this.Monitor = monitor; - // cache assembly metadata - this.AssemblyNameReferences = assemblyMap.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - // collect type => assembly lookup this.TypeAssemblies = new Dictionary(); foreach (Assembly assembly in assemblyMap.Targets) { - foreach (Module assemblyModule in assembly.Modules) + ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; + foreach (TypeDefinition type in module.GetTypes()) { - 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; - } + if (!type.IsPublic) + continue; // no need to rewrite + if (type.Namespace.Contains("<")) + continue; // ignore assembly metadata + this.TypeAssemblies[type.FullName] = assembly; } } } @@ -64,13 +57,12 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting public void RewriteAssembly(AssemblyDefinition assembly) { 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 + bool shouldRewrite = false; for (int i = 0; i < module.AssemblyReferences.Count; i++) { - bool shouldRemove = this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name); - if (shouldRemove) + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { this.Monitor.Log($"removing reference to {module.AssemblyReferences[i]}", LogLevel.Trace); shouldRewrite = true; @@ -78,25 +70,52 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting i--; } } + if (!shouldRewrite) + return; - // replace references - if (shouldRewrite) + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values) - { - this.Monitor.Log($" adding reference to {target}", LogLevel.Trace); - module.AssemblyReferences.Add(target); - } + this.Monitor.Log($" adding reference to {target}", LogLevel.Trace); + module.AssemblyReferences.Add(target); + } - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - string lastTypeLogged = null; - foreach (TypeReference type in typeReferences) + // 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; + } + + // rewrite incompatible methods + IMethodRewriter[] methodRewriters = Constants.GetMethodRewriters().ToArray(); + foreach (MethodDefinition method in this.GetMethods(module)) + { + // skip methods with no rewritable method + bool hasMethodToRewrite = method.Body.Instructions.Any(op => (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt) && methodRewriters.Any(rewriter => rewriter.ShouldRewrite((MethodReference)op.Operand))); + if (!hasMethodToRewrite) + continue; + + // rewrite method references + method.Body.SimplifyMacros(); + ILProcessor cil = method.Body.GetILProcessor(); + Instruction[] instructions = cil.Body.Instructions.ToArray(); + foreach (Instruction op in instructions) { - this.ChangeTypeScope(type, shouldLog: type.FullName != lastTypeLogged); - lastTypeLogged = type.FullName; + if (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt) + { + IMethodRewriter rewriter = methodRewriters.FirstOrDefault(p => p.ShouldRewrite((MethodReference)op.Operand)); + if (rewriter != null) + { + MethodReference methodRef = (MethodReference)op.Operand; + this.Monitor.Log($"rewriting method reference {methodRef.DeclaringType.FullName}.{methodRef.Name}"); + rewriter.Rewrite(module, cil, op, methodRef, this.AssemblyMap); + } + } } + method.Body.OptimizeMacros(); } } @@ -119,10 +138,23 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting return; // replace scope - AssemblyNameReference assemblyRef = this.AssemblyNameReferences[assembly]; + AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; if (shouldLog) this.Monitor.Log($"redirecting {type.FullName} from {type.Scope.Name} to {assemblyRef.Name}", LogLevel.Trace); type.Scope = assemblyRef; } + + /// Get all methods in a module. + /// The module to search. + private IEnumerable GetMethods(ModuleDefinition module) + { + return ( + from type in module.GetTypes() + where type.HasMethods + from method in type.Methods + where method.HasBody + select method + ); + } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 6f3bcb4b..f57c1b6f 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -252,5 +252,6 @@ + \ No newline at end of file -- cgit From 9ff9d02db5255d5c9ddf9a63cde9e48b28a9eff7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 29 Nov 2016 19:21:11 -0500 Subject: rewrite SpriteBatch.Begin calls for compatibility (#166) --- .../Rewriters/BaseMethodRewriter.cs | 92 ++++++++++++++++++++++ .../Rewriters/SpriteBatchRewriter.cs | 30 +++++++ .../StardewModdingAPI.AssemblyRewriters.csproj | 3 + .../Wrappers/CompatibleSpriteBatch.cs | 52 ++++++++++++ src/StardewModdingAPI/Constants.cs | 6 +- 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs (limited to 'src') diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs new file mode 100644 index 00000000..1af6e6c4 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.AssemblyRewriters.Rewriters +{ + /// Base class for a method rewriter. + public abstract class BaseMethodRewriter : IMethodRewriter + { + /********* + ** Public methods + *********/ + /// Get whether the given method reference can be rewritten. + /// The method reference. + public abstract bool ShouldRewrite(MethodReference methodRef); + + /// Rewrite a method for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction which calls the method. + /// The method reference invoked by the . + /// Metadata for mapping assemblies to the current platform. + public abstract void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap); + + + /********* + ** Protected methods + *********/ + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + protected bool HasMatchingSignature(MethodInfo definition, MethodReference reference) + { + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterInfo[] definitionParameters = definition.GetParameters(); + ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); + if (referenceParameters.Length != definitionParameters.Length) + return false; + for (int i = 0; i < referenceParameters.Length; i++) + { + if (!this.IsMatchingType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) + return false; + } + return true; + } + + /// Get whether a type has a method whose signature matches the one expected by a method reference. + /// The type to check. + /// The method reference. + protected bool HasMatchingSignature(Type type, MethodReference reference) + { + return type + .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) + .Any(method => this.HasMatchingSignature(method, reference)); + } + + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + private bool IsMatchingType(Type type, TypeReference reference) + { + // same namespace & name + if (type.Namespace != reference.Namespace || type.Name != reference.Name) + return false; + + // same generic parameters + if (type.IsGenericType) + { + if (!reference.IsGenericInstance) + return false; + + Type[] defGenerics = type.GetGenericArguments(); + TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); + if (defGenerics.Length != refGenerics.Length) + return false; + for (int i = 0; i < defGenerics.Length; i++) + { + if (!this.IsMatchingType(defGenerics[i], refGenerics[i])) + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs new file mode 100644 index 00000000..1c0a5cf3 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs @@ -0,0 +1,30 @@ +using Microsoft.Xna.Framework.Graphics; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.AssemblyRewriters.Wrappers; + +namespace StardewModdingAPI.AssemblyRewriters.Rewriters +{ + /// Rewrites references to to fix inconsistent method signatures between MonoGame and XNA. + /// MonoGame has one SpriteBatch.Begin method with optional arguments, but XNA has multiple method overloads. Incompatible method references are rewritten to use , which redirects all method signatures to the proper compiled MonoGame/XNA method. + public class SpriteBatchRewriter : BaseMethodRewriter + { + /// Get whether the given method reference can be rewritten. + /// The method reference. + public override bool ShouldRewrite(MethodReference methodRef) + { + return methodRef.DeclaringType.FullName == typeof(SpriteBatch).FullName && this.HasMatchingSignature(typeof(CompatibleSpriteBatch), methodRef); + } + + /// Rewrite a method for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction which calls the method. + /// The method reference invoked by the . + /// Metadata for mapping assemblies to the current platform. + public override void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap) + { + methodRef.DeclaringType = module.Import(typeof(CompatibleSpriteBatch)); + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index b2533566..51a49da0 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -75,6 +75,9 @@ + + + diff --git a/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs b/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs new file mode 100644 index 00000000..e28d1a68 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs @@ -0,0 +1,52 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +#pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required +namespace StardewModdingAPI.AssemblyRewriters.Wrappers +{ + /// Wraps methods that are incompatible when converting compiled code between MonoGame and XNA. + public class CompatibleSpriteBatch : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public CompatibleSpriteBatch(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + /**** + ** MonoGame signatures + ****/ + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + public new void Begin() + { + base.Begin(); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 4f2f00a1..815e5427 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.AssemblyRewriters.Rewriters; using StardewValley; namespace StardewModdingAPI @@ -116,7 +117,10 @@ namespace StardewModdingAPI /// Get method rewriters which fix incompatible method calls in mod assemblies. internal static IEnumerable GetMethodRewriters() { - yield break; + return new[] + { + new SpriteBatchRewriter() + }; } -- cgit From 528a934786ac845681354f28fa8564997767116f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 29 Nov 2016 19:22:59 -0500 Subject: exclude rewriters project from IL weaving (#166) --- src/StardewModdingAPI/FodyWeavers.xml | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/StardewModdingAPI/FodyWeavers.xml b/src/StardewModdingAPI/FodyWeavers.xml index 0ef30506..bd5a8e78 100644 --- a/src/StardewModdingAPI/FodyWeavers.xml +++ b/src/StardewModdingAPI/FodyWeavers.xml @@ -2,6 +2,7 @@ + StardewModdingAPI.AssemblyRewriters Stardew Valley xTile Steamworks.NET -- cgit