From 71928a9b44450e52ae4d584f85a52f8a2ebca8d6 Mon Sep 17 00:00:00 2001 From: terrymacdonald Date: Tue, 21 Jul 2020 23:40:33 +1200 Subject: [PATCH] [WIP] Created SteamLibrary class to handle library Pulled out the the library list mgmt from SteamGame and put it in a new SteamLibrary class. This means I can replicate the learnings from the ShortcutRepo amd Profile Repo, and can save separate JSON files in the future if I so desire. There is a little bit outstanding to make the SteamGame Properties to be writeable as well as readable, otherwise the SteamGame.CopyTo function won't work. --- HeliosPlus/GameLibraries/SteamGame.cs | 347 +---------- HeliosPlus/GameLibraries/SteamLibrary.cs | 608 +++++++++++++++++++ HeliosPlus/HeliosPlus.csproj | 1 + HeliosPlus/Program.cs | 690 +++++++++------------- HeliosPlus/Resources/Language.Designer.cs | 29 +- HeliosPlus/Resources/Language.resx | 9 +- HeliosPlus/ShortcutItem.cs | 62 +- HeliosPlus/ShortcutRepository.cs | 7 +- HeliosPlus/UIForms/MainForm.cs | 2 +- HeliosPlus/UIForms/ShortcutForm.cs | 10 +- HeliosPlus/Validators.cs | 49 +- 11 files changed, 1003 insertions(+), 811 deletions(-) create mode 100644 HeliosPlus/GameLibraries/SteamLibrary.cs diff --git a/HeliosPlus/GameLibraries/SteamGame.cs b/HeliosPlus/GameLibraries/SteamGame.cs index d3fb64d..2d6cfd2 100644 --- a/HeliosPlus/GameLibraries/SteamGame.cs +++ b/HeliosPlus/GameLibraries/SteamGame.cs @@ -30,27 +30,27 @@ namespace HeliosPlus.GameLibraries { public class SteamGame { - private static string _steamExe; - private static string _steamPath; + /*private static string SteamLibrary.SteamExe; + private static string SteamLibrary.SteamPath; private static string _steamConfigVdfFile; private static string _registrySteamKey = @"SOFTWARE\\Valve\\Steam"; - private static string _registryAppsKey = $@"{_registrySteamKey}\\Apps"; + private static string _registryAppsKey = $@"{_registrySteamKey}\\Apps";*/ private string _gameRegistryKey; private uint _steamGameId; private string _steamGameName; private string _steamGamePath; private string _steamGameExe; private string _steamGameIconPath; - private static List _allSteamGames; + private static List _allInstalledSteamGames = null; - private struct SteamAppInfo + /*private struct SteamAppInfo { public uint GameID; public string GameName; public List GameExes; public string GameInstallDir; public string GameSteamIconPath; - } + }*/ static SteamGame() { @@ -62,27 +62,18 @@ namespace HeliosPlus.GameLibraries public SteamGame(uint steamGameId, string steamGameName, string steamGamePath, string steamGameExe, string steamGameIconPath) { - _gameRegistryKey = $@"{_registryAppsKey}\\{steamGameId}"; + _gameRegistryKey = $@"{SteamLibrary.SteamAppsRegistryKey}\\{steamGameId}"; _steamGameId = steamGameId; _steamGameName = steamGameName; _steamGamePath = steamGamePath; _steamGameExe = steamGameExe; _steamGameIconPath = steamGameIconPath; - // Find the SteamExe location, and the SteamPath for later - using (var key = Registry.CurrentUser.OpenSubKey(_registrySteamKey, RegistryKeyPermissionCheck.ReadSubTree)) - { - _steamExe = (string)key?.GetValue(@"SteamExe", string.Empty) ?? string.Empty; - _steamExe = _steamExe.Replace('/', '\\'); - _steamPath = (string)key?.GetValue(@"SteamPath", string.Empty) ?? string.Empty; - _steamPath = _steamPath.Replace('/', '\\'); - } - } public uint GameId { get => _steamGameId; } - public static SupportedGameLibrary GameLibrary { get => SupportedGameLibrary.Steam; } + public SupportedGameLibrary GameLibrary { get => SupportedGameLibrary.Steam; } public string GameIconPath { get => _steamGameIconPath; } @@ -158,324 +149,22 @@ namespace HeliosPlus.GameLibraries public string GameName { get => _steamGameName; } - public static string SteamExe { get => _steamExe; } - public string GamePath { get => _steamGamePath; } - public static List AllGames { get => _allSteamGames; } - - public static bool SteamInstalled + public bool CopyTo(SteamGame steamGame) { - get - { - if (!string.IsNullOrWhiteSpace(SteamGame._steamExe) && File.Exists(SteamGame._steamExe)) - { - return true; - } + if (!(steamGame is SteamGame)) return false; - } - } + // Copy all the shortcut data over to the other Shortcut + steamGame.GameIconPath = GameIconPath; + steamGame.GameId = GameId; + steamGame.GameName = GameName; + steamGame.GamePath = GamePath; + steamGame.IsRunning = IsRunning; + steamGame.IsUpdating = IsUpdating; - public static List GetAllInstalledGames() - { - List steamGameList = new List(); - _allSteamGames = steamGameList; - - try - { - - // Find the SteamExe location, and the SteamPath for later - using (var key = Registry.CurrentUser.OpenSubKey(_registrySteamKey, RegistryKeyPermissionCheck.ReadSubTree)) - { - _steamExe = (string)key?.GetValue(@"SteamExe", string.Empty) ?? string.Empty; - _steamExe = _steamExe.Replace('/','\\'); - _steamPath = (string)key?.GetValue(@"SteamPath", string.Empty) ?? string.Empty; - _steamPath = _steamPath.Replace('/', '\\'); - } - - if (_steamExe == string.Empty || !File.Exists(_steamExe)) - { - // Steam isn't installed, so we return an empty list. - return steamGameList; - } - - //Icon _steamIcon = Icon.ExtractAssociatedIcon(_steamExe); - //IconExtractor steamIconExtractor = new IconExtractor(_steamExe); - //Icon _steamIcon = steamIconExtractor.GetIcon(0); - MultiIcon _steamIcon = new MultiIcon(); - _steamIcon.Load(_steamExe); - - List steamAppIdsInstalled = new List(); - // Now look for what games app id's are actually installed on this computer - using (RegistryKey steamAppsKey = Registry.CurrentUser.OpenSubKey(_registryAppsKey, RegistryKeyPermissionCheck.ReadSubTree)) - { - if (steamAppsKey != null) - { - // Loop through the subKeys as they are the Steam Game IDs - foreach (string steamGameKeyName in steamAppsKey.GetSubKeyNames()) - { - uint steamAppId = 0; - if (uint.TryParse(steamGameKeyName, out steamAppId)) - { - string steamGameKeyFullName = $"{_registryAppsKey}\\{steamGameKeyName}"; - using (RegistryKey steamGameKey = Registry.CurrentUser.OpenSubKey(steamGameKeyFullName, RegistryKeyPermissionCheck.ReadSubTree)) - { - // If the Installed Value is set to 1, then the game is installed - // We want to keep track of that for later - if ((int)steamGameKey.GetValue(@"Installed", 0) == 1) - { - // Add this Steam App ID to the list we're keeping for later - steamAppIdsInstalled.Add(steamAppId); - } - - } - - } - } - } - } - - // Now we parse the steam appinfo.vdf to get access to things like: - // - The game name - // - THe game installation dir - // - Sometimes the game icon - // - Sometimes the game executable name (from which we can get the icon) - Dictionary steamAppInfo = new Dictionary(); - - string appInfoVdfFile = Path.Combine(_steamPath, "appcache", "appinfo.vdf"); - var newAppInfo = new AppInfo(); - newAppInfo.Read(appInfoVdfFile); - - Console.WriteLine($"{newAppInfo.Apps.Count} apps"); - - // Chec through all the apps we've extracted - foreach (var app in newAppInfo.Apps) - { - // We only care about the appIDs we have listed as actual games - // (The AppIds include all other DLC and Steam specific stuff too) - if ( steamAppIdsInstalled.Contains(app.AppID)) - { - - try - { - - SteamAppInfo steamGameAppInfo = new SteamAppInfo(); - steamGameAppInfo.GameID = app.AppID; - steamGameAppInfo.GameExes = new List(); - - foreach (KVObject data in app.Data) - { - //Console.WriteLine($"App: {app.AppID} - Data.Name: {data.Name}"); - - if (data.Name == "common") - { - foreach (KVObject common in data.Children) { - - //Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); - - if (common.Name == "name") - { - Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); - steamGameAppInfo.GameName = common.Value.ToString(); - } - else if (common.Name == "clienticon") - { - Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); - steamGameAppInfo.GameSteamIconPath = Path.Combine(_steamPath, @"steam", @"games", String.Concat(common.Value, @".ico")); - } - else if (common.Name == "type") - { - Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); - } - } - } - else if (data.Name == "config") - { - foreach (KVObject config in data.Children) - { - //Console.WriteLine($"App: {app.AppID} - Config {config.Name}: {config.Value}"); - - if (config.Name == "installdir") - { - Console.WriteLine($"App: {app.AppID} - Config {config.Name}: {config.Value}"); - steamGameAppInfo.GameInstallDir = config.Value.ToString(); - } - else if (config.Name == "launch") - { - foreach (KVObject launch in config.Children) - { - foreach (KVObject launch_num in launch.Children) - { - if (launch_num.Name == "executable") - { - Console.WriteLine($"App: {app.AppID} - Config - Launch {launch.Name} - {launch_num.Name}: {launch_num.Value}"); - steamGameAppInfo.GameExes.Add(launch_num.Value.ToString()); - } - - } - } - } - } - } - - } - steamAppInfo.Add(app.AppID, steamGameAppInfo); - } - catch (ArgumentException ex) - { - Console.WriteLine($"SteamGame/GetAllInstalledGames exception: {ex.Message}: {ex.InnerException}"); - //we just want to ignore it if we try to add it twice.... - } - - Console.WriteLine($"App: {app.AppID} - Token: {app.Token}"); - } - } - - - - // Now we access the config.vdf that lives in the Steam Config file, as that lists all - // the SteamLibraries. We need to find out where they areso we can interrogate them - _steamConfigVdfFile = Path.Combine(_steamPath, "config", "config.vdf"); - string steamConfigVdfText = File.ReadAllText(_steamConfigVdfFile, Encoding.UTF8); - - List steamLibrariesPaths = new List(); - // Now we have to parse the config.vdf looking for the location of the SteamLibraries - // We look for lines similar to this: "BaseInstallFolder_1" "E:\\SteamLibrary" - // There may be multiple so we need to check the whole file - Regex steamLibrariesRegex = new Regex(@"""BaseInstallFolder_\d+""\s+""(.*)""", RegexOptions.IgnoreCase); - // Try to match all lines against the Regex. - Match steamLibrariesMatches = steamLibrariesRegex.Match(steamConfigVdfText); - // If at least one of them matched! - if (steamLibrariesMatches.Success) - { - // Loop throug the results and add to an array - for (int i = 1; i < steamLibrariesMatches.Groups.Count; i++) - { - string steamLibraryPath = Regex.Unescape(steamLibrariesMatches.Groups[i].Value); - Console.WriteLine($"Found steam library: {steamLibraryPath}"); - steamLibrariesPaths.Add(steamLibraryPath); - } - } - - // Now we go off and find the details for the games in each Steam Library - foreach (string steamLibraryPath in steamLibrariesPaths) - { - // Work out the path to the appmanifests for this steamLibrary - string steamLibraryAppManifestPath = Path.Combine(steamLibraryPath, @"steamapps"); - // Get the names of the App Manifests for the games installed in this SteamLibrary - string[] steamLibraryAppManifestFilenames = Directory.GetFiles(steamLibraryAppManifestPath, "appmanifest_*.acf"); - // Go through each app and extract it's details - foreach (string steamLibraryAppManifestFilename in steamLibraryAppManifestFilenames) - { - // Read in the contents of the file - string steamLibraryAppManifestText = File.ReadAllText(steamLibraryAppManifestFilename); - // Grab the appid from the file - Regex appidRegex = new Regex(@"""appid""\s+""(\d+)""", RegexOptions.IgnoreCase); - Match appidMatches = appidRegex.Match(steamLibraryAppManifestText); - if (appidMatches.Success) - { - - uint steamGameId = 0; - if (uint.TryParse(appidMatches.Groups[1].Value, out steamGameId)) - { - // Check if this game is one that was installed - if (steamAppInfo.ContainsKey(steamGameId)) { - // This game is an installed game! so we start to populate it with data! - string steamGameExe = ""; - - string steamGameName = steamAppInfo[steamGameId].GameName; - - // Construct the full path to the game dir from the appInfo and libraryAppManifest data - string steamGameInstallDir = Path.Combine(steamLibraryPath, @"steamapps", @"common", steamAppInfo[steamGameId].GameInstallDir); - - // Next, we need to get the Icons we want to use, and make sure it's the latest one. - string steamGameIconPath = ""; - // First of all, we attempt to use the Icon that Steam has cached, if it's available, as that will be updated to the latest - if (File.Exists(steamAppInfo[steamGameId].GameSteamIconPath)) - { - steamGameIconPath = steamAppInfo[steamGameId].GameSteamIconPath; - } - // If there isn't an icon for us to use, then we need to extract one from the Game Executables - else if (steamAppInfo[steamGameId].GameExes.Count > 0) - { - foreach (string gameExe in steamAppInfo[steamGameId].GameExes) - { - steamGameExe = Path.Combine(steamGameInstallDir, gameExe); - // If the game executable exists, then we can proceed - if (File.Exists(steamGameExe)) - { - // Now we need to get the Icon from the app if possible if it's not in the games folder - steamGameIconPath = steamGameExe; - } - } - - } - // The absolute worst case means we don't have an icon to use. SO we use the Steam one. - else - { - // And we have to make do with a Steam Icon - steamGameIconPath = _steamPath; - - } - - // And finally we try to populate the 'where', to see what gets run - // And so we can extract the process name - if (steamAppInfo[steamGameId].GameExes.Count > 0) - { - foreach (string gameExe in steamAppInfo[steamGameId].GameExes) - { - steamGameExe = Path.Combine(steamGameInstallDir, gameExe); - // If the game executable exists, then we can proceed - if (File.Exists(steamGameExe)) - { - break; - } - } - - } - - // And we add the Game to the list of games we have! - steamGameList.Add(new SteamGame(steamGameId, steamGameName, steamGameInstallDir, steamGameExe, steamGameIconPath)); - - } - } - } - } - } - } - catch (SecurityException ex) - { - Console.WriteLine($"SteamGame/GetAllInstalledGames securityexception: {ex.Message}: {ex.InnerException}"); - if (ex.Source != null) - Console.WriteLine("SecurityException source: {0} - Message: {1}", ex.Source, ex.Message); - throw; - } - catch (UnauthorizedAccessException ex) - { - Console.WriteLine($"SteamGame/GetAllInstalledGames unauthorizedaccessexception: {ex.Message}: {ex.InnerException}"); - if (ex.Source != null) - Console.WriteLine("UnauthorizedAccessException source: {0} - Message: {1}", ex.Source, ex.Message); - throw; - } - catch (ObjectDisposedException ex) - { - Console.WriteLine($"SteamGame/GetAllInstalledGames objectdisposedexceptions: {ex.Message}: {ex.InnerException}"); - if (ex.Source != null) - Console.WriteLine("ObjectDisposedException source: {0} - Message: {1}", ex.Source, ex.Message); - throw; - } - catch (IOException ex) - { - Console.WriteLine($"SteamGame/GetAllInstalledGames ioexception: {ex.Message}: {ex.InnerException}"); - // Extract some information from this exception, and then - // throw it to the parent method. - if (ex.Source != null) - Console.WriteLine("IOException source: {0} - Message: {1}", ex.Source, ex.Message); - throw; - } - - return steamGameList; + return true; } public override string ToString() diff --git a/HeliosPlus/GameLibraries/SteamLibrary.cs b/HeliosPlus/GameLibraries/SteamLibrary.cs new file mode 100644 index 0000000..e42d610 --- /dev/null +++ b/HeliosPlus/GameLibraries/SteamLibrary.cs @@ -0,0 +1,608 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using ValveKeyValue; +using HeliosPlus.GameLibraries.SteamAppInfoParser; +using Microsoft.Win32; +using System.IO; +using System.Drawing.IconLib; +using System.Security; + +namespace HeliosPlus.GameLibraries +{ + public static class SteamLibrary + { + #region Class Variables + // Common items to the class + private static List _allSteamGames = null; + private static string steamAppIdRegex = @"/^[0-9A-F]{1,10}$"; + private static string _steamExe; + private static string _steamPath; + private static string _steamConfigVdfFile; + private static string _registrySteamKey = @"SOFTWARE\\Valve\\Steam"; + private static string _registryAppsKey = $@"{_registrySteamKey}\\Apps"; + // Other constants that are useful + #endregion + + private struct SteamAppInfo + { + public uint GameID; + public string GameName; + public List GameExes; + public string GameInstallDir; + public string GameSteamIconPath; + } + + #region Class Constructors + static SteamLibrary() + { + // Find the SteamExe location, and the SteamPath for later + using (var key = Registry.CurrentUser.OpenSubKey(SteamLibrary.SteamRegistryKey, RegistryKeyPermissionCheck.ReadSubTree)) + { + _steamExe = (string)key?.GetValue(@"SteamExe", string.Empty) ?? string.Empty; + _steamExe = _steamExe.Replace('/', '\\'); + _steamPath = (string)key?.GetValue(@"SteamPath", string.Empty) ?? string.Empty; + _steamPath = _steamPath.Replace('/', '\\'); + } + + // Load the Shortcuts from storage + LoadInstalledSteamGames(); + } + #endregion + + #region Class Properties + public static List AllInstalledGames + { + get + { + // Load the Steam Games from Steam Client if needed + if (_allSteamGames == null) + LoadInstalledSteamGames(); + return _allSteamGames; + } + } + + + public static int InstalledSteamGameCount + { + get + { + return _allSteamGames.Count; + } + } + + public static string SteamRegistryKey + { + get + { + return _registrySteamKey; + } + } + + public static string SteamAppsRegistryKey + { + get + { + return _registryAppsKey; + } + } + + public static string SteamExe + { + get + { + return _steamExe; + } + } + + public static string SteamPath + { + get + { + return _steamPath; + } + } + + public static bool IsSteamInstalled + { + get + { + if (!string.IsNullOrWhiteSpace(SteamExe) && File.Exists(SteamExe)) + return true; + + return false; + } + + } + + + #endregion + + #region Class Methods + public static bool AddSteamGame(SteamGame steamGame) + { + if (!(steamGame is SteamGame)) + return false; + + // Doublecheck if it already exists + // Because then we just update the one that already exists + if (ContainsSteamGame(steamGame)) + { + // We update the existing Shortcut with the data over + SteamGame steamGameToUpdate = GetSteamGame(steamGame.GameId.ToString()); + steamGame.CopyTo(steamGameToUpdate); + } + else + { + // Add the steamGame to the list of steamGames + _allSteamGames.Add(steamGame); + } + + //Doublecheck it's been added + if (ContainsSteamGame(steamGame)) + { + return true; + } + else + return false; + + } + + public static bool RemoveSteamGame(SteamGame steamGame) + { + if (!(steamGame is SteamGame)) + return false; + + // Remove the steamGame from the list. + int numRemoved = _allSteamGames.RemoveAll(item => item.GameId.Equals(steamGame.GameId)); + + if (numRemoved == 1) + { + return true; + } + else if (numRemoved == 0) + return false; + else + throw new SteamLibraryException(); + } + + + public static bool RemoveSteamGame(string steamGameNameOrUuid) + { + if (String.IsNullOrWhiteSpace(steamGameNameOrUuid)) + return false; + + int numRemoved; + Match match = Regex.Match(steamGameNameOrUuid, steamAppIdRegex, RegexOptions.IgnoreCase); + if (match.Success) + numRemoved = _allSteamGames.RemoveAll(item => steamGameNameOrUuid.Equals(Convert.ToUInt32(item.GameId))); + else + numRemoved = _allSteamGames.RemoveAll(item => steamGameNameOrUuid.Equals(item.GameName)); + + if (numRemoved == 1) + return true; + else if (numRemoved == 0) + return false; + else + throw new SteamLibraryException(); + + } + + + public static bool ContainsSteamGame(SteamGame steamGame) + { + if (!(steamGame is SteamGame)) + return false; + + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (testSteamGame.GameId.Equals(steamGame.GameId)) + return true; + } + + return false; + } + + public static bool ContainsSteamGame(string steamGameNameOrUuid) + { + if (String.IsNullOrWhiteSpace(steamGameNameOrUuid)) + return false; + + + Match match = Regex.Match(steamGameNameOrUuid, steamAppIdRegex, RegexOptions.IgnoreCase); + if (match.Success) + { + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (steamGameNameOrUuid.Equals(Convert.ToUInt32(testSteamGame.GameId))) + return true; + } + + } + else + { + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (steamGameNameOrUuid.Equals(testSteamGame.GameName)) + return true; + } + + } + + return false; + + } + + public static bool ContainsSteamGame(uint steamGameId) + { + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (steamGameId == testSteamGame.GameId) + return true; + } + + + return false; + + } + + + public static SteamGame GetSteamGame(string steamGameNameOrUuid) + { + if (String.IsNullOrWhiteSpace(steamGameNameOrUuid)) + return null; + + Match match = Regex.Match(steamGameNameOrUuid, steamAppIdRegex, RegexOptions.IgnoreCase); + if (match.Success) + { + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (steamGameNameOrUuid.Equals(Convert.ToUInt32(testSteamGame.GameId))) + return testSteamGame; + } + + } + else + { + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (steamGameNameOrUuid.Equals(testSteamGame.GameName)) + return testSteamGame; + } + + } + + return null; + + } + + public static SteamGame GetSteamGame(uint steamGameId) + { + foreach (SteamGame testSteamGame in _allSteamGames) + { + if (steamGameId == testSteamGame.GameId) + return testSteamGame; + } + + return null; + + } + + private static bool LoadInstalledSteamGames() + { + try + { + + // Find the SteamExe location, and the SteamPath for later + using (var key = Registry.CurrentUser.OpenSubKey(_registrySteamKey, RegistryKeyPermissionCheck.ReadSubTree)) + { + _steamExe = (string)key?.GetValue(@"SteamExe", string.Empty) ?? string.Empty; + _steamExe = _steamExe.Replace('/', '\\'); + _steamPath = (string)key?.GetValue(@"SteamPath", string.Empty) ?? string.Empty; + _steamPath = _steamPath.Replace('/', '\\'); + } + + if (_steamExe == string.Empty || !File.Exists(_steamExe)) + { + // Steam isn't installed, so we return an empty list. + return false; + } + + //Icon _steamIcon = Icon.ExtractAssociatedIcon(_steamExe); + //IconExtractor steamIconExtractor = new IconExtractor(_steamExe); + //Icon _steamIcon = steamIconExtractor.GetIcon(0); + MultiIcon _steamIcon = new MultiIcon(); + _steamIcon.Load(_steamExe); + + List steamAppIdsInstalled = new List(); + // Now look for what games app id's are actually installed on this computer + using (RegistryKey steamAppsKey = Registry.CurrentUser.OpenSubKey(_registryAppsKey, RegistryKeyPermissionCheck.ReadSubTree)) + { + if (steamAppsKey != null) + { + // Loop through the subKeys as they are the Steam Game IDs + foreach (string steamGameKeyName in steamAppsKey.GetSubKeyNames()) + { + uint steamAppId = 0; + if (uint.TryParse(steamGameKeyName, out steamAppId)) + { + string steamGameKeyFullName = $"{_registryAppsKey}\\{steamGameKeyName}"; + using (RegistryKey steamGameKey = Registry.CurrentUser.OpenSubKey(steamGameKeyFullName, RegistryKeyPermissionCheck.ReadSubTree)) + { + // If the Installed Value is set to 1, then the game is installed + // We want to keep track of that for later + if ((int)steamGameKey.GetValue(@"Installed", 0) == 1) + { + // Add this Steam App ID to the list we're keeping for later + steamAppIdsInstalled.Add(steamAppId); + } + + } + + } + } + } + } + + // Now we parse the steam appinfo.vdf to get access to things like: + // - The game name + // - THe game installation dir + // - Sometimes the game icon + // - Sometimes the game executable name (from which we can get the icon) + Dictionary steamAppInfo = new Dictionary(); + + string appInfoVdfFile = Path.Combine(_steamPath, "appcache", "appinfo.vdf"); + var newAppInfo = new AppInfo(); + newAppInfo.Read(appInfoVdfFile); + + Console.WriteLine($"{newAppInfo.Apps.Count} apps"); + + // Chec through all the apps we've extracted + foreach (var app in newAppInfo.Apps) + { + // We only care about the appIDs we have listed as actual games + // (The AppIds include all other DLC and Steam specific stuff too) + if (steamAppIdsInstalled.Contains(app.AppID)) + { + + try + { + + SteamAppInfo steamGameAppInfo = new SteamAppInfo(); + steamGameAppInfo.GameID = app.AppID; + steamGameAppInfo.GameExes = new List(); + + foreach (KVObject data in app.Data) + { + //Console.WriteLine($"App: {app.AppID} - Data.Name: {data.Name}"); + + if (data.Name == "common") + { + foreach (KVObject common in data.Children) + { + + //Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); + + if (common.Name == "name") + { + Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); + steamGameAppInfo.GameName = common.Value.ToString(); + } + else if (common.Name == "clienticon") + { + Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); + steamGameAppInfo.GameSteamIconPath = Path.Combine(_steamPath, @"steam", @"games", String.Concat(common.Value, @".ico")); + } + else if (common.Name == "type") + { + Console.WriteLine($"App: {app.AppID} - Common {common.Name}: {common.Value}"); + } + } + } + else if (data.Name == "config") + { + foreach (KVObject config in data.Children) + { + //Console.WriteLine($"App: {app.AppID} - Config {config.Name}: {config.Value}"); + + if (config.Name == "installdir") + { + Console.WriteLine($"App: {app.AppID} - Config {config.Name}: {config.Value}"); + steamGameAppInfo.GameInstallDir = config.Value.ToString(); + } + else if (config.Name == "launch") + { + foreach (KVObject launch in config.Children) + { + foreach (KVObject launch_num in launch.Children) + { + if (launch_num.Name == "executable") + { + Console.WriteLine($"App: {app.AppID} - Config - Launch {launch.Name} - {launch_num.Name}: {launch_num.Value}"); + steamGameAppInfo.GameExes.Add(launch_num.Value.ToString()); + } + + } + } + } + } + } + + } + steamAppInfo.Add(app.AppID, steamGameAppInfo); + } + catch (ArgumentException ex) + { + Console.WriteLine($"SteamGame/GetAllInstalledGames exception: {ex.Message}: {ex.InnerException}"); + //we just want to ignore it if we try to add it twice.... + } + + Console.WriteLine($"App: {app.AppID} - Token: {app.Token}"); + } + } + + + + // Now we access the config.vdf that lives in the Steam Config file, as that lists all + // the SteamLibraries. We need to find out where they areso we can interrogate them + _steamConfigVdfFile = Path.Combine(_steamPath, "config", "config.vdf"); + string steamConfigVdfText = File.ReadAllText(_steamConfigVdfFile, Encoding.UTF8); + + List steamLibrariesPaths = new List(); + // Now we have to parse the config.vdf looking for the location of the SteamLibraries + // We look for lines similar to this: "BaseInstallFolder_1" "E:\\SteamLibrary" + // There may be multiple so we need to check the whole file + Regex steamLibrariesRegex = new Regex(@"""BaseInstallFolder_\d+""\s+""(.*)""", RegexOptions.IgnoreCase); + // Try to match all lines against the Regex. + Match steamLibrariesMatches = steamLibrariesRegex.Match(steamConfigVdfText); + // If at least one of them matched! + if (steamLibrariesMatches.Success) + { + // Loop throug the results and add to an array + for (int i = 1; i < steamLibrariesMatches.Groups.Count; i++) + { + string steamLibraryPath = Regex.Unescape(steamLibrariesMatches.Groups[i].Value); + Console.WriteLine($"Found steam library: {steamLibraryPath}"); + steamLibrariesPaths.Add(steamLibraryPath); + } + } + + // Now we go off and find the details for the games in each Steam Library + foreach (string steamLibraryPath in steamLibrariesPaths) + { + // Work out the path to the appmanifests for this steamLibrary + string steamLibraryAppManifestPath = Path.Combine(steamLibraryPath, @"steamapps"); + // Get the names of the App Manifests for the games installed in this SteamLibrary + string[] steamLibraryAppManifestFilenames = Directory.GetFiles(steamLibraryAppManifestPath, "appmanifest_*.acf"); + // Go through each app and extract it's details + foreach (string steamLibraryAppManifestFilename in steamLibraryAppManifestFilenames) + { + // Read in the contents of the file + string steamLibraryAppManifestText = File.ReadAllText(steamLibraryAppManifestFilename); + // Grab the appid from the file + Regex appidRegex = new Regex(@"""appid""\s+""(\d+)""", RegexOptions.IgnoreCase); + Match appidMatches = appidRegex.Match(steamLibraryAppManifestText); + if (appidMatches.Success) + { + + uint steamGameId = 0; + if (uint.TryParse(appidMatches.Groups[1].Value, out steamGameId)) + { + // Check if this game is one that was installed + if (steamAppInfo.ContainsKey(steamGameId)) + { + // This game is an installed game! so we start to populate it with data! + string steamGameExe = ""; + + string steamGameName = steamAppInfo[steamGameId].GameName; + + // Construct the full path to the game dir from the appInfo and libraryAppManifest data + string steamGameInstallDir = Path.Combine(steamLibraryPath, @"steamapps", @"common", steamAppInfo[steamGameId].GameInstallDir); + + // Next, we need to get the Icons we want to use, and make sure it's the latest one. + string steamGameIconPath = ""; + // First of all, we attempt to use the Icon that Steam has cached, if it's available, as that will be updated to the latest + if (File.Exists(steamAppInfo[steamGameId].GameSteamIconPath)) + { + steamGameIconPath = steamAppInfo[steamGameId].GameSteamIconPath; + } + // If there isn't an icon for us to use, then we need to extract one from the Game Executables + else if (steamAppInfo[steamGameId].GameExes.Count > 0) + { + foreach (string gameExe in steamAppInfo[steamGameId].GameExes) + { + steamGameExe = Path.Combine(steamGameInstallDir, gameExe); + // If the game executable exists, then we can proceed + if (File.Exists(steamGameExe)) + { + // Now we need to get the Icon from the app if possible if it's not in the games folder + steamGameIconPath = steamGameExe; + } + } + + } + // The absolute worst case means we don't have an icon to use. SO we use the Steam one. + else + { + // And we have to make do with a Steam Icon + steamGameIconPath = _steamPath; + + } + + // And finally we try to populate the 'where', to see what gets run + // And so we can extract the process name + if (steamAppInfo[steamGameId].GameExes.Count > 0) + { + foreach (string gameExe in steamAppInfo[steamGameId].GameExes) + { + steamGameExe = Path.Combine(steamGameInstallDir, gameExe); + // If the game executable exists, then we can proceed + if (File.Exists(steamGameExe)) + { + break; + } + } + + } + + // And we add the Game to the list of games we have! + _allSteamGames.Add(new SteamGame(steamGameId, steamGameName, steamGameInstallDir, steamGameExe, steamGameIconPath)); + + } + } + } + } + } + } + catch (SecurityException ex) + { + Console.WriteLine($"SteamGame/GetAllInstalledGames securityexception: {ex.Message}: {ex.InnerException}"); + if (ex.Source != null) + Console.WriteLine("SecurityException source: {0} - Message: {1}", ex.Source, ex.Message); + throw; + } + catch (UnauthorizedAccessException ex) + { + Console.WriteLine($"SteamGame/GetAllInstalledGames unauthorizedaccessexception: {ex.Message}: {ex.InnerException}"); + if (ex.Source != null) + Console.WriteLine("UnauthorizedAccessException source: {0} - Message: {1}", ex.Source, ex.Message); + throw; + } + catch (ObjectDisposedException ex) + { + Console.WriteLine($"SteamGame/GetAllInstalledGames objectdisposedexceptions: {ex.Message}: {ex.InnerException}"); + if (ex.Source != null) + Console.WriteLine("ObjectDisposedException source: {0} - Message: {1}", ex.Source, ex.Message); + throw; + } + catch (IOException ex) + { + Console.WriteLine($"SteamGame/GetAllInstalledGames ioexception: {ex.Message}: {ex.InnerException}"); + // Extract some information from this exception, and then + // throw it to the parent method. + if (ex.Source != null) + Console.WriteLine("IOException source: {0} - Message: {1}", ex.Source, ex.Message); + throw; + } + + return true; + } + + #endregion + + } + + [global::System.Serializable] + public class SteamLibraryException : Exception + { + public SteamLibraryException() { } + public SteamLibraryException(string message) : base(message) { } + public SteamLibraryException(string message, Exception inner) : base(message, inner) { } + protected SteamLibraryException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + +} diff --git a/HeliosPlus/HeliosPlus.csproj b/HeliosPlus/HeliosPlus.csproj index 49880c7..3bfc52c 100644 --- a/HeliosPlus/HeliosPlus.csproj +++ b/HeliosPlus/HeliosPlus.csproj @@ -77,6 +77,7 @@ + diff --git a/HeliosPlus/Program.cs b/HeliosPlus/Program.cs index 88d8177..c88c015 100644 --- a/HeliosPlus/Program.cs +++ b/HeliosPlus/Program.cs @@ -21,6 +21,13 @@ using System.Text.RegularExpressions; using System.Drawing; namespace HeliosPlus { + public enum SupportedProgramMode + { + RunShortcut, + EditProfile, + StartUpNormally + } + public enum SupportedGameLibrary { Unknown, @@ -41,76 +48,7 @@ namespace HeliosPlus { //internal static string ShortcutIconCachePath; - internal static ProfileItem GetProfile(string profileName) - { - // Create an array of display profiles we have - var profiles = ProfileRepository.AllProfiles.ToArray(); - // Check if the user supplied a --profile option using the profiles' ID - var profileIndex = profiles.Length > 0 ? Array.FindIndex(profiles, p => p.UUID.Equals(profileName, StringComparison.InvariantCultureIgnoreCase)) : -1; - // If the profileID wasn't there, maybe they used the profile name? - if (profileIndex == -1) - { - // Try and lookup the profile in the profiles' Name fields - profileIndex = profiles.Length > 0 ? Array.FindIndex(profiles, p => p.Name.StartsWith(profileName, StringComparison.InvariantCultureIgnoreCase)) : -1; - } - - return profiles[profileIndex]; - } - - internal static bool GoProfile(ProfileItem profile) - { - // If we're already on the wanted profile then no need to change! - if (ProfileRepository.IsActiveProfile(profile)) - return true; - - var instanceStatus = IPCService.GetInstance().Status; - - try - { - IPCService.GetInstance().Status = InstanceStatus.Busy; - var failed = false; - - if (new ApplyingChangesForm(() => - { - Task.Factory.StartNew(() => - { - if (!(ProfileRepository.ApplyProfile(profile))) - { - failed = true; - } - }, TaskCreationOptions.LongRunning); - }, 3, 30).ShowDialog() != - DialogResult.Cancel) - { - if (failed) - { - throw new Exception(Language.Profile_is_invalid_or_not_possible_to_apply); - } - - return true; - } - - return false; - } - finally - { - IPCService.GetInstance().Status = instanceStatus; - } - } - - private static void EditProfile(ProfileItem profile) - { - // Get the status of the thing - IPCService.GetInstance().Status = InstanceStatus.User; - // Load all the profiles from JSON - //ProfileRepository.AllProfiles - // Start up the DisplayProfileForm directly - new DisplayProfileForm(profile).ShowDialog(); - // Then we close down as we're only here to edit one profile - Application.Exit(); - } - - + /// /// The main entry point for the application. /// @@ -125,7 +63,7 @@ namespace HeliosPlus { Console.Write("="); } Console.WriteLine("="); - Console.WriteLine(@"Copyright © Terry MacDonald 2020-{DateTime.Today.Year}"); + Console.WriteLine($"Copyright © Terry MacDonald 2020-{DateTime.Today.Year}"); Console.WriteLine(@"Based on Helios Display Management - Copyright © Soroush Falahati 2017-2020"); var app = new CommandLineApplication(); @@ -144,28 +82,26 @@ namespace HeliosPlus { return string.Format("Version {0}", Assembly.GetExecutingAssembly().GetName().Version); }); - // This is the SwitchProfile command - app.Command("RunShortcut", (switchProfileCmd) => + // This is the RunShortcut command + app.Command(SupportedProgramMode.RunShortcut.ToString(), (switchProfileCmd) => { - var argumentShortcut = switchProfileCmd.Argument("\"SHORTCUT_NAME\"", "(required) The name of the shortcut to run from those stored in the shortcut library.").IsRequired(); + var argumentShortcut = switchProfileCmd.Argument("\"SHORTCUT_UUID\"", "(required) The UUID of the shortcut to run from those stored in the shortcut library.").IsRequired(); argumentShortcut.Validators.Add(new ShortcutMustExistValidator()); //description and help text of the command. - switchProfileCmd.Description = "Use this command to temporarily change profiles, and load your favourite game or application."; + switchProfileCmd.Description = "Use this command to run favourite game or application with a display profile of your choosing."; switchProfileCmd.OnExecute(() => { - Console.WriteLine($"Editing profile {argumentShortcut.Value}"); - - SwitchToProfile(GetProfile(argumentShortcut.Value)); - + // + RunShortcut(argumentShortcut.Value); return 0; }); }); - // This is the EditProfile command - app.Command("EditProfile", (editProfileCmd) => + /*// This is the EditProfile command + app.Command(SupportedProgramMode.EditProfile.ToString(), (editProfileCmd) => { //description and help text of the command. editProfileCmd.Description = "Use this command to edit a HeliosDMPlus profile."; @@ -184,7 +120,7 @@ namespace HeliosPlus { return 0; }); - }); + });*/ app.OnExecute(() => { @@ -312,15 +248,15 @@ namespace HeliosPlus { } - private static void SwitchToExecutable(ProfileItem profile, string executableToRun, string processToMonitor, uint timeout, string executableArguments) + + // ReSharper disable once CyclomaticComplexity + private static void RunShortcut(string shortcutUUID) { - var rollbackProfile = ProfileRepository.CurrentProfile; - - if (!profile.IsPossible) - { - throw new Exception(Language.Selected_profile_is_not_possible); - } + ProfileItem rollbackProfile = ProfileRepository.CurrentProfile; + ShortcutItem shortcutToRun = null; + // Check there is only one version of this application so we won't + // mess with another monitoring session if ( IPCClient.QueryAll() .Any( @@ -333,384 +269,298 @@ namespace HeliosPlus { .Another_instance_of_this_program_is_in_working_state_Please_close_other_instances_before_trying_to_switch_profile); } - if (!GoProfile(profile)) + // Match the ShortcutName to the actual shortcut listed in the shortcut library + // And error if we can't find it. + if (ShortcutRepository.ContainsShortcut(shortcutUUID)) { - throw new Exception(Language.Can_not_change_active_profile); + // make sure we trim the "" if there are any + shortcutUUID = shortcutUUID.Trim('"'); + shortcutToRun = ShortcutRepository.GetShortcut(shortcutUUID); + } + else + { + throw new Exception(Language.Cannot_find_shortcut_in_library); } - var process = System.Diagnostics.Process.Start(executableToRun, executableArguments); - var processes = new System.Diagnostics.Process[0]; - - var ticks = 0; - - while (ticks < timeout * 1000) + // Do some validation to make sure the shortcut is sensible + // And that we have enough to try and action within the shortcut + // (in other words check everything in the shortcut is still valid) + (bool valid, string reason) = shortcutToRun.IsValid(); + if (!valid) { + throw new Exception(string.Format("Unable to run the shortcut '{0}': {1}",shortcutToRun.Name,reason)); + } - processes = System.Diagnostics.Process.GetProcessesByName(processToMonitor); + // Try to change to the wanted profile + if (!SwitchProfile(shortcutToRun.ProfileToUse)) + { + throw new Exception(Language.Cannot_change_active_profile); + } - if (processes.Length > 0) + // Now run the pre-start applications + // TODO: Add the prestart applications + + // Now start the main game, and wait if we have to + if (shortcutToRun.Category.Equals(ShortcutCategory.Application)) + { + // Start the executable + Process process = null; + if (shortcutToRun.ExecutableArgumentsRequired) + process = System.Diagnostics.Process.Start(shortcutToRun.ExecutableNameAndPath, shortcutToRun.ExecutableArguments); + else + process = System.Diagnostics.Process.Start(shortcutToRun.ExecutableNameAndPath); + + // Create a list of processes to monitor + Process[] processesToMonitor = Array.Empty(); + + // Work out if we are monitoring another process other than the main executable + if (shortcutToRun.ProcessNameToMonitorUsesExecutable) { - break; + // If we are monitoring the same executable we started, then lets do that + processesToMonitor = new[] { process }; } - - Thread.Sleep(300); - ticks += 300; - } - - - if (processes.Length == 0) - { - processes = new[] { process }; - } - - IPCService.GetInstance().HoldProcessId = processes.FirstOrDefault()?.Id ?? 0; - IPCService.GetInstance().Status = InstanceStatus.OnHold; - NotifyIcon notify = null; - - try - { - notify = new NotifyIcon + else { - Icon = Properties.Resources.HeliosPlus, - Text = string.Format( - Language.Waiting_for_the_0_to_terminate, - processes[0].ProcessName), - Visible = true - }; - Application.DoEvents(); - } - catch (Exception ex) - { - Console.WriteLine($"Program/SwitchToExecutable exception: {ex.Message}: {ex.InnerException}"); - // ignored - } + // Now wait a little while for all the processes we want to monitor to start up + var ticks = 0; + while (ticks < shortcutToRun.ExecutableTimeout * 1000) + { + // Look for the processes with the ProcessName we want (which in Windows is the filename without the extension) + processesToMonitor = System.Diagnostics.Process.GetProcessesByName(Path.GetFileNameWithoutExtension(shortcutToRun.DifferentExecutableToMonitor)); - foreach (var p in processes) - { + // TODO: Fix this logic error that will only ever wait for the first process.... + if (processesToMonitor.Length > 0) + { + break; + } + + Thread.Sleep(300); + ticks += 300; + } + + // If none started up before the timeout, then ignore the + if (processesToMonitor.Length == 0) + { + processesToMonitor = new[] { process }; + } + } + + // Store the process to monitor for later + IPCService.GetInstance().HoldProcessId = processesToMonitor.FirstOrDefault()?.Id ?? 0; + IPCService.GetInstance().Status = InstanceStatus.OnHold; + + // Add a status notification icon in the status area + NotifyIcon notify = null; try { - p.WaitForExit(); + notify = new NotifyIcon + { + Icon = Properties.Resources.HeliosPlus, + Text = string.Format( + Language.Waiting_for_the_0_to_terminate, + processesToMonitor[0].ProcessName), + Visible = true + }; + Application.DoEvents(); } catch (Exception ex) { - Console.WriteLine($"Program/SwitchToExecutable exception 2: {ex.Message}: {ex.InnerException}"); + Console.WriteLine($"Program/SwitchToExecutable exception: {ex.Message}: {ex.InnerException}"); // ignored } + + // Wait for the monitored process to exit + foreach (var p in processesToMonitor) + { + try + { + p.WaitForExit(); + } + catch (Exception ex) + { + Console.WriteLine($"Program/SwitchToExecutable exception 2: {ex.Message}: {ex.InnerException}"); + // ignored + } + } + + // Remove the status notification icon from the status area + // once we've existed the game + if (notify != null) + { + notify.Visible = false; + notify.Dispose(); + Application.DoEvents(); + } + + } + else if (shortcutToRun.Category.Equals(ShortcutCategory.Game)) + { + // If the game is a Steam Game we check for that + if (shortcutToRun.GameLibrary.Equals(SupportedGameLibrary.Steam)) + { + // We now need to get the SteamGame info + SteamGame steamGameToRun = SteamLibrary.GetSteamGame(shortcutToRun.GameAppId); + + // If the GameAppID matches a Steam game, then lets run it + if (steamGameToRun is SteamGame) + { + // Prepare to start the steam game using the URI interface + // as used by Steam for it's own desktop shortcuts. + var address = $"steam://rungameid/{steamGameToRun.GameId}"; + if (shortcutToRun.GameArgumentsRequired) + { + address += "/" + shortcutToRun.GameArguments; + } + + // Start the URI Handler to run Steam + var steamProcess = System.Diagnostics.Process.Start(address); + + // Wait for Steam game to update if needed + var ticks = 0; + while (ticks < shortcutToRun.GameTimeout * 1000) + { + if (steamGameToRun.IsRunning) + { + break; + } + + Thread.Sleep(300); + + if (!steamGameToRun.IsUpdating) + { + ticks += 300; + } + } + + // Store the Steam Process ID for later + IPCService.GetInstance().HoldProcessId = steamProcess?.Id ?? 0; + IPCService.GetInstance().Status = InstanceStatus.OnHold; + + // Add a status notification icon in the status area + NotifyIcon notify = null; + try + { + notify = new NotifyIcon + { + Icon = Properties.Resources.HeliosPlus, + Text = string.Format( + Language.Waiting_for_the_0_to_terminate, + steamGameToRun.GameName), + Visible = true + }; + Application.DoEvents(); + } + catch (Exception ex) + { + Console.WriteLine($"Program/SwitchToSteamGame exception: {ex.Message}: {ex.InnerException}"); + // ignored + } + + // Wait for the game to exit + if (steamGameToRun.IsRunning) + { + while (true) + { + if (!steamGameToRun.IsRunning) + { + break; + } + + Thread.Sleep(300); + } + } + + // Remove the status notification icon from the status area + // once we've existed the game + if (notify != null) + { + notify.Visible = false; + notify.Dispose(); + Application.DoEvents(); + } + + } + + } + // If the game is a Uplay Game we check for that + /*else if (GameLibrary.Equals(SupportedGameLibrary.Uplay)) + { + // We need to look up details about the game + if (!UplayGame.IsInstalled(GameAppId)) + { + return (false, string.Format("The Uplay Game with AppID '{0}' is not installed on this computer.", GameAppId)); + } + + }*/ + + } - if (notify != null) - { - notify.Visible = false; - notify.Dispose(); - Application.DoEvents(); - } IPCService.GetInstance().Status = InstanceStatus.Busy; // Change back to the original profile if it is different if (!ProfileRepository.IsActiveProfile(rollbackProfile)) { - if (!GoProfile(rollbackProfile)) + if (!SwitchProfile(rollbackProfile)) { - throw new Exception(Language.Can_not_change_active_profile); + throw new Exception(Language.Cannot_change_active_profile); } } + } - - - private static void SwitchToSteamGame(ProfileItem profile, string steamGameIdToRun, uint timeout, string steamGameArguments) + internal static bool SwitchProfile(ProfileItem profile) { + // If we're already on the wanted profile then no need to change! + if (ProfileRepository.IsActiveProfile(profile)) + return true; - // Convert the steamGameIdToRun string to a uint for Steam Games - uint steamGameIdUint = 0; - if (!uint.TryParse(steamGameIdToRun, out steamGameIdUint)) - { - throw new Exception("ERROR - Couldn't convert the string steamGameIdToRun parameter to steamGameIdUint in SwitchToSteamGame!"); - } - - // Save the profile we're on now - var rollbackProfile = ProfileRepository.CurrentProfile; - - // Check that the profile we've been asked to change to will actually work - if (!profile.IsPossible) - { - throw new Exception(Language.Selected_profile_is_not_possible); - } - - // - if ( IPCClient.QueryAll().Any( - client => - client.Status == InstanceStatus.Busy || - client.Status == InstanceStatus.OnHold)) - { - throw new Exception( - Language - .Another_instance_of_this_program_is_in_working_state_Please_close_other_instances_before_trying_to_switch_profile); - } - - // Create the SteamGame objects so we can use them shortly - // Get the game information relevant to the game we're switching to - List allSteamGames = SteamGame.GetAllInstalledGames(); - - // Check if Steam is installed and error if it isn't - if (!SteamGame.SteamInstalled) - { - throw new Exception(Language.Steam_is_not_installed); - } - - // Otherwise try to find the game we've been asked to run - SteamGame steamGameToRun = null; - foreach (SteamGame steamGameToCheck in allSteamGames) - { - if (steamGameToCheck.GameId == steamGameIdUint) - { - steamGameToRun = steamGameToCheck; - break; - } - - } - - // Attempt to change to a different profile if it's needed - if (!GoProfile(profile)) - { - throw new Exception(Language.Can_not_change_active_profile); - } - - // Prepare to start the steam game using the URI interface - // as used by Steam for it's own desktop shortcuts. - var address = $"steam://rungameid/{steamGameToRun.GameId}"; - if (!string.IsNullOrWhiteSpace(steamGameArguments)) - { - address += "/" + steamGameArguments; - } - - - var steamProcess = System.Diagnostics.Process.Start(address); - // Wait for steam game to update and then run - var ticks = 0; - - while (ticks < timeout * 1000) - { - if (steamGameToRun.IsRunning) - { - break; - } - - Thread.Sleep(300); - - if (!steamGameToRun.IsUpdating) - { - ticks += 300; - } - } - - IPCService.GetInstance().HoldProcessId = steamProcess?.Id ?? 0; - IPCService.GetInstance().Status = InstanceStatus.OnHold; - NotifyIcon notify = null; + var instanceStatus = IPCService.GetInstance().Status; try { - notify = new NotifyIcon - { - Icon = Properties.Resources.HeliosPlus, - Text = string.Format( - Language.Waiting_for_the_0_to_terminate, - steamGameToRun.GameName), - Visible = true - }; - Application.DoEvents(); - } - catch (Exception ex) - { - Console.WriteLine($"Program/SwitchToSteamGame exception: {ex.Message}: {ex.InnerException}"); - // ignored - } + IPCService.GetInstance().Status = InstanceStatus.Busy; + var failed = false; - // Wait for the game to exit - if (steamGameToRun.IsRunning) - { - while (true) + if (new ApplyingChangesForm(() => { - if (!steamGameToRun.IsRunning) + Task.Factory.StartNew(() => { - break; + if (!(ProfileRepository.ApplyProfile(profile))) + { + failed = true; + } + }, TaskCreationOptions.LongRunning); + }, 3, 30).ShowDialog() != + DialogResult.Cancel) + { + if (failed) + { + throw new Exception(Language.Profile_is_invalid_or_not_possible_to_apply); } - Thread.Sleep(300); + return true; } - } - if (notify != null) + return false; + } + finally { - notify.Visible = false; - notify.Dispose(); - Application.DoEvents(); + IPCService.GetInstance().Status = instanceStatus; } - - IPCService.GetInstance().Status = InstanceStatus.Busy; - - // Change back to the original profile if it is different - if (!ProfileRepository.IsActiveProfile(rollbackProfile)) - { - if (!GoProfile(rollbackProfile)) - { - throw new Exception(Language.Can_not_change_active_profile); - } - } - } - private static void SwitchToUplayGame(ProfileItem profile, string uplayGameIdToRun, uint timeout, string uplayGameArguments) + private static void EditProfile(ProfileItem profile) { - - var rollbackProfile = ProfileRepository.CurrentProfile; - - if (!profile.IsPossible) - { - throw new Exception(Language.Selected_profile_is_not_possible); - } - - if ( - IPCClient.QueryAll() - .Any( - client => - client.Status == InstanceStatus.Busy || - client.Status == InstanceStatus.OnHold)) - { - throw new Exception( - Language - .Another_instance_of_this_program_is_in_working_state_Please_close_other_instances_before_trying_to_switch_profile); - } - - - - - /*var steamGame = new SteamGame(Convert.ToUInt32(uplayGameIdToRun)); - - if (!SteamGame.SteamInstalled) - { - throw new Exception(Language.Steam_is_not_installed); - } - - if (!File.Exists(SteamGame.SteamExe)) - { - throw new Exception(Language.Steam_executable_file_not_found); - } - - if (!steamGame.IsInstalled) - { - throw new Exception(Language.Steam_game_is_not_installed); - } - - if (!GoProfile(profile)) - { - throw new Exception(Language.Can_not_change_active_profile); - } - - var address = $"uplay://rungameid/{steamGame.AppId}"; - - if (!string.IsNullOrWhiteSpace(uplayGameArguments)) - { - address += "/" + uplayGameArguments; - } - - var steamProcess = System.Diagnostics.Process.Start(address); - // Wait for steam game to update and then run - var ticks = 0; - - while (ticks < timeout * 1000) - { - if (steamGame.IsRunning) - { - break; - } - - Thread.Sleep(300); - - if (!steamGame.IsUpdating) - { - ticks += 300; - } - } - - IPCService.GetInstance().HoldProcessId = steamProcess?.Id ?? 0; - IPCService.GetInstance().Status = InstanceStatus.OnHold; - NotifyIcon notify = null; - - try - { - notify = new NotifyIcon - { - Icon = Properties.Resources.Icon, - Text = string.Format( - Language.Waiting_for_the_0_to_terminate, - steamGame.Name), - Visible = true - }; - Application.DoEvents(); - } - catch - { - // ignored - } - - // Wait for the game to exit - if (steamGame.IsRunning) - { - while (true) - { - if (!steamGame.IsRunning) - { - break; - } - - Thread.Sleep(300); - } - } - - if (notify != null) - { - notify.Visible = false; - notify.Dispose(); - Application.DoEvents(); - } - - IPCService.GetInstance().Status = InstanceStatus.Busy; - - // Change back to the original profile if it is different - if (!ProfileRepository.IsActiveProfile(rollbackProfile)) - { - if (!GoProfile(rollbackProfile)) - { - throw new Exception(Language.Can_not_change_active_profile); - } - }*/ - - } - - - // ReSharper disable once CyclomaticComplexity - private static void SwitchToProfile(ProfileItem profile) - { - var rollbackProfile = ProfileRepository.CurrentProfile; - - if ( - IPCClient.QueryAll() - .Any( - client => - client.Status == InstanceStatus.Busy || - client.Status == InstanceStatus.OnHold)) - { - throw new Exception( - Language - .Another_instance_of_this_program_is_in_working_state_Please_close_other_instances_before_trying_to_switch_profile); - } - - if (!GoProfile(profile)) - { - throw new Exception(Language.Can_not_change_active_profile); - } + // Get the status of the thing + IPCService.GetInstance().Status = InstanceStatus.User; + // Load all the profiles from JSON + //ProfileRepository.AllProfiles + // Start up the DisplayProfileForm directly + new DisplayProfileForm(profile).ShowDialog(); + // Then we close down as we're only here to edit one profile + Application.Exit(); } public static bool IsValidFilename(string testName) diff --git a/HeliosPlus/Resources/Language.Designer.cs b/HeliosPlus/Resources/Language.Designer.cs index 9de9547..7f3f2a0 100644 --- a/HeliosPlus/Resources/Language.Designer.cs +++ b/HeliosPlus/Resources/Language.Designer.cs @@ -133,15 +133,6 @@ namespace HeliosPlus.Resources { } } - /// - /// Looks up a localized string similar to Can not change active profile.. - /// - internal static string Can_not_change_active_profile { - get { - return ResourceManager.GetString("Can_not_change_active_profile", resourceCulture); - } - } - /// /// Looks up a localized string similar to Can not open a named pipe for Inter-process communication.. /// @@ -160,6 +151,24 @@ namespace HeliosPlus.Resources { } } + /// + /// Looks up a localized string similar to Cannot change active profile.. + /// + internal static string Cannot_change_active_profile { + get { + return ResourceManager.GetString("Cannot_change_active_profile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find the shortcut '{0}' in the Shortcut library.. + /// + internal static string Cannot_find_shortcut_in_library { + get { + return ResourceManager.GetString("Cannot_find_shortcut_in_library", resourceCulture); + } + } + /// /// Looks up a localized string similar to C&lone. /// @@ -791,7 +800,7 @@ namespace HeliosPlus.Resources { } /// - /// Looks up a localized string similar to Waiting for the '{0}' to terminate. + /// Looks up a localized string similar to Waiting for the '{0}' process to terminate. /// internal static string Waiting_for_the_0_to_terminate { get { diff --git a/HeliosPlus/Resources/Language.resx b/HeliosPlus/Resources/Language.resx index 251f66c..49688ff 100644 --- a/HeliosPlus/Resources/Language.resx +++ b/HeliosPlus/Resources/Language.resx @@ -306,8 +306,8 @@ Another instance of this program is in working state. Please close other instances before trying to switch profile. - - Can not change active profile. + + Cannot change active profile. Steam executable file not found. @@ -358,7 +358,7 @@ Saved Profiles - Waiting for the '{0}' to terminate + Waiting for the '{0}' process to terminate Apply_Profile @@ -369,4 +369,7 @@ Press ESC to cancel + + Cannot find the shortcut '{0}' in the Shortcut library. + \ No newline at end of file diff --git a/HeliosPlus/ShortcutItem.cs b/HeliosPlus/ShortcutItem.cs index 97e5844..0725542 100644 --- a/HeliosPlus/ShortcutItem.cs +++ b/HeliosPlus/ShortcutItem.cs @@ -510,6 +510,66 @@ namespace HeliosPlus return multiIcon; } + + public (bool,string) IsValid() + { + // Do some validation checks to make sure the shortcut is sensible + // And that we have enough to try and action within the shortcut + // (in other words check everything in the shortcut is still valid) + + // Does the profile we want to Use still exist? + // Is the profile still valid right now? i.e. are all the screens available? + if (!ProfileToUse.IsPossible) + { + return (false,string.Format("The profile '{0}' is not valid right now and cannot be used.",ProfileToUse.Name)); + } + // Is the main application still installed? + if (Category.Equals(ShortcutCategory.Application)) + { + // We need to check if the Application still exists + if (!System.IO.File.Exists(ExecutableNameAndPath)) + { + return (false, string.Format("The application executable '{0}' does not exist, or cannot be accessed by HeliosPlus.", ExecutableNameAndPath)); + } + + } else if (Category.Equals(ShortcutCategory.Game)) + { + // If the game is a Steam Game we check for that + if (GameLibrary.Equals(SupportedGameLibrary.Steam)) + { + + // First check if Steam is installed + // Check if Steam is installed and error if it isn't + if (!SteamLibrary.IsSteamInstalled) + { + return (false, Language.Steam_executable_file_not_found); + } + + // We need to look up details about the game + if (!SteamLibrary.ContainsSteamGame(GameAppId)) + { + return (false, string.Format("The Steam Game with AppID '{0}' is not installed on this computer.", GameAppId)); + } + } + // If the game is a Uplay Game we check for that + /*else if (GameLibrary.Equals(SupportedGameLibrary.Uplay)) + { + // We need to look up details about the game + if (!UplayGame.IsInstalled(GameAppId)) + { + return (false, string.Format("The Uplay Game with AppID '{0}' is not installed on this computer.", GameAppId)); + } + + }*/ + + + } + // Do all the specified pre-start apps still exist? + + return (true, "Shortcut is valid"); + + } + // ReSharper disable once FunctionComplexityOverflow // ReSharper disable once CyclomaticComplexity public bool CreateShortcut(string shortcutFileName) @@ -522,7 +582,7 @@ namespace HeliosPlus { // Add the SwitchProfile command as the first argument to start to switch to another profile $"{HeliosStartupAction.SwitchProfile}", - $"\"{Name}\"" + $"\"{UUID}\"" }; // Only add the rest of the options if the permanence is temporary diff --git a/HeliosPlus/ShortcutRepository.cs b/HeliosPlus/ShortcutRepository.cs index bdfdfb1..0b92f0b 100644 --- a/HeliosPlus/ShortcutRepository.cs +++ b/HeliosPlus/ShortcutRepository.cs @@ -24,6 +24,7 @@ namespace HeliosPlus // Other constants that are useful private static string _shortcutStorageJsonPath = Path.Combine(Program.AppDataPath, $"Shortcuts"); private static string _shortcutStorageJsonFileName = Path.Combine(_shortcutStorageJsonPath, $"Shortcuts_{Version.ToString(2)}.json"); + private static string uuidV4Regex = @"(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$"; #endregion #region Class Constructors @@ -147,7 +148,7 @@ namespace HeliosPlus List shortcutsToRemove; int numRemoved; - string uuidV4Regex = @"/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i"; + //string uuidV4Regex = @"/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i"; Match match = Regex.Match(shortcutNameOrUuid, uuidV4Regex, RegexOptions.IgnoreCase); if (match.Success) { @@ -207,7 +208,7 @@ namespace HeliosPlus return false; - string uuidV4Regex = @"/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i"; + //string uuidV4Regex = @"(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$"; Match match = Regex.Match(shortcutNameOrUuid, uuidV4Regex, RegexOptions.IgnoreCase); if (match.Success) { @@ -238,7 +239,7 @@ namespace HeliosPlus if (String.IsNullOrWhiteSpace(shortcutNameOrUuid)) return null; - string uuidV4Regex = @"/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i"; + //string uuidV4Regex = @"/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i"; Match match = Regex.Match(shortcutNameOrUuid, uuidV4Regex, RegexOptions.IgnoreCase); if (match.Success) { diff --git a/HeliosPlus/UIForms/MainForm.cs b/HeliosPlus/UIForms/MainForm.cs index ec6fc2b..c12af52 100644 --- a/HeliosPlus/UIForms/MainForm.cs +++ b/HeliosPlus/UIForms/MainForm.cs @@ -56,7 +56,7 @@ namespace HeliosPlus.UIForms private void MainForm_Load(object sender, EventArgs e) { // Start loading the Steam Games just after the Main form opens - SteamGame.GetAllInstalledGames(); + //SteamGame.GetAllInstalledGames(); } } diff --git a/HeliosPlus/UIForms/ShortcutForm.cs b/HeliosPlus/UIForms/ShortcutForm.cs index 374bd10..f969d3a 100644 --- a/HeliosPlus/UIForms/ShortcutForm.cs +++ b/HeliosPlus/UIForms/ShortcutForm.cs @@ -436,12 +436,12 @@ namespace HeliosPlus.UIForms if (txt_game_launcher.Text == SupportedGameLibrary.Steam.ToString()) { - _shortcutToEdit.OriginalIconPath = (from steamGame in SteamGame.AllGames where steamGame.GameId == _shortcutToEdit.GameAppId select steamGame.GameIconPath).First(); + _shortcutToEdit.OriginalIconPath = (from steamGame in SteamLibrary.AllInstalledGames where steamGame.GameId == _shortcutToEdit.GameAppId select steamGame.GameIconPath).First(); _shortcutToEdit.GameLibrary = SupportedGameLibrary.Steam; } else if (txt_game_launcher.Text == SupportedGameLibrary.Uplay.ToString()) { - _shortcutToEdit.OriginalIconPath = (from uplayGame in UplayGame.AllGames where uplayGame.GameId == _shortcutToEdit.GameAppId select uplayGame.GameIconPath).First(); + _shortcutToEdit.OriginalIconPath = (from uplayGame in UplayLibrary.AllInstalledGames where uplayGame.GameId == _shortcutToEdit.GameAppId select uplayGame.GameIconPath).First(); _shortcutToEdit.GameLibrary = SupportedGameLibrary.Uplay; } else if (rb_standalone.Checked) @@ -646,9 +646,7 @@ namespace HeliosPlus.UIForms // Start finding the games and loading the Games ListView - List allSteamGames = SteamGame.GetAllInstalledGames(); - _allSteamGames = allSteamGames; - foreach (var game in allSteamGames.OrderBy(game => game.GameName)) + foreach (var game in SteamLibrary.AllInstalledGames.OrderBy(game => game.GameName)) { if (File.Exists(game.GameIconPath)) { @@ -835,7 +833,7 @@ namespace HeliosPlus.UIForms { if (_loadedShortcut) _isUnsaved = true; - txt_game_launcher.Text = SteamGame.GameLibrary.ToString(); + txt_game_launcher.Text = game.GameLibrary.ToString(); _gameId = game.GameId; } } diff --git a/HeliosPlus/Validators.cs b/HeliosPlus/Validators.cs index 59302ff..6729ca9 100644 --- a/HeliosPlus/Validators.cs +++ b/HeliosPlus/Validators.cs @@ -20,25 +20,16 @@ namespace HeliosPlus { // This validator only runs if there is a value if (!optionProfile.HasValue()) return ValidationResult.Success; - var profile = optionProfile.Value(); + var profileName = (string) optionProfile.Value(); - // Create an array of display profiles we have - var profiles = ProfileRepository.AllProfiles.ToArray(); - // Check if the user supplied a --profile option using the profiles' ID - var profileIndex = profiles.Length > 0 ? Array.FindIndex(profiles, p => p.UUID.Equals(profile, StringComparison.InvariantCultureIgnoreCase)) : -1; - // If the profileID wasn't there, maybe they used the profile name? - if (profileIndex == -1) + // Try to find the Profile Name + if (!ProfileRepository.ContainsProfile(profileName)) { - // Try and lookup the profile in the profiles' Name fields - profileIndex = profiles.Length > 0 ? Array.FindIndex(profiles, p => p.Name.StartsWith(profile, StringComparison.InvariantCultureIgnoreCase)) : -1; - } - // If the profileID still isn't there, then raise the alarm - if (profileIndex == -1) - { - return new ValidationResult($"Couldn't find Profile Name or ID supplied via command line: '{optionProfile.LongName}'. Please check the Profile Name or ID you supplied on the command line is correct."); + return new ValidationResult($"Couldn't find Profile Name or ID supplied via command line: '{profileName}'. Please check the Profile Name or ID you supplied on the command line is correct."); } - Console.WriteLine($"Using Profile: '{profiles[profileIndex].Name}' (ID:{profiles[profileIndex].UUID})"); + ProfileItem profile = ProfileRepository.GetProfile(profileName); + Console.WriteLine($"Using Profile: '{profile.Name}' (ID:{profile.UUID})"); return ValidationResult.Success; } } @@ -49,34 +40,16 @@ namespace HeliosPlus { // This validator only runs if there is a string provided if (argumentShortcutName.Value == "") return ValidationResult.Success; - string shortcutNameProvided = (string) argumentShortcutName.Value; - string shortcutName = ""; + string shortcutName = (string) argumentShortcutName.Value; - // check if the shortcut name is surrounded by speech marks - int shortcutNameIndexLeft = shortcutNameProvided.IndexOf('"'); - int shortcutNameIndexRight = shortcutNameProvided.LastIndexOf('"'); - if (shortcutNameIndexLeft != -1 && shortcutNameIndexRight != -1 && shortcutNameIndexLeft != shortcutNameIndexRight) - { - MatchCollection matches = Regex.Matches(shortcutNameProvided, @"'(.*?)'"); - shortcutName = matches[0].Groups[1].Value; // (Index 1 is the first group) - } - else - { - shortcutName = shortcutNameProvided; - } - - // Create an array of shortcuts we have - var shortcuts = ShortcutRepository.AllShortcuts.ToArray(); - // Check if the user supplied a valid shortcut name - int profileIndex = shortcuts.Length > 0 ? Array.FindIndex(shortcuts, p => p.Name.Contains(shortcutName)) : -1; - - // If the profileID still isn't there, then raise the alarm - if (profileIndex == -1) + // Check if the UUID or ShortcutName are provided + if (!ShortcutRepository.ContainsShortcut(shortcutName)) { return new ValidationResult($"Couldn't find Shortcut Name supplied via command line: '{shortcutName}'. Please check the Shortcut Name you supplied on the command line is correct."); } - Console.WriteLine($"Using Shortcut: '{shortcuts[profileIndex].Name}'"); + ShortcutItem shortcut = ShortcutRepository.GetShortcut(shortcutName); + Console.WriteLine($"Using Shortcut: '{shortcut.Name}' (ID: {shortcut.UUID})"); return ValidationResult.Success; } }