diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-11-19 13:48:19 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-11-19 13:48:19 -0500 |
commit | 593723b7940ba72a786fc4c7366c56f9813d977b (patch) | |
tree | 4d23fbef5bc5a20115f10ca04ae3379df78cc8e1 /src/SMAPI.Installer | |
parent | 4f28ea33bd7cc65485402c5e85259083e86b49e1 (diff) | |
parent | 3dc27a5681dcfc4ae30e95570d9966f2e14a4dd7 (diff) | |
download | SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.gz SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.tar.bz2 SMAPI-593723b7940ba72a786fc4c7366c56f9813d977b.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Installer')
-rw-r--r-- | src/SMAPI.Installer/Framework/InstallerPaths.cs | 18 | ||||
-rw-r--r-- | src/SMAPI.Installer/InteractiveInstaller.cs | 270 | ||||
-rw-r--r-- | src/SMAPI.Installer/Program.cs | 69 | ||||
-rw-r--r-- | src/SMAPI.Installer/README.txt (renamed from src/SMAPI.Installer/readme.txt) | 20 | ||||
-rw-r--r-- | src/SMAPI.Installer/StardewModdingAPI.Installer.csproj | 16 | ||||
-rw-r--r-- | src/SMAPI.Installer/unix-install.sh | 4 | ||||
-rw-r--r-- | src/SMAPI.Installer/unix-launcher.sh | 18 | ||||
-rw-r--r-- | src/SMAPI.Installer/windows-exe-config.xml | 5 | ||||
-rw-r--r-- | src/SMAPI.Installer/windows-install.bat | 1 |
9 files changed, 277 insertions, 144 deletions
diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs index d212876a..e5396018 100644 --- a/src/SMAPI.Installer/Framework/InstallerPaths.cs +++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs @@ -8,8 +8,8 @@ namespace StardewModdingAPI.Installer.Framework /********* ** Accessors *********/ - /// <summary>The directory containing the installer files for the current platform.</summary> - public DirectoryInfo PackageDir { get; } + /// <summary>The directory path containing the files to copy into the game folder.</summary> + public DirectoryInfo BundleDir { get; } /// <summary>The directory containing the installed game.</summary> public DirectoryInfo GameDir { get; } @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Installer.Framework /// <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 directory path containing the files to copy into the game folder.</summary> + public string BundlePath => this.BundleDir.FullName; /// <summary>The full path to the directory containing the installed game.</summary> public string GamePath => this.GameDir.FullName; @@ -26,6 +26,9 @@ namespace StardewModdingAPI.Installer.Framework /// <summary>The full path to the directory into which to install mods.</summary> public string ModsPath => this.ModsDir.FullName; + /// <summary>The full path to SMAPI's internal configuration file.</summary> + public string ApiConfigPath { get; } + /// <summary>The full path to the installed SMAPI executable file.</summary> public string ExecutablePath { get; } @@ -43,12 +46,12 @@ namespace StardewModdingAPI.Installer.Framework ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="packageDir">The directory path containing the installer files for the current platform.</param> + /// <param name="bundleDir">The directory path containing the files to copy into the game folder.</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) + public InstallerPaths(DirectoryInfo bundleDir, DirectoryInfo gameDir, string gameExecutableName) { - this.PackageDir = packageDir; + this.BundleDir = bundleDir; this.GameDir = gameDir; this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods")); @@ -56,6 +59,7 @@ namespace StardewModdingAPI.Installer.Framework this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley"); this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI"); this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original"); + this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "StardewModdingAPI.config.json"); } } } diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 0aac1da2..d5866c74 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; using StardewModdingAPI.Installer.Framework; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.ConsoleWriting; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingApi.Installer { @@ -19,18 +22,19 @@ namespace StardewModdingApi.Installer /********* ** Properties *********/ - /// <summary>The name of the installer file in the package.</summary> - private readonly string InstallerFileName = "install.exe"; - - /// <summary>Mod files which shouldn't be deleted when deploying bundled mods (mod folder name => file names).</summary> - private readonly IDictionary<string, HashSet<string>> ProtectBundledFiles = new Dictionary<string, HashSet<string>>(StringComparer.InvariantCultureIgnoreCase) - { - ["SaveBackup"] = new HashSet<string>(new[] { "backups", "config.json" }, StringComparer.InvariantCultureIgnoreCase) - }; + /// <summary>The absolute path to the directory containing the files to copy into the game folder.</summary> + private readonly string BundlePath; /// <summary>The <see cref="Environment.OSVersion"/> value that represents Windows 7.</summary> private readonly Version Windows7Version = new Version(6, 1); + /// <summary>The mod IDs which the installer should allow as bundled mods.</summary> + private readonly string[] BundledModIDs = new[] + { + "SMAPI.SaveBackup", + "SMAPI.ConsoleCommands" + }; + /// <summary>The default file paths where Stardew Valley can be installed.</summary> /// <param name="platform">The target platform.</param> /// <remarks>Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. </remarks> @@ -58,8 +62,12 @@ namespace StardewModdingApi.Installer case Platform.Windows: { // Windows - yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley"; - yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"; + foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) + { + yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; + yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley"; + yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; + } // Windows registry IDictionary<string, string> registryKeys = new Dictionary<string, string> @@ -93,40 +101,42 @@ namespace StardewModdingApi.Installer { string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); - // common - yield return GetInstallPath("0Harmony.dll"); - yield return GetInstallPath("0Harmony.pdb"); - yield return GetInstallPath("Mono.Cecil.dll"); - yield return GetInstallPath("Newtonsoft.Json.dll"); + // current files + yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI.exe"); - yield return GetInstallPath("StardewModdingAPI.config.json"); - yield return GetInstallPath("StardewModdingAPI.metadata.json"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); + yield return GetInstallPath("StardewModdingAPI.exe.config"); + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); - yield return GetInstallPath("System.ValueTuple.dll"); - yield return GetInstallPath("steam_appid.txt"); - - // Linux/Mac only - yield return GetInstallPath("libgdiplus.dylib"); - yield return GetInstallPath("StardewModdingAPI"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); - yield return GetInstallPath("System.Numerics.dll"); - yield return GetInstallPath("System.Runtime.Caching.dll"); - - // Windows only - yield return GetInstallPath("StardewModdingAPI.pdb"); + yield return GetInstallPath("smapi-internal"); // obsolete - yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 + yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) - yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 + yield return GetInstallPath("0Harmony.dll"); // moved in 2.8 + yield return GetInstallPath("0Harmony.pdb"); // moved in 2.8 + yield return GetInstallPath("Mono.Cecil.dll"); // moved in 2.8 + yield return GetInstallPath("Newtonsoft.Json.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.config.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.crash.marker"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.metadata.json"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.update.marker"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI.xml"); // moved in 2.8 + yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 + yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8 + yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8 + yield return GetInstallPath("steam_appid.txt"); // moved in 2.8 + if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) @@ -143,8 +153,10 @@ namespace StardewModdingApi.Installer ** Public methods *********/ /// <summary>Construct an instance.</summary> - public InteractiveInstaller() + /// <param name="bundlePath">The absolute path to the directory containing the files to copy into the game folder.</param> + public InteractiveInstaller(string bundlePath) { + this.BundlePath = bundlePath; this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.AutoDetect); } @@ -319,10 +331,8 @@ namespace StardewModdingApi.Installer } // 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)); + DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath); + paths = new InstallerPaths(bundleDir, installDir, EnvironmentUtility.GetExecutableName(platform)); } Console.Clear(); @@ -330,23 +340,11 @@ namespace StardewModdingApi.Installer /********* ** Step 4: validate assumptions *********/ + if (!File.Exists(paths.ExecutablePath)) { - if (!paths.PackageDir.Exists) - { - 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})." - ); - Console.ReadLine(); - return; - } - - if (!File.Exists(paths.ExecutablePath)) - { - this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); - Console.ReadLine(); - return; - } + this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); + Console.ReadLine(); + return; } @@ -438,20 +436,18 @@ namespace StardewModdingApi.Installer { // copy SMAPI files to game dir this.PrintDebug("Adding SMAPI files..."); - foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo sourceEntry in paths.BundleDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { - if (sourceFile.Name == this.InstallerFileName) - continue; - - string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name); - this.InteractivelyDelete(targetPath); - sourceFile.CopyTo(targetPath); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name)); + this.RecursiveCopy(sourceEntry, paths.GameDir); } // replace mod launcher (if possible) if (platform.IsMono()) { this.PrintDebug("Safely replacing game launcher..."); + + // back up & remove current launcher if (File.Exists(paths.UnixLauncherPath)) { if (!File.Exists(paths.UnixBackupLauncherPath)) @@ -460,7 +456,20 @@ namespace StardewModdingApi.Installer this.InteractivelyDelete(paths.UnixLauncherPath); } + // add new launcher File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath); + + // mark file executable + // (MSBuild doesn't keep permission flags for files zipped in a build task.) + new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "chmod", + Arguments = $"755 \"{paths.UnixLauncherPath}\"", + CreateNoWindow = true + } + }.Start(); } // create mods directory (if needed) @@ -471,60 +480,56 @@ namespace StardewModdingApi.Installer } // add or replace bundled mods - DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(paths.PackageDir.FullName, "Mods")); - if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) + DirectoryInfo bundledModsDir = new DirectoryInfo(Path.Combine(paths.BundlePath, "Mods")); + if (bundledModsDir.Exists && bundledModsDir.EnumerateDirectories().Any()) { this.PrintDebug("Adding bundled mods..."); - // special case: rename Omegasis' SaveBackup mod + ModToolkit toolkit = new ModToolkit(); + ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray(); + foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName)) { - 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)) + // validate source mod + if (sourceMod.Manifest == null) { - this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}..."); - this.Move(oldFolder, newFolder.FullName); + this.PrintWarning($" ignored invalid bundled mod {sourceMod.DisplayName}: {sourceMod.ManifestParseError}"); + continue; } - } - - // 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) + if (!this.BundledModIDs.Contains(sourceMod.Manifest.UniqueID)) { - 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); - } + this.PrintWarning($" ignored unknown '{sourceMod.DisplayName}' mod in the installer folder. To add mods, put them here instead: {paths.ModsPath}"); + continue; } - else - targetDir.Create(); + + // find target folder + ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase) == true); + DirectoryInfo defaultTargetFolder = new DirectoryInfo(Path.Combine(paths.ModsPath, sourceMod.Directory.Name)); + DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder; + this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName + ? $" adding {sourceMod.Manifest.Name}..." + : $" adding {sourceMod.Manifest.Name} to {Path.Combine(paths.ModsDir.Name, PathUtilities.GetRelativePath(paths.ModsPath, targetFolder.FullName))}..." + ); + + // remove existing folder + if (targetFolder.Exists) + this.InteractivelyDelete(targetFolder.FullName); // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) - sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); + this.RecursiveCopy(sourceMod.Directory, paths.ModsDir, filter: this.ShouldCopy); } + } - // set SMAPI's color scheme if defined - if (scheme != MonitorColorScheme.AutoDetect) - { - string configPath = Path.Combine(paths.GamePath, "StardewModdingAPI.config.json"); - string text = File - .ReadAllText(configPath) - .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}"""); - File.WriteAllText(configPath, text); - } + // set SMAPI's color scheme if defined + if (scheme != MonitorColorScheme.AutoDetect) + { + string text = File + .ReadAllText(paths.ApiConfigPath) + .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}"""); + File.WriteAllText(paths.ApiConfigPath, text); } // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(paths.ModsDir, packagedModsDir); + this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir); } } Console.WriteLine(); @@ -690,6 +695,35 @@ namespace StardewModdingApi.Installer } } + /// <summary>Recursively copy a directory or file.</summary> + /// <param name="source">The file or folder to copy.</param> + /// <param name="targetFolder">The folder to copy into.</param> + /// <param name="filter">A filter which matches directories and files to copy, or <c>null</c> to match all.</param> + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter = null) + { + if (filter != null && !filter(source)) + return; + + if (!targetFolder.Exists) + targetFolder.Create(); + + switch (source) + { + case FileInfo sourceFile: + sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); + break; + + case DirectoryInfo sourceDir: + DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); + foreach (var entry in sourceDir.EnumerateFileSystemInfos()) + this.RecursiveCopy(entry, targetSubfolder, filter); + break; + + default: + throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); + } + } + /// <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> @@ -871,7 +905,7 @@ namespace StardewModdingApi.Installer this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); // move mods if no conflicts (else warn) - foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { // get type bool isDir = entry is DirectoryInfo; @@ -928,22 +962,26 @@ namespace StardewModdingApi.Installer Directory.CreateDirectory(newPath); DirectoryInfo directory = (DirectoryInfo)entry; - foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopyFile)) + foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopy)) this.Move(child, Path.Combine(newPath, child.Name)); directory.Delete(recursive: true); } } - /// <summary>Get whether a file should be copied when moving a folder.</summary> - /// <param name="file">The file info.</param> - private bool ShouldCopyFile(FileSystemInfo file) + /// <summary>Get whether a file or folder should be copied from the installer files.</summary> + /// <param name="entry">The file or folder info.</param> + private bool ShouldCopy(FileSystemInfo entry) { - // ignore Mac symlink - if (file is FileInfo && file.Name == "mcs") - return false; - - return true; + switch (entry.Name) + { + case "mcs": + return false; // ignore Mac symlink + case "Mods": + return false; // Mods folder handled separately + default: + return true; + } } } } diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index 8f328ecf..0ca5aea0 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -1,17 +1,82 @@ -namespace StardewModdingApi.Installer +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Reflection; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingApi.Installer { /// <summary>The entry point for SMAPI's install and uninstall console app.</summary> internal class Program { /********* + ** Properties + *********/ + /// <summary>The absolute path of the installer folder.</summary> + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + /// <summary>The absolute path of the folder containing the unzipped installer files.</summary> + private static readonly string ExtractedBundlePath = Path.Combine(Path.GetTempPath(), $"SMAPI-installer-{Guid.NewGuid():N}"); + + /// <summary>The absolute path for referenced assemblies.</summary> + private static readonly string InternalFilesPath = Path.Combine(Program.ExtractedBundlePath, "smapi-internal"); + + /********* ** Public methods *********/ /// <summary>Run the install or uninstall script.</summary> /// <param name="args">The command line arguments.</param> public static void Main(string[] args) { - var installer = new InteractiveInstaller(); + // find install bundle + PlatformID platform = Environment.OSVersion.Platform; + FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-install.dat")); + if (!zipFile.Exists) + { + Console.WriteLine($"Oops! Some of the installer files are missing; try redownloading the installer. (Missing file: {zipFile.FullName})"); + Console.ReadLine(); + return; + } + + // unzip bundle into temp folder + DirectoryInfo bundleDir = new DirectoryInfo(Program.ExtractedBundlePath); + Console.WriteLine("Extracting install files..."); + ZipFile.ExtractToDirectory(zipFile.FullName, bundleDir.FullName); + + // set up assembly resolution + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; + + // launch installer + var installer = new InteractiveInstaller(bundleDir.FullName); installer.Run(args); } + + /********* + ** Private methods + *********/ + /// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary> + /// <param name="sender">The event sender.</param> + /// <param name="e">The event arguments.</param> + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) + { + try + { + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll")) + { + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + return Assembly.LoadFrom(dll.FullName); + } + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error resolving assembly: {ex}"); + return null; + } + } } } diff --git a/src/SMAPI.Installer/readme.txt b/src/SMAPI.Installer/README.txt index 2ee5473c..79c90cc0 100644 --- a/src/SMAPI.Installer/readme.txt +++ b/src/SMAPI.Installer/README.txt @@ -16,7 +16,7 @@ SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separ Player's guide -------------------------------- -See https://stardewvalleywiki.com/Modding:Player_Guide +See https://stardewvalleywiki.com/Modding:Player_Guide for help installing SMAPI, adding mods, etc. Manual install @@ -24,15 +24,21 @@ Manual install THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. If you really want to install SMAPI manually, here's how. -1. Download the latest version of SMAPI: https://github.com/Pathoschild/SMAPI/releases -2. Unzip the .zip file somewhere (not in your game folder). -3. Copy the files from the "internal/Windows" folder (on Windows) or "internal/Mono" folder (on - Linux/Mac) into your game folder. The `StardewModdingAPI.exe` file should be right next to the - game's executable. -4. +1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on Linux/Mac). + You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent confusion. +2. Copy the files from the folder you just unzipped into your game folder. The + `StardewModdingAPI.exe` file should be right next to the game's executable. +3. - Windows only: if you use Steam, see the install guide above to enable achievements and overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. - Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to play with mods. + +When installing on Linux or Mac: +- Make sure Mono is installed (normally the installer checks for you). While it's not required, + many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or + freeze the game.) +- To configure the color scheme, edit the `smapi-internal/StardewModdingAPI.config.json` file and + see instructions there for the 'ColorScheme' setting. diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index e82c6093..8000e4e7 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -34,6 +34,8 @@ </PropertyGroup> <ItemGroup> <Reference Include="System" /> + <Reference Include="System.IO.Compression" /> + <Reference Include="System.IO.Compression.FileSystem" /> </ItemGroup> <ItemGroup> <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> @@ -46,11 +48,17 @@ <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> <ItemGroup> - <Content Include="readme.txt"> + <Content Include="README.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> <ItemGroup> + <Content Include="windows-exe-config.xml"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <None Include="windows-install.bat"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> <None Include="unix-install.sh"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> @@ -58,6 +66,12 @@ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj"> + <Project>{ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6}</Project> + <Name>StardewModdingAPI.Toolkit</Name> + </ProjectReference> + </ItemGroup> <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="..\..\build\common.targets" /> diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh index df02bb37..e3a5d8cc 100644 --- a/src/SMAPI.Installer/unix-install.sh +++ b/src/SMAPI.Installer/unix-install.sh @@ -14,8 +14,8 @@ fi # validate Mono & run installer if $COMMAND mono >/dev/null 2>&1; then - mono internal/Mono/install.exe + mono internal/unix-install.exe else - echo "Oops! Looks like Mono isn't installed. Please install Mono from http://mono-project.com, reboot, and run this installer again." + echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again." read fi diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index 1e969c20..3c332472 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -62,27 +62,27 @@ else fi # open SMAPI in terminal - if $COMMAND x-terminal-emulator 2>/dev/null; then + if $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" + elif $COMMAND x-terminal-emulator 2>/dev/null; then # Terminator converts -e to -x when used through x-terminal-emulator for some reason (per # `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator # is mapped to Terminator, invoke it directly instead. if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then - terminator -e "$LAUNCHER" + terminator -e "sh -c 'TERM=xterm $LAUNCHER'" else - x-terminal-emulator -e "$LAUNCHER" + x-terminal-emulator -e "sh -c 'TERM=xterm $LAUNCHER'" fi - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" elif $COMMAND xfce4-terminal 2>/dev/null; then - xfce4-terminal -e "env TERM=xterm; $LAUNCHER" + xfce4-terminal -e "sh -c 'TERM=xterm $LAUNCHER'" elif $COMMAND gnome-terminal 2>/dev/null; then - gnome-terminal -e "env TERM=xterm; $LAUNCHER" + gnome-terminal -e "sh -c 'TERM=xterm $LAUNCHER'" elif $COMMAND konsole 2>/dev/null; then konsole -p Environment=TERM=xterm -e "$LAUNCHER" elif $COMMAND terminal 2>/dev/null; then - terminal -e "$LAUNCHER" + terminal -e "sh -c 'TERM=xterm $LAUNCHER'" else - $LAUNCHER + sh -c 'TERM=xterm $LAUNCHER' fi # some Linux users get error 127 (command not found) from the above block, even though diff --git a/src/SMAPI.Installer/windows-exe-config.xml b/src/SMAPI.Installer/windows-exe-config.xml new file mode 100644 index 00000000..386c7f1a --- /dev/null +++ b/src/SMAPI.Installer/windows-exe-config.xml @@ -0,0 +1,5 @@ +<configuration> + <runtime> + <loadFromRemoteSources enabled="true"/> + </runtime> +</configuration> diff --git a/src/SMAPI.Installer/windows-install.bat b/src/SMAPI.Installer/windows-install.bat new file mode 100644 index 00000000..7a8b409b --- /dev/null +++ b/src/SMAPI.Installer/windows-install.bat @@ -0,0 +1 @@ +START /WAIT /B internal/windows-install.exe |