summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md3
-rw-r--r--docs/technical-docs.md1
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs27
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json2
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json2
-rw-r--r--src/SMAPI/Constants.cs11
-rw-r--r--src/SMAPI/Program.cs68
-rw-r--r--src/SMAPI/StardewModdingAPI.metadata.json12
-rw-r--r--src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs46
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}");
+ }
+ }
+}