diff options
Diffstat (limited to 'src/SMAPI.Installer/InteractiveInstaller.cs')
-rw-r--r-- | src/SMAPI.Installer/InteractiveInstaller.cs | 152 |
1 files changed, 103 insertions, 49 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 3d673719..83ecd257 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -10,9 +11,6 @@ using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Utilities; -#if !SMAPI_FOR_WINDOWS -using System.Diagnostics; -#endif namespace StardewModdingApi.Installer { @@ -40,11 +38,11 @@ namespace StardewModdingApi.Installer string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); // current files - yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only - yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only + yield return GetInstallPath("libgdiplus.dylib"); // Linux/macOS only + yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.exe.config"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("smapi-internal"); @@ -106,13 +104,13 @@ namespace StardewModdingApi.Installer /// 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. + /// 1. On Linux/macOS: 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. - /// 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.) + /// 3. On Linux/macOS: 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. @@ -143,7 +141,7 @@ namespace StardewModdingApi.Installer #else if (context.IsWindows) { - this.PrintError($"This is the installer for Linux/Mac. Run the 'install on Windows.exe' file instead."); + this.PrintError($"This is the installer for Linux/macOS. Run the 'install on Windows.exe' file instead."); Console.ReadLine(); return; } @@ -196,7 +194,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 2: choose a theme (can't auto-detect on Linux/Mac) + ** Step 2: choose a theme (can't auto-detect on Linux/macOS) *********/ MonitorColorScheme scheme = MonitorColorScheme.AutoDetect; if (context.IsUnix) @@ -260,7 +258,6 @@ namespace StardewModdingApi.Installer ** collect details ****/ // get game path - this.PrintInfo("Where is your game folder?"); DirectoryInfo installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg); if (installDir == null) { @@ -277,7 +274,20 @@ namespace StardewModdingApi.Installer /********* - ** Step 4: validate assumptions + ** Step 4: detect 64-bit Stardew Valley + *********/ + // detect 64-bit mode + bool isWindows64Bit = false; + if (context.Platform == Platform.Windows) + { + FileInfo linuxExecutable = new FileInfo(Path.Combine(paths.GamePath, "StardewValley.exe")); + isWindows64Bit = linuxExecutable.Exists && this.Is64Bit(linuxExecutable.FullName); + if (isWindows64Bit) + paths.SetExecutableFileName(linuxExecutable.Name); + } + + /********* + ** Step 5: validate assumptions *********/ // executable exists if (!File.Exists(paths.ExecutablePath)) @@ -300,7 +310,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 5: ask what to do + ** Step 6: ask what to do *********/ ScriptAction action; { @@ -308,7 +318,7 @@ namespace StardewModdingApi.Installer ** 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($"Game path: {paths.GamePath}{(context.IsWindows ? $" [{(isWindows64Bit ? "64-bit" : "32-bit")}]" : "")}"); this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); @@ -346,14 +356,14 @@ namespace StardewModdingApi.Installer /********* - ** Step 6: apply + ** Step 7: apply *********/ { /**** ** print header ****/ this.PrintInfo($"That's all I need! I'll {action.ToString().ToLower()} SMAPI now."); - this.PrintDebug($"Game path: {paths.GamePath}"); + this.PrintDebug($"Game path: {paths.GamePath}{(context.IsWindows ? $" [{(isWindows64Bit ? "64-bit" : "32-bit")}]" : "")}"); this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); @@ -414,6 +424,27 @@ namespace StardewModdingApi.Installer this.RecursiveCopy(sourceEntry, paths.GameDir); } + if (isWindows64Bit) + { + this.PrintDebug("Making SMAPI 64-bit..."); + FileInfo x64Executable = new FileInfo(Path.Combine(paths.BundleDir.FullName, "StardewModdingAPI-x64.exe")); + if (x64Executable.Exists) + { + string targetName = "StardewModdingAPI.exe"; + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, targetName)); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, x64Executable.Name)); + + this.RecursiveCopy(x64Executable, paths.GameDir); + File.Move(Path.Combine(paths.GamePath, x64Executable.Name), Path.Combine(paths.GamePath, targetName)); + } + else + { + this.PrintError($"Oops! Could not find the required '{x64Executable.Name}' installer file. SMAPI was unable to install correctly."); + Console.ReadLine(); + return; + } + } + // replace mod launcher (if possible) if (context.IsUnix) { @@ -433,8 +464,6 @@ namespace StardewModdingApi.Installer // mark file executable // (MSBuild doesn't keep permission flags for files zipped in a build task.) - // (Note: exclude from Windows build because antivirus apps can flag the process start code as suspicious.) -#if !SMAPI_FOR_WINDOWS new Process { StartInfo = new ProcessStartInfo @@ -444,7 +473,6 @@ namespace StardewModdingApi.Installer CreateNoWindow = true } }.Start(); -#endif } // create mods directory (if needed) @@ -540,6 +568,13 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ + /// <summary>Get whether an executable is 64-bit.</summary> + /// <param name="executablePath">The absolute path to the executable file.</param> + private bool Is64Bit(string executablePath) + { + return AssemblyName.GetAssemblyName(executablePath).ProcessorArchitecture != ProcessorArchitecture.X86; + } + /// <summary>Get the display text for a color scheme.</summary> /// <param name="scheme">The color scheme.</param> private string GetDisplayText(MonitorColorScheme scheme) @@ -676,56 +711,44 @@ namespace StardewModdingApi.Installer return dir; } - // use game folder which contains the installer, if any - { - DirectoryInfo curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; - while (curPath?.Parent != null) // must be in a folder (not at the root) - { - if (context.LooksLikeGameFolder(curPath)) - return curPath; - - curPath = curPath.Parent; - } - } - - // use an installed path - DirectoryInfo[] defaultPaths = toolkit.GetGameFolders().ToArray(); + // let user choose detected path + DirectoryInfo[] defaultPaths = this.DetectGameFolders(toolkit, context).ToArray(); if (defaultPaths.Any()) { - // only one path - if (defaultPaths.Length == 1) - return defaultPaths.First(); - - // let user choose path + this.PrintInfo("Where do you want to add or remove SMAPI?"); Console.WriteLine(); - this.PrintInfo("Found multiple copies of the game:"); for (int i = 0; i < defaultPaths.Length; i++) this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}"); + this.PrintInfo($"[{defaultPaths.Length + 1}] Enter a custom game path."); Console.WriteLine(); - string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - string choice = this.InteractivelyChoose("Where do you want to add/remove SMAPI? Type the number next to your choice, then press enter.", validOptions); + string[] validOptions = Enumerable.Range(1, defaultPaths.Length + 1).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); + string choice = this.InteractivelyChoose("Type the number next to your choice, then press enter.", validOptions); int index = int.Parse(choice, CultureInfo.InvariantCulture) - 1; - return defaultPaths[index]; + + if (index < defaultPaths.Length) + return defaultPaths[index]; } + else + this.PrintInfo("Oops, couldn't find the game automatically."); - // ask user - this.PrintInfo("Oops, couldn't find the game automatically."); + // let user enter manual path while (true) { // get path from user + Console.WriteLine(); this.PrintInfo($"Type the file path to the game directory (the one containing '{context.ExecutableName}'), then press enter."); string path = Console.ReadLine()?.Trim(); if (string.IsNullOrWhiteSpace(path)) { - this.PrintInfo(" You must specify a directory path to continue."); + this.PrintWarning("You must specify a directory path to continue."); continue; } // normalize path path = context.IsWindows ? path.Replace("\"", "") // in Windows, quotes are used to escape spaces and aren't part of the file path - : path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line + : path.Replace("\\ ", " "); // in Linux/macOS, spaces in paths may be escaped if copied from the command line if (path.StartsWith("~/")) { string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE"); @@ -740,12 +763,12 @@ namespace StardewModdingApi.Installer // validate path if (!directory.Exists) { - this.PrintInfo(" That directory doesn't seem to exist."); + this.PrintWarning("That directory doesn't seem to exist."); continue; } if (!context.LooksLikeGameFolder(directory)) { - this.PrintInfo(" That directory doesn't contain a Stardew Valley executable."); + this.PrintWarning("That directory doesn't contain a Stardew Valley executable."); continue; } @@ -755,6 +778,37 @@ namespace StardewModdingApi.Installer } } + /// <summary>Get the possible game paths to update.</summary> + /// <param name="toolkit">The mod toolkit.</param> + /// <param name="context">The installer context.</param> + private IEnumerable<DirectoryInfo> DetectGameFolders(ModToolkit toolkit, InstallerContext context) + { + HashSet<string> foundPaths = new HashSet<string>(); + + // game folder which contains the installer, if any + { + DirectoryInfo curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + while (curPath?.Parent != null) // must be in a folder (not at the root) + { + if (context.LooksLikeGameFolder(curPath)) + { + foundPaths.Add(curPath.FullName); + yield return curPath; + break; + } + + curPath = curPath.Parent; + } + } + + // game paths detected by toolkit + foreach (DirectoryInfo dir in toolkit.GetGameFolders()) + { + if (foundPaths.Add(dir.FullName)) + yield return dir; + } + } + /// <summary>Interactively move mods out of the appdata directory.</summary> /// <param name="properModsDir">The directory which should contain all mods.</param> /// <param name="packagedModsDir">The installer directory containing packaged mods.</param> @@ -845,7 +899,7 @@ namespace StardewModdingApi.Installer switch (entry.Name) { case "mcs": - return false; // ignore Mac symlink + return false; // ignore macOS symlink case "Mods": return false; // Mods folder handled separately default: |