using System; using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using Mono.Cecil; using StardewModdingAPI.Framework.AssemblyRewriting; namespace StardewModdingAPI.Framework { /// Preprocesses and loads mod assemblies. internal class ModAssemblyLoader { /********* ** Properties *********/ /// 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; /********* ** Public methods *********/ /// Construct an instance. /// The cache directory. /// The current game platform. /// Encapsulates monitoring and logging for a given module. public ModAssemblyLoader(string cacheDirPath, Platform targetPlatform, IMonitor monitor) { this.CacheDirPath = cacheDirPath; this.Monitor = monitor; this.AssemblyTypeRewriter = this.GetAssemblyRewriter(targetPlatform); } /// Preprocess an assembly and cache the modified version. /// The assembly file path. 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); // 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) { this.Monitor.Log($"Preprocessing new assembly {assemblyFileName}..."); // read assembly definition AssemblyDefinition assembly; using (Stream readStream = new MemoryStream(assemblyBytes)) assembly = AssemblyDefinition.ReadAssembly(readStream); // rewrite assembly to match platform this.AssemblyTypeRewriter.RewriteAssembly(assembly); // 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); } } } } /// 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); // unsafe load allows DLLs downloaded from the Internet without the user needing to 'unblock' them } /********* ** 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); } /// 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); } } }