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 +++++++++++---------- src/StardewModdingAPI/Program.cs | 34 ++++-- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 + 7 files changed, 193 insertions(+), 74 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/CacheEntry.cs create mode 100644 src/StardewModdingAPI/Framework/AssemblyRewriting/RewriteResult.cs (limited to 'src/StardewModdingAPI') 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); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index e648ed64..a46f7a3e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -12,6 +13,7 @@ using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.AssemblyRewriting; using StardewModdingAPI.Inheritance; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -38,8 +40,8 @@ namespace StardewModdingAPI /// The full path to the folder containing mods. private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); - /// The full path to the folder containing cached SMAPI data. - private static readonly string CachePath = Path.Combine(Program.ModPath, ".cache"); + /// The name of the folder containing a mod's cached assembly data. + private static readonly string CacheDirName = ".cache"; /// The log file to which to write messages. private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); @@ -134,7 +136,6 @@ namespace StardewModdingAPI Program.Monitor.Log("Loading SMAPI..."); Console.Title = Constants.ConsoleTitle; Program.VerifyPath(Program.ModPath); - Program.VerifyPath(Program.CachePath); Program.VerifyPath(Constants.LogDir); if (!File.Exists(Program.GameExecutablePath)) { @@ -304,7 +305,7 @@ namespace StardewModdingAPI Program.Monitor.Log("Loading mods..."); // get assembly loader - ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath, Program.TargetPlatform, Program.Monitor); + ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CacheDirName, Program.TargetPlatform, Program.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // load mods @@ -401,14 +402,15 @@ namespace StardewModdingAPI } } - // preprocess mod assemblies + // preprocess mod assemblies for compatibility + var processedAssemblies = new List(); { bool succeeded = true; foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll")) { try { - modAssemblyLoader.ProcessAssembly(assemblyPath); + processedAssemblies.Add(modAssemblyLoader.ProcessAssemblyUnlessCached(assemblyPath)); } catch (Exception ex) { @@ -420,13 +422,27 @@ namespace StardewModdingAPI if (!succeeded) 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); - // load assembly + // get entry assembly path + string mainAssemblyPath; + { + 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; + } + + // load entry assembly Assembly modAssembly; try { - string assemblyPath = Path.Combine(directory, manifest.EntryDll); - modAssembly = modAssemblyLoader.LoadCachedAssembly(assemblyPath); + 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); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 96eb038e..8a827ace 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -154,8 +154,10 @@ + + -- cgit From b751e7dd2e1abeeb2adbbb099f1473ca10092c88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Dec 2016 17:29:38 -0500 Subject: skip mod folder with a warning if it has no manifest (#186) --- src/StardewModdingAPI/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a46f7a3e..62b9dabd 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -327,7 +327,14 @@ namespace StardewModdingAPI // get manifest path string manifestPath = Path.Combine(directory, "manifest.json"); + if (!File.Exists(manifestPath)) + { + Program.Monitor.Log($"Ignored folder \"{new DirectoryInfo(directory).Name}\" which doesn't have a manifest.json.", LogLevel.Warn); + continue; + } string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; + + // read manifest Manifest manifest; try { -- 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 --- src/StardewModdingAPI/Framework/InternalExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI') 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 acbd33fb027093c7ff4a0a138eb42e7518eeabf8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 23:15:37 -0500 Subject: open a terminal for SMAPI output on Mac (#183) --- src/StardewModdingAPI/unix-launcher.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh index 0bfe0d5c..ce732588 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/StardewModdingAPI/unix-launcher.sh @@ -1,7 +1,7 @@ #!/bin/bash # MonoKickstart Shell Script # Written by Ethan "flibitijibibo" Lee -# Modified for StardewModdingAPI by Viz +# Modified for StardewModdingAPI by Viz and Pathoschild # Move to script's directory cd "`dirname "$0"`" @@ -28,7 +28,7 @@ if [ "$UNAME" == "Darwin" ]; then ln -sf mcs.bin.osx mcs cp StardewValley.bin.osx StardewModdingAPI.bin.osx - ./StardewModdingAPI.bin.osx $@ + open -a Terminal ./StardewModdingAPI.bin.osx $@ else if [ "$ARCH" == "x86_64" ]; then ln -sf mcs.bin.x86_64 mcs -- cgit From 7af722ec1f0157ab9e894ef565ff3b8a9b4df938 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 23:38:22 -0500 Subject: rename .targets file to better reflect contents, add to installer project --- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8a827ace..a90a0686 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -79,7 +79,6 @@ icon.ico - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll @@ -223,6 +222,7 @@ StardewModdingAPI.AssemblyRewriters + -- cgit From 25d2eb477729e929f3534b6f828c3fd96632e107 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 8 Dec 2016 12:18:56 -0500 Subject: intercept mod errors in menu draw code so they don't crash the game --- src/StardewModdingAPI/Inheritance/SGame.cs | 42 ++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Inheritance/SGame.cs b/src/StardewModdingAPI/Inheritance/SGame.cs index 93d56553..f70d0696 100644 --- a/src/StardewModdingAPI/Inheritance/SGame.cs +++ b/src/StardewModdingAPI/Inheritance/SGame.cs @@ -373,7 +373,7 @@ namespace StardewModdingAPI.Inheritance /// The method called to draw everything to the screen. /// A snapshot of the game timing state. - /// This implementation is identical to , except for minor formatting and added events. + /// This implementation is identical to , except for try..catch around menu draw code, minor formatting, and added events. protected override void Draw(GameTime gameTime) { // track frame rate @@ -388,9 +388,25 @@ namespace StardewModdingAPI.Inheritance if (Game1.options.showMenuBackground && Game1.activeClickableMenu != null && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + try + { + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing its background. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); + try + { + Game1.activeClickableMenu.draw(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); Game1.spriteBatch.End(); if (!this.ZoomLevelIsOne) @@ -434,7 +450,15 @@ namespace StardewModdingAPI.Inheritance if (Game1.showingEndOfNightStuff) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - Game1.activeClickableMenu?.draw(Game1.spriteBatch); + try + { + Game1.activeClickableMenu?.draw(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } Game1.spriteBatch.End(); if (!this.ZoomLevelIsOne) { @@ -742,7 +766,15 @@ namespace StardewModdingAPI.Inheritance if (Game1.activeClickableMenu != null) { GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); + try + { + Game1.activeClickableMenu.draw(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); } else -- 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 + 12 files changed, 523 insertions(+), 9 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') 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.