diff options
-rw-r--r-- | build/common.targets | 1 | ||||
-rw-r--r-- | build/prepare-install-package.targets | 123 | ||||
-rw-r--r-- | docs/release-notes.md | 2 | ||||
-rw-r--r-- | docs/technical-docs.md | 53 | ||||
-rw-r--r-- | src/SMAPI.Installer/Framework/InstallerPaths.cs | 14 | ||||
-rw-r--r-- | src/SMAPI.Installer/InteractiveInstaller.cs | 88 | ||||
-rw-r--r-- | src/SMAPI.Installer/Program.cs | 32 | ||||
-rw-r--r-- | src/SMAPI.Installer/README.txt | 13 | ||||
-rw-r--r-- | src/SMAPI.Installer/StardewModdingAPI.Installer.csproj | 2 | ||||
-rw-r--r-- | src/SMAPI.Installer/unix-install.sh | 2 | ||||
-rw-r--r-- | src/SMAPI.Installer/windows-install.bat | 2 |
11 files changed, 203 insertions, 129 deletions
diff --git a/build/common.targets b/build/common.targets index 320b3e00..d9ad89f4 100644 --- a/build/common.targets +++ b/build/common.targets @@ -108,7 +108,6 @@ <Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\$(TargetName).metadata.json" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)\smapi-internal" /> - <Copy SourceFiles="$(TargetDir)\0Harmony.pdb" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)\smapi-internal" /> </Target> diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 09114aee..dd15fb52 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -10,9 +10,10 @@ <CompiledRootPath>$(SolutionDir)\..\bin\$(Configuration)</CompiledRootPath> <CompiledSmapiPath>$(CompiledRootPath)\SMAPI</CompiledSmapiPath> <CompiledToolkitPath>$(CompiledRootPath)\SMAPI.Toolkit\net4.5</CompiledToolkitPath> - <PackagePath>$(SolutionDir)\..\bin\Packaged</PackagePath> - <PackageInternalPath Condition="$(OS) == 'Windows_NT'">$(PackagePath)\internal\Windows</PackageInternalPath> - <PackageInternalPath Condition="$(OS) != 'Windows_NT'">$(PackagePath)\internal\Mono</PackageInternalPath> + <PackagePath>$(SolutionDir)\..\bin\SMAPI installer</PackagePath> + <PackageDevPath>$(SolutionDir)\..\bin\SMAPI installer for developers</PackageDevPath> + <PackageBundlePlatform>windows</PackageBundlePlatform> + <PackageBundlePlatform Condition="$(OS) != 'Windows_NT'">mono</PackageBundlePlatform> </PropertyGroup> <ItemGroup> <CompiledMods Include="$(SolutionDir)\..\bin\$(Configuration)\Mods\**\*.*" /> @@ -20,40 +21,102 @@ <!-- reset package directory --> <RemoveDir Directories="$(PackagePath)" /> + <RemoveDir Directories="$(PackageDevPath)" /> <!-- copy installer files --> - <Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackageInternalPath)\install.exe" /> - <Copy SourceFiles="$(TargetDir)\unix-launcher.sh" DestinationFiles="$(PackageInternalPath)\StardewModdingAPI" /> - <Copy SourceFiles="$(TargetDir)\README.txt" DestinationFiles="$(PackagePath)\README.txt" /> <Copy SourceFiles="$(TargetDir)\unix-install.sh" DestinationFiles="$(PackagePath)\install on Linux.sh" /> <Copy SourceFiles="$(TargetDir)\unix-install.sh" DestinationFiles="$(PackagePath)\install on Mac.command" /> <Copy SourceFiles="$(TargetDir)\windows-install.bat" DestinationFiles="$(PackagePath)\install on Windows.bat" /> + <Copy SourceFiles="$(TargetDir)\README.txt" DestinationFiles="$(PackagePath)\README.txt" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackagePath)\internal\install.exe" /> + <Copy SourceFiles="$(TargetDir)\windows-exe-config.xml" DestinationFiles="$(PackagePath)\internal\install.exe.config" /> - <!--copy *.exe.config files (needed to avoid security errors when loading DLLs)--> - <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(TargetDir)\windows-exe-config.xml" DestinationFiles="$(PackageInternalPath)\install.exe.config" /> - <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(TargetDir)\windows-exe-config.xml" DestinationFiles="$(PackageInternalPath)\StardewModdingAPI.exe.config" /> + <!--copy bundle files--> + <Copy SourceFiles="$(TargetDir)\unix-launcher.sh" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI" /> + <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackagePath)\bundle" /> + <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.pdb" DestinationFolder="$(PackagePath)\bundle" /> + <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.xml" DestinationFolder="$(PackagePath)\bundle" /> + <Copy SourceFiles="$(CompiledSmapiPath)\steam_appid.txt" DestinationFolder="$(PackagePath)\bundle" /> + <Copy SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.metadata.json" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy SourceFiles="@(CompiledMods)" DestinationFolder="$(PackagePath)\bundle\Mods\%(RecursiveDir)" /> + <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(TargetDir)\windows-exe-config.xml" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI.exe.config" /> + <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> + <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> - <!-- copy SMAPI files --> - <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackageInternalPath)" /> - <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.pdb" DestinationFolder="$(PackageInternalPath)" /> - <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.xml" DestinationFolder="$(PackageInternalPath)" /> - <Copy SourceFiles="$(CompiledSmapiPath)\steam_appid.txt" DestinationFolder="$(PackageInternalPath)" /> - <Copy SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledSmapiPath)\0Harmony.pdb" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.metadata.json" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.pdb" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.xml" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy SourceFiles="@(CompiledMods)" DestinationFolder="$(PackageInternalPath)\Mods\%(RecursiveDir)" /> + <!-- fix Linux/Mac permissions --> + <Exec Condition="$(OS) != 'Windows_NT'" Command="chmod 755 "$(PackagePath)\install on Linux.sh"" /> + <Exec Condition="$(OS) != 'Windows_NT'" Command="chmod 755 "$(PackagePath)\install on Mac.command"" /> - <!--copy SMAPI files for Linux/Mac only --> - <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> - <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackageInternalPath)\smapi-internal" /> + <!-- finalise 'for developers' installer --> + <ItemGroup> + <PackageFiles Include="$(PackagePath)\**\*.*" /> + </ItemGroup> + <Copy SourceFiles="@(PackageFiles)" DestinationFolder="$(PackageDevPath)\%(RecursiveDir)" /> + <ZipDirectory FromDirPath="$(PackageDevPath)\bundle" ToFilePath="$(PackageDevPath)\internal\$(PackageBundlePlatform).dat" /> + <RemoveDir Directories="$(PackageDevPath)\bundle" /> + + <!-- finalise normal installer --> + <ReplaceFileText FilePath="$(PackagePath)\bundle\smapi-internal\StardewModdingAPI.config.json" Search=""DeveloperMode": true" Replace=""DeveloperMode": false" /> + <ZipDirectory FromDirPath="$(PackagePath)\bundle" ToFilePath="$(PackagePath)\internal\$(PackageBundlePlatform).dat" /> + <RemoveDir Directories="$(PackagePath)\bundle" /> </Target> + + <!-- Create a zip file with the contents of a given folder path. Derived from https://stackoverflow.com/a/38127938/262123. --> + <UsingTask TaskName="ZipDirectory" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll"> + <ParameterGroup> + <FromDirPath ParameterType="System.String" Required="true" /> + <ToFilePath ParameterType="System.String" Required="true" /> + </ParameterGroup> + <Task> + <Reference Include="System.IO.Compression.FileSystem" /> + <Using Namespace="System.IO.Compression" /> + <Code Type="Fragment" Language="cs"> + <![CDATA[ + try + { + ZipFile.CreateFromDirectory(FromDirPath, ToFilePath); + return true; + } + catch(Exception ex) + { + Log.LogErrorFromException(ex); + return false; + } + ]]> + </Code> + </Task> + </UsingTask> + + <!-- Replace text in a file based on a regex pattern. Derived from https://stackoverflow.com/a/22571621/262123. --> + <UsingTask TaskName="ReplaceFileText" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll"> + <ParameterGroup> + <FilePath ParameterType="System.String" Required="true" /> + <Search ParameterType="System.String" Required="true" /> + <Replace ParameterType="System.String" Required="true" /> + </ParameterGroup> + <Task> + <Reference Include="System.Core" /> + <Using Namespace="System" /> + <Using Namespace="System.IO" /> + <Using Namespace="System.Text.RegularExpressions" /> + <Code Type="Fragment" Language="cs"> + <![CDATA[ + File.WriteAllText( + FilePath, + Regex.Replace(File.ReadAllText(FilePath), Search, Replace) + ); + ]]> + </Code> + </Task> + </UsingTask> </Project> diff --git a/docs/release-notes.md b/docs/release-notes.md index 5762c623..d39e6f3e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Reorganised SMAPI files: * Most SMAPI files are now tucked into a `smapi-internal` subfolder. * Save backups are now in a `save-backups` subfolder, so they're easier to access. Note that previous backups will be deleted when you update. + * Simplified the installer files to avoid confusion. * Added support for organising mods: * You can now group mods into subfolders to organise them. * You can now mark a mod folder ignored by starting the name with a dot (like `.disabled mods`). @@ -18,6 +19,7 @@ * Fixed friendly error no longer shown when SMAPI isn't run from the game folder. * Fixed some Windows install paths not detected. * Fixed installer duplicating bundled mods if you moved them after the last install. + * Fixed installer allowing custom mods to be bundled with the install. * Fixed translation issues not shown as warnings. * Fixed dependencies not correctly enforced if the dependency is installed but failed to load. * Fixed some errors logged as SMAPI instead of the affected mod. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 4a8f45f1..83e4ea2b 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -44,7 +44,7 @@ executed. This doesn't work in MonoDevelop on Linux, unfortunately. ### Preparing a release To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See -[crossplatforming info](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) +[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) on the wiki for the first-time setup. 1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a @@ -57,47 +57,16 @@ on the wiki for the first-time setup. release | `<version>` | `3.0` 2. In Windows: - 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI <version>` (e.g. `SMAPI 3.0`). - 2. Transfer the `SMAPI <version>` folder to Linux or Mac. - _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, - since we need to set Unix file permissions that Windows won't save._ - -2. In Linux or Mac: - 1. Rebuild the solution in _Release_ mode. - 2. Copy `bin/internal/Packaged/Mono` into the `SMAPI <version>` folder. - 3. If you did everything right so far, you should have a folder like this: - - ``` - SMAPI 3.0 installer/ - install on Linux.sh - install on Mac.command - install on Windows.exe - README.txt - internal/ - Mono/ - Mods/* - smapi-internal/* - StardewModdingAPI - StardewModdingAPI.exe - StardewModdingAPI.pdb - StardewModdingAPI.xml - steam_appid.txt - Windows/ - Mods/* - smapi-internal/* - StardewModdingAPI.exe - StardewModdingAPI.pdb - StardewModdingAPI.xml - steam_appid.txt - ``` - 4. Open a terminal in the `SMAPI <version>` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. - 5. Copy & paste the `SMAPI <version>` folder as `SMAPI <version> for developers`. - 6. In the `SMAPI <version>` folder... - * edit `internal/Mono/StardewModdingAPI.config.json` and - `internal/Windows/StardewModdingAPI.config.json` to disable developer mode; - * delete `internal/Windows/StardewModdingAPI.xml`. - 7. Compress the two folders into `SMAPI <version>.zip` and `SMAPI <version> for developers.zip`. + 1. Rebuild the solution in Release mode. + 2. Copy `windows.dat` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to + Linux/Mac. + +3. In Linux/Mac: + 1. Rebuild the solution in Release mode. + 2. Add the `windows.dat` files to the `bin/SMAPI installer` and + `bin/SMAPI installer for developers` folders. + 3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`. + 4. Zip the two folders. ## Customisation ### Configuration file diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs index 65e7699b..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; @@ -46,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")); diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 85882283..d5866c74 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -1,9 +1,9 @@ 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; @@ -22,12 +22,19 @@ namespace StardewModdingApi.Installer /********* ** Properties *********/ - /// <summary>The name of the installer file in the package.</summary> - private readonly string InstallerFileName = "install.exe"; + /// <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> @@ -146,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); } @@ -322,8 +331,8 @@ namespace StardewModdingApi.Installer } // get folders - DirectoryInfo packageDir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - paths = new InstallerPaths(packageDir, installDir, EnvironmentUtility.GetExecutableName(platform)); + DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath); + paths = new InstallerPaths(bundleDir, installDir, EnvironmentUtility.GetExecutableName(platform)); } Console.Clear(); @@ -331,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; } @@ -439,11 +436,8 @@ namespace StardewModdingApi.Installer { // copy SMAPI files to game dir this.PrintDebug("Adding SMAPI files..."); - foreach (FileSystemInfo sourceEntry in paths.PackageDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) + foreach (FileSystemInfo sourceEntry in paths.BundleDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { - if (sourceEntry.Name.StartsWith(this.InstallerFileName)) // e.g. install.exe or install.exe.config - continue; - this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name)); this.RecursiveCopy(sourceEntry, paths.GameDir); } @@ -452,6 +446,8 @@ namespace StardewModdingApi.Installer 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,14 +480,14 @@ 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..."); ModToolkit toolkit = new ModToolkit(); ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray(); - foreach (ModFolder sourceMod in toolkit.GetModFolders(packagedModsDir.FullName)) + foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName)) { // validate source mod if (sourceMod.Manifest == null) @@ -486,6 +495,11 @@ namespace StardewModdingApi.Installer this.PrintWarning($" ignored invalid bundled mod {sourceMod.DisplayName}: {sourceMod.ManifestParseError}"); continue; } + if (!this.BundledModIDs.Contains(sourceMod.Manifest.UniqueID)) + { + this.PrintWarning($" ignored unknown '{sourceMod.DisplayName}' mod in the installer folder. To add mods, put them here instead: {paths.ModsPath}"); + continue; + } // find target folder ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase) == true); @@ -496,14 +510,12 @@ namespace StardewModdingApi.Installer : $" adding {sourceMod.Manifest.Name} to {Path.Combine(paths.ModsDir.Name, PathUtilities.GetRelativePath(paths.ModsPath, targetFolder.FullName))}..." ); - // (re)create target folder + // remove existing folder if (targetFolder.Exists) this.InteractivelyDelete(targetFolder.FullName); - targetFolder.Create(); // copy files - foreach (FileInfo sourceFile in sourceMod.Directory.EnumerateFiles().Where(this.ShouldCopy)) - sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); + this.RecursiveCopy(sourceMod.Directory, paths.ModsDir, filter: this.ShouldCopy); } } @@ -517,7 +529,7 @@ namespace StardewModdingApi.Installer } // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(paths.ModsDir, packagedModsDir); + this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir); } } Console.WriteLine(); @@ -686,8 +698,12 @@ 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> - private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder) + /// <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(); @@ -700,7 +716,7 @@ namespace StardewModdingApi.Installer case DirectoryInfo sourceDir: DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); foreach (var entry in sourceDir.EnumerateFileSystemInfos()) - this.RecursiveCopy(entry, targetSubfolder); + this.RecursiveCopy(entry, targetSubfolder, filter); break; default: diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index ad5cf47f..c56c9f80 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -1,7 +1,10 @@ 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 { @@ -11,9 +14,15 @@ namespace StardewModdingApi.Installer /********* ** Properties *********/ - /// <summary>The absolute path to search for referenced assemblies.</summary> + /// <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 DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal"); + 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 @@ -22,11 +31,26 @@ namespace StardewModdingApi.Installer /// <param name="args">The command line arguments.</param> public static void Main(string[] args) { + // find install bundle + PlatformID platform = Environment.OSVersion.Platform; + FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "mono")}.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(); + var installer = new InteractiveInstaller(bundleDir.FullName); installer.Run(args); } @@ -41,7 +65,7 @@ namespace StardewModdingApi.Installer try { AssemblyName name = new AssemblyName(e.Name); - foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) + 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); diff --git a/src/SMAPI.Installer/README.txt b/src/SMAPI.Installer/README.txt index 88fe4a03..abff9938 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,12 +24,11 @@ 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.dat" (on Windows) or "internal/mono.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. diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 22c55af7..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"> diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh index 8379ed87..57e48f4b 100644 --- a/src/SMAPI.Installer/unix-install.sh +++ b/src/SMAPI.Installer/unix-install.sh @@ -14,7 +14,7 @@ fi # validate Mono & run installer if $COMMAND mono >/dev/null 2>&1; then - mono internal/Mono/install.exe + mono internal/install.exe else echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again." read diff --git a/src/SMAPI.Installer/windows-install.bat b/src/SMAPI.Installer/windows-install.bat index 716c7789..1ebec989 100644 --- a/src/SMAPI.Installer/windows-install.bat +++ b/src/SMAPI.Installer/windows-install.bat @@ -1 +1 @@ -START /WAIT /B internal/Windows/install.exe +START /WAIT /B internal/install.exe |