diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-02-04 15:30:46 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-02-04 15:30:46 -0500 |
commit | 9c9833c9086b758589dafee10243e3bf47e12d73 (patch) | |
tree | 65c279c65e6edd598844b2dc41ad6e141fc48dd9 /src/StardewModdingAPI | |
parent | e9cb691251668af87f25549fdedaf382e820075f (diff) | |
parent | 3919ab7a4aed7acd579e471f5660df5fbc890ae2 (diff) | |
download | SMAPI-9c9833c9086b758589dafee10243e3bf47e12d73.tar.gz SMAPI-9c9833c9086b758589dafee10243e3bf47e12d73.tar.bz2 SMAPI-9c9833c9086b758589dafee10243e3bf47e12d73.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/StardewModdingAPI')
-rw-r--r-- | src/StardewModdingAPI/Constants.cs | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs | 4 | ||||
-rw-r--r-- | src/StardewModdingAPI/Extensions.cs | 30 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs | 61 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/AssemblyLoader.cs (renamed from src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs) | 142 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/AssemblyParseResult.cs | 31 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs | 61 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs | 33 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs | 49 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ModAssemblyLoader.cs | 143 | ||||
-rw-r--r-- | src/StardewModdingAPI/Program.cs | 62 | ||||
-rw-r--r-- | src/StardewModdingAPI/SemanticVersion.cs | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI/StardewModdingAPI.csproj | 10 |
13 files changed, 245 insertions, 385 deletions
diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index df527dfe..a62a0d58 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI public static readonly Version Version = (Version)Constants.ApiVersion; /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion => new Version(1, 7, 0, null, suppressDeprecationWarning: true); + public static ISemanticVersion ApiVersion => new Version(1, 8, 0, null, suppressDeprecationWarning: true); /// <summary>The minimum supported version of Stardew Valley.</summary> public const string MinimumGameVersion = "1.1"; diff --git a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs b/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs index 443429aa..aa0bb377 100644 --- a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs @@ -9,10 +9,10 @@ namespace StardewModdingAPI.Events /********* ** Accessors *********/ - /// <summary>The player's previous location.</summary> + /// <summary>The player's current location.</summary> public GameLocation NewLocation { get; private set; } - /// <summary>The player's current location.</summary> + /// <summary>The player's previous location.</summary> public GameLocation PriorLocation { get; private set; } diff --git a/src/StardewModdingAPI/Extensions.cs b/src/StardewModdingAPI/Extensions.cs index 1229ca97..0e9dbbf7 100644 --- a/src/StardewModdingAPI/Extensions.cs +++ b/src/StardewModdingAPI/Extensions.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.Random)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.Random)}", "1.0", DeprecationLevel.PendingRemoval); return Extensions._random; } } @@ -40,7 +40,7 @@ namespace StardewModdingAPI /// <param name="key">The key to check.</param> public static bool IsKeyDown(this Keys key) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsKeyDown)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsKeyDown)}", "1.0", DeprecationLevel.PendingRemoval); return Keyboard.GetState().IsKeyDown(key); } @@ -48,7 +48,7 @@ namespace StardewModdingAPI /// <summary>Get a random color.</summary> public static Color RandomColour() { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.RandomColour)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.RandomColour)}", "1.0", DeprecationLevel.PendingRemoval); return new Color(Extensions.Random.Next(0, 255), Extensions.Random.Next(0, 255), Extensions.Random.Next(0, 255)); } @@ -69,7 +69,7 @@ namespace StardewModdingAPI /// <param name="split">The value separator.</param> public static string ToSingular<T>(this IEnumerable<T> ienum, string split = ", ") { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.ToSingular)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.ToSingular)}", "1.0", DeprecationLevel.PendingRemoval); //Apparently Keys[] won't split normally :l if (typeof(T) == typeof(Keys)) @@ -83,7 +83,7 @@ namespace StardewModdingAPI /// <param name="o">The value.</param> public static bool IsInt32(this object o) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsInt32)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsInt32)}", "1.0", DeprecationLevel.PendingRemoval); int i; return int.TryParse(o.ToString(), out i); @@ -93,7 +93,7 @@ namespace StardewModdingAPI /// <param name="o">The value.</param> public static int AsInt32(this object o) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.AsInt32)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.AsInt32)}", "1.0", DeprecationLevel.PendingRemoval); return int.Parse(o.ToString()); } @@ -102,7 +102,7 @@ namespace StardewModdingAPI /// <param name="o">The value.</param> public static bool IsBool(this object o) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsBool)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsBool)}", "1.0", DeprecationLevel.PendingRemoval); bool b; return bool.TryParse(o.ToString(), out b); @@ -112,7 +112,7 @@ namespace StardewModdingAPI /// <param name="o">The value.</param> public static bool AsBool(this object o) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.AsBool)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.AsBool)}", "1.0", DeprecationLevel.PendingRemoval); return bool.Parse(o.ToString()); } @@ -121,7 +121,7 @@ namespace StardewModdingAPI /// <param name="enumerable">The values to hash.</param> public static int GetHash(this IEnumerable enumerable) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetHash)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetHash)}", "1.0", DeprecationLevel.PendingRemoval); var hash = 0; foreach (var v in enumerable) @@ -134,7 +134,7 @@ namespace StardewModdingAPI /// <param name="o">The value.</param> public static T Cast<T>(this object o) where T : class { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.Cast)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.Cast)}", "1.0", DeprecationLevel.PendingRemoval); return o as T; } @@ -143,7 +143,7 @@ namespace StardewModdingAPI /// <param name="o">The object to scan.</param> public static FieldInfo[] GetPrivateFields(this object o) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetPrivateFields)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetPrivateFields)}", "1.0", DeprecationLevel.PendingRemoval); return o.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); } @@ -152,7 +152,7 @@ namespace StardewModdingAPI /// <param name="name">The name of the field to find.</param> public static FieldInfo GetBaseFieldInfo(this Type t, string name) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetBaseFieldValue)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetBaseFieldValue)}", "1.0", DeprecationLevel.PendingRemoval); return t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); } @@ -162,7 +162,7 @@ namespace StardewModdingAPI /// <param name="name">The name of the field to find.</param> public static T GetBaseFieldValue<T>(this Type t, object o, string name) where T : class { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetBaseFieldValue)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetBaseFieldValue)}", "1.0", DeprecationLevel.PendingRemoval); return t.GetBaseFieldInfo(name).GetValue(o) as T; } @@ -173,7 +173,7 @@ namespace StardewModdingAPI /// <param name="newValue">The value to set.</param> public static void SetBaseFieldValue<T>(this Type t, object o, string name, object newValue) where T : class { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.SetBaseFieldValue)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.SetBaseFieldValue)}", "1.0", DeprecationLevel.PendingRemoval); t.GetBaseFieldInfo(name).SetValue(o, newValue as T); } @@ -181,7 +181,7 @@ namespace StardewModdingAPI /// <param name="st">The string to copy.</param> public static string RemoveNumerics(this string st) { - Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.RemoveNumerics)}", "1.0", DeprecationLevel.Info); + Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.RemoveNumerics)}", "1.0", DeprecationLevel.PendingRemoval); var s = st; foreach (var c in s) { diff --git a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs new file mode 100644 index 00000000..b4e69fcd --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework +{ + /// <summary>A minimal assembly definition resolver which resolves references to known assemblies.</summary> + internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + { + /********* + ** Properties + *********/ + /// <summary>The known assemblies.</summary> + private readonly IDictionary<string, AssemblyDefinition> Loaded = new Dictionary<string, AssemblyDefinition>(); + + + /********* + ** Public methods + *********/ + /// <summary>Add known assemblies to the resolver.</summary> + /// <param name="assemblies">The known assemblies.</param> + public void Add(params AssemblyDefinition[] assemblies) + { + foreach (AssemblyDefinition assembly in assemblies) + { + this.Loaded[assembly.Name.Name] = assembly; + this.Loaded[assembly.Name.FullName] = assembly; + } + } + + /// <summary>Resolve an assembly reference.</summary> + /// <param name="name">The assembly name.</param> + public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); + + /// <summary>Resolve an assembly reference.</summary> + /// <param name="name">The assembly name.</param> + /// <param name="parameters">The assembly reader parameters.</param> + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); + + /// <summary>Resolve an assembly reference.</summary> + /// <param name="fullName">The assembly full name (including version, etc).</param> + public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); + + /// <summary>Resolve an assembly reference.</summary> + /// <param name="fullName">The assembly full name (including version, etc).</param> + /// <param name="parameters">The assembly reader parameters.</param> + public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); + + + /********* + ** Private methods + *********/ + /// <summary>Resolve a known assembly definition based on its short or full name.</summary> + /// <param name="name">The assembly's short or full name.</param> + private AssemblyDefinition ResolveName(string name) + { + return this.Loaded.ContainsKey(name) + ? this.Loaded[name] + : null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs index 9d4d6b11..123211b9 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyLoader.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using Mono.Cecil; @@ -6,15 +8,15 @@ using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using StardewModdingAPI.AssemblyRewriters; -namespace StardewModdingAPI.Framework.AssemblyRewriting +namespace StardewModdingAPI.Framework { - /// <summary>Rewrites type references.</summary> - internal class AssemblyTypeRewriter + /// <summary>Preprocesses and loads mod assemblies.</summary> + internal class AssemblyLoader { /********* ** Properties *********/ - /// <summary>Metadata for mapping assemblies to the current <see cref="Platform"/>.</summary> + /// <summary>Metadata for mapping assemblies to the current platform.</summary> private readonly PlatformAssemblyMap AssemblyMap; /// <summary>A type => assembly lookup for types which should be rewritten.</summary> @@ -28,17 +30,16 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current <see cref="Platform"/>.</param> + /// <param name="targetPlatform">The current game platform.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public AssemblyTypeRewriter(PlatformAssemblyMap assemblyMap, IMonitor monitor) + public AssemblyLoader(Platform targetPlatform, IMonitor monitor) { - // save config - this.AssemblyMap = assemblyMap; this.Monitor = monitor; + this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - // collect type => assembly lookup + // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary<string, Assembly>(); - foreach (Assembly assembly in assemblyMap.Targets) + foreach (Assembly assembly in this.AssemblyMap.Targets) { ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; foreach (TypeDefinition type in module.GetTypes()) @@ -52,10 +53,107 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting } } + /// <summary>Preprocess and load an assembly.</summary> + /// <param name="assemblyPath">The assembly file path.</param> + /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> + public Assembly Load(string assemblyPath) + { + // get referenced local assemblies + AssemblyParseResult[] assemblies; + { + AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); + assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), new HashSet<string>(), 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) + { + this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); + bool changed = this.RewriteAssembly(assembly.Definition); + if (changed) + { + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + } + + // last assembly loaded is the root + return lastAssembly; + } + + /// <summary>Resolve an assembly by its name.</summary> + /// <param name="name">The assembly name.</param> + /// <remarks> + /// 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 <see cref="AppDomain.AssemblyResolve"/>, + /// the implicit assumption is that loading the exact assembly failed. + /// </remarks> + 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 + ****/ + /// <summary>Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root.</summary> + /// <param name="file">The assembly file to load.</param> + /// <param name="visitedAssemblyPaths">The assembly paths that should be skipped.</param> + /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> + private IEnumerable<AssemblyParseResult> GetReferencedLocalAssemblies(FileInfo file, HashSet<string> visitedAssemblyPaths, IAssemblyResolver assemblyResolver) + { + // validate + if (file.Directory == null) + throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); + if (visitedAssemblyPaths.Contains(file.FullName)) + yield break; // already visited + if (!file.Exists) + yield break; // not a local assembly + visitedAssemblyPaths.Add(file.FullName); + + // 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 }); + + // 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, visitedAssemblyPaths, assemblyResolver)) + yield return result; + } + + // yield assembly + yield return new AssemblyParseResult(file, assembly); + } + + /**** + ** Assembly rewriting + ****/ /// <summary>Rewrite the types referenced by an assembly.</summary> /// <param name="assembly">The assembly to rewrite.</param> /// <returns>Returns whether the assembly was modified.</returns> - public bool RewriteAssembly(AssemblyDefinition assembly) + private bool 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 @@ -65,7 +163,6 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting { if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.Monitor.Log($"removing reference to {module.AssemblyReferences[i]}", LogLevel.Trace); shouldRewrite = true; module.AssemblyReferences.RemoveAt(i); i--; @@ -76,19 +173,12 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting // add target assembly references foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - { - this.Monitor.Log($" adding reference to {target}", LogLevel.Trace); module.AssemblyReferences.Add(target); - } // rewrite type scopes to use target assemblies IEnumerable<TypeReference> 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; - } + this.ChangeTypeScope(type); // rewrite incompatible methods IMethodRewriter[] methodRewriters = Constants.GetMethodRewriters().ToArray(); @@ -111,7 +201,6 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting if (rewriter != null) { MethodReference methodRef = (MethodReference)op.Operand; - this.Monitor.Log($"rewriting method reference {methodRef.DeclaringType.FullName}.{methodRef.Name}", LogLevel.Trace); rewriter.Rewrite(module, cil, op, methodRef, this.AssemblyMap); } } @@ -121,14 +210,9 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting return true; } - - /********* - ** Private methods - *********/ /// <summary>Get the correct reference to use for compatibility with the current platform.</summary> /// <param name="type">The type reference to rewrite.</param> - /// <param name="shouldLog">Whether to log a message.</param> - private void ChangeTypeScope(TypeReference type, bool shouldLog) + private void ChangeTypeScope(TypeReference type) { // check skip conditions if (type == null || type.FullName.StartsWith("System.")) @@ -141,8 +225,6 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting // replace scope 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; } diff --git a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/AssemblyParseResult.cs new file mode 100644 index 00000000..bff976aa --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyParseResult.cs @@ -0,0 +1,31 @@ +using System.IO; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework +{ + /// <summary>Metadata about a parsed assembly definition.</summary> + internal class AssemblyParseResult + { + /********* + ** Accessors + *********/ + /// <summary>The original assembly file.</summary> + public readonly FileInfo File; + + /// <summary>The assembly definition.</summary> + public readonly AssemblyDefinition Definition; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="file">The original assembly file.</param> + /// <param name="assembly">The assembly definition.</param> + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) + { + this.File = file; + this.Definition = assembly; + } + } +}
\ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs deleted file mode 100644 index 4c3b86fe..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.IO; -using StardewModdingAPI.AssemblyRewriters; - -namespace StardewModdingAPI.Framework.AssemblyRewriting -{ - /// <summary>Represents cached metadata for a rewritten assembly.</summary> - internal class CacheEntry - { - /********* - ** Accessors - *********/ - /// <summary>The MD5 hash for the original assembly.</summary> - public readonly string Hash; - - /// <summary>The SMAPI version used to rewrite the assembly.</summary> - public readonly string ApiVersion; - - /// <summary>The target platform.</summary> - public readonly Platform Platform; - - /// <summary>The <see cref="System.Environment.MachineName"/> value for the machine used to rewrite the assembly.</summary> - public readonly string MachineName; - - /// <summary>Whether to use the cached assembly instead of the original assembly.</summary> - public readonly bool UseCachedAssembly; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="hash">The MD5 hash for the original assembly.</param> - /// <param name="apiVersion">The SMAPI version used to rewrite the assembly.</param> - /// <param name="platform">The target platform.</param> - /// <param name="machineName">The <see cref="System.Environment.MachineName"/> value for the machine used to rewrite the assembly.</param> - /// <param name="useCachedAssembly">Whether to use the cached assembly instead of the original assembly.</param> - public CacheEntry(string hash, string apiVersion, Platform platform, string machineName, bool useCachedAssembly) - { - this.Hash = hash; - this.ApiVersion = apiVersion; - this.Platform = platform; - this.MachineName = machineName; - this.UseCachedAssembly = useCachedAssembly; - } - - /// <summary>Get whether the cache entry is up-to-date for the given assembly hash.</summary> - /// <param name="paths">The paths for the cached assembly.</param> - /// <param name="hash">The MD5 hash of the original assembly.</param> - /// <param name="currentVersion">The current SMAPI version.</param> - /// <param name="platform">The target platform.</param> - /// <param name="machineName">The <see cref="System.Environment.MachineName"/> value for the machine reading the assembly.</param> - public bool IsUpToDate(CachePaths paths, string hash, ISemanticVersion currentVersion, Platform platform, string machineName) - { - return hash == this.Hash - && this.ApiVersion == currentVersion.ToString() - && this.Platform == platform - && this.MachineName == machineName - && (!this.UseCachedAssembly || File.Exists(paths.Assembly)); - } - } -}
\ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs deleted file mode 100644 index 18861873..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace StardewModdingAPI.Framework.AssemblyRewriting -{ - /// <summary>Contains the paths for an assembly's cached data.</summary> - internal struct CachePaths - { - /********* - ** Accessors - *********/ - /// <summary>The directory path which contains the assembly.</summary> - public string Directory { get; } - - /// <summary>The file path of the assembly file.</summary> - public string Assembly { get; } - - /// <summary>The file path containing the assembly metadata.</summary> - public string Metadata { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="directory">The directory path which contains the assembly.</param> - /// <param name="assembly">The file path of the assembly file.</param> - /// <param name="metadata">The file path containing the assembly metadata.</param> - public CachePaths(string directory, string assembly, string metadata) - { - this.Directory = directory; - this.Assembly = assembly; - this.Metadata = metadata; - } - } -}
\ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs deleted file mode 100644 index 8f34bb20..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace StardewModdingAPI.Framework.AssemblyRewriting -{ - /// <summary>Metadata about a preprocessed assembly.</summary> - internal class RewriteResult - { - /********* - ** Accessors - *********/ - /// <summary>The original assembly path.</summary> - public readonly string OriginalAssemblyPath; - - /// <summary>The cache paths.</summary> - public readonly CachePaths CachePaths; - - /// <summary>The rewritten assembly bytes.</summary> - public readonly byte[] AssemblyBytes; - - /// <summary>The MD5 hash for the original assembly.</summary> - public readonly string Hash; - - /// <summary>Whether to use the cached assembly instead of the original assembly.</summary> - public readonly bool UseCachedAssembly; - - /// <summary>Whether this data is newer than the cache.</summary> - public readonly bool IsNewerThanCache; - - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="originalAssemblyPath"></param> - /// <param name="cachePaths">The cache paths.</param> - /// <param name="assemblyBytes">The rewritten assembly bytes.</param> - /// <param name="hash">The MD5 hash for the original assembly.</param> - /// <param name="useCachedAssembly">Whether to use the cached assembly instead of the original assembly.</param> - /// <param name="isNewerThanCache">Whether this data is newer than the cache.</param> - public RewriteResult(string originalAssemblyPath, CachePaths cachePaths, byte[] assemblyBytes, string hash, bool useCachedAssembly, bool isNewerThanCache) - { - this.OriginalAssemblyPath = originalAssemblyPath; - this.CachePaths = cachePaths; - this.Hash = hash; - this.AssemblyBytes = assemblyBytes; - this.UseCachedAssembly = useCachedAssembly; - this.IsNewerThanCache = isNewerThanCache; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs deleted file mode 100644 index e4760398..00000000 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security.Cryptography; -using Mono.Cecil; -using Newtonsoft.Json; -using StardewModdingAPI.AssemblyRewriters; -using StardewModdingAPI.Framework.AssemblyRewriting; - -namespace StardewModdingAPI.Framework -{ - /// <summary>Preprocesses and loads mod assemblies.</summary> - internal class ModAssemblyLoader - { - /********* - ** Properties - *********/ - /// <summary>The name of the directory containing a mod's cached data.</summary> - private readonly string CacheDirName; - - /// <summary>Metadata for mapping assemblies to the current <see cref="Platform"/>.</summary> - private readonly PlatformAssemblyMap AssemblyMap; - - /// <summary>Rewrites assembly types to match the current platform.</summary> - private readonly AssemblyTypeRewriter AssemblyTypeRewriter; - - /// <summary>Encapsulates monitoring and logging.</summary> - private readonly IMonitor Monitor; - - /// <summary>The current game platform.</summary> - private readonly Platform TargetPlatform; - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="cacheDirName">The name of the directory containing a mod's cached data.</param> - /// <param name="targetPlatform">The current game platform.</param> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - public ModAssemblyLoader(string cacheDirName, Platform targetPlatform, IMonitor monitor) - { - this.CacheDirName = cacheDirName; - this.TargetPlatform = targetPlatform; - this.Monitor = monitor; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor); - } - - /// <summary>Preprocess an assembly unless the cache is up to date.</summary> - /// <param name="assemblyPath">The assembly file path.</param> - /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> - public RewriteResult ProcessAssemblyUnlessCached(string assemblyPath) - { - // read assembly data - byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); - string hash = string.Join("", MD5.Create().ComputeHash(assemblyBytes).Select(p => p.ToString("X2"))); - - // get cached result if current - CachePaths cachePaths = this.GetCachePaths(assemblyPath); - { - CacheEntry cacheEntry = File.Exists(cachePaths.Metadata) ? JsonConvert.DeserializeObject<CacheEntry>(File.ReadAllText(cachePaths.Metadata)) : null; - if (cacheEntry != null && cacheEntry.IsUpToDate(cachePaths, hash, Constants.ApiVersion, this.TargetPlatform, Environment.MachineName)) - return new RewriteResult(assemblyPath, cachePaths, assemblyBytes, cacheEntry.Hash, cacheEntry.UseCachedAssembly, isNewerThanCache: false); // no rewrite needed - } - this.Monitor.Log($"Preprocessing {Path.GetFileName(assemblyPath)} for compatibility...", LogLevel.Trace); - - // rewrite assembly - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream); - bool modified = this.AssemblyTypeRewriter.RewriteAssembly(assembly); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Write(outStream); - byte[] outBytes = outStream.ToArray(); - return new RewriteResult(assemblyPath, cachePaths, outBytes, hash, useCachedAssembly: modified, isNewerThanCache: true); - } - } - - /// <summary>Write rewritten assembly metadata to the cache for a mod.</summary> - /// <param name="results">The rewrite results.</param> - /// <param name="forceCacheAssemblies">Whether to write all assemblies to the cache, even if they weren't modified.</param> - /// <exception cref="InvalidOperationException">There are no results to write, or the results are not all for the same directory.</exception> - public void WriteCache(IEnumerable<RewriteResult> results, bool forceCacheAssemblies) - { - results = results.ToArray(); - - // get cache directory - if (!results.Any()) - throw new InvalidOperationException("There are no assemblies to cache."); - if (results.Select(p => p.CachePaths.Directory).Distinct().Count() > 1) - throw new InvalidOperationException("The assemblies can't be cached together because they have different source directories."); - string cacheDir = results.Select(p => p.CachePaths.Directory).First(); - - // reset cache - if (Directory.Exists(cacheDir)) - Directory.Delete(cacheDir, recursive: true); - Directory.CreateDirectory(cacheDir); - - // cache all results - foreach (RewriteResult result in results) - { - CacheEntry cacheEntry = new CacheEntry(result.Hash, Constants.ApiVersion.ToString(), this.TargetPlatform, Environment.MachineName, forceCacheAssemblies || result.UseCachedAssembly); - File.WriteAllText(result.CachePaths.Metadata, JsonConvert.SerializeObject(cacheEntry)); - if (forceCacheAssemblies || result.UseCachedAssembly) - File.WriteAllBytes(result.CachePaths.Assembly, result.AssemblyBytes); - } - } - - /// <summary>Resolve an assembly from its name.</summary> - /// <param name="name">The assembly name.</param> - /// <remarks> - /// 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 <see cref="AppDomain.AssemblyResolve"/>, - /// the implicit assumption is that loading the exact assembly failed. - /// </remarks> - 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 - *********/ - /// <summary>Get the cache details for an assembly.</summary> - /// <param name="assemblyPath">The assembly file path.</param> - private CachePaths GetCachePaths(string assemblyPath) - { - string fileName = Path.GetFileName(assemblyPath); - string dirPath = Path.Combine(Path.GetDirectoryName(assemblyPath), this.CacheDirName); - string cacheAssemblyPath = Path.Combine(dirPath, fileName); - string metadataPath = Path.Combine(dirPath, $"{fileName}.json"); - return new CachePaths(dirPath, cacheAssemblyPath, metadataPath); - } - } -} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ec3ccce7..45bf1238 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -13,7 +13,6 @@ using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.AssemblyRewriting; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Inheritance; using StardewValley; @@ -41,9 +40,6 @@ namespace StardewModdingAPI /// <summary>The full path to the folder containing mods.</summary> private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); - /// <summary>The name of the folder containing a mod's cached assembly data.</summary> - private static readonly string CacheDirName = ".cache"; - /// <summary>The log file to which to write messages.</summary> private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); @@ -319,7 +315,7 @@ namespace StardewModdingAPI Program.Monitor.Log("Loading mods..."); // get assembly loader - ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CacheDirName, Program.TargetPlatform, Program.Monitor); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Program.TargetPlatform, Program.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // get known incompatible mods @@ -342,10 +338,6 @@ namespace StardewModdingAPI { string directoryName = new DirectoryInfo(directory).Name; - // ignore internal directory - if (directoryName == ".cache") - continue; - // check for cancellation if (Program.CancellationTokenSource.IsCancellationRequested) { @@ -458,47 +450,29 @@ namespace StardewModdingAPI } } - // preprocess mod assemblies for compatibility - var processedAssemblies = new List<RewriteResult>(); + // validate mod path to simplify errors + string assemblyPath = Path.Combine(directory, manifest.EntryDll); + if (!File.Exists(assemblyPath)) { - bool succeeded = true; - foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll")) - { - try - { - processedAssemblies.Add(modAssemblyLoader.ProcessAssemblyUnlessCached(assemblyPath)); - } - catch (Exception ex) - { - Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{Path.GetFileName(assemblyPath)}'.\n{ex.GetLogSummary()}", LogLevel.Error); - succeeded = false; - break; - } - } - if (!succeeded) - continue; + Program.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); + continue; } - bool forceUseCachedAssembly = processedAssemblies.Any(p => p.UseCachedAssembly); // make sure DLLs are kept together for dependency resolution - if (processedAssemblies.Any(p => p.IsNewerThanCache)) - modAssemblyLoader.WriteCache(processedAssemblies, forceUseCachedAssembly); - // get entry assembly path - string mainAssemblyPath; + // preprocess & load mod assembly + Assembly modAssembly; + try { - RewriteResult mainProcessedAssembly = processedAssemblies.FirstOrDefault(p => p.OriginalAssemblyPath == Path.Combine(directory, manifest.EntryDll)); - if (mainProcessedAssembly == null) - { - Program.Monitor.Log($"{errorPrefix}: the specified mod DLL does not exist.", LogLevel.Error); - continue; - } - mainAssemblyPath = forceUseCachedAssembly ? mainProcessedAssembly.CachePaths.Assembly : mainProcessedAssembly.OriginalAssemblyPath; + modAssembly = modAssemblyLoader.Load(assemblyPath); + } + catch (Exception ex) + { + Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error); + continue; } - // load entry assembly - Assembly modAssembly; + // validate assembly try { - modAssembly = Assembly.UnsafeLoadFrom(mainAssemblyPath); // unsafe load allows downloaded DLLs 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); @@ -507,11 +481,11 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while optimising the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } - // get mod instance + // initialise mod Mod mod; try { diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs index daefda51..c29f2cf7 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -56,7 +56,7 @@ namespace StardewModdingAPI /// <summary>Construct an instance.</summary> /// <param name="version">The semantic version string.</param> /// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception> - internal SemanticVersion(string version) + public SemanticVersion(string version) { var match = SemanticVersion.Regex.Match(version); if (!match.Success) diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index d56b6866..337929e2 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -148,6 +148,8 @@ <Compile Include="Events\EventArgsStringChanged.cs" /> <Compile Include="Events\GameEvents.cs" /> <Compile Include="Events\GraphicsEvents.cs" /> + <Compile Include="Framework\AssemblyDefinitionResolver.cs" /> + <Compile Include="Framework\AssemblyParseResult.cs" /> <Compile Include="IModRegistry.cs" /> <Compile Include="Events\LocationEvents.cs" /> <Compile Include="Events\MenuEvents.cs" /> @@ -156,15 +158,11 @@ <Compile Include="Events\SaveEvents.cs" /> <Compile Include="Events\TimeEvents.cs" /> <Compile Include="Extensions.cs" /> - <Compile Include="Framework\AssemblyRewriting\RewriteResult.cs" /> - <Compile Include="Framework\AssemblyRewriting\CachePaths.cs" /> - <Compile Include="Framework\AssemblyRewriting\AssemblyTypeRewriter.cs" /> - <Compile Include="Framework\AssemblyRewriting\CacheEntry.cs" /> <Compile Include="Framework\DeprecationLevel.cs" /> <Compile Include="Framework\DeprecationManager.cs" /> <Compile Include="Framework\InternalExtensions.cs" /> <Compile Include="Framework\Models\IncompatibleMod.cs" /> - <Compile Include="Framework\ModAssemblyLoader.cs" /> + <Compile Include="Framework\AssemblyLoader.cs" /> <Compile Include="Framework\Reflection\CacheEntry.cs" /> <Compile Include="Framework\Reflection\PrivateField.cs" /> <Compile Include="Framework\Reflection\PrivateMethod.cs" /> @@ -268,4 +266,4 @@ <Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" /> <Copy SourceFiles="$(TargetDir)\Mono.Cecil.Rocks.dll" DestinationFolder="$(GamePath)" /> </Target> -</Project> +</Project>
\ No newline at end of file |