using System;
using System.IO;
using System.Linq;
using System.Reflection;
using StardewModdingApi.Installer.Enums;
namespace StardewModdingApi.Installer
{
/// Interactively performs the install and uninstall logic.
internal class InteractiveInstaller
{
/*********
** Properties
*********/
/// The default file paths where Stardew Valley can be installed.
/// Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig.
private readonly string[] DefaultInstallPaths = {
// Linux
$"{Environment.GetEnvironmentVariable("HOME")}/GOG Games/Stardew Valley/game",
$"{Environment.GetEnvironmentVariable("HOME")}/.local/share/Steam/steamapps/common/Stardew Valley",
// Mac
$"{Environment.GetEnvironmentVariable("HOME")}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS",
// Windows
@"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley",
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"
};
/*********
** Public methods
*********/
/// Run the install or uninstall script.
/// The command line arguments.
///
/// Initialisation flow:
/// 1. Collect information (mainly OS and install path) and validate it.
/// 2. Ask the user whether to install or uninstall.
///
/// Install flow:
/// 1. Copy the SMAPI files from package/Windows or package/Mono into the game directory.
/// 2. 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. Create the 'Mods' directory.
/// 4. Copy the bundled mods into the 'Mods' directory (deleting any existing versions).
/// 5. Move any mods from app data into game's mods directory.
///
/// Uninstall logic:
/// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup.
/// 2. Delete all files in the game directory matching a file under package/Windows or package/Mono.
///
public void Run(string[] args)
{
/****
** collect details
****/
Platform platform = this.DetectPlatform();
DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), platform.ToString()));
DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform);
var paths = new
{
executable = Path.Combine(installDir.FullName, platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe"),
unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"),
unixLauncher = Path.Combine(installDir.FullName, "StardewValley"),
unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original")
};
this.PrintDebug($"Detected {(platform == Platform.Windows ? "Windows" : "Linux or Mac")} with game in {installDir}.");
/****
** validate assumptions
****/
if (!packageDir.Exists)
{
this.ExitError($"The '{platform}' package directory is missing (should be at {packageDir}).");
return;
}
if (!File.Exists(paths.executable))
{
this.ExitError("The detected game install path doesn't contain a Stardew Valley executable.");
return;
}
Console.WriteLine();
/****
** ask user what to do
****/
Console.WriteLine("You can....");
Console.WriteLine(platform == Platform.Mono
? "[1] Install SMAPI. This will safely update the files so you can launch the game the same way as before."
: "[1] Install SMAPI. You'll need to launch StardewModdingAPI.exe instead afterwards; see the readme.txt for details."
);
Console.WriteLine("[2] Uninstall SMAPI.");
ScriptAction action;
{
string choice = this.InteractivelyChoose("What do you want to do?", "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();
/****
** Perform action
****/
switch (action)
{
case ScriptAction.Uninstall:
{
// restore game launcher
if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup))
{
this.PrintDebug("Restoring game launcher...");
if (File.Exists(paths.unixLauncher))
File.Delete(paths.unixLauncher);
File.Move(paths.unixLauncherBackup, paths.unixLauncher);
}
// remove SMAPI files
this.PrintDebug("Removing SMAPI files...");
foreach (FileInfo sourceFile in packageDir.EnumerateFiles())
{
string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
if (File.Exists(targetPath))
File.Delete(targetPath);
}
}
break;
case ScriptAction.Install:
{
// copy SMAPI files to game dir
this.PrintDebug("Copying SMAPI files to game directory...");
foreach (FileInfo sourceFile in packageDir.EnumerateFiles())
{
string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
if (File.Exists(targetPath))
File.Delete(targetPath);
sourceFile.CopyTo(targetPath);
}
// replace mod launcher (if possible)
if (platform == Platform.Mono)
{
this.PrintDebug("Safely replacing game launcher...");
if (!File.Exists(paths.unixLauncherBackup))
File.Move(paths.unixLauncher, paths.unixLauncherBackup);
else if (File.Exists(paths.unixLauncher))
File.Delete(paths.unixLauncher);
File.Move(paths.unixSmapiLauncher, paths.unixLauncher);
}
// create mods directory (if needed)
DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods"));
if (!modsDir.Exists)
{
this.PrintDebug("Creating mods directory...");
modsDir.Create();
}
// add or replace bundled mods
Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods"));
DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods"));
if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
{
this.PrintDebug("Adding bundled mods...");
foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
{
this.PrintDebug($" adding {sourceDir.Name}...");
// initialise target dir
DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name));
if (targetDir.Exists)
targetDir.Delete(recursive: true);
targetDir.Create();
// copy files
foreach (FileInfo sourceFile in sourceDir.EnumerateFiles())
sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
}
}
// remove obsolete appdata mods
this.InteractivelyRemoveAppDataMods(platform, modsDir);
}
break;
}
Console.WriteLine();
/****
** exit
****/
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("Done!");
if (platform == Platform.Windows)
{
Console.WriteLine(action == ScriptAction.Install
? "Don't forget to launch StardewModdingAPI.exe instead of the normal game executable. See the readme.txt for details."
: "If you manually changed shortcuts or Steam to launch SMAPI, don't forget to change those back."
);
}
else if (action == ScriptAction.Install)
Console.WriteLine("You can launch the game the same way as before to play with mods.");
Console.ResetColor();
Console.ReadKey();
}
/*********
** Private methods
*********/
/// Detect the game's platform.
/// The platform is not supported.
private Platform DetectPlatform()
{
switch (Environment.OSVersion.Platform)
{
case PlatformID.MacOSX:
case PlatformID.Unix:
return Platform.Mono;
default:
return Platform.Windows;
}
}
/// Print a debug message.
/// The text to print.
private void PrintDebug(string text)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine(text);
Console.ResetColor();
}
/// Print a warning message.
/// The text to print.
private void PrintWarning(string text)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine(text);
Console.ResetColor();
}
/// Print an error and pause the console if needed.
/// The error text.
private void ExitError(string error)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(error);
Console.ResetColor();
Console.ReadLine();
}
/// Interactively ask the user to choose a value.
/// The message to print.
/// The allowed options (not case sensitive).
private string InteractivelyChoose(string message, params string[] options)
{
while (true)
{
Console.WriteLine(message);
string input = Console.ReadLine()?.Trim().ToLowerInvariant();
if (!options.Contains(input))
{
Console.WriteLine("That's not a valid option.");
continue;
}
return input;
}
}
/// Interactively locate the game's install path.
/// The current platform.
private DirectoryInfo InteractivelyGetInstallPath(Platform platform)
{
// try default paths
foreach (string defaultPath in this.DefaultInstallPaths)
{
if (Directory.Exists(defaultPath))
return new DirectoryInfo(defaultPath);
}
// ask user
Console.WriteLine("Oops, couldn't find your Stardew Valley install path automatically. You'll need to specify where the game is installed (or install SMAPI manually).");
while (true)
{
// get path from user
Console.WriteLine(" Enter the game's full directory path (the one containing 'StardewValley.exe' or 'Stardew Valley.exe').");
Console.Write(" > ");
string path = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine(" You must specify a directory path to continue.");
continue;
}
// normalise on Windows
if (platform == Platform.Windows)
path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path
// get directory
if (File.Exists(path))
path = Path.GetDirectoryName(path);
DirectoryInfo directory = new DirectoryInfo(path);
// validate path
if (!directory.Exists)
{
Console.WriteLine(" That directory doesn't seem to exist.");
continue;
}
if (!directory.EnumerateFiles("*.exe").Any(p => p.Name == "StardewValley.exe" || p.Name == "Stardew Valley.exe"))
{
Console.WriteLine(" That directory doesn't contain a Stardew Valley executable.");
continue;
}
// looks OK
Console.WriteLine(" OK!");
return directory;
}
}
/// Interactively move mods out of the appdata directory.
/// The current platform.
/// The directory which should contain all mods.
private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir)
{
// get path
string homePath = platform == Platform.Windows
? Environment.GetEnvironmentVariable("APPDATA")
: Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".config");
string appDataPath = Path.Combine(homePath, "StardewValley");
DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods"));
// check if migration needed
if (!modDir.Exists)
return;
this.PrintDebug($"Found an obsolete mod path: {modDir.FullName}");
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())
{
// get type
bool isDir = entry is DirectoryInfo;
if (!isDir && !(entry is FileInfo))
continue; // should never happen
// check paths
string newPath = Path.Combine(properModsDir.FullName, entry.Name);
if (isDir ? Directory.Exists(newPath) : File.Exists(newPath))
{
this.PrintWarning($" Can't move {entry.Name} because it already exists in your game's mod directory.");
continue;
}
// move into mods
this.PrintDebug($" Moving {entry.Name} into the game's mod directory...");
if (isDir)
(entry as DirectoryInfo).MoveTo(newPath);
else
(entry as FileInfo).MoveTo(newPath);
}
// delete if empty
if (modDir.EnumerateFileSystemInfos().Any())
this.PrintWarning(" You have files in this folder which couldn't be moved automatically. These will be ignored by SMAPI.");
else
{
this.PrintDebug(" Deleted empty directory.");
modDir.Delete();
}
}
}
}