diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-08-11 23:17:36 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-08-11 23:17:36 -0400 |
commit | 9029633f7f1076415fcb9e76fd3e0e58357ddcec (patch) | |
tree | 83289d1155ef23d099e6eb109c10bc6b4c7c0955 /src/SMAPI.Installer | |
parent | b7907293349e95f84583e682f38e0eb491ac2e5d (diff) | |
download | SMAPI-9029633f7f1076415fcb9e76fd3e0e58357ddcec.tar.gz SMAPI-9029633f7f1076415fcb9e76fd3e0e58357ddcec.tar.bz2 SMAPI-9029633f7f1076415fcb9e76fd3e0e58357ddcec.zip |
overhaul installer display (#554)
The installer now validates preconditions earlier when possible, and after each step will reset the text and condense details from previous steps. This way players only see info for the current question to avoid confusion, and it's easier to add new steps.
Diffstat (limited to 'src/SMAPI.Installer')
-rw-r--r-- | src/SMAPI.Installer/Framework/InstallerPaths.cs | 61 | ||||
-rw-r--r-- | src/SMAPI.Installer/InteractiveInstaller.cs | 385 | ||||
-rw-r--r-- | src/SMAPI.Installer/StardewModdingAPI.Installer.csproj | 1 |
3 files changed, 282 insertions, 165 deletions
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" /> |