From 31301988e97a9460ea2cb4898eb263a4e6c297d2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Dec 2016 02:14:25 -0500 Subject: deploy trainer mod when building SMAPI in debug mode --- src/TrainerMod/TrainerMod.csproj | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 229e6b4d..6d8b5f34 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -127,4 +127,10 @@ + + + + + + \ No newline at end of file -- cgit 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') 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 f625fd51a0fa33c87feeb6890390a6b253ef38a1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Dec 2016 00:37:12 -0500 Subject: always clean up files during install (#188) --- .../InteractiveInstaller.cs | 156 ++++++++++----------- 1 file changed, 77 insertions(+), 79 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 1d3802ab..7b082893 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -131,90 +131,88 @@ namespace StardewModdingApi.Installer Console.WriteLine(); /**** - ** Perform action + ** Always uninstall old files ****/ - switch (action) + // restore game launcher + if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup)) { - case ScriptAction.Uninstall: - { - // restore game launcher - if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup)) - { - this.PrintDebug("Restoring game launcher..."); - if (File.Exists(paths.unixLauncher)) - File.Delete(paths.unixLauncher); - File.Move(paths.unixLauncherBackup, paths.unixLauncher); - } - - // remove SMAPI files - this.PrintDebug("Removing SMAPI files..."); - foreach (string filename in this.UninstallFiles) - { - string targetPath = Path.Combine(installDir.FullName, filename); - if (File.Exists(targetPath)) - File.Delete(targetPath); - } - } - break; + this.PrintDebug("Removing SMAPI launcher..."); + if (File.Exists(paths.unixLauncher)) + File.Delete(paths.unixLauncher); + File.Move(paths.unixLauncherBackup, paths.unixLauncher); + } + + // remove old files + string[] removeFiles = this.UninstallFiles + .Select(path => Path.Combine(installDir.FullName, path)) + .Where(File.Exists) + .ToArray(); + if (removeFiles.Any()) + { + this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); + foreach (string path in removeFiles) + File.Delete(path); + } + + /**** + ** Install new files + ****/ + if (action == ScriptAction.Install) + { + // copy SMAPI files to game dir + this.PrintDebug("Adding SMAPI files..."); + foreach (FileInfo sourceFile in packageDir.EnumerateFiles()) + { + string targetPath = Path.Combine(installDir.FullName, sourceFile.Name); + if (File.Exists(targetPath)) + File.Delete(targetPath); + sourceFile.CopyTo(targetPath); + } - case ScriptAction.Install: + // replace mod launcher (if possible) + if (platform == Platform.Mono) + { + this.PrintDebug("Safely replacing game launcher..."); + if (!File.Exists(paths.unixLauncherBackup)) + File.Move(paths.unixLauncher, paths.unixLauncherBackup); + else if (File.Exists(paths.unixLauncher)) + File.Delete(paths.unixLauncher); + + File.Move(paths.unixSmapiLauncher, paths.unixLauncher); + } + + // create mods directory (if needed) + DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); + if (!modsDir.Exists) + { + this.PrintDebug("Creating mods directory..."); + modsDir.Create(); + } + + // add or replace bundled mods + Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods")); + DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods")); + if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) + { + this.PrintDebug("Adding bundled mods..."); + foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) { - // copy SMAPI files to game dir - this.PrintDebug("Copying SMAPI files to game directory..."); - foreach (FileInfo sourceFile in packageDir.EnumerateFiles()) - { - string targetPath = Path.Combine(installDir.FullName, sourceFile.Name); - if (File.Exists(targetPath)) - File.Delete(targetPath); - sourceFile.CopyTo(targetPath); - } - - // replace mod launcher (if possible) - if (platform == Platform.Mono) - { - this.PrintDebug("Safely replacing game launcher..."); - if (!File.Exists(paths.unixLauncherBackup)) - File.Move(paths.unixLauncher, paths.unixLauncherBackup); - else if (File.Exists(paths.unixLauncher)) - File.Delete(paths.unixLauncher); - - File.Move(paths.unixSmapiLauncher, paths.unixLauncher); - } - - // create mods directory (if needed) - DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); - if (!modsDir.Exists) - { - this.PrintDebug("Creating mods directory..."); - modsDir.Create(); - } - - // add or replace bundled mods - Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods")); - DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods")); - if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) - { - this.PrintDebug("Adding bundled mods..."); - foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) - { - this.PrintDebug($" adding {sourceDir.Name}..."); - - // initialise target dir - DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name)); - if (targetDir.Exists) - targetDir.Delete(recursive: true); - targetDir.Create(); - - // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles()) - sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); - } - } - - // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(platform, modsDir); + this.PrintDebug($" adding {sourceDir.Name}..."); + + // initialise target dir + DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name)); + if (targetDir.Exists) + targetDir.Delete(recursive: true); + targetDir.Create(); + + // copy files + foreach (FileInfo sourceFile in sourceDir.EnumerateFiles()) + sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); } - break; + } + + // remove obsolete appdata mods + this.InteractivelyRemoveAppDataMods(platform, modsDir); } Console.WriteLine(); -- cgit From 8304227cea5b971f17f0dbe980bc3edc76fb5e61 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Dec 2016 00:41:12 -0500 Subject: remove obsolete mods/.cache directory on install (#187, #188) --- .../InteractiveInstaller.cs | 41 +++++++++++++--------- 1 file changed, 25 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 7b082893..c3b9a2e3 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -27,8 +27,8 @@ namespace StardewModdingApi.Installer @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley" }; - /// The files to remove when uninstalling SMAPI. - private readonly string[] UninstallFiles = + /// The directory or file paths to remove when uninstalling SMAPI, relative to the game directory. + private readonly string[] UninstallPaths = { // common "StardewModdingAPI.exe", @@ -45,7 +45,10 @@ namespace StardewModdingApi.Installer "System.Numerics.dll", // Windows only - "StardewModdingAPI.pdb" + "StardewModdingAPI.pdb", + + // obsolete + "Mods/.cache" }; @@ -59,16 +62,17 @@ namespace StardewModdingApi.Installer /// 1. Collect information (mainly OS and install path) and validate it. /// 2. Ask the user whether to install or uninstall. /// - /// Install flow: - /// 1. Copy the SMAPI files from package/Windows or package/Mono into the game directory. - /// 2. On Linux/Mac: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) - /// 3. Create the 'Mods' directory. - /// 4. Copy the bundled mods into the 'Mods' directory (deleting any existing versions). - /// 5. Move any mods from app data into game's mods directory. - /// /// Uninstall logic: /// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup. - /// 2. Delete all files in the game directory matching one of the . + /// 2. Delete all files and folders in the game directory matching one of the . + /// + /// Install flow: + /// 1. Run the uninstall flow. + /// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory. + /// 3. On Linux/Mac: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) + /// 4. Create the 'Mods' directory. + /// 5. Copy the bundled mods into the 'Mods' directory (deleting any existing versions). + /// 6. Move any mods from app data into game's mods directory. /// public void Run(string[] args) { @@ -143,15 +147,20 @@ namespace StardewModdingApi.Installer } // remove old files - string[] removeFiles = this.UninstallFiles + string[] removePaths = this.UninstallPaths .Select(path => Path.Combine(installDir.FullName, path)) - .Where(File.Exists) + .Where(path => Directory.Exists(path) || File.Exists(path)) .ToArray(); - if (removeFiles.Any()) + if (removePaths.Any()) { this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); - foreach (string path in removeFiles) - File.Delete(path); + foreach (string path in removePaths) + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + else + File.Delete(path); + } } /**** -- cgit From 748e45aefbfc4d8e15519a71052d48f25d1c6da9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Dec 2016 17:13:57 -0500 Subject: add dependencies.targets to project --- src/StardewModdingAPI.sln | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index d97e4645..37c08950 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86 ProjectSection(SolutionItems) = preProject ..\.gitattributes = ..\.gitattributes ..\.gitignore = ..\.gitignore + dependencies.targets = dependencies.targets GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs ..\LICENSE = ..\LICENSE ..\README.md = ..\README.md -- 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') 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 f0433e5a41c01d73edcd20a6767b7979f636c0e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Dec 2016 22:19:38 -0500 Subject: tweak installer wording to avoid confusion --- release-notes.md | 3 ++- src/StardewModdingAPI.Installer/InteractiveInstaller.cs | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 3b16dcd6..20bb74ba 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,9 +6,10 @@ See [log](https://github.com/CLxS/SMAPI/compare/stable...develop). For players: * Installing SMAPI will now automatically clean up old SMAPI files. * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. + * Tweaked installer wording to avoid confusion. For developers: - * Fixed issue where you could no longer debug into an assembly because it was copied into the `.cache` directory. That will now only happen if necessary. + * 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. ## 1.3 See [log](https://github.com/CLxS/SMAPI/compare/1.2...1.3). diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index c3b9a2e3..d7bf7b0e 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -119,7 +119,7 @@ namespace StardewModdingApi.Installer ScriptAction action; { - string choice = this.InteractivelyChoose("What do you want to do?", "1", "2"); + string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2"); switch (choice) { case "1": @@ -320,12 +320,11 @@ namespace StardewModdingApi.Installer } // ask user - Console.WriteLine("Oops, couldn't find your Stardew Valley install path automatically. You'll need to specify where the game is installed (or install SMAPI manually)."); + Console.WriteLine("Oops, couldn't find the game automatically."); while (true) { // get path from user - Console.WriteLine(" Enter the game's full directory path (the one containing 'StardewValley.exe' or 'Stardew Valley.exe')."); - Console.Write(" > "); + Console.WriteLine($"Type the file path to the game directory (the one containing '{(platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe")}'), then press enter."); string path = Console.ReadLine()?.Trim(); if (string.IsNullOrWhiteSpace(path)) { -- cgit From b019dd4f69c9fefeba9f14c2049fb352127e448f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 20:36:28 -0500 Subject: replace out_items, out_melee, and out_rings commands with a searchable list_items command --- release-notes.md | 1 + src/TrainerMod/ItemData/ISearchItem.cs | 21 +++++ src/TrainerMod/ItemData/ItemType.cs | 15 ++++ src/TrainerMod/ItemData/SearchableObject.cs | 48 ++++++++++ src/TrainerMod/ItemData/SearchableRing.cs | 48 ++++++++++ src/TrainerMod/ItemData/SearchableWeapon.cs | 48 ++++++++++ src/TrainerMod/TrainerMod.cs | 135 +++++++++++++++++++--------- src/TrainerMod/TrainerMod.csproj | 5 ++ 8 files changed, 279 insertions(+), 42 deletions(-) create mode 100644 src/TrainerMod/ItemData/ISearchItem.cs create mode 100644 src/TrainerMod/ItemData/ItemType.cs create mode 100644 src/TrainerMod/ItemData/SearchableObject.cs create mode 100644 src/TrainerMod/ItemData/SearchableRing.cs create mode 100644 src/TrainerMod/ItemData/SearchableWeapon.cs (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 20bb74ba..3426b622 100644 --- a/release-notes.md +++ b/release-notes.md @@ -10,6 +10,7 @@ For players: For developers: * 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/TrainerMod/ItemData/ISearchItem.cs b/src/TrainerMod/ItemData/ISearchItem.cs new file mode 100644 index 00000000..b2f7c2b8 --- /dev/null +++ b/src/TrainerMod/ItemData/ISearchItem.cs @@ -0,0 +1,21 @@ +namespace TrainerMod.ItemData +{ + /// An item that can be searched and added to the player's inventory through the console. + internal interface ISearchItem + { + /********* + ** Accessors + *********/ + /// Whether the item is valid. + bool IsValid { get; } + + /// The item ID. + int ID { get; } + + /// The item name. + string Name { get; } + + /// The item type. + ItemType Type { get; } + } +} \ No newline at end of file diff --git a/src/TrainerMod/ItemData/ItemType.cs b/src/TrainerMod/ItemData/ItemType.cs new file mode 100644 index 00000000..2e049aa1 --- /dev/null +++ b/src/TrainerMod/ItemData/ItemType.cs @@ -0,0 +1,15 @@ +namespace TrainerMod.ItemData +{ + /// An item type that can be searched and added to the player through the console. + internal enum ItemType + { + /// Any object in (except rings). + Object, + + /// A ring in . + Ring, + + /// A weapon from Data\weapons. + Weapon + } +} diff --git a/src/TrainerMod/ItemData/SearchableObject.cs b/src/TrainerMod/ItemData/SearchableObject.cs new file mode 100644 index 00000000..30362f54 --- /dev/null +++ b/src/TrainerMod/ItemData/SearchableObject.cs @@ -0,0 +1,48 @@ +using StardewValley; + +namespace TrainerMod.ItemData +{ + /// An object that can be searched and added to the player's inventory through the console. + internal class SearchableObject : ISearchItem + { + /********* + ** Properties + *********/ + /// The underlying item. + private readonly Item Item; + + + /********* + ** Accessors + *********/ + /// Whether the item is valid. + public bool IsValid => this.Item != null && this.Item.Name != "Broken Item"; + + /// The item ID. + public int ID => this.Item.parentSheetIndex; + + /// The item name. + public string Name => this.Item.Name; + + /// The item type. + public ItemType Type => ItemType.Object; + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The item ID. + public SearchableObject(int id) + { + try + { + this.Item = new Object(id, 1); + } + catch + { + // invalid + } + } + } +} \ No newline at end of file diff --git a/src/TrainerMod/ItemData/SearchableRing.cs b/src/TrainerMod/ItemData/SearchableRing.cs new file mode 100644 index 00000000..7751e6aa --- /dev/null +++ b/src/TrainerMod/ItemData/SearchableRing.cs @@ -0,0 +1,48 @@ +using StardewValley.Objects; + +namespace TrainerMod.ItemData +{ + /// A ring that can be searched and added to the player's inventory through the console. + internal class SearchableRing : ISearchItem + { + /********* + ** Properties + *********/ + /// The underlying item. + private readonly Ring Ring; + + + /********* + ** Accessors + *********/ + /// Whether the item is valid. + public bool IsValid => this.Ring != null; + + /// The item ID. + public int ID => this.Ring.parentSheetIndex; + + /// The item name. + public string Name => this.Ring.Name; + + /// The item type. + public ItemType Type => ItemType.Ring; + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The ring ID. + public SearchableRing(int id) + { + try + { + this.Ring = new Ring(id); + } + catch + { + // invalid + } + } + } +} \ No newline at end of file diff --git a/src/TrainerMod/ItemData/SearchableWeapon.cs b/src/TrainerMod/ItemData/SearchableWeapon.cs new file mode 100644 index 00000000..cc9ef459 --- /dev/null +++ b/src/TrainerMod/ItemData/SearchableWeapon.cs @@ -0,0 +1,48 @@ +using StardewValley.Tools; + +namespace TrainerMod.ItemData +{ + /// A weapon that can be searched and added to the player's inventory through the console. + internal class SearchableWeapon : ISearchItem + { + /********* + ** Properties + *********/ + /// The underlying item. + private readonly MeleeWeapon Weapon; + + + /********* + ** Accessors + *********/ + /// Whether the item is valid. + public bool IsValid => this.Weapon != null; + + /// The item ID. + public int ID => this.Weapon.initialParentTileIndex; + + /// The item name. + public string Name => this.Weapon.Name; + + /// The item type. + public ItemType Type => ItemType.Weapon; + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The weapon ID. + public SearchableWeapon(int id) + { + try + { + this.Weapon = new MeleeWeapon(id); + } + catch + { + // invalid + } + } + } +} \ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs index dda72564..9572c494 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/TrainerMod/TrainerMod.cs @@ -9,6 +9,7 @@ using StardewValley.Menus; using StardewValley.Objects; using StardewValley.Tools; using TrainerMod.Framework; +using TrainerMod.ItemData; using Object = StardewValley.Object; namespace TrainerMod @@ -99,9 +100,7 @@ namespace TrainerMod Command.RegisterCommand("player_addmelee", "Gives the player a melee item | player_addmelee ", new[] { "?" }).CommandFired += this.HandlePlayerAddMelee; Command.RegisterCommand("player_addring", "Gives the player a ring | player_addring ", new[] { "?" }).CommandFired += this.HandlePlayerAddRing; - Command.RegisterCommand("out_items", "Outputs a list of items | out_items", new[] { "" }).CommandFired += this.HandleOutItems; - Command.RegisterCommand("out_melee", "Outputs a list of melee weapons | out_melee", new[] { "" }).CommandFired += this.HandleOutMelee; - Command.RegisterCommand("out_rings", "Outputs a list of rings | out_rings", new[] { "" }).CommandFired += this.HandleOutRings; + Command.RegisterCommand("list_items", "Lists items in the game data | list_items [search]", new[] { "(String)" }).CommandFired += this.HandleListItems; Command.RegisterCommand("world_settime", "Sets the time to the specified value | world_settime ", new[] { "(Int32) The target time [06:00 AM is 600]" }).CommandFired += this.HandleWorldSetTime; Command.RegisterCommand("world_freezetime", "Freezes or thaws time | world_freezetime ", new[] { "(0 - 1) Whether or not to freeze time. 0 is thawed, 1 is frozen" }).CommandFired += this.HandleWorldFreezeTime; @@ -657,49 +656,19 @@ namespace TrainerMod this.LogObjectValueNotSpecified(); } - /// The event raised when the 'out_items' command is triggered. + /// The event raised when the 'list_items' command is triggered. /// The event sender. /// The event arguments. - private void HandleOutItems(object sender, EventArgsCommand e) + private void HandleListItems(object sender, EventArgsCommand e) { - for (var itemID = 0; itemID < 1000; itemID++) - { - try - { - Item itemName = new Object(itemID, 1); - if (itemName.Name != "Error Item") - this.Monitor.Log($"{itemID} | {itemName.Name}", LogLevel.Info); - } - catch { } - } - } - - /// The event raised when the 'out_melee' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleOutMelee(object sender, EventArgsCommand e) - { - var data = Game1.content.Load>("Data\\weapons"); - this.Monitor.Log("DATA\\WEAPONS: ", LogLevel.Info); - foreach (var pair in data) - this.Monitor.Log($"{pair.Key} | {pair.Value}", LogLevel.Info); - } + var matches = this.GetItems(e.Command.CalledArgs).ToArray(); - /// The event raised when the 'out_rings' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleOutRings(object sender, EventArgsCommand e) - { - for (var ringID = 0; ringID < 100; ringID++) - { - try - { - Item item = new Ring(ringID); - if (item.Name != "Error Item") - this.Monitor.Log($"{ringID} | {item.Name}", LogLevel.Info); - } - catch { } - } + // show matches + string summary = "Searching...\n"; + if (matches.Any()) + this.Monitor.Log(summary + this.GetTableString(matches, new[] { "type", "id", "name" }, val => new[] { val.Type.ToString(), val.ID.ToString(), val.Name }), LogLevel.Info); + else + this.Monitor.Log(summary + "No items found", LogLevel.Info); } /// The event raised when the 'world_downMineLevel' command is triggered. @@ -725,6 +694,88 @@ namespace TrainerMod else this.LogValueNotSpecified(); } + + /**** + ** Helpers + ****/ + /// Get all items which can be searched and added to the player's inventory through the console. + /// The search string to find. + private IEnumerable GetItems(string[] searchWords) + { + // normalise search term + searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); + if (searchWords?.Any() == false) + searchWords = null; + + // find matches + return ( + from item in this.GetItems() + let term = $"{item.ID}|{item.Type}|{item.Name}" + where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1) + select item + ); + } + + /// Get all items which can be searched and added to the player's inventory through the console. + private IEnumerable GetItems() + { + // objects + foreach (int id in Game1.objectInformation.Keys) + { + ISearchItem obj = id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange + ? new SearchableRing(id) + : (ISearchItem)new SearchableObject(id); + if (obj.IsValid) + yield return obj; + } + + // weapons + foreach (int id in Game1.content.Load>("Data\\weapons").Keys) + { + ISearchItem weapon = new SearchableWeapon(id); + if (weapon.IsValid) + yield return weapon; + } + } + + /// Get an ASCII table for a set of tabular data. + /// The data type. + /// The data to display. + /// The table header. + /// Returns a set of fields for a data value. + private string GetTableString(IEnumerable data, string[] header, Func getRow) + { + // get table data + int[] widths = header.Select(p => p.Length).ToArray(); + string[][] rows = data + .Select(item => + { + string[] fields = getRow(item); + if (fields.Length != widths.Length) + throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); + + for (int i = 0; i < fields.Length; i++) + widths[i] = Math.Max(widths[i], fields[i].Length); + + return fields; + }) + .ToArray(); + + // render fields + List lines = new List(rows.Length + 2) + { + header, + header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() + }; + lines.AddRange(rows); + + return string.Join( + Environment.NewLine, + lines.Select(line => string.Join(" | ", + line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) + ) + ); + } /**** ** Logging diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 6d8b5f34..e262e135 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -102,6 +102,11 @@ Properties\GlobalAssemblyInfo.cs + + + + + -- 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') 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 860ccb90f790807c0f551e10dfff3ae03279d965 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 22:17:38 -0500 Subject: fix the installer not removing TrainerMod from appdata if it's already in the game mods folder --- release-notes.md | 1 + src/StardewModdingAPI.Installer/InteractiveInstaller.cs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index e92ccf35..133ae929 100644 --- a/release-notes.md +++ b/release-notes.md @@ -7,6 +7,7 @@ For players: * Installing SMAPI will now automatically clean up old SMAPI files. * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. * Tweaked installer wording to avoid confusion. + * Fixed the installer not removing TrainerMod from the legacy appdata mods directory if it's already present in the game mods directory. For developers: * Added a searchable `list_items` command to replace the `out_items`, `out_melee`, and `out_rings` commands. diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index d7bf7b0e..cfd64458 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -89,7 +89,6 @@ namespace StardewModdingApi.Installer unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") }; - this.PrintDebug($"Detected {(platform == Platform.Windows ? "Windows" : "Linux or Mac")} with game in {installDir}."); /**** @@ -221,7 +220,7 @@ namespace StardewModdingApi.Installer } // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(platform, modsDir); + this.InteractivelyRemoveAppDataMods(platform, modsDir, packagedModsDir); } Console.WriteLine(); @@ -362,8 +361,12 @@ namespace StardewModdingApi.Installer /// Interactively move mods out of the appdata directory. /// The current platform. /// The directory which should contain all mods. - private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir) + /// The installer directory containing packaged mods. + private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) { + // get packaged mods to delete + string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); + // get path string homePath = platform == Platform.Windows ? Environment.GetEnvironmentVariable("APPDATA") @@ -385,6 +388,14 @@ namespace StardewModdingApi.Installer if (!isDir && !(entry is FileInfo)) continue; // should never happen + // delete packaged mods (newer version bundled into SMAPI) + if (isDir && packagedModNames.Contains(entry.Name, StringComparer.InvariantCultureIgnoreCase)) + { + this.PrintDebug($" Deleting {entry.Name} because it's bundled into SMAPI..."); + entry.Delete(); + continue; + } + // check paths string newPath = Path.Combine(properModsDir.FullName, entry.Name); if (isDir ? Directory.Exists(newPath) : File.Exists(newPath)) -- cgit From ae44f17205961116baef018bae0b8fe9196b27f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 22:43:05 -0500 Subject: fix installer not moving mods out of appdata if the game isn't installed on the same Windows partition (#184) --- release-notes.md | 3 +- .../InteractiveInstaller.cs | 32 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 133ae929..e7712011 100644 --- a/release-notes.md +++ b/release-notes.md @@ -7,7 +7,8 @@ For players: * Installing SMAPI will now automatically clean up old SMAPI files. * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. * Tweaked installer wording to avoid confusion. - * Fixed the installer not removing TrainerMod from the legacy appdata mods directory if it's already present in the game mods directory. + * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. + * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. For developers: * Added a searchable `list_items` command to replace the `out_items`, `out_melee`, and `out_rings` commands. diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index cfd64458..ffdef37b 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -406,10 +406,7 @@ namespace StardewModdingApi.Installer // move into mods this.PrintDebug($" Moving {entry.Name} into the game's mod directory..."); - if (isDir) - (entry as DirectoryInfo).MoveTo(newPath); - else - (entry as FileInfo).MoveTo(newPath); + this.Move(entry, newPath); } // delete if empty @@ -421,5 +418,32 @@ namespace StardewModdingApi.Installer modDir.Delete(); } } + + /// Move a filesystem entry to a new parent directory. + /// The filesystem entry to move. + /// The destination path. + /// We can't use or , because those don't work across partitions. + private void Move(FileSystemInfo entry, string newPath) + { + // file + if (entry is FileInfo) + { + FileInfo file = (FileInfo)entry; + file.CopyTo(newPath); + file.Delete(); + } + + // directory + else + { + Directory.CreateDirectory(newPath); + + DirectoryInfo directory = (DirectoryInfo)entry; + foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos()) + this.Move(child, Path.Combine(newPath, child.Name)); + + directory.Delete(); + } + } } } -- 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) --- release-notes.md | 1 + src/StardewModdingAPI/unix-launcher.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index e7712011..4581779c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ For players: * Tweaked installer wording to avoid confusion. * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. + * Fixed the SMAPI terminal not opening by default on Mac. Linux users are out of luck. For developers: * Added a searchable `list_items` command to replace the `out_items`, `out_melee`, and `out_rings` commands. 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 47d5aef404e0a5c27d49b37faff9bd32ced6f3ff Mon Sep 17 00:00:00 2001 From: Patrick Müssig Date: Tue, 6 Dec 2016 23:02:37 +0100 Subject: SMAPI installer is able to read SDV install path from registry key --- .../InteractiveInstaller.cs | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index ffdef37b..49014352 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Reflection; +using Microsoft.Win32; using StardewModdingApi.Installer.Enums; namespace StardewModdingApi.Installer @@ -318,6 +319,41 @@ namespace StardewModdingApi.Installer return new DirectoryInfo(defaultPath); } + if (platform == Platform.Windows) + { + // Needed to get 64Keys + RegistryKey localKey = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; + + string stardewValleyPath; + var registry = localKey.OpenSubKey(@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"); + if (registry != null) + { + stardewValleyPath = (string)registry.GetValue("PATH"); + if (!string.IsNullOrEmpty(stardewValleyPath)) + { + registry.Close(); + if (Directory.Exists(stardewValleyPath)) + { + return new DirectoryInfo(stardewValleyPath); + } + } + } + + registry = localKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"); + if (registry != null) + { + stardewValleyPath = (string)registry.GetValue("InstallLocation"); + if (!string.IsNullOrEmpty(stardewValleyPath)) + { + registry.Close(); + if (Directory.Exists(stardewValleyPath)) + { + return new DirectoryInfo(stardewValleyPath); + } + } + } + } + // ask user Console.WriteLine("Oops, couldn't find the game automatically."); while (true) -- 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 --- .../StardewModdingAPI.AssemblyRewriters.csproj | 10 +--- .../StardewModdingAPI.Installer.csproj | 1 + src/StardewModdingAPI.sln | 2 +- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- src/crossplatform.targets | 62 ++++++++++++++++++++++ src/dependencies.targets | 62 ---------------------- 6 files changed, 66 insertions(+), 73 deletions(-) create mode 100644 src/crossplatform.targets delete mode 100644 src/dependencies.targets (limited to 'src') diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index 51a49da0..1e6caacc 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -47,7 +47,6 @@ prompt MinimumRecommendedRules.ruleset - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll @@ -82,13 +81,6 @@ - + - \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index 0a33cd57..9baa0d14 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -50,6 +50,7 @@ Always + diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 37c08950..6e13b16b 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -11,7 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86 ProjectSection(SolutionItems) = preProject ..\.gitattributes = ..\.gitattributes ..\.gitignore = ..\.gitignore - dependencies.targets = dependencies.targets + crossplatform.targets = crossplatform.targets GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs ..\LICENSE = ..\LICENSE ..\README.md = ..\README.md 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 + diff --git a/src/crossplatform.targets b/src/crossplatform.targets new file mode 100644 index 00000000..d5428967 --- /dev/null +++ b/src/crossplatform.targets @@ -0,0 +1,62 @@ + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + + + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + + + False + + + False + + + False + + + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + $(DefineConstants);SMAPI_FOR_UNIX + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + \ No newline at end of file diff --git a/src/dependencies.targets b/src/dependencies.targets deleted file mode 100644 index d5428967..00000000 --- a/src/dependencies.targets +++ /dev/null @@ -1,62 +0,0 @@ - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - - - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - - - False - - - False - - - False - - - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - $(DefineConstants);SMAPI_FOR_UNIX - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - - \ No newline at end of file -- cgit From 7e76d90c5567fbc1d4ebeb8e3fc25095d4af4ce9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 23:52:56 -0500 Subject: refactor registry checks for crossplatform compatibility (#189) --- .../InteractiveInstaller.cs | 92 +++++++++++----------- 1 file changed, 47 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 49014352..9c8f8af9 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +#if SMAPI_FOR_WINDOWS using Microsoft.Win32; +#endif using StardewModdingApi.Installer.Enums; namespace StardewModdingApi.Installer @@ -15,18 +18,37 @@ namespace StardewModdingApi.Installer *********/ /// The default file paths where Stardew Valley can be installed. /// Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. - private readonly string[] DefaultInstallPaths = { - // Linux - $"{Environment.GetEnvironmentVariable("HOME")}/GOG Games/Stardew Valley/game", - $"{Environment.GetEnvironmentVariable("HOME")}/.local/share/Steam/steamapps/common/Stardew Valley", + private IEnumerable DefaultInstallPaths + { + get + { + // Linux + yield return $"{Environment.GetEnvironmentVariable("HOME")}/GOG Games/Stardew Valley/game"; + yield return $"{Environment.GetEnvironmentVariable("HOME")}/.local/share/Steam/steamapps/common/Stardew Valley"; - // Mac - $"{Environment.GetEnvironmentVariable("HOME")}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS", + // Mac + yield return $"{Environment.GetEnvironmentVariable("HOME")}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; - // Windows - @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley", - @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley" - }; + // Windows + yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley"; + yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"; + + // Windows registry +#if SMAPI_FOR_WINDOWS + IDictionary registryKeys = new Dictionary + { + [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam + [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows + }; + foreach (var pair in registryKeys) + { + string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); + if (!string.IsNullOrWhiteSpace(path)) + yield return path; + } +#endif + } + } /// The directory or file paths to remove when uninstalling SMAPI, relative to the game directory. private readonly string[] UninstallPaths = @@ -262,6 +284,21 @@ namespace StardewModdingApi.Installer } } +#if SMAPI_FOR_WINDOWS + /// Get the value of a key in the Windows registry. + /// The full path of the registry key relative to HKLM. + /// The name of the value. + private string GetLocalMachineRegistryValue(string key, string name) + { + RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; + RegistryKey openKey = localMachine.OpenSubKey(key); + if (openKey == null) + return null; + using (openKey) + return (string)openKey.GetValue(name); + } +#endif + /// Print a debug message. /// The text to print. private void PrintDebug(string text) @@ -319,41 +356,6 @@ namespace StardewModdingApi.Installer return new DirectoryInfo(defaultPath); } - if (platform == Platform.Windows) - { - // Needed to get 64Keys - RegistryKey localKey = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; - - string stardewValleyPath; - var registry = localKey.OpenSubKey(@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"); - if (registry != null) - { - stardewValleyPath = (string)registry.GetValue("PATH"); - if (!string.IsNullOrEmpty(stardewValleyPath)) - { - registry.Close(); - if (Directory.Exists(stardewValleyPath)) - { - return new DirectoryInfo(stardewValleyPath); - } - } - } - - registry = localKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"); - if (registry != null) - { - stardewValleyPath = (string)registry.GetValue("InstallLocation"); - if (!string.IsNullOrEmpty(stardewValleyPath)) - { - registry.Close(); - if (Directory.Exists(stardewValleyPath)) - { - return new DirectoryInfo(stardewValleyPath); - } - } - } - } - // ask user Console.WriteLine("Oops, couldn't find the game automatically."); while (true) -- cgit From 22a2e2e8539b3d78bbe396d4d1755b9a8fba9abc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Dec 2016 23:55:32 -0500 Subject: add registry paths to build configuration (#189) --- src/crossplatform.targets | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/crossplatform.targets b/src/crossplatform.targets index d5428967..0eb1c776 100644 --- a/src/crossplatform.targets +++ b/src/crossplatform.targets @@ -8,6 +8,8 @@ C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150@InstallLocation) + $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\1453375253@PATH) -- 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 --- release-notes.md | 1 + src/StardewModdingAPI/Inheritance/SGame.cs | 42 ++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 6b4f03cc..871ffabc 100644 --- a/release-notes.md +++ b/release-notes.md @@ -8,6 +8,7 @@ For players: * The installer will now automatically clean up old SMAPI files. * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. The installer will automatically remove the old `.cache` folder. * Improved installer wording to avoid confusion. + * SMAPI will now intercept mod errors in menu draw code, and exit the menu to prevent your game from crashing. * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. * Fixed the SMAPI terminal not opening by default on Mac. Linux users are out of luck. 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 + 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') 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') 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 1fbe6be43df972142f9be3eff111470d4888c3cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Dec 2016 18:43:15 -0500 Subject: remove redundant output paths --- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 6e87fbdb..5f380cc6 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -53,7 +53,6 @@ x86 - bin\Debug\ false DEBUG;TRACE true @@ -65,7 +64,6 @@ x86 - bin\Release\ false $(SolutionDir)\..\bin\Release\SMAPI $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml -- cgit From 7511db4c833f6d25422d4340ff228ab76eba7477 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 10 Dec 2016 00:11:50 -0500 Subject: open terminal on most Linux distros (#183) --- src/StardewModdingAPI/unix-launcher.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh index ce732588..6def449d 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/StardewModdingAPI/unix-launcher.sh @@ -30,13 +30,30 @@ if [ "$UNAME" == "Darwin" ]; then cp StardewValley.bin.osx StardewModdingAPI.bin.osx open -a Terminal ./StardewModdingAPI.bin.osx $@ else + # get launcher + COMMAND="" if [ "$ARCH" == "x86_64" ]; then ln -sf mcs.bin.x86_64 mcs cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - ./StardewModdingAPI.bin.x86_64 $@ + COMMAND="./StardewModdingAPI.bin.x86_64 $@" else ln -sf mcs.bin.x86 mcs cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - ./StardewModdingAPI.bin.x86 $@ + COMMAND="./StardewModdingAPI.bin.x86 $@" + fi + + # open terminal + if command -v x-terminal-emulator 2>/dev/null; then + x-terminal-emulator -e "$COMMAND" + elif command -v gnome-terminal 2>/dev/null; then + gnome-terminal -e "$COMMAND" + elif command -v xterm 2>/dev/null; then + xterm -e "$COMMAND" + elif command -v konsole 2>/dev/null; then + konsole -e "$COMMAND" + elif command -v terminal 2>/dev/null; then + terminal -e "$COMMAND" + else + $COMMAND fi fi -- cgit From dccd73e4fb86a6af38072ae9a35b404502f4602c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 10 Dec 2016 13:01:58 -0500 Subject: detect which Linux command detects whether a command exists (#183) --- release-notes.md | 2 +- src/StardewModdingAPI/unix-launcher.sh | 40 ++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 188dd52b..e550a835 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,7 +11,7 @@ For players: * SMAPI will now intercept mod errors in menu draw code, and exit the menu to prevent your game from crashing. * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. - * Fixed the SMAPI terminal not opening by default on Mac. Linux users are out of luck. + * Fixed the SMAPI terminal not opening by default on Linux and Mac. For developers: * Added a reflection API (accessible as `helper.Reflection`) that simplifies robust access to the game's private fields and methods. diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh index 6def449d..93e33c78 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/StardewModdingAPI/unix-launcher.sh @@ -30,30 +30,38 @@ if [ "$UNAME" == "Darwin" ]; then cp StardewValley.bin.osx StardewModdingAPI.bin.osx open -a Terminal ./StardewModdingAPI.bin.osx $@ else - # get launcher - COMMAND="" + # choose launcher + LAUNCHER="" if [ "$ARCH" == "x86_64" ]; then ln -sf mcs.bin.x86_64 mcs cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - COMMAND="./StardewModdingAPI.bin.x86_64 $@" + LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" else ln -sf mcs.bin.x86 mcs cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - COMMAND="./StardewModdingAPI.bin.x86 $@" + LAUNCHER="./StardewModdingAPI.bin.x86 $@" + fi + + # get cross-distro version of POSIX command + COMMAND="" + if command -v command 2>/dev/null; then + COMMAND="command -v" + elif type type 2>/dev/null; then + COMMAND="type" fi - # open terminal - if command -v x-terminal-emulator 2>/dev/null; then - x-terminal-emulator -e "$COMMAND" - elif command -v gnome-terminal 2>/dev/null; then - gnome-terminal -e "$COMMAND" - elif command -v xterm 2>/dev/null; then - xterm -e "$COMMAND" - elif command -v konsole 2>/dev/null; then - konsole -e "$COMMAND" - elif command -v terminal 2>/dev/null; then - terminal -e "$COMMAND" + # open SMAPI in terminal + if $COMMAND x-terminal-emulator 2>/dev/null; then + x-terminal-emulator -e "$LAUNCHER" + elif $COMMAND gnome-terminal 2>/dev/null; then + gnome-terminal -e "$LAUNCHER" + elif $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" + elif $COMMAND konsole 2>/dev/null; then + konsole -e "$LAUNCHER" + elif $COMMAND terminal 2>/dev/null; then + terminal -e "$LAUNCHER" else - $COMMAND + $LAUNCHER fi fi -- 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') 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 From a3376e2a6257c01c52a3c64c4f5f1f8de9a9c906 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 12 Dec 2016 11:20:31 -0500 Subject: update for 1.4 release --- release-notes.md | 6 +++--- src/GlobalAssemblyInfo.cs | 4 ++-- src/StardewModdingAPI/Constants.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 087fd9b8..eb2d619e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,12 +1,12 @@ # Release notes -## 1.4 (upcoming) -See [log](https://github.com/CLxS/SMAPI/compare/stable...develop). +## 1.4 +See [log](https://github.com/CLxS/SMAPI/compare/1.3...1.4). For players: + * SMAPI will now prevent mods from crashing your game with menu errors. * The installer will now automatically detect most custom install paths. * The installer will now automatically clean up old SMAPI files. - * SMAPI will now prevent mods from crashing your game due to broken custom menus. * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. * Improved installer wording to reduce confusion. * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index 239c5eba..0dfb42bb 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.3.0.0")] -[assembly: AssemblyFileVersion("1.3.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.4.0.0")] +[assembly: AssemblyFileVersion("1.4.0.0")] \ No newline at end of file diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 3feb0830..f5b9e70a 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI ** Accessors *********/ /// SMAPI's current semantic version. - public static readonly Version Version = new Version(1, 3, 0, null); + public static readonly Version Version = new Version(1, 4, 0, null); /// The minimum supported version of Stardew Valley. public const string MinimumGameVersion = "1.1"; -- cgit