summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md3
-rw-r--r--src/SMAPI.Installer/Framework/InstallerPaths.cs61
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs385
-rw-r--r--src/SMAPI.Installer/StardewModdingAPI.Installer.csproj1
4 files changed, 285 insertions, 165 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 9556d58c..342ff217 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -2,6 +2,9 @@
## 2.7
* For players:
* Improved how mod issues are listed in the console and log.
+ * Revamped installer. It now...
+ * uses a new format that should be more intuitive for players;
+ * and validates requirements earlier.
* Fixed custom festival maps always using spring tilesheets.
* Fixed `player_add` command not recognising return scepter.
* Fixed `player_add` command showing fish twice.
diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs
new file mode 100644
index 00000000..d212876a
--- /dev/null
+++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs
@@ -0,0 +1,61 @@
+using System.IO;
+
+namespace StardewModdingAPI.Installer.Framework
+{
+ /// <summary>Manages paths for the SMAPI installer.</summary>
+ internal class InstallerPaths
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The directory containing the installer files for the current platform.</summary>
+ public DirectoryInfo PackageDir { get; }
+
+ /// <summary>The directory containing the installed game.</summary>
+ public DirectoryInfo GameDir { get; }
+
+ /// <summary>The directory into which to install mods.</summary>
+ public DirectoryInfo ModsDir { get; }
+
+ /// <summary>The full path to the directory containing the installer files for the current platform.</summary>
+ public string PackagePath => this.PackageDir.FullName;
+
+ /// <summary>The full path to the directory containing the installed game.</summary>
+ public string GamePath => this.GameDir.FullName;
+
+ /// <summary>The full path to the directory into which to install mods.</summary>
+ public string ModsPath => this.ModsDir.FullName;
+
+ /// <summary>The full path to the installed SMAPI executable file.</summary>
+ public string ExecutablePath { get; }
+
+ /// <summary>The full path to the vanilla game launcher on Linux/Mac.</summary>
+ public string UnixLauncherPath { get; }
+
+ /// <summary>The full path to the installed SMAPI launcher on Linux/Mac before it's renamed.</summary>
+ public string UnixSmapiLauncherPath { get; }
+
+ /// <summary>The full path to the vanilla game launcher on Linux/Mac after SMAPI is installed.</summary>
+ public string UnixBackupLauncherPath { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="packageDir">The directory path containing the installer files for the current platform.</param>
+ /// <param name="gameDir">The directory path for the installed game.</param>
+ /// <param name="gameExecutableName">The name of the game's executable file for the current platform.</param>
+ public InstallerPaths(DirectoryInfo packageDir, DirectoryInfo gameDir, string gameExecutableName)
+ {
+ this.PackageDir = packageDir;
+ this.GameDir = gameDir;
+ this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods"));
+
+ this.ExecutablePath = Path.Combine(gameDir.FullName, gameExecutableName);
+ this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley");
+ this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI");
+ this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original");
+ }
+ }
+}
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index f39486e1..b686e5bc 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -7,6 +7,7 @@ using System.Reflection;
using System.Threading;
using Microsoft.Win32;
using StardewModdingApi.Installer.Enums;
+using StardewModdingAPI.Installer.Framework;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.ConsoleWriting;
@@ -168,6 +169,9 @@ namespace StardewModdingApi.Installer
/// </remarks>
public void Run(string[] args)
{
+ /*********
+ ** Step 1: initial setup
+ *********/
/****
** Get platform & set window title
****/
@@ -175,6 +179,9 @@ namespace StardewModdingApi.Installer
Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}";
Console.WriteLine();
+ /****
+ ** Check if correct installer
+ ****/
#if SMAPI_FOR_WINDOWS
if (platform == Platform.Linux || platform == Platform.Mac)
{
@@ -182,9 +189,40 @@ namespace StardewModdingApi.Installer
Console.ReadLine();
return;
}
+#else
+ if (platform == Platform.Windows)
+ {
+ this.PrintError($"This is the installer for Linux/Mac. Run the 'install on Windows.exe' file instead.");
+ Console.ReadLine();
+ return;
+ }
#endif
/****
+ ** Check Windows dependencies
+ ****/
+ if (platform == Platform.Windows)
+ {
+ // .NET Framework 4.5+
+ if (!this.HasNetFramework45(platform))
+ {
+ this.PrintError(Environment.OSVersion.Version >= this.Windows7Version
+ ? "Please install the latest version of .NET Framework before installing SMAPI." // Windows 7+
+ : "Please install .NET Framework 4.5 before installing SMAPI." // Windows Vista or earlier
+ );
+ this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details.");
+ Console.ReadLine();
+ return;
+ }
+ if (!this.HasXna(platform))
+ {
+ this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup.");
+ Console.ReadLine();
+ return;
+ }
+ }
+
+ /****
** read command-line arguments
****/
// get action from CLI
@@ -205,230 +243,242 @@ namespace StardewModdingApi.Installer
gamePathArg = args[pathIndex];
}
- /****
- ** collect details
- ****/
- // get game path
- DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg);
- if (installDir == null)
- {
- this.PrintError("Failed finding your game path.");
- Console.ReadLine();
- return;
- }
- // get folders
- DirectoryInfo packageDir = platform.IsMono()
- ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono
- : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows"));
- DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods"));
- var paths = new
+ /*********
+ ** Step 2: find game folder
+ *********/
+ InstallerPaths paths;
{
- executable = Path.Combine(installDir.FullName, EnvironmentUtility.GetExecutableName(platform)),
- unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"),
- unixLauncher = Path.Combine(installDir.FullName, "StardewValley"),
- unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original")
- };
+ /****
+ ** print header
+ ****/
+ this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just a few questions first.");
+ this.PrintDebug("----------------------------------------------------------------------------");
+ Console.WriteLine();
- // show output
- this.PrintInfo($"Your game folder: {installDir}.");
+ /****
+ ** collect details
+ ****/
+ // get game path
+ this.PrintInfo("Where is your game folder?");
+ DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg);
+ if (installDir == null)
+ {
+ this.PrintError("Failed finding your game path.");
+ Console.ReadLine();
+ return;
+ }
- /****
- ** validate assumptions
- ****/
- if (!packageDir.Exists)
- {
- this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip")
- ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder."
- : $"The 'internal/{packageDir.Name}' package folder is missing (should be at {packageDir})."
- );
- Console.ReadLine();
- return;
- }
- if (!File.Exists(paths.executable))
- {
- this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
- Console.ReadLine();
- return;
+ // get folders
+ DirectoryInfo packageDir = platform.IsMono()
+ ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono
+ : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows"));
+ paths = new InstallerPaths(packageDir, installDir, EnvironmentUtility.GetExecutableName(platform));
}
+ Console.Clear();
- /****
- ** validate Windows dependencies
- ****/
- if (platform == Platform.Windows)
+
+ /*********
+ ** Step 3: validate assumptions
+ *********/
{
- // .NET Framework 4.5+
- if (!this.HasNetFramework45(platform))
+ if (!paths.PackageDir.Exists)
{
- this.PrintError(Environment.OSVersion.Version >= this.Windows7Version
- ? "Please install the latest version of .NET Framework before installing SMAPI." // Windows 7+
- : "Please install .NET Framework 4.5 before installing SMAPI." // Windows Vista or earlier
+ this.PrintError(platform == Platform.Windows && paths.PackagePath.Contains(Path.GetTempPath()) && paths.PackagePath.Contains(".zip")
+ ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder."
+ : $"The 'internal/{paths.PackageDir.Name}' package folder is missing (should be at {paths.PackagePath})."
);
- this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details.");
Console.ReadLine();
return;
}
- if (!this.HasXna(platform))
+
+ if (!File.Exists(paths.ExecutablePath))
{
- this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup.");
+ this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
Console.ReadLine();
return;
}
}
- Console.WriteLine();
- /****
- ** ask user what to do
- ****/
+ /*********
+ ** Step 4: ask what to do
+ *********/
ScriptAction action;
-
- if (installArg)
- action = ScriptAction.Install;
- else if (uninstallArg)
- action = ScriptAction.Uninstall;
- else
{
- this.PrintInfo("You can....");
- this.PrintInfo("[1] Install SMAPI.");
- this.PrintInfo("[2] Uninstall SMAPI.");
+ /****
+ ** print header
+ ****/
+ this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just one question first.");
+ this.PrintDebug($"Game path: {paths.GamePath}");
+ this.PrintDebug("----------------------------------------------------------------------------");
Console.WriteLine();
- string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2");
- switch (choice)
+ /****
+ ** ask what to do
+ ****/
+ if (installArg)
+ action = ScriptAction.Install;
+ else if (uninstallArg)
+ action = ScriptAction.Uninstall;
+ else
{
- case "1":
- action = ScriptAction.Install;
- break;
- case "2":
- action = ScriptAction.Uninstall;
- break;
- default:
- throw new InvalidOperationException($"Unexpected action key '{choice}'.");
+ this.PrintInfo("What do you want to do?");
+ Console.WriteLine();
+ this.PrintInfo("[1] Install SMAPI.");
+ this.PrintInfo("[2] Uninstall SMAPI.");
+ Console.WriteLine();
+
+ string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", new[] { "1", "2" });
+ switch (choice)
+ {
+ case "1":
+ action = ScriptAction.Install;
+ break;
+ case "2":
+ action = ScriptAction.Uninstall;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected action key '{choice}'.");
+ }
}
- Console.WriteLine();
}
+ Console.Clear();
- /****
- ** Always uninstall old files
- ****/
- // restore game launcher
- if (platform.IsMono() && File.Exists(paths.unixLauncherBackup))
- {
- this.PrintDebug("Removing SMAPI launcher...");
- this.InteractivelyDelete(paths.unixLauncher);
- File.Move(paths.unixLauncherBackup, paths.unixLauncher);
- }
- // remove old files
- string[] removePaths = this.GetUninstallPaths(installDir, modsDir)
- .Where(path => Directory.Exists(path) || File.Exists(path))
- .ToArray();
- if (removePaths.Any())
+ /*********
+ ** Step 5: apply
+ *********/
{
- this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files...");
- foreach (string path in removePaths)
- this.InteractivelyDelete(path);
- }
+ /****
+ ** print header
+ ****/
+ this.PrintInfo($"That's all I need! I'll {action.ToString().ToLower()} SMAPI now.");
+ this.PrintDebug($"Game path: {paths.GamePath}");
+ this.PrintDebug("----------------------------------------------------------------------------");
+ Console.WriteLine();
- /****
- ** Install new files
- ****/
- if (action == ScriptAction.Install)
- {
- // copy SMAPI files to game dir
- this.PrintDebug("Adding SMAPI files...");
- foreach (FileInfo sourceFile in packageDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ /****
+ ** Always uninstall old files
+ ****/
+ // restore game launcher
+ if (platform.IsMono() && File.Exists(paths.UnixBackupLauncherPath))
{
- if (sourceFile.Name == this.InstallerFileName)
- continue;
-
- string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
- this.InteractivelyDelete(targetPath);
- sourceFile.CopyTo(targetPath);
+ this.PrintDebug("Removing SMAPI launcher...");
+ this.InteractivelyDelete(paths.UnixLauncherPath);
+ File.Move(paths.UnixBackupLauncherPath, paths.UnixLauncherPath);
}
- // replace mod launcher (if possible)
- if (platform.IsMono())
+ // remove old files
+ string[] removePaths = this.GetUninstallPaths(paths.GameDir, paths.ModsDir)
+ .Where(path => Directory.Exists(path) || File.Exists(path))
+ .ToArray();
+ if (removePaths.Any())
{
- this.PrintDebug("Safely replacing game launcher...");
- if (File.Exists(paths.unixLauncher))
- {
- if (!File.Exists(paths.unixLauncherBackup))
- File.Move(paths.unixLauncher, paths.unixLauncherBackup);
- else
- this.InteractivelyDelete(paths.unixLauncher);
- }
-
- File.Move(paths.unixSmapiLauncher, paths.unixLauncher);
+ this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files...");
+ foreach (string path in removePaths)
+ this.InteractivelyDelete(path);
}
- // create mods directory (if needed)
- if (!modsDir.Exists)
+ /****
+ ** Install new files
+ ****/
+ if (action == ScriptAction.Install)
{
- this.PrintDebug("Creating mods directory...");
- modsDir.Create();
- }
+ // copy SMAPI files to game dir
+ this.PrintDebug("Adding SMAPI files...");
+ foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ {
+ if (sourceFile.Name == this.InstallerFileName)
+ continue;
- // add or replace bundled mods
- modsDir.Create();
- DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods"));
- if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
- {
- this.PrintDebug("Adding bundled mods...");
+ string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name);
+ this.InteractivelyDelete(targetPath);
+ sourceFile.CopyTo(targetPath);
+ }
- // special case: rename Omegasis' SaveBackup mod
+ // replace mod launcher (if possible)
+ if (platform.IsMono())
{
- DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup"));
- DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "AdvancedSaveBackup"));
- FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json"));
- if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1))
+ this.PrintDebug("Safely replacing game launcher...");
+ if (File.Exists(paths.UnixLauncherPath))
{
- this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}...");
- this.Move(oldFolder, newFolder.FullName);
+ if (!File.Exists(paths.UnixBackupLauncherPath))
+ File.Move(paths.UnixLauncherPath, paths.UnixBackupLauncherPath);
+ else
+ this.InteractivelyDelete(paths.UnixLauncherPath);
}
+
+ File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath);
}
- // add bundled mods
- foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
+ // create mods directory (if needed)
+ if (!paths.ModsDir.Exists)
{
- this.PrintDebug($" adding {sourceDir.Name}...");
+ this.PrintDebug("Creating mods directory...");
+ paths.ModsDir.Create();
+ }
- // init/clear target dir
- DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name));
- if (targetDir.Exists)
+ // add or replace bundled mods
+ DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(paths.PackageDir.FullName, "Mods"));
+ if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
+ {
+ this.PrintDebug("Adding bundled mods...");
+
+ // special case: rename Omegasis' SaveBackup mod
{
- this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles);
- foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos())
+ DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "SaveBackup"));
+ DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "AdvancedSaveBackup"));
+ FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json"));
+ if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1))
{
- if (protectedFiles == null || !protectedFiles.Contains(entry.Name))
- this.InteractivelyDelete(entry.FullName);
+ this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}...");
+ this.Move(oldFolder, newFolder.FullName);
}
}
- else
- targetDir.Create();
- // copy files
- foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
- sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
+ // add bundled mods
+ foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
+ {
+ this.PrintDebug($" adding {sourceDir.Name}...");
+
+ // init/clear target dir
+ DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, sourceDir.Name));
+ if (targetDir.Exists)
+ {
+ this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles);
+ foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos())
+ {
+ if (protectedFiles == null || !protectedFiles.Contains(entry.Name))
+ this.InteractivelyDelete(entry.FullName);
+ }
+ }
+ else
+ targetDir.Create();
+
+ // copy files
+ foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
+ }
}
- }
- // remove obsolete appdata mods
- this.InteractivelyRemoveAppDataMods(modsDir, packagedModsDir);
+ // remove obsolete appdata mods
+ this.InteractivelyRemoveAppDataMods(paths.ModsDir, packagedModsDir);
+ }
}
Console.WriteLine();
Console.WriteLine();
- /****
- ** final instructions
- ****/
+
+ /*********
+ ** Step 6: final instructions
+ *********/
if (platform == Platform.Windows)
{
if (action == ScriptAction.Install)
{
this.PrintSuccess("SMAPI is installed! If you use Steam, set your launch options to enable achievements (see smapi.io/install):");
- this.PrintSuccess($" \"{Path.Combine(installDir.FullName, "StardewModdingAPI.exe")}\" %command%");
+ this.PrintSuccess($" \"{Path.Combine(paths.GamePath, "StardewModdingAPI.exe")}\" %command%");
Console.WriteLine();
this.PrintSuccess("If you don't use Steam, launch StardewModdingAPI.exe in your game folder to play with mods.");
}
@@ -594,17 +644,22 @@ namespace StardewModdingApi.Installer
}
/// <summary>Interactively ask the user to choose a value.</summary>
+ /// <param name="print">A callback which prints a message to the console.</param>
/// <param name="message">The message to print.</param>
/// <param name="options">The allowed options (not case sensitive).</param>
- private string InteractivelyChoose(string message, params string[] options)
+ /// <param name="indent">The indentation to prefix to output.</param>
+ private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
{
+ print = print ?? this.PrintInfo;
+
while (true)
{
- this.PrintInfo(message);
+ print(indent + message);
+ Console.Write(indent);
string input = Console.ReadLine()?.Trim().ToLowerInvariant();
if (!options.Contains(input))
{
- this.PrintInfo("That's not a valid option.");
+ print($"{indent}That's not a valid option.");
continue;
}
return input;
diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
index 2ad7e82a..e82c6093 100644
--- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
+++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
@@ -40,6 +40,7 @@
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Enums\ScriptAction.cs" />
+ <Compile Include="Framework\InstallerPaths.cs" />
<Compile Include="InteractiveInstaller.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />