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 --- src/StardewModdingAPI.Installer/InteractiveInstaller.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') 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 --- 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 ++ 7 files changed, 278 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/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 --- src/StardewModdingAPI/Framework/InternalExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src') 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 --- src/StardewModdingAPI.Installer/InteractiveInstaller.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'src') 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