using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using StardewModdingAPI.Toolkit.Utilities; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; #endif namespace StardewModdingAPI.Toolkit.Framework.GameScanning { /// Finds installed game folders. public class GameScanner { /********* ** Fields *********/ /// The current OS. private readonly Platform Platform; /********* ** Public methods *********/ /// Construct an instance. public GameScanner() { this.Platform = EnvironmentUtility.DetectPlatform(); } /// Find all valid Stardew Valley install folders. /// This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS. public IEnumerable Scan() { // get install paths IEnumerable paths = this .GetCustomInstallPaths() .Concat(this.GetDefaultInstallPaths()) .Select(PathUtilities.NormalizePath) .Distinct(StringComparer.OrdinalIgnoreCase); // yield valid folders foreach (string path in paths) { DirectoryInfo folder = new DirectoryInfo(path); if (this.LooksLikeGameFolder(folder)) yield return folder; } } /// Get whether a folder seems to contain the game. /// The folder to check. public bool LooksLikeGameFolder(DirectoryInfo dir) { return dir.Exists && dir.EnumerateFiles("Stardew Valley.dll").Any(); } /********* ** Private methods *********/ /// The default file paths where Stardew Valley can be installed. /// Derived from the crossplatform mod config. private IEnumerable GetDefaultInstallPaths() { switch (this.Platform) { case Platform.Linux: case Platform.Mac: { string home = Environment.GetEnvironmentVariable("HOME"); // Linux yield return $"{home}/GOG Games/Stardew Valley/game"; yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley") ? $"{home}/.steam/steam/steamapps/common/Stardew Valley" : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley"; // macOS yield return "/Applications/Stardew Valley.app/Contents/MacOS"; yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; } break; case Platform.Windows: { // Windows registry #if SMAPI_FOR_WINDOWS IDictionary registryKeys = new Dictionary { [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows }; foreach (var pair in registryKeys) { string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); if (!string.IsNullOrWhiteSpace(path)) yield return path; } // via Steam library path string steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); if (steamPath != null) yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); #endif // default paths 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}\GOG Games\Stardew Valley"; yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; } } break; default: throw new InvalidOperationException($"Unknown platform '{this.Platform}'."); } } /// Get the custom install path from the stardewvalley.targets file in the home directory, if any. private IEnumerable GetCustomInstallPaths() { // get home path string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME"); if (string.IsNullOrWhiteSpace(homePath)) yield break; // get targets file FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets")); if (!file.Exists) yield break; // parse file XElement root; try { using FileStream stream = file.OpenRead(); root = XElement.Load(stream); } catch { yield break; } // get install path XElement element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace if (!string.IsNullOrWhiteSpace(element?.Value)) yield return element.Value.Trim(); } #if SMAPI_FOR_WINDOWS /// Get the value of a key in the Windows HKLM registry. /// The full path of the registry key relative to HKLM. /// The name of the value. private string GetLocalMachineRegistryValue(string key, string name) { RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; RegistryKey openKey = localMachine.OpenSubKey(key); if (openKey == null) return null; using (openKey) return (string)openKey.GetValue(name); } /// Get the value of a key in the Windows HKCU registry. /// The full path of the registry key relative to HKCU. /// The name of the value. private string GetCurrentUserRegistryValue(string key, string name) { RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; RegistryKey openKey = currentuser.OpenSubKey(key); if (openKey == null) return null; using (openKey) return (string)openKey.GetValue(name); } #endif } }