diff options
-rw-r--r-- | docs/release-notes.md | 3 | ||||
-rw-r--r-- | docs/technical-docs.md | 1 | ||||
-rw-r--r-- | src/SMAPI.Installer/InteractiveInstaller.cs | 27 | ||||
-rw-r--r-- | src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 | ||||
-rw-r--r-- | src/SMAPI.Mods.SaveBackup/manifest.json | 2 | ||||
-rw-r--r-- | src/SMAPI/Constants.cs | 11 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 68 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.metadata.json | 12 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs | 46 |
9 files changed, 144 insertions, 28 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index ff092fe1..9cda4e76 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,8 @@ * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed detection of GOG Galaxy install path in rare cases. * Fixed install error on Linux/Mac in some cases. + * Fixed installer not finding game path in some cases. + * Fixed installer showing duplicate game paths in some cases. * Fixed `smapi.io/install` not linking to a useful page. * Fixed `world_setseason` command not running season-change logic. * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. @@ -30,6 +32,7 @@ * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. + * Added support for launching multiple instances transparently. This removes the former `--log-path` command-line argument. * Added Harmony DLL managed by SMAPI. * Fixed error when loading an unpacked `.tbin` map that references custom seasonal tilesheets. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). diff --git a/docs/technical-docs.md b/docs/technical-docs.md index a988eefc..f4358e31 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -137,7 +137,6 @@ change without warning. argument | purpose -------- | ------- -`--log-path "path"` | The relative or absolute path of the log file SMAPI should write. `--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) ### Compile flags diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index d3db4d72..f9239604 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -66,6 +66,11 @@ namespace StardewModdingApi.Installer if (!string.IsNullOrWhiteSpace(path)) yield return path; } + + // via Steam library path + string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); + if (steampath != null) + yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); } break; @@ -138,11 +143,11 @@ namespace StardewModdingApi.Installer /// Initialisation flow: /// 1. Collect information (mainly OS and install path) and validate it. /// 2. Ask the user whether to install or uninstall. - /// + /// /// Uninstall logic: /// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup. /// 2. Delete all files and folders in the game directory matching one of the values returned by <see cref="GetUninstallPaths"/>. - /// + /// /// Install flow: /// 1. Run the uninstall flow. /// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory. @@ -436,7 +441,7 @@ namespace StardewModdingApi.Installer return str; } - /// <summary>Get the value of a key in the Windows registry.</summary> + /// <summary>Get the value of a key in the Windows HKLM registry.</summary> /// <param name="key">The full path of the registry key relative to HKLM.</param> /// <param name="name">The name of the value.</param> private string GetLocalMachineRegistryValue(string key, string name) @@ -449,6 +454,19 @@ namespace StardewModdingApi.Installer return (string)openKey.GetValue(name); } + /// <summary>Get the value of a key in the Windows HKCU registry.</summary> + /// <param name="key">The full path of the registry key relative to HKCU.</param> + /// <param name="name">The name of the value.</param> + private string GetCurrentUserRegistryValue(string key, string name) + { + RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; + RegistryKey openKey = currentuser.OpenSubKey(key); + if (openKey == null) + return null; + using (openKey) + return (string)openKey.GetValue(name); + } + /// <summary>Print a debug message.</summary> /// <param name="text">The text to print.</param> private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug); @@ -523,6 +541,7 @@ namespace StardewModdingApi.Installer /// <summary>Delete a file or folder regardless of file permissions, and block until deletion completes.</summary> /// <param name="entry">The file or folder to reset.</param> + /// <remarks>This method is mirred from <c>FileUtilities.ForceDelete</c> in the toolkit.</remarks> private void ForceDelete(FileSystemInfo entry) { // ignore if already deleted @@ -606,6 +625,8 @@ namespace StardewModdingApi.Installer where dir.Exists && dir.EnumerateFiles(executableFilename).Any() select dir ) + .GroupBy(p => p.FullName, StringComparer.InvariantCultureIgnoreCase) // ignore duplicate paths + .Select(p => p.First()) .ToArray(); // choose where to install diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index d75b42d5..c4bafbce 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.15", + "Version": "2.6.0-beta.16", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index d076d84f..4d7589e6 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.15", + "Version": "2.6.0-beta.16", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index d96c5839..532112ff 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -82,8 +82,11 @@ namespace StardewModdingAPI /// <summary>The file path for the SMAPI metadata file.</summary> internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); - /// <summary>The file path to the log where the latest output should be saved.</summary> - internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); + /// <summary>The filename prefix for SMAPI log files.</summary> + internal static string LogNamePrefix { get; } = "SMAPI-latest"; + + /// <summary>The filename extension for SMAPI log files.</summary> + internal static string LogNameExtension { get; } = "txt"; /// <summary>A copy of the log leading up to the previous fatal crash, if any.</summary> internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); @@ -113,8 +116,8 @@ namespace StardewModdingAPI /// <summary>Initialise the static values.</summary> static Constants() { - Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.15"); - Constants.MinimumGameVersion = new GameVersion("1.3.13"); + Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.16"); + Constants.MinimumGameVersion = new GameVersion("1.3.17"); Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 83ce6f99..4ede45d5 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -118,30 +118,19 @@ namespace StardewModdingAPI // get flags from arguments bool writeToConsole = !args.Contains("--no-terminal"); - // get log path from arguments - string logPath = null; - { - int pathIndex = Array.LastIndexOf(args, "--log-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - { - logPath = args[pathIndex]; - if (!Path.IsPathRooted(logPath)) - logPath = Path.Combine(Constants.LogDir, logPath); - } - } - if (string.IsNullOrWhiteSpace(logPath)) - logPath = Constants.DefaultLogPath; - // load SMAPI - using (Program program = new Program(writeToConsole, logPath)) + using (Program program = new Program(writeToConsole)) program.RunInteractively(); } /// <summary>Construct an instance.</summary> /// <param name="writeToConsole">Whether to output log messages to the console.</param> - /// <param name="logPath">The full file path to which to write log messages.</param> - public Program(bool writeToConsole, string logPath) + public Program(bool writeToConsole) { + // init log file + this.PurgeLogFiles(); + string logPath = this.GetLogPath(); + // init basics this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); @@ -1257,5 +1246,50 @@ namespace StardewModdingAPI if (this.Settings.VerboseLogging) this.Monitor.Log(message, LogLevel.Trace); } + + /// <summary>Get the absolute path to the next available log file.</summary> + private string GetLogPath() + { + // default path + { + FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.{Constants.LogNameExtension}")); + if (!defaultFile.Exists) + return defaultFile.FullName; + } + + // get first disambiguated path + for (int i = 2; i < int.MaxValue; i++) + { + FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.player-{i}.{Constants.LogNameExtension}")); + if (!file.Exists) + return file.FullName; + } + + // should never happen + throw new InvalidOperationException("Could not find an available log path."); + } + + /// <summary>Delete all log files created by SMAPI.</summary> + private void PurgeLogFiles() + { + DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); + if (!logsDir.Exists) + return; + + foreach (FileInfo logFile in logsDir.EnumerateFiles("*.txt")) + { + if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + FileUtilities.ForceDelete(logFile); + } + catch (IOException) + { + // ignore file if it's in use + } + } + } + } } } diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 47c45b24..adf5fdd1 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -64,7 +64,7 @@ "~1.1 | Status": "AssumeBroken" }, - "AdjustArtisanPrices": { + "Adjust Artisan Prices": { "ID": "ThatNorthernMonkey.AdjustArtisanPrices", "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update "MapRemoteVersions": { "0.01": "0.0.1" }, @@ -130,6 +130,11 @@ "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "Arcade Pong": { + "ID": "Platonymous.ArcadePong", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + "Ashley Mod": { "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 @@ -1474,6 +1479,11 @@ "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 }, + "Split Screen": { + "ID": "Ilyaki.SplitScreen", + "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + "Sprinkler Range": { "ID": "cat.sprinklerrange", "Default | UpdateKey": "Nexus:1179" diff --git a/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs new file mode 100644 index 00000000..7856fdb1 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs @@ -0,0 +1,46 @@ +using System.IO; +using System.Threading; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// <summary>Provides utilities for dealing with files.</summary> + public static class FileUtilities + { + /********* + ** Public methods + *********/ + /// <summary>Delete a file or folder regardless of file permissions, and block until deletion completes.</summary> + /// <param name="entry">The file or folder to reset.</param> + public static void ForceDelete(FileSystemInfo entry) + { + // ignore if already deleted + entry.Refresh(); + if (!entry.Exists) + return; + + // delete children + if (entry is DirectoryInfo folder) + { + foreach (FileSystemInfo child in folder.GetFileSystemInfos()) + FileUtilities.ForceDelete(child); + } + + // reset permissions & delete + entry.Attributes = FileAttributes.Normal; + entry.Delete(); + + // wait for deletion to finish + for (int i = 0; i < 10; i++) + { + entry.Refresh(); + if (entry.Exists) + Thread.Sleep(500); + } + + // throw exception if deletion didn't happen before timeout + entry.Refresh(); + if (entry.Exists) + throw new IOException($"Timed out trying to delete {entry.FullName}"); + } + } +} |