From bb165f2079e33d02c0e673db73ac5b336272a3fa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 May 2017 23:21:02 -0400 Subject: organise a few framework classes --- .../Framework/AssemblyDefinitionResolver.cs | 61 ----- src/StardewModdingAPI/Framework/AssemblyLoader.cs | 292 --------------------- .../Framework/AssemblyParseResult.cs | 31 --- src/StardewModdingAPI/Framework/Manifest.cs | 45 ---- .../ModLoading/AssemblyDefinitionResolver.cs | 61 +++++ .../Framework/ModLoading/AssemblyLoader.cs | 292 +++++++++++++++++++++ .../Framework/ModLoading/AssemblyParseResult.cs | 31 +++ .../Framework/ModLoading/ModMetadata.cs | 40 +++ src/StardewModdingAPI/Framework/ModMetadata.cs | 40 --- src/StardewModdingAPI/Framework/Models/Manifest.cs | 44 ++++ src/StardewModdingAPI/Mod.cs | 1 + src/StardewModdingAPI/Program.cs | 1 + src/StardewModdingAPI/StardewModdingAPI.csproj | 10 +- 13 files changed, 475 insertions(+), 474 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs delete mode 100644 src/StardewModdingAPI/Framework/AssemblyLoader.cs delete mode 100644 src/StardewModdingAPI/Framework/AssemblyParseResult.cs delete mode 100644 src/StardewModdingAPI/Framework/Manifest.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs delete mode 100644 src/StardewModdingAPI/Framework/ModMetadata.cs create mode 100644 src/StardewModdingAPI/Framework/Models/Manifest.cs (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs deleted file mode 100644 index b4e69fcd..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework -{ - /// A minimal assembly definition resolver which resolves references to known assemblies. - internal class AssemblyDefinitionResolver : DefaultAssemblyResolver - { - /********* - ** Properties - *********/ - /// The known assemblies. - private readonly IDictionary Loaded = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Add known assemblies to the resolver. - /// The known assemblies. - public void Add(params AssemblyDefinition[] assemblies) - { - foreach (AssemblyDefinition assembly in assemblies) - { - this.Loaded[assembly.Name.Name] = assembly; - this.Loaded[assembly.Name.FullName] = assembly; - } - } - - /// Resolve an assembly reference. - /// The assembly name. - public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); - - /// Resolve an assembly reference. - /// The assembly name. - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); - - - /********* - ** Private methods - *********/ - /// Resolve a known assembly definition based on its short or full name. - /// The assembly's short or full name. - private AssemblyDefinition ResolveName(string name) - { - return this.Loaded.ContainsKey(name) - ? this.Loaded[name] - : null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs deleted file mode 100644 index 2c9973c1..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters; - -namespace StardewModdingAPI.Framework -{ - /// Preprocesses and loads mod assemblies. - internal class AssemblyLoader - { - /********* - ** Properties - *********/ - /// Metadata for mapping assemblies to the current platform. - private readonly PlatformAssemblyMap AssemblyMap; - - /// A type => assembly lookup for types which should be rewritten. - private readonly IDictionary TypeAssemblies; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current game platform. - /// Encapsulates monitoring and logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor) - { - this.Monitor = monitor; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - - // generate type => assembly lookup for types which should be rewritten - this.TypeAssemblies = new Dictionary(); - foreach (Assembly assembly in this.AssemblyMap.Targets) - { - ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; - 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; - } - } - } - - /// Preprocess and load an assembly. - /// The assembly file path. - /// Assume the mod is compatible, even if incompatible code is detected. - /// Returns the rewrite metadata for the preprocessed assembly. - /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(string assemblyPath, bool assumeCompatible) - { - // get referenced local assemblies - AssemblyParseResult[] assemblies; - { - AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); - HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded - assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); - if (!assemblies.Any()) - throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); - resolver.Add(assemblies.Select(p => p.Definition).ToArray()); - } - - // rewrite & load assemblies in leaf-to-root order - Assembly lastAssembly = null; - foreach (AssemblyParseResult assembly in assemblies) - { - bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); - if (changed) - { - this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } - } - else - { - this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); - lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); - } - } - - // last assembly loaded is the root - return lastAssembly; - } - - /// Resolve an assembly by its name. - /// The assembly name. - /// - /// This implementation returns the first loaded assembly which matches the short form of - /// the assembly name, to resolve assembly resolution issues when rewriting - /// assemblies (especially with Mono). Since this is meant to be called on , - /// the implicit assumption is that loading the exact assembly failed. - /// - public Assembly ResolveAssembly(string name) - { - string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) - return AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(p => p.GetName().Name == shortName); - } - - - /********* - ** Private methods - *********/ - /**** - ** Assembly parsing - ****/ - /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. - /// The assembly file to load. - /// The assembly names that should be skipped. - /// A resolver which resolves references to known assemblies. - /// Returns the rewrite metadata for the preprocessed assembly. - private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) - { - // validate - if (file.Directory == null) - throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); - if (!file.Exists) - yield break; // not a local assembly - - // read assembly - byte[] assemblyBytes = File.ReadAllBytes(file.FullName); - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); - - // skip if already visited - if (visitedAssemblyNames.Contains(assembly.Name.Name)) - yield break; - visitedAssemblyNames.Add(assembly.Name.Name); - - // yield referenced assemblies - foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) - { - FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); - foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) - yield return result; - } - - // yield assembly - yield return new AssemblyParseResult(file, assembly); - } - - /**** - ** Assembly rewriting - ****/ - /// Rewrite the types referenced by an assembly. - /// The assembly to rewrite. - /// Assume the mod is compatible, even if incompatible code is detected. - /// Returns whether the assembly was modified. - /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) - { - ModuleDefinition module = assembly.MainModule; - HashSet loggedMessages = new HashSet(); - - // swap assembly references if needed (e.g. XNA => MonoGame) - bool platformChanged = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) - { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) - { - this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; - } - } - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); - } - - // find (and optionally rewrite) incompatible instructions - bool anyRewritten = false; - IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) - { - // check method definition - foreach (IInstructionRewriter rewriter in rewriters) - { - try - { - if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) - { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } - } - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - foreach (Instruction instruction in cil.Body.Instructions.ToArray()) - { - foreach (IInstructionRewriter rewriter in rewriters) - { - try - { - if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) - { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } - } - } - } - - return platformChanged || anyRewritten; - } - - /// Get the correct reference to use for compatibility with the current platform. - /// The type reference to rewrite. - private void ChangeTypeScope(TypeReference type) - { - // check skip conditions - if (type == null || type.FullName.StartsWith("System.")) - return; - - // get assembly - Assembly assembly; - if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) - return; - - // replace scope - AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; - 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 - ); - } - - /// Log a message for the player or developer the first time it occurs. - /// The monitor through which to log the message. - /// The hash of logged messages. - /// The message to log. - /// The log severity level. - private void LogOnce(IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) - { - if (!hash.Contains(message)) - { - monitor.Log(message, level); - hash.Add(message); - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/AssemblyParseResult.cs deleted file mode 100644 index bff976aa..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework -{ - /// Metadata about a parsed assembly definition. - internal class AssemblyParseResult - { - /********* - ** Accessors - *********/ - /// The original assembly file. - public readonly FileInfo File; - - /// The assembly definition. - public readonly AssemblyDefinition Definition; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The original assembly file. - /// The assembly definition. - public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) - { - this.File = file; - this.Definition = assembly; - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Manifest.cs b/src/StardewModdingAPI/Framework/Manifest.cs deleted file mode 100644 index 62c711e2..00000000 --- a/src/StardewModdingAPI/Framework/Manifest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework -{ - /// A manifest which describes a mod for SMAPI. - internal class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// A brief description of the mod. - public string Description { get; set; } - - /// The mod author's name. - public string Author { get; set; } - - /// The mod version. - [JsonConverter(typeof(SemanticVersionConverter))] - public ISemanticVersion Version { get; set; } - - /// The minimum SMAPI version required by this mod, if any. - public string MinimumApiVersion { get; set; } - - /// The name of the DLL in the directory that has the method. - public string EntryDll { get; set; } - - /// The unique mod ID. - public string UniqueID { get; set; } - - /// Whether the mod uses per-save config files. - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public bool PerSaveConfigs { get; set; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs new file mode 100644 index 00000000..4378798c --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// A minimal assembly definition resolver which resolves references to known assemblies. + internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + { + /********* + ** Properties + *********/ + /// The known assemblies. + private readonly IDictionary Loaded = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Add known assemblies to the resolver. + /// The known assemblies. + public void Add(params AssemblyDefinition[] assemblies) + { + foreach (AssemblyDefinition assembly in assemblies) + { + this.Loaded[assembly.Name.Name] = assembly; + this.Loaded[assembly.Name.FullName] = assembly; + } + } + + /// Resolve an assembly reference. + /// The assembly name. + public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); + + /// Resolve an assembly reference. + /// The assembly name. + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); + + /// Resolve an assembly reference. + /// The assembly full name (including version, etc). + public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); + + /// Resolve an assembly reference. + /// The assembly full name (including version, etc). + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); + + + /********* + ** Private methods + *********/ + /// Resolve a known assembly definition based on its short or full name. + /// The assembly's short or full name. + private AssemblyDefinition ResolveName(string name) + { + return this.Loaded.ContainsKey(name) + ? this.Loaded[name] + : null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs new file mode 100644 index 00000000..42bd7bfb --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.AssemblyRewriters; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Preprocesses and loads mod assemblies. + internal class AssemblyLoader + { + /********* + ** Properties + *********/ + /// Metadata for mapping assemblies to the current platform. + private readonly PlatformAssemblyMap AssemblyMap; + + /// A type => assembly lookup for types which should be rewritten. + private readonly IDictionary TypeAssemblies; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current game platform. + /// Encapsulates monitoring and logging. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor) + { + this.Monitor = monitor; + this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); + + // generate type => assembly lookup for types which should be rewritten + this.TypeAssemblies = new Dictionary(); + foreach (Assembly assembly in this.AssemblyMap.Targets) + { + ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; + 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; + } + } + } + + /// Preprocess and load an assembly. + /// The assembly file path. + /// Assume the mod is compatible, even if incompatible code is detected. + /// Returns the rewrite metadata for the preprocessed assembly. + /// An incompatible CIL instruction was found while rewriting the assembly. + public Assembly Load(string assemblyPath, bool assumeCompatible) + { + // get referenced local assemblies + AssemblyParseResult[] assemblies; + { + AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); + HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded + assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); + if (!assemblies.Any()) + throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); + resolver.Add(assemblies.Select(p => p.Definition).ToArray()); + } + + // rewrite & load assemblies in leaf-to-root order + Assembly lastAssembly = null; + foreach (AssemblyParseResult assembly in assemblies) + { + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); + if (changed) + { + this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + { + this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + } + } + + // last assembly loaded is the root + return lastAssembly; + } + + /// Resolve an assembly by its name. + /// The assembly name. + /// + /// This implementation returns the first loaded assembly which matches the short form of + /// the assembly name, to resolve assembly resolution issues when rewriting + /// assemblies (especially with Mono). Since this is meant to be called on , + /// the implicit assumption is that loading the exact assembly failed. + /// + public Assembly ResolveAssembly(string name) + { + string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) + return AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(p => p.GetName().Name == shortName); + } + + + /********* + ** Private methods + *********/ + /**** + ** Assembly parsing + ****/ + /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. + /// The assembly file to load. + /// The assembly names that should be skipped. + /// A resolver which resolves references to known assemblies. + /// Returns the rewrite metadata for the preprocessed assembly. + private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) + { + // validate + if (file.Directory == null) + throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); + if (!file.Exists) + yield break; // not a local assembly + + // read assembly + byte[] assemblyBytes = File.ReadAllBytes(file.FullName); + AssemblyDefinition assembly; + using (Stream readStream = new MemoryStream(assemblyBytes)) + assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); + + // skip if already visited + if (visitedAssemblyNames.Contains(assembly.Name.Name)) + yield break; + visitedAssemblyNames.Add(assembly.Name.Name); + + // yield referenced assemblies + foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) + { + FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); + foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) + yield return result; + } + + // yield assembly + yield return new AssemblyParseResult(file, assembly); + } + + /**** + ** Assembly rewriting + ****/ + /// Rewrite the types referenced by an assembly. + /// The assembly to rewrite. + /// Assume the mod is compatible, even if incompatible code is detected. + /// Returns whether the assembly was modified. + /// An incompatible CIL instruction was found while rewriting the assembly. + private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) + { + ModuleDefinition module = assembly.MainModule; + HashSet loggedMessages = new HashSet(); + + // swap assembly references if needed (e.g. XNA => MonoGame) + bool platformChanged = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) + { + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); + } + + // find (and optionally rewrite) incompatible instructions + bool anyRewritten = false; + IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); + foreach (MethodDefinition method in this.GetMethods(module)) + { + // check method definition + foreach (IInstructionRewriter rewriter in rewriters) + { + try + { + if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + anyRewritten = true; + } + } + catch (IncompatibleInstructionException) + { + if (!assumeCompatible) + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); + this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + } + } + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + foreach (Instruction instruction in cil.Body.Instructions.ToArray()) + { + foreach (IInstructionRewriter rewriter in rewriters) + { + try + { + if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + anyRewritten = true; + } + } + catch (IncompatibleInstructionException) + { + if (!assumeCompatible) + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); + this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + } + } + } + } + + return platformChanged || anyRewritten; + } + + /// Get the correct reference to use for compatibility with the current platform. + /// The type reference to rewrite. + private void ChangeTypeScope(TypeReference type) + { + // check skip conditions + if (type == null || type.FullName.StartsWith("System.")) + return; + + // get assembly + Assembly assembly; + if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) + return; + + // replace scope + AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; + 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 + ); + } + + /// Log a message for the player or developer the first time it occurs. + /// The monitor through which to log the message. + /// The hash of logged messages. + /// The message to log. + /// The log severity level. + private void LogOnce(IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs new file mode 100644 index 00000000..69c99afe --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -0,0 +1,31 @@ +using System.IO; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata about a parsed assembly definition. + internal class AssemblyParseResult + { + /********* + ** Accessors + *********/ + /// The original assembly file. + public readonly FileInfo File; + + /// The assembly definition. + public readonly AssemblyDefinition Definition; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The original assembly file. + /// The assembly definition. + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) + { + this.File = file; + this.Definition = assembly; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs new file mode 100644 index 00000000..1ac167dc --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -0,0 +1,40 @@ +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for a mod. + internal class ModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string DisplayName { get; } + + /// The mod's full directory path. + public string DirectoryPath { get; } + + /// The mod manifest. + public IManifest Manifest { get; } + + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModCompatibility Compatibility { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's display name. + /// The mod's full directory path. + /// The mod manifest. + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + { + this.DisplayName = displayName; + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Compatibility = compatibility; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModMetadata.cs deleted file mode 100644 index aeb9261a..00000000 --- a/src/StardewModdingAPI/Framework/ModMetadata.cs +++ /dev/null @@ -1,40 +0,0 @@ -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework -{ - /// Metadata for a mod. - internal class ModMetadata - { - /********* - ** Accessors - *********/ - /// The mod's display name. - public string DisplayName { get; } - - /// The mod's full directory path. - public string DirectoryPath { get; } - - /// The mod manifest. - public IManifest Manifest { get; } - - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModCompatibility Compatibility { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's display name. - /// The mod's full directory path. - /// The mod manifest. - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) - { - this.DisplayName = displayName; - this.DirectoryPath = directoryPath; - this.Manifest = manifest; - this.Compatibility = compatibility; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs new file mode 100644 index 00000000..79be2075 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.Models +{ + /// A manifest which describes a mod for SMAPI. + internal class Manifest : IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + public string MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the method. + public string EntryDll { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Whether the mod uses per-save config files. + [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] + public bool PerSaveConfigs { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + } +} diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index a3169fb3..a65b135c 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -1,6 +1,7 @@ using System; using System.IO; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI { diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a5bd7788..b8d70ad7 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewValley; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 69c167c9..aec32560 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -144,8 +144,8 @@ - - + + @@ -158,7 +158,7 @@ - + @@ -182,7 +182,7 @@ - + @@ -202,7 +202,7 @@ - + -- cgit