using Microsoft.Win32; using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; namespace SpaceEngineersBlueprintEditor.GameAssemblyLoader; /// /// Locates the installed Steam copy of Space Engineers without loading game code. /// public static class GameInstallationLocator { private const string AppId = "244850"; private const string DefaultInstallDirectory = "SpaceEngineers"; /// /// Finds and validates the game root. The preferred path may point to the root, /// Bin64 directory, or SpaceEngineers.exe. /// public static string? FindGameRootPath(string? preferredPath = null) { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var candidate in EnumerateCandidates(preferredPath)) { var gameRoot = NormalizeAndValidate(candidate); if (gameRoot is not null && seen.Add(gameRoot)) { return gameRoot; } } return null; } /// /// Gets the validated Bin64 directory for the installed game. /// public static string? FindGameBinPath(string? preferredPath = null) { var gameRoot = FindGameRootPath(preferredPath); return gameRoot is null ? null : Path.Combine(gameRoot, "Bin64"); } private static IEnumerable EnumerateCandidates(string? preferredPath) { if (!string.IsNullOrWhiteSpace(preferredPath)) { yield return preferredPath!; } var environmentBin = Environment.GetEnvironmentVariable("SPACE_ENGINEERS_BIN"); if (!string.IsNullOrWhiteSpace(environmentBin)) { yield return environmentBin!; } var environmentRoot = Environment.GetEnvironmentVariable("SPACE_ENGINEERS_ROOT"); if (!string.IsNullOrWhiteSpace(environmentRoot)) { yield return environmentRoot!; } foreach (var installLocation in ReadRegistryValues( @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App " + AppId, "InstallLocation")) { yield return installLocation; } foreach (var installLocation in ReadRegistryValues( @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Steam App " + AppId, "InstallLocation")) { yield return installLocation; } foreach (var steamRoot in EnumerateSteamRoots()) { foreach (var candidate in EnumerateSteamLibraryCandidates(steamRoot)) { yield return candidate; } } foreach (var drive in DriveInfo.GetDrives()) { string? root = null; try { if (drive.IsReady && drive.DriveType == DriveType.Fixed) { root = drive.RootDirectory.FullName; } } catch (IOException) { } catch (UnauthorizedAccessException) { } if (root is not null) { yield return Path.Combine(root, "SteamLibrary", "steamapps", "common", DefaultInstallDirectory); } } } private static IEnumerable EnumerateSteamRoots() { foreach (var path in ReadRegistryValues(@"SOFTWARE\Valve\Steam", "SteamPath", "InstallPath")) { yield return path; } foreach (var path in ReadRegistryValues(@"SOFTWARE\WOW6432Node\Valve\Steam", "SteamPath", "InstallPath")) { yield return path; } var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); if (!string.IsNullOrWhiteSpace(programFilesX86)) { yield return Path.Combine(programFilesX86, "Steam"); } } private static IEnumerable EnumerateSteamLibraryCandidates(string steamRoot) { yield return Path.Combine(steamRoot, "steamapps", "common", DefaultInstallDirectory); var libraryFile = Path.Combine(steamRoot, "steamapps", "libraryfolders.vdf"); string libraryText; try { if (!File.Exists(libraryFile)) { yield break; } libraryText = File.ReadAllText(libraryFile); } catch (IOException) { yield break; } catch (UnauthorizedAccessException) { yield break; } foreach (Match match in Regex.Matches(libraryText, "\"(?:path|\\d+)\"\\s+\"(?[^\"]+)\"")) { var libraryRoot = match.Groups["path"].Value.Replace("\\\\", "\\"); var installDirectory = ReadInstallDirectory(libraryRoot) ?? DefaultInstallDirectory; yield return Path.Combine(libraryRoot, "steamapps", "common", installDirectory); } } private static string? ReadInstallDirectory(string libraryRoot) { var manifest = Path.Combine(libraryRoot, "steamapps", "appmanifest_" + AppId + ".acf"); try { if (!File.Exists(manifest)) { return null; } var match = Regex.Match(File.ReadAllText(manifest), "\"installdir\"\\s+\"(?[^\"]+)\""); return match.Success ? match.Groups["name"].Value : null; } catch (IOException) { return null; } catch (UnauthorizedAccessException) { return null; } } private static IEnumerable ReadRegistryValues(string subKey, params string[] valueNames) { var values = new List(); foreach (var hive in new[] { Registry.CurrentUser, Registry.LocalMachine }) { try { using (var key = hive.OpenSubKey(subKey, false)) { if (key is null) { continue; } foreach (var valueName in valueNames) { if (key.GetValue(valueName) is string value && !string.IsNullOrWhiteSpace(value)) { values.Add(value); } } } } catch (UnauthorizedAccessException) { } catch (System.Security.SecurityException) { } } return values; } private static string? NormalizeAndValidate(string candidate) { try { candidate = Environment.ExpandEnvironmentVariables(candidate.Trim().Trim('"')); if (string.Equals(Path.GetFileName(candidate), "SpaceEngineers.exe", StringComparison.OrdinalIgnoreCase)) { candidate = Path.GetDirectoryName(candidate) ?? candidate; } if (string.Equals(Path.GetFileName(candidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), "Bin64", StringComparison.OrdinalIgnoreCase)) { candidate = Path.GetDirectoryName(candidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) ?? candidate; } var gameRoot = Path.GetFullPath(candidate).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var binPath = Path.Combine(gameRoot, "Bin64"); if (File.Exists(Path.Combine(binPath, "SpaceEngineers.exe")) && File.Exists(Path.Combine(binPath, "VRage.Game.dll")) && File.Exists(Path.Combine(binPath, "Sandbox.Game.dll")) && File.Exists(Path.Combine(binPath, "SpaceEngineers.Game.dll")) && Directory.Exists(Path.Combine(gameRoot, "Content"))) { return gameRoot; } } catch (ArgumentException) { } catch (IOException) { } catch (NotSupportedException) { } catch (UnauthorizedAccessException) { } return null; } }