From 315943614573f0e1973bafc761c27207b8ea2b45 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Dec 2016 23:51:09 -0500 Subject: reimplement assembly caching (#187) This commit ensures DLLs are copied to the cache directory only if they changed, to avoid breaking debugging support unless necessary. To support this change, the assembly hash file has been replaced with a more detailed JSON structure, which is used to determine whether the cache is up-to-date and whether to use the cached or original assembly. Some mods contain multiple DLLs, which must be kept together to prevent assembly resolution issues; to simplify that (and avoid orphaned cache entries), each mod now has its own separate cache. --- .../AssemblyRewriting/AssemblyTypeRewriter.cs | 6 +- .../Framework/AssemblyRewriting/CacheEntry.cs | 46 ++++++++ .../Framework/AssemblyRewriting/CachePaths.cs | 10 +- .../Framework/AssemblyRewriting/RewriteResult.cs | 49 +++++++++ .../Framework/ModAssemblyLoader.cs | 120 +++++++++++---------- 5 files changed, 166 insertions(+), 65 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs index 3459488e..9d4d6b11 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs @@ -54,7 +54,8 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// Rewrite the types referenced by an assembly. /// The assembly to rewrite. - public void RewriteAssembly(AssemblyDefinition assembly) + /// Returns whether the assembly was modified. + public 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 @@ -71,7 +72,7 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting } } if (!shouldRewrite) - return; + return false; // add target assembly references foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) @@ -117,6 +118,7 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting } method.Body.OptimizeMacros(); } + return true; } diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs new file mode 100644 index 00000000..3dfbc78c --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs @@ -0,0 +1,46 @@ +using System.IO; + +namespace StardewModdingAPI.Framework.AssemblyRewriting +{ + /// Represents cached metadata for a rewritten assembly. + internal class CacheEntry + { + /********* + ** Accessors + *********/ + /// The MD5 hash for the original assembly. + public readonly string Hash; + + /// The SMAPI version used to rewrite the assembly. + public readonly string ApiVersion; + + /// Whether to use the cached assembly instead of the original assembly. + public readonly bool UseCachedAssembly; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The MD5 hash for the original assembly. + /// The SMAPI version used to rewrite the assembly. + /// Whether to use the cached assembly instead of the original assembly. + public CacheEntry(string hash, string apiVersion, bool useCachedAssembly) + { + this.Hash = hash; + this.ApiVersion = apiVersion; + this.UseCachedAssembly = useCachedAssembly; + } + + /// Get whether the cache entry is up-to-date for the given assembly hash. + /// The paths for the cached assembly. + /// The MD5 hash of the original assembly. + /// The current SMAPI version. + public bool IsUpToDate(CachePaths paths, string hash, Version currentVersion) + { + return hash == this.Hash + && this.ApiVersion == currentVersion.ToString() + && (!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 index 17c4d188..18861873 100644 --- a/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs @@ -12,8 +12,8 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// 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; } + /// The file path containing the assembly metadata. + public string Metadata { get; } /********* @@ -22,12 +22,12 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting /// 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) + /// The file path containing the assembly metadata. + public CachePaths(string directory, string assembly, string metadata) { this.Directory = directory; this.Assembly = assembly; - this.Hash = hash; + 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 new file mode 100644 index 00000000..8f34bb20 --- /dev/null +++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs @@ -0,0 +1,49 @@ +namespace StardewModdingAPI.Framework.AssemblyRewriting +{ + /// Metadata about a preprocessed assembly. + internal class RewriteResult + { + /********* + ** Accessors + *********/ + /// The original assembly path. + public readonly string OriginalAssemblyPath; + + /// The cache paths. + public readonly CachePaths CachePaths; + + /// The rewritten assembly bytes. + public readonly byte[] AssemblyBytes; + + /// The MD5 hash for the original assembly. + public readonly string Hash; + + /// Whether to use the cached assembly instead of the original assembly. + public readonly bool UseCachedAssembly; + + /// Whether this data is newer than the cache. + public readonly bool IsNewerThanCache; + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// + /// The cache paths. + /// The rewritten assembly bytes. + /// The MD5 hash for the original assembly. + /// Whether to use the cached assembly instead of the original assembly. + /// Whether this data is newer than the cache. + 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 index 51018b0b..1ceb8ad2 100644 --- a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs @@ -1,9 +1,11 @@ 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; @@ -15,8 +17,8 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ - /// The directory in which to cache data. - private readonly string CacheDirPath; + /// The name of the directory containing a mod's cached data. + private readonly string CacheDirName; /// Metadata for mapping assemblies to the current . private readonly PlatformAssemblyMap AssemblyMap; @@ -32,74 +34,76 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. - /// The cache directory. + /// The name of the directory containing a mod's cached data. /// The current game platform. /// Encapsulates monitoring and logging. - public ModAssemblyLoader(string cacheDirPath, Platform targetPlatform, IMonitor monitor) + public ModAssemblyLoader(string cacheDirName, Platform targetPlatform, IMonitor monitor) { - this.CacheDirPath = cacheDirPath; + this.CacheDirName = cacheDirName; this.Monitor = monitor; this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor); } - /// Preprocess an assembly and cache the modified version. + /// Preprocess an assembly unless the cache is up to date. /// The assembly file path. - public void ProcessAssembly(string assemblyPath) + /// Returns the rewrite metadata for the preprocessed assembly. + public RewriteResult ProcessAssemblyUnlessCached(string assemblyPath) { // read assembly data - string assemblyFileName = Path.GetFileName(assemblyPath); - string assemblyDir = Path.GetDirectoryName(assemblyPath); byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); - string hash = $"SMAPI {Constants.Version}|" + string.Join("", MD5.Create().ComputeHash(assemblyBytes).Select(p => p.ToString("X2"))); + string hash = string.Join("", MD5.Create().ComputeHash(assemblyBytes).Select(p => p.ToString("X2"))); - // check cache - CachePaths cachePaths = this.GetCacheInfo(assemblyPath); - bool canUseCache = File.Exists(cachePaths.Assembly) && File.Exists(cachePaths.Hash) && hash == File.ReadAllText(cachePaths.Hash); - - // process assembly if not cached - if (!canUseCache) + // get cached result if current + CachePaths cachePaths = this.GetCachePaths(assemblyPath); + { + CacheEntry cacheEntry = File.Exists(cachePaths.Metadata) ? JsonConvert.DeserializeObject(File.ReadAllText(cachePaths.Metadata)) : null; + if (cacheEntry != null && cacheEntry.IsUpToDate(cachePaths, hash, Constants.Version)) + 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()) { - this.Monitor.Log($"Loading {assemblyFileName} for the first time; preprocessing...", LogLevel.Trace); - - // 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.WriteAllText(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); - } - } + assembly.Write(outStream); + byte[] outBytes = outStream.ToArray(); + return new RewriteResult(assemblyPath, cachePaths, outBytes, hash, useCachedAssembly: modified, isNewerThanCache: true); } } - /// Load a preprocessed assembly. - /// The assembly file path. - public Assembly LoadCachedAssembly(string assemblyPath) + /// Write rewritten assembly metadata to the cache for a mod. + /// The rewrite results. + /// Whether to write all assemblies to the cache, even if they weren't modified. + /// There are no results to write, or the results are not all for the same directory. + public void WriteCache(IEnumerable results, bool forceCacheAssemblies) { - 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 + 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.Version.ToString(), forceCacheAssemblies || result.UseCachedAssembly); + File.WriteAllText(result.CachePaths.Metadata, JsonConvert.SerializeObject(cacheEntry)); + if (forceCacheAssemblies || result.UseCachedAssembly) + File.WriteAllBytes(result.CachePaths.Assembly, result.AssemblyBytes); + } } /// Resolve an assembly from its name. @@ -124,13 +128,13 @@ namespace StardewModdingAPI.Framework *********/ /// Get the cache details for an assembly. /// The assembly file path. - private CachePaths GetCacheInfo(string assemblyPath) + private CachePaths GetCachePaths(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); + 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); } } } -- cgit From 2c11ce1bff5da9820b3207ad1aa83ac7350741b9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 22:05:14 -0500 Subject: add TypeLoadException details when intercepted by SMAPI --- release-notes.md | 3 ++- src/StardewModdingAPI/Framework/InternalExtensions.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 3426b622..e92ccf35 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,8 +9,9 @@ For players: * Tweaked installer wording to avoid confusion. For developers: + * Added a searchable `list_items` command to replace the `out_items`, `out_melee`, and `out_rings` commands. + * Added `TypeLoadException` details when intercepted by SMAPI. * Fixed an issue where you couldn't debug into an assembly because it was copied into the `.cache` directory. That will now only happen if necessary. - * Replaced the `out_items`, `out_melee`, and `out_rings` console commands with `list_items`, which also supports searching. ## 1.3 See [log](https://github.com/CLxS/SMAPI/compare/1.2...1.3). diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index 71f70fd5..415785d9 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -70,15 +70,21 @@ namespace StardewModdingAPI.Framework /// The error to summarise. public static string GetLogSummary(this Exception exception) { - string summary = exception.ToString(); + // type load exception + if (exception is TypeLoadException) + return $"Failed loading type: {((TypeLoadException)exception).TypeName}: {exception}"; + // reflection type load exception if (exception is ReflectionTypeLoadException) { + string summary = exception.ToString(); foreach (Exception childEx in ((ReflectionTypeLoadException)exception).LoaderExceptions) summary += $"\n\n{childEx.GetLogSummary()}"; + return summary; } - return summary; + // anything else + return exception.ToString(); } } } -- cgit From cd0e5961d454e5861e2fd760388eb6920a1e2257 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Dec 2016 12:25:53 -0500 Subject: add reflection API for mods (#185) --- src/StardewModdingAPI/Advanced/ConfigFile.cs | 2 +- src/StardewModdingAPI/Advanced/IConfigFile.cs | 2 +- .../Framework/Reflection/PrivateField.cs | 94 ++++++++++ .../Framework/Reflection/PrivateMethod.cs | 100 +++++++++++ .../Framework/Reflection/ReflectionHelper.cs | 197 +++++++++++++++++++++ src/StardewModdingAPI/IModHelper.cs | 9 +- src/StardewModdingAPI/Mod.cs | 7 +- src/StardewModdingAPI/ModHelper.cs | 9 +- src/StardewModdingAPI/Reflection/IPrivateField.cs | 26 +++ src/StardewModdingAPI/Reflection/IPrivateMethod.cs | 27 +++ .../Reflection/IReflectionHelper.cs | 53 ++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 6 + src/TrainerMod/TrainerMod.cs | 4 +- 13 files changed, 525 insertions(+), 11 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Reflection/PrivateField.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs create mode 100644 src/StardewModdingAPI/Reflection/IPrivateField.cs create mode 100644 src/StardewModdingAPI/Reflection/IPrivateMethod.cs create mode 100644 src/StardewModdingAPI/Reflection/IReflectionHelper.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Advanced/ConfigFile.cs b/src/StardewModdingAPI/Advanced/ConfigFile.cs index 1aba2f2c..1a2e6618 100644 --- a/src/StardewModdingAPI/Advanced/ConfigFile.cs +++ b/src/StardewModdingAPI/Advanced/ConfigFile.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Advanced /********* ** Accessors *********/ - /// Provides methods for interacting with the mod directory, including read/writing the config file. + /// Provides simplified APIs for writing mods. public IModHelper ModHelper { get; set; } /// The file path from which the model was loaded, relative to the mod directory. diff --git a/src/StardewModdingAPI/Advanced/IConfigFile.cs b/src/StardewModdingAPI/Advanced/IConfigFile.cs index 841f4c58..5bc31a88 100644 --- a/src/StardewModdingAPI/Advanced/IConfigFile.cs +++ b/src/StardewModdingAPI/Advanced/IConfigFile.cs @@ -6,7 +6,7 @@ /********* ** Accessors *********/ - /// Provides methods for interacting with the mod directory, including read/writing the config file. + /// Provides simplified APIs for writing mods. IModHelper ModHelper { get; set; } /// The file path from which the model was loaded, relative to the mod directory. diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs new file mode 100644 index 00000000..6e7e3382 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs @@ -0,0 +1,94 @@ +using System; +using System.Reflection; +using StardewModdingAPI.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A private field obtained through reflection. + /// The field value type. + internal class PrivateField : IPrivateField + { + /********* + ** Properties + *********/ + /// The type that has the field. + private readonly Type ParentType; + + /// The object that has the instance field (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.FieldInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the field. + /// The object that has the instance field (if applicable). + /// The reflection metadata. + /// Whether the field is static. + /// The or is null. + /// The is null for a non-static field, or not null for a static field. + public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (field == null) + throw new ArgumentNullException(nameof(field)); + if (isStatic && obj != null) + throw new ArgumentException("A static field cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static field must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.FieldInfo = field; + } + + /// Get the field value. + public TValue GetValue() + { + try + { + return (TValue)this.FieldInfo.GetValue(this.Parent); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex); + } + } + + /// Set the field value. + //// The value to set. + public void SetValue(TValue value) + { + try + { + this.FieldInfo.SetValue(this.Parent, value); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs new file mode 100644 index 00000000..5b882eed --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs @@ -0,0 +1,100 @@ +using System; +using System.Reflection; +using StardewModdingAPI.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A private method obtained through reflection. + internal class PrivateMethod : IPrivateMethod + { + /********* + ** Properties + *********/ + /// The type that has the method. + private readonly Type ParentType; + + /// The object that has the instance method (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.MethodInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the method. + /// The object that has the instance method(if applicable). + /// The reflection metadata. + /// Whether the field is static. + /// The or is null. + /// The is null for a non-static method, or not null for a static method. + public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (method == null) + throw new ArgumentNullException(nameof(method)); + if (isStatic && obj != null) + throw new ArgumentException("A static method cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static method must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.MethodInfo = method; + } + + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + public TValue Invoke(params object[] arguments) + { + // invoke method + object result; + try + { + result = this.MethodInfo.Invoke(this.Parent, arguments); + } + catch (Exception ex) + { + throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + } + + // cast return value + try + { + return (TValue)result; + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); + } + } + + /// Invoke the method. + /// The method arguments to pass in. + public void Invoke(params object[] arguments) + { + // invoke method + try + { + this.MethodInfo.Invoke(this.Parent, arguments); + } + catch (Exception ex) + { + throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + } + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs new file mode 100644 index 00000000..17758a39 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs @@ -0,0 +1,197 @@ +using System; +using System.Reflection; +using StardewModdingAPI.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Provides helper methods for accessing private game code. + internal class ReflectionHelper : IReflectionHelper + { + /********* + ** Public methods + *********/ + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); + + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && field == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); + return field; + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && field == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); + return field; + } + + /**** + ** Field values + ** (shorthand since this is the most common case) + ****/ + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + public TValue GetPrivateValue(object obj, string name, bool required = true) + { + return this.GetPrivateField(obj, name, required).GetValue(); + } + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + public TValue GetPrivateValue(Type type, string name, bool required = true) + { + return this.GetPrivateField(type, name, required).GetValue(); + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); + return method; + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + // validate parent + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + // get field from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); + return method; + } + + + /********* + ** Private methods + *********/ + /// Get a field from the type hierarchy. + /// The expected field type. + /// The type which has the field. + /// The object which has the field. + /// The field name. + /// The reflection binding which flags which indicates what type of field to find. + private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + FieldInfo field = null; + for (; type != null && field == null; type = type.BaseType) + field = type.GetField(name, bindingFlags); + + return field != null + ? new PrivateField(type, obj, field, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + MethodInfo method = null; + for (; type != null && method == null; type = type.BaseType) + method = type.GetMethod(name, bindingFlags); + + return method != null + ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + /// The argument types of the method signature to find. + private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) + { + MethodInfo method = null; + for (; type != null && method == null; type = type.BaseType) + method = type.GetMethod(name, bindingFlags, null, argumentTypes, null); + + return method != null + ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs index 1af7df6b..709c8692 100644 --- a/src/StardewModdingAPI/IModHelper.cs +++ b/src/StardewModdingAPI/IModHelper.cs @@ -1,6 +1,8 @@ -namespace StardewModdingAPI +using StardewModdingAPI.Reflection; + +namespace StardewModdingAPI { - /// Provides methods for interacting with a mod directory. + /// Provides simplified APIs for writing mods. public interface IModHelper { /********* @@ -9,6 +11,9 @@ /// The mod directory path. string DirectoryPath { get; } + /// Simplifies access to private game code. + IReflectionHelper Reflection { get; } + /********* ** Public methods diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index 05122df5..21551771 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -13,10 +13,11 @@ namespace StardewModdingAPI /// The backing field for . private string _pathOnDisk; + /********* ** Accessors *********/ - /// Provides methods for interacting with the mod directory, such as read/writing a config file or custom JSON files. + /// Provides simplified APIs for writing mods. public IModHelper Helper { get; internal set; } /// Writes messages to the console and log file. @@ -74,12 +75,12 @@ namespace StardewModdingAPI public virtual void Entry(params object[] objects) { } /// The mod entry point, called after the mod is first loaded. - /// Provides methods for interacting with the mod directory, such as read/writing a config file or custom JSON files. + /// Provides simplified APIs for writing mods. [Obsolete("This overload is obsolete since SMAPI 1.1.")] public virtual void Entry(ModHelper helper) { } /// The mod entry point, called after the mod is first loaded. - /// Provides methods for interacting with the mod directory, such as read/writing a config file or custom JSON files. + /// Provides simplified APIs for writing mods. public virtual void Entry(IModHelper helper) { } diff --git a/src/StardewModdingAPI/ModHelper.cs b/src/StardewModdingAPI/ModHelper.cs index 6a7e200a..781deff4 100644 --- a/src/StardewModdingAPI/ModHelper.cs +++ b/src/StardewModdingAPI/ModHelper.cs @@ -2,11 +2,13 @@ using System.IO; using Newtonsoft.Json; using StardewModdingAPI.Advanced; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Reflection; namespace StardewModdingAPI { - /// Provides methods for interacting with a mod directory. - [Obsolete("Use " + nameof(IModHelper) + " instead.")] + /// Provides simplified APIs for writing mods. + [Obsolete("Use " + nameof(IModHelper) + " instead.")] // only direct mod access to this class is obsolete public class ModHelper : IModHelper { /********* @@ -15,6 +17,9 @@ namespace StardewModdingAPI /// The mod directory path. public string DirectoryPath { get; } + /// Simplifies access to private game code. + public IReflectionHelper Reflection { get; } = new ReflectionHelper(); + /********* ** Public methods diff --git a/src/StardewModdingAPI/Reflection/IPrivateField.cs b/src/StardewModdingAPI/Reflection/IPrivateField.cs new file mode 100644 index 00000000..f758902f --- /dev/null +++ b/src/StardewModdingAPI/Reflection/IPrivateField.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI.Reflection +{ + /// A private field obtained through reflection. + /// The field value type. + public interface IPrivateField + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the field value. + TValue GetValue(); + + /// Set the field value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Reflection/IPrivateMethod.cs b/src/StardewModdingAPI/Reflection/IPrivateMethod.cs new file mode 100644 index 00000000..4790303b --- /dev/null +++ b/src/StardewModdingAPI/Reflection/IPrivateMethod.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace StardewModdingAPI.Reflection +{ + /// A private method obtained through reflection. + public interface IPrivateMethod + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + TValue Invoke(params object[] arguments); + + /// Invoke the method. + /// The method arguments to pass in. + void Invoke(params object[] arguments); + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Reflection/IReflectionHelper.cs b/src/StardewModdingAPI/Reflection/IReflectionHelper.cs new file mode 100644 index 00000000..f5d7d547 --- /dev/null +++ b/src/StardewModdingAPI/Reflection/IReflectionHelper.cs @@ -0,0 +1,53 @@ +using System; + +namespace StardewModdingAPI.Reflection +{ + /// Simplifies access to private game code. + public interface IReflectionHelper + { + /********* + ** Public methods + *********/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateField GetPrivateField(object obj, string name, bool required = true); + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateField GetPrivateField(Type type, string name, bool required = true); + + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + TValue GetPrivateValue(object obj, string name, bool required = true); + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + TValue GetPrivateValue(Type type, string name, bool required = true); + + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index a90a0686..59edc0c9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -161,6 +161,9 @@ + + + @@ -182,6 +185,9 @@ + + + diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs index 9572c494..f0c7549f 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/TrainerMod/TrainerMod.cs @@ -40,7 +40,7 @@ namespace TrainerMod ** Public methods *********/ /// The mod entry point, called after the mod is first loaded. - /// Provides methods for interacting with the mod directory, such as read/writing a config file or custom JSON files. + /// Provides simplified APIs for writing mods. public override void Entry(IModHelper helper) { this.RegisterCommands(); @@ -694,7 +694,7 @@ namespace TrainerMod else this.LogValueNotSpecified(); } - + /**** ** Helpers ****/ -- cgit From 80b6e208418b7d9237bc4aa98c68d2bb849b49d5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Dec 2016 14:15:14 -0500 Subject: cache reflection lookups with sliding expiry (#185) --- .../Framework/Reflection/ReflectionHelper.cs | 67 ++++++++++++++++++---- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 2 files changed, 56 insertions(+), 12 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs index 17758a39..fd916bbe 100644 --- a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs @@ -1,12 +1,25 @@ using System; +using System.Linq; using System.Reflection; +using System.Runtime.Caching; using StardewModdingAPI.Reflection; namespace StardewModdingAPI.Framework.Reflection { /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). internal class ReflectionHelper : IReflectionHelper { + /********* + ** Properties + *********/ + /// The cached fields and methods found via reflection. + private readonly MemoryCache Cache = new MemoryCache(typeof(ReflectionHelper).FullName); + + /// The sliding cache expiration time. + private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + + /********* ** Public methods *********/ @@ -152,12 +165,17 @@ namespace StardewModdingAPI.Framework.Reflection /// The reflection binding which flags which indicates what type of field to find. private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { - FieldInfo field = null; - for (; type != null && field == null; type = type.BaseType) - field = type.GetField(name, bindingFlags); + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => + { + FieldInfo fieldInfo = null; + for (; type != null && fieldInfo == null; type = type.BaseType) + fieldInfo = type.GetField(name, bindingFlags); + return fieldInfo; + }); return field != null - ? new PrivateField(type, obj, field, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + ? new PrivateField(type, obj, field, isStatic) : null; } @@ -168,9 +186,14 @@ namespace StardewModdingAPI.Framework.Reflection /// The reflection binding which flags which indicates what type of method to find. private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { - MethodInfo method = null; - for (; type != null && method == null; type = type.BaseType) - method = type.GetMethod(name, bindingFlags); + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags); + return methodInfo; + }); return method != null ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) @@ -185,13 +208,33 @@ namespace StardewModdingAPI.Framework.Reflection /// The argument types of the method signature to find. private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) { - MethodInfo method = null; - for (; type != null && method == null; type = type.BaseType) - method = type.GetMethod(name, bindingFlags, null, argumentTypes, null); - + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); + return methodInfo; + }); return method != null - ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + ? new PrivateMethod(type, obj, method, isStatic) : null; } + + /// Get a method or field through the cache. + /// The expected type. + /// The cache key. + /// Fetches a new value to cache. + private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo + { + // get from cache + if (this.Cache.Contains(key)) + return (TMemberInfo)this.Cache[key]; + + // fetch & cache new value + TMemberInfo result = fetch(); + this.Cache.Add(key, result, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); + return result; + } } } \ No newline at end of file diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 59edc0c9..6e87fbdb 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -106,6 +106,7 @@ True + -- cgit From df7d41fc37521ecfe039e9661cf288c933cb2bdc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 11 Dec 2016 21:03:57 -0500 Subject: move interfaces into root (#185) --- .../Framework/Reflection/PrivateField.cs | 1 - .../Framework/Reflection/PrivateMethod.cs | 1 - .../Framework/Reflection/ReflectionHelper.cs | 1 - src/StardewModdingAPI/IModHelper.cs | 4 +- src/StardewModdingAPI/IPrivateField.cs | 26 +++++++++++ src/StardewModdingAPI/IPrivateMethod.cs | 27 +++++++++++ src/StardewModdingAPI/IReflectionHelper.cs | 53 ++++++++++++++++++++++ src/StardewModdingAPI/ModHelper.cs | 1 - src/StardewModdingAPI/Reflection/IPrivateField.cs | 26 ----------- src/StardewModdingAPI/Reflection/IPrivateMethod.cs | 27 ----------- .../Reflection/IReflectionHelper.cs | 53 ---------------------- src/StardewModdingAPI/StardewModdingAPI.csproj | 6 +-- 12 files changed, 110 insertions(+), 116 deletions(-) create mode 100644 src/StardewModdingAPI/IPrivateField.cs create mode 100644 src/StardewModdingAPI/IPrivateMethod.cs create mode 100644 src/StardewModdingAPI/IReflectionHelper.cs delete mode 100644 src/StardewModdingAPI/Reflection/IPrivateField.cs delete mode 100644 src/StardewModdingAPI/Reflection/IPrivateMethod.cs delete mode 100644 src/StardewModdingAPI/Reflection/IReflectionHelper.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs index 6e7e3382..0bf45969 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs +++ b/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs @@ -1,6 +1,5 @@ using System; using System.Reflection; -using StardewModdingAPI.Reflection; namespace StardewModdingAPI.Framework.Reflection { diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs index 5b882eed..ba2374f4 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs +++ b/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs @@ -1,6 +1,5 @@ using System; using System.Reflection; -using StardewModdingAPI.Reflection; namespace StardewModdingAPI.Framework.Reflection { diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs index fd916bbe..38b4e357 100644 --- a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Reflection; using System.Runtime.Caching; -using StardewModdingAPI.Reflection; namespace StardewModdingAPI.Framework.Reflection { diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs index 709c8692..183b3b2b 100644 --- a/src/StardewModdingAPI/IModHelper.cs +++ b/src/StardewModdingAPI/IModHelper.cs @@ -1,6 +1,4 @@ -using StardewModdingAPI.Reflection; - -namespace StardewModdingAPI +namespace StardewModdingAPI { /// Provides simplified APIs for writing mods. public interface IModHelper diff --git a/src/StardewModdingAPI/IPrivateField.cs b/src/StardewModdingAPI/IPrivateField.cs new file mode 100644 index 00000000..3e681c12 --- /dev/null +++ b/src/StardewModdingAPI/IPrivateField.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A private field obtained through reflection. + /// The field value type. + public interface IPrivateField + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the field value. + TValue GetValue(); + + /// Set the field value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/IPrivateMethod.cs b/src/StardewModdingAPI/IPrivateMethod.cs new file mode 100644 index 00000000..67fc8b3c --- /dev/null +++ b/src/StardewModdingAPI/IPrivateMethod.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A private method obtained through reflection. + public interface IPrivateMethod + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + TValue Invoke(params object[] arguments); + + /// Invoke the method. + /// The method arguments to pass in. + void Invoke(params object[] arguments); + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/StardewModdingAPI/IReflectionHelper.cs new file mode 100644 index 00000000..5d747eda --- /dev/null +++ b/src/StardewModdingAPI/IReflectionHelper.cs @@ -0,0 +1,53 @@ +using System; + +namespace StardewModdingAPI +{ + /// Simplifies access to private game code. + public interface IReflectionHelper + { + /********* + ** Public methods + *********/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateField GetPrivateField(object obj, string name, bool required = true); + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateField GetPrivateField(Type type, string name, bool required = true); + + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + TValue GetPrivateValue(object obj, string name, bool required = true); + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + TValue GetPrivateValue(Type type, string name, bool required = true); + + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); + } +} diff --git a/src/StardewModdingAPI/ModHelper.cs b/src/StardewModdingAPI/ModHelper.cs index 781deff4..1fcc0182 100644 --- a/src/StardewModdingAPI/ModHelper.cs +++ b/src/StardewModdingAPI/ModHelper.cs @@ -3,7 +3,6 @@ using System.IO; using Newtonsoft.Json; using StardewModdingAPI.Advanced; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Reflection; namespace StardewModdingAPI { diff --git a/src/StardewModdingAPI/Reflection/IPrivateField.cs b/src/StardewModdingAPI/Reflection/IPrivateField.cs deleted file mode 100644 index f758902f..00000000 --- a/src/StardewModdingAPI/Reflection/IPrivateField.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI.Reflection -{ - /// A private field obtained through reflection. - /// The field value type. - public interface IPrivateField - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - FieldInfo FieldInfo { get; } - - - /********* - ** Public methods - *********/ - /// Get the field value. - TValue GetValue(); - - /// Set the field value. - //// The value to set. - void SetValue(TValue value); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Reflection/IPrivateMethod.cs b/src/StardewModdingAPI/Reflection/IPrivateMethod.cs deleted file mode 100644 index 4790303b..00000000 --- a/src/StardewModdingAPI/Reflection/IPrivateMethod.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI.Reflection -{ - /// A private method obtained through reflection. - public interface IPrivateMethod - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - MethodInfo MethodInfo { get; } - - - /********* - ** Public methods - *********/ - /// Invoke the method. - /// The return type. - /// The method arguments to pass in. - TValue Invoke(params object[] arguments); - - /// Invoke the method. - /// The method arguments to pass in. - void Invoke(params object[] arguments); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Reflection/IReflectionHelper.cs b/src/StardewModdingAPI/Reflection/IReflectionHelper.cs deleted file mode 100644 index f5d7d547..00000000 --- a/src/StardewModdingAPI/Reflection/IReflectionHelper.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; - -namespace StardewModdingAPI.Reflection -{ - /// Simplifies access to private game code. - public interface IReflectionHelper - { - /********* - ** Public methods - *********/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateField GetPrivateField(object obj, string name, bool required = true); - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateField GetPrivateField(Type type, string name, bool required = true); - - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// This is a shortcut for followed by . - TValue GetPrivateValue(object obj, string name, bool required = true); - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// This is a shortcut for followed by . - TValue GetPrivateValue(Type type, string name, bool required = true); - - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); - } -} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 5f380cc6..4e547fa0 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -184,9 +184,9 @@ - - - + + + -- cgit