From b02155d9b1c6e985e95753f996ea6a053850a08c Mon Sep 17 00:00:00 2001 From: Terry MacDonald Date: Fri, 15 Oct 2021 21:51:53 +1300 Subject: [PATCH] Added robust Uplay config file game parsing Used Josef Nemec's AMAZING code as a starter at https://github.com/JosefNemec/PlayniteExtensions/blob/master/source/Libraries/UplayLibrary/Models/ProductSchema.cs to build a proper Uplay Game parser! Uplay use protobuf encoded files that contain YAML embedded within them, just to annoy developers like me. Uplay processing should be much more robust now. --- DisplayMagician/DisplayMagician.csproj | 8 +- .../UplayConfigurationParser.cs | 104 ---- .../GameLibraries/UplayFileStructure.cs | 144 +++++ DisplayMagician/GameLibraries/UplayLibrary.cs | 508 +++++++++++------- DisplayMagician/Properties/AssemblyInfo.cs | 4 +- 5 files changed, 470 insertions(+), 298 deletions(-) delete mode 100644 DisplayMagician/GameLibraries/UplayConfigurationParser/UplayConfigurationParser.cs create mode 100644 DisplayMagician/GameLibraries/UplayFileStructure.cs diff --git a/DisplayMagician/DisplayMagician.csproj b/DisplayMagician/DisplayMagician.csproj index ae13c6b..0b110c3 100644 --- a/DisplayMagician/DisplayMagician.csproj +++ b/DisplayMagician/DisplayMagician.csproj @@ -108,7 +108,7 @@ - + @@ -312,6 +312,9 @@ 4.7.11 + + 3.0.101 + 1.0.0 @@ -324,6 +327,9 @@ 1.6.0.4 + + 11.2.1 + diff --git a/DisplayMagician/GameLibraries/UplayConfigurationParser/UplayConfigurationParser.cs b/DisplayMagician/GameLibraries/UplayConfigurationParser/UplayConfigurationParser.cs deleted file mode 100644 index 4ef7932..0000000 --- a/DisplayMagician/GameLibraries/UplayConfigurationParser/UplayConfigurationParser.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -// This configuration parses logic is kept here for possible future use -// It was really difficult to find this logic in some obscure webpage -// so I'm keeping it in case I need it later. -namespace DisplayMagician.GameLibraries.UplayConfigurationParser -{ - class UplayConfigurationParser - { - - /*def _convert_data(self, data): - # calculate object size (konrad's formula) - if data > 256 * 256: - data = data - (128 * 256 * math.ceil(data / (256 * 256))) - data = data - (128 * math.ceil(data / 256)) - else: - if data > 256: - data = data - (128 * math.ceil(data / 256)) - return data*/ - - internal static decimal ConvertData (decimal data) - { - if (data > 65536) - { - data = data - (128 * 256 * Math.Ceiling(data / 65536)); - } - else if (data > 256) - { - data = data - (128 * Math.Ceiling(data / 256)); - } - - return data; - - } - - - - /*def _parse_configuration_header(self, header, second_eight= False): - try: - offset = 1 - multiplier = 1 - record_size = 0 - tmp_size = 0 - - if second_eight: - while header[offset] != 0x08 or(header[offset] == 0x08 and header[offset + 1] == 0x08) : - record_size += header[offset] * multiplier - multiplier *= 256 - offset += 1 - tmp_size += 1 - else: - while header[offset] != 0x08 or record_size == 0: - record_size += header[offset] * multiplier - multiplier *= 256 - offset += 1 - tmp_size += 1 - - record_size = self._convert_data(record_size) - - offset += 1 # skip 0x08 - - # look for launch_id - multiplier = 1 - launch_id = 0 - - while header[offset] != 0x10 or header[offset + 1] == 0x10: - launch_id += header[offset] * multiplier - multiplier *= 256 - offset += 1 - - launch_id = self._convert_data(launch_id) - - offset += 1 # skip 0x10 - - multiplier = 1 - launch_id_2 = 0 - while header[offset] != 0x1A or(header[offset] == 0x1A and header[offset + 1] == 0x1A) : - launch_id_2 += header[offset] * multiplier - multiplier *= 256 - offset += 1 - - launch_id_2 = self._convert_data(launch_id_2) - - #if object size is smaller than 128b, there might be a chance that secondary size will not occupy 2b - if record_size - offset < 128 <= record_size: - tmp_size -= 1 - record_size += 1 - -# we end up in the middle of header, return values normalized -# to end of record as well real yaml size and game launch_id - return record_size - offset, launch_id, launch_id_2, offset + tmp_size + 1 - except: -# something went horribly wrong, do not crash it, -# just return 0s, this way it will be handled later in the code -# 10 is to step a little in configuration file in order to find next game - return 0, 0, 0, 10*/ - - //internal static decimal ParseConfigurationHeader(decimal data); - } -} diff --git a/DisplayMagician/GameLibraries/UplayFileStructure.cs b/DisplayMagician/GameLibraries/UplayFileStructure.cs new file mode 100644 index 0000000..f54813f --- /dev/null +++ b/DisplayMagician/GameLibraries/UplayFileStructure.cs @@ -0,0 +1,144 @@ +using ProtoBuf; +using System.Collections.Generic; + +namespace DisplayMagician.GameLibraries +{ + // ##################################################################################################### + // # This set of classes are used for deserialising Uplay protobuf files + // ##################################################################################################### + + [ProtoContract] + public class UplayCachedGame + { + [ProtoMember(1)] + public uint UplayId { get; set; } + [ProtoMember(2)] + public uint InstallId { get; set; } + [ProtoMember(3)] + public string GameInfo { get; set; } + } + + [ProtoContract] + public class UplayCachedGameCollection + { + [ProtoMember(1)] + public List Games { get; set; } + } + + // ##################################################################################################### + // # This set of classes are used for deserialising Uplay YAML enbedded within the protobuf file format + // ##################################################################################################### + public class ProductInformation + { + public class Executable + { + public class Path + { + public string relative; + } + + public class WorkingDirectory + { + public string register; + public string append; + } + + public Path path; + public WorkingDirectory working_directory; + public string internal_name; + public string description; + public string shortcut_name; + public string icon_image; + } + + public class StartGameItem + { + public bool after_game_report_enabled; + public bool overlay_supported; + public bool overlay_product_activation_enabled; + public bool overlay_required; + public bool overlay_shop_enabled; + public bool legacy_ticket_enabled; + public List executables; + + } + + public class StartGame + { + public StartGameItem online; + public StartGameItem offline; + } + + public class DigitalDistribution + { + public int version; + } + + public class Localization + { + public string l1; + } + + public class Club + { + public bool enabled; + } + + public class Addon + { + public uint id; + public bool is_visible; + public string name; + public string description; + public string thumb_image; + } + + public class Uplay + { + public string game_code; + public string achievements; + public string achievements_sync_id; + } + + public class ThirdPartyPlatform + { + public string name; + } + + public class Product + { + public string name; + public string background_image; + public string thumb_image; + public string logo_image; + public string dialog_image; + public string icon_image; + public ThirdPartyPlatform third_party_platform; + public string sort_string; + public bool cloud_saves; + public string forum_url; + public string homepage_url; + public string facebook_url; + public string help_url; + public bool after_game_report_ad; + public bool force_safe_mode; + public bool uplay_pipe_required; + public bool show_properties; + public bool game_streaming_enabled; + public Uplay uplay; + public List addons; + public Club club; + public DigitalDistribution digital_distribution; + public bool is_ulc; + public bool is_visible; + public StartGame start_game; + } + + public string version; + public Product root; + public Dictionary localizations; + public uint uplay_id; + public uint install_id; + } + +} diff --git a/DisplayMagician/GameLibraries/UplayLibrary.cs b/DisplayMagician/GameLibraries/UplayLibrary.cs index 3c281bc..db1fccc 100644 --- a/DisplayMagician/GameLibraries/UplayLibrary.cs +++ b/DisplayMagician/GameLibraries/UplayLibrary.cs @@ -6,6 +6,10 @@ using Microsoft.Win32; using System.IO; using System.Security; using System.Diagnostics; +using ProtoBuf; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using System.Globalization; namespace DisplayMagician.GameLibraries { @@ -404,6 +408,52 @@ namespace DisplayMagician.GameLibraries } + public bool GetInstallDirFromRegKey(string regKeyPath, out string filePath) + { + filePath = ""; + + RegistryKey uplayGameInstallKey; + if (regKeyPath.StartsWith("HKEY_LOCAL_MACHINE")) + { + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Accessing HKLM reg key {regKeyPath}"); + string regKeyText = regKeyPath.Replace(@"HKEY_LOCAL_MACHINE\", ""); + uplayGameInstallKey = Registry.LocalMachine.OpenSubKey(regKeyText, RegistryKeyPermissionCheck.ReadSubTree); + } + else if (regKeyPath.StartsWith("HKEY_CURRENT_USER")) + { + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Accessing HKCU reg key {regKeyPath}"); + string regKeyText = regKeyPath.Replace(@"HKEY_CURRENT_USER\", ""); + uplayGameInstallKey = Registry.LocalMachine.OpenSubKey(regKeyText, RegistryKeyPermissionCheck.ReadSubTree); + } + else + { + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Skipping processing as regkey supplied was odd: {regKeyPath}"); + return false; + } + + // If the key doesn't exist we skip it as the game isn't installed any longer! + if (uplayGameInstallKey == null) + { + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Skipping Uplay Game as it isn't installed at the moment (it was uninstalled at some point)"); + return false; + } + + // From that we lookup the actual game path + string gameInstallDir = uplayGameInstallKey.GetValue("InstallDir", "").ToString(); + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: gameInstallDir found = {gameInstallDir}"); + if (!String.IsNullOrWhiteSpace(gameInstallDir)) + { + filePath = Path.GetFullPath(gameInstallDir).TrimEnd('\\'); + return true; + } + else + { + logger.Warn($"UplayLibrary/GetInstallDirFromRegKey: gameInstallDir is null or all whitespace!"); + return false; + } + } + + public override bool LoadInstalledGames() { try @@ -475,211 +525,287 @@ namespace DisplayMagician.GameLibraries // Access {installdir}\\cache\\configuration\\configurations file string uplayConfigFilePath = _uplayPath + @"cache\configuration\configurations"; logger.Trace($"UplayLibrary/LoadInstalledGames: Uplay Config File Path = {uplayConfigFilePath }"); - string uplayConfigFileString = File.ReadAllText(uplayConfigFilePath); - uplayConfigFileString = uplayConfigFileString.Remove(0, 12); - string[] dividingText = { "version: 2.0" }; - List uplayConfigFile = uplayConfigFileString.Split(dividingText,StringSplitOptions.RemoveEmptyEntries).ToList(); - // Split the file into records at the SOH unicode character - //List uplayConfigFile = uplayConfigFileString.Split((Char)1).ToList(); - // Go through every record and attempt to parse it - foreach (string uplayEntry in uplayConfigFile) { - // Skip any Uplay entry records that don't start with 'version:' - //if (!uplayEntry.StartsWith("version:",StringComparison.OrdinalIgnoreCase)) - // continue; + var deserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); - logger.Trace($"UplayLibrary/LoadInstalledGames: Uplay Entry that starts with 'version: 2.0') = {uplayEntry}"); - - //Split the record into entrylines - string[] delimeters = { "\r\n" }; - List uplayEntryLines = uplayEntry.Split(delimeters, System.StringSplitOptions.RemoveEmptyEntries).ToList(); - - // Skip any records NOT containing an entryline with ' start_game:' (note 2 leading spaces) - // All games contain a start_game entry - if (!uplayEntryLines.Exists(a => a.StartsWith(" start_game:"))) - continue; - - // Skip any records containing an entryline with ' third_party_platform:' (note 2 leading spaces) - // We only want the native uplay games.... - if (uplayEntryLines.Exists(a => a.StartsWith(" third_party_platform:"))) - continue; - - // if we get here then we have a real game to parse! - // Yay us :). - - // First we want to know the index of the start_game entry to use later - //int startGameIndex = uplayEntryLines.FindIndex(a => a.StartsWith(" start_game:")); - MatchCollection mc; - - // First we check if there are any localization CONSTANTS that we will need to map later. - Dictionary localizations = new Dictionary(); - int localizationsIndex = uplayEntryLines.FindIndex(a => a == "localizations:"); - // If there are localizations, then we need to store them for later - if (localizationsIndex != -1) + using (var file = File.OpenRead(uplayConfigFilePath)) + { + try { - // grab the localizations: -> default: entries to use as a lookup table for the info we need - int defaultIndex = localizationsIndex + 1; - int currentIndex = defaultIndex + 1; - - // Grab all EntryLines with 4 leading spaces (these are all the localizations) - while (uplayEntryLines[currentIndex].StartsWith(" ")){ - string[] split = uplayEntryLines[currentIndex].Split(':'); - localizations.Add(split[0].Trim(), split[1].Trim()); - currentIndex++; - } - - } - - // for each game record grab: - GameAppInfo uplayGameAppInfo = new GameAppInfo(); - - // find the exe name looking at root: -> start_game: -> online: -> executables: -> path: -> relative: (get ACU.exe) - // Lookup the Game registry key from looking at root: -> start_game: -> online: -> executables: -> working_directory: -> register: (get HKEY_LOCAL_MACHINE\SOFTWARE\Ubisoft\Launcher\Installs\720\InstallDir) - // Extract the GameAppID from the number in the working directory (e.g. 720) - // Lookup the Game install path by reading the game registry key: D:/Ubisoft Game Launcher/Assassin's Creed Unity/ - // join the Game install path and the exe name to get the full game exe path: D:/Ubisoft Game Launcher/Assassin's Creed Unity/ACU.exe - - //if (uplayEntryLines.Find (a => a.StartsWith(" icon_image:", StringComparison.InvariantCultureIgnoreCase))) - - bool gotGameIconPath = false; - bool gotGameName = false; - string gameFileName = ""; - bool gotGameFileName = false; - string gameId = ""; - bool gotGameId = false; - string gameRegistryKey = ""; - bool gotGameRegistryKey = false; - for (int i = 0; i <= 50; i++) - { - // Stop this loop once we have both filname and gameid - if (gotGameFileName && gotGameId && gotGameIconPath && gotGameName && gotGameRegistryKey) + var gameCollection = ProtoBuf.Serializer.Deserialize(file).Games; + foreach (var item in gameCollection) { - logger.Trace($"UplayLibrary/LoadInstalledGames: We got all the entries: gameFileName = {gameFileName } && gameId = {gameId } && gameIconPath = {uplayGameAppInfo.GameIconPath} && gameName = {uplayGameAppInfo.GameName}"); - break; - } - - // This line contains the Game Name - if (uplayEntryLines[i].StartsWith(" name:", StringComparison.OrdinalIgnoreCase) && !gotGameName) - { - mc = Regex.Matches(uplayEntryLines[i], @" name\: (.*)"); - if (mc.Count > 0) + if (!String.IsNullOrEmpty(item.GameInfo)) { - uplayGameAppInfo.GameName = mc[0].Groups[1].ToString(); - // if the name contains a localization reference, then dereference it - if (localizations.ContainsKey(uplayGameAppInfo.GameName)) + ProductInformation productInfo; + try { - uplayGameAppInfo.GameName = localizations[uplayGameAppInfo.GameName]; + productInfo = deserializer.Deserialize(item.GameInfo); + var root = productInfo.root; + + string gameName = ""; + string gameExePath = ""; + string gameIconPath = ""; + + // Try finding the Game Name using the localisation currently in use as a first step + logger.Trace($"UplayLibrary/LoadInstalledGames: Looking for the Uplay game name."); + string currentLang = CultureInfo.CurrentCulture.Name; + foreach (var lang in productInfo.localizations) + { + // If we find the same language as the user is using, then let's use that! + if (lang.Key.Equals(currentLang)) + { + gameName = lang.Value.l1; + logger.Trace($"UplayLibrary/LoadInstalledGames: We found the Uplay game name '{gameName}' in the user's language of {currentLang}."); + break; + } + } + // If the gameName isn't available in the users language, then we go for default + if (String.IsNullOrEmpty(gameName) && productInfo.localizations.ContainsKey("default")) + { + gameName = productInfo.localizations["default"].l1; + if (!String.IsNullOrEmpty(gameName)) + { + logger.Trace($"UplayLibrary/LoadInstalledGames: Looking for the Uplay game name with the en language as the local language didn't work. We found game name '{gameName}'. "); + } + else + { + logger.Trace($"UplayLibrary/LoadInstalledGames: Looking for the Uplay game name with the en language as the local language didn't work. We found no en language. "); + } + } + + + // Now we'll try to sort out the rest of the game data! + // We first look for the online executable information + if (root.start_game.online.executables.Count > 0) + { + logger.Trace($"UplayLibrary/LoadInstalledGames: Uplay game {gameName} has some online executables to process! "); + + // First up we look at the online games, cause they're just better! + foreach (var executable in root.start_game.online.executables) + { + string exePath = ""; + + // Check if its a full path or a relative path + if (!String.IsNullOrEmpty(executable.path.relative)) + { + if (executable.working_directory.register.StartsWith("HKEY_LOCAL_MACHINE")) + { + // This copes with relative files using a HKEY_LOCAL_MACHINE registry + + string regKeyText = executable.working_directory.register; + regKeyText = regKeyText.Replace(@"\InstallDir", ""); + regKeyText = regKeyText.Replace(@"Ubisoft", @"WOW6432Node\Ubisoft"); + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Accessing HKLM reg key {regKeyText}"); + + if (this.GetInstallDirFromRegKey(regKeyText, out exePath)) + { + gameExePath = Path.Combine(exePath, executable.path.relative); + logger.Trace($"UplayLibrary/LoadInstalledGames: Relative executable uses local machine registry key: {executable.working_directory.register} "); + } + } + /*else if (executable.working_directory.register.StartsWith("HKEY_CURRENT_USER")) + { + // This copes with relative files using a HKEY_CURRENT_USER registry + + string regKeyText = executable.working_directory.register; + regKeyText = regKeyText.Replace(@"\InstallDir", ""); + regKeyText = regKeyText.Replace(@"Ubisoft", @"WOW6432Node\Ubisoft"); + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Accessing HKLM reg key {regKeyText}"); + + if (this.GetInstallDirFromRegKey(executable.working_directory.register, out exePath)) + { + gameExePath = Path.Combine(exePath, executable.path.relative); + logger.Trace($"UplayLibrary/LoadInstalledGames: Relative executable uses current user registry key: {executable.working_directory.register} "); + } + }*/ + else if (!String.IsNullOrEmpty(executable.working_directory.append)) + { + // This copes with relative files using an appended path + gameExePath = Path.Combine(executable.working_directory.append, executable.path.relative); + gameIconPath = Path.Combine(executable.working_directory.append, executable.icon_image); + logger.Trace($"UplayLibrary/LoadInstalledGames: Relative executable uses appended file path: {executable.working_directory.append} "); + } + else + { + // Problem! + logger.Error($"UplayLibrary/LoadInstalledGames: Found relative GameExePath {executable.path.relative} for Uplay game {gameName} but no registry key or appended file path! Skipping this game."); + continue; + } + } + else + { + // This should cope with full pathed files, but we have no examples to test! So log it + logger.Error($"UplayLibrary/LoadInstalledGames: Found non-relative GameExePath {executable.path} for Uplay game {gameName} but we've not seen it before so no idea how to handle it! Skipping this game."); + logger.Error($"UplayLibrary/LoadInstalledGames: executable.path for troubleshooting: {executable.path}"); + continue; + } + + // We should check the exe file exists, and if it doesn't then we need to do the next exe + if (!File.Exists(gameExePath)) + { + logger.Error($"UplayLibrary/LoadInstalledGames: Couldn't find the GameExePath {gameExePath} for Uplay game {gameName} so skipping this exe, and trying the next one."); + continue; + } + + // Now try to get the Uplay game icon + if (!String.IsNullOrEmpty(executable.icon_image)) + { + gameIconPath = Path.Combine(_uplayPath, "data", "games", executable.icon_image); + + // If the icon file isn't actually there, then use the game exe instead. + if (!File.Exists(gameIconPath)) + { + gameIconPath = gameExePath; + } + } + + logger.Trace($"UplayLibrary/LoadInstalledGames: Found GameExePath {exePath} and Icon Path {gameIconPath} for Uplay game {gameName}."); + + // We do a final check to make sure that we do have a GameName, and if not we use the shortcut + if (String.IsNullOrEmpty(gameName) && !String.IsNullOrEmpty(executable.shortcut_name)) + { + gameName = executable.shortcut_name; + logger.Trace($"UplayLibrary/LoadInstalledGames: Game Name was still empty, so we're using the shortcut name as a last resort: {executable.shortcut_name} "); + } + + // Now we need to save the game name, cause if we're here then we're good enough to save + // Then we have the gameID, the thumbimage, the icon, the name, the exe path + // And we add the Game to the list of games we have! + _allGames.Add(new UplayGame(productInfo.uplay_id.ToString(), gameName, gameExePath, gameIconPath)); + logger.Trace($"UplayLibrary/LoadInstalledGames: Adding Uplay Game with game id {productInfo.uplay_id}, name {gameName}, game exe {gameExePath} and icon path {gameIconPath}"); + break; + } + + } + // This is the offline exes + else if (root.start_game.offline.executables.Count > 0) + { + logger.Trace($"UplayLibrary/LoadInstalledGames: Uplay game {gameName} has some offline executables to process! "); + + // we look at the offline games, cause there weren't any online ones + foreach (var executable in root.start_game.offline.executables) + { + string exePath = ""; + + // Check if its a full path or a relative path + if (!String.IsNullOrEmpty(executable.path.relative)) + { + if (executable.working_directory.register.StartsWith("HKEY_LOCAL_MACHINE")) + { + // This copes with relative files using a HKEY_LOCAL_MACHINE registry + + string regKeyText = executable.working_directory.register; + regKeyText = regKeyText.Replace(@"\InstallDir", ""); + regKeyText = regKeyText.Replace(@"Ubisoft", @"WOW6432Node\Ubisoft"); + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Accessing HKLM reg key {regKeyText}"); + + if (this.GetInstallDirFromRegKey(regKeyText, out exePath)) + { + gameExePath = Path.Combine(exePath, executable.path.relative); + logger.Trace($"UplayLibrary/LoadInstalledGames: Relative executable uses local machine registry key: {executable.working_directory.register} "); + } + } + /*else if (executable.working_directory.register.StartsWith("HKEY_CURRENT_USER")) + { + // This copes with relative files using a HKEY_CURRENT_USER registry + + string regKeyText = executable.working_directory.register; + regKeyText = regKeyText.Replace(@"\InstallDir", ""); + regKeyText = regKeyText.Replace(@"Ubisoft", @"WOW6432Node\Ubisoft"); + logger.Trace($"UplayLibrary/GetInstallDirFromRegKey: Accessing HKLM reg key {regKeyText}"); + + if (this.GetInstallDirFromRegKey(executable.working_directory.register, out exePath)) + { + gameExePath = Path.Combine(exePath, executable.path.relative); + logger.Trace($"UplayLibrary/LoadInstalledGames: Relative executable uses current user registry key: {executable.working_directory.register} "); + } + }*/ + else if (!String.IsNullOrEmpty(executable.working_directory.append)) + { + // This copes with relative files using an appended path + gameExePath = Path.Combine(executable.working_directory.append, executable.path.relative); + gameIconPath = Path.Combine(executable.working_directory.append, executable.icon_image); + logger.Trace($"UplayLibrary/LoadInstalledGames: Relative executable uses appended file path: {executable.working_directory.append} "); + } + else + { + // Problem! + logger.Error($"UplayLibrary/LoadInstalledGames: Found relative GameExePath {executable.path.relative} for Uplay game {gameName} but no registry key or appended file path! Skipping this game."); + continue; + } + } + else + { + // This should cope with full pathed files, but we have no examples to test! So log it + logger.Error($"UplayLibrary/LoadInstalledGames: Found non-relative GameExePath {executable.path} for Uplay game {gameName} but we've not seen it before so no idea how to handle it! Skipping this game."); + logger.Error($"UplayLibrary/LoadInstalledGames: executable.path for troubleshooting: {executable.path}"); + continue; + } + + // We should check the exe file exists, and if it doesn't then we need to do the next exe + if (!File.Exists(gameExePath)) + { + logger.Error($"UplayLibrary/LoadInstalledGames: Couldn't find the GameExePath {gameExePath} for Uplay game {gameName} so skipping this exe, and trying the next one."); + continue; + } + + // Now try to get the Uplay game icon + if (!String.IsNullOrEmpty(executable.icon_image)) + { + gameIconPath = Path.Combine(_uplayPath, "data", "games", executable.icon_image); + + // If the icon file isn't actually there, then use the game exe instead. + if (!File.Exists(gameIconPath)) + { + gameIconPath = gameExePath; + } + } + + logger.Trace($"UplayLibrary/LoadInstalledGames: Found GameExePath {exePath} and Icon Path {gameIconPath} for Uplay game {gameName}."); + + // We do a final check to make sure that we do have a GameName, and if not we use the shortcut + if (String.IsNullOrEmpty(gameName) && !String.IsNullOrEmpty(executable.shortcut_name)) + { + gameName = executable.shortcut_name; + logger.Trace($"UplayLibrary/LoadInstalledGames: Game Name was still empty, so we're using the shortcut name as a last resort: {executable.shortcut_name} "); + } + + // Now we need to save the game name, cause if we're here then we're good enough to save + // Then we have the gameID, the thumbimage, the icon, the name, the exe path + // And we add the Game to the list of games we have! + _allGames.Add(new UplayGame(productInfo.uplay_id.ToString(), gameName, gameExePath, gameIconPath)); + logger.Trace($"UplayLibrary/LoadInstalledGames: Adding Uplay Game with game id {productInfo.uplay_id}, name {gameName}, game exe {gameExePath} and icon path {gameIconPath}"); + break; + } + + } + else + { + logger.Trace($"UplayLibrary/LoadInstalledGames: Uplay Entry {gameName} doesn't have any executables associated with it! We have to skip adding this game."); + continue; + } + } - logger.Trace($"UplayLibrary/LoadInstalledGames: Found uplayGameAppInfo.GameName = {uplayGameAppInfo.GameName}"); - gotGameName = true; - } - } - else if (uplayEntryLines[i].StartsWith(" icon_image:", StringComparison.OrdinalIgnoreCase) && !gotGameIconPath) - { - mc = Regex.Matches(uplayEntryLines[i], @"icon_image: (.*)"); - if (mc.Count > 0) - { - string iconImageFileName = mc[0].Groups[1].ToString(); - // if the icon_image contains a localization reference, then dereference it - if (localizations.ContainsKey(iconImageFileName)) + catch (Exception ex) { - iconImageFileName = localizations[iconImageFileName]; - logger.Trace($"UplayLibrary/LoadInstalledGames: Found iconImageFile = {iconImageFileName }"); + // If we get an error processing the game YAML, lets try and skip this game and try the next one. It might work! + logger.Error($"UplayLibrary/LoadInstalledGames: Problem deserialising the YAML embedded in the Uplay configuration file {uplayConfigFilePath}. Cannot process this games!"); + continue; } - //61fdd16f06ae08158d0a6d476f1c6bd5.ico - string uplayGameIconPath = _uplayPath + @"data\games\" + iconImageFileName; - if (File.Exists(uplayGameIconPath) && uplayGameIconPath.EndsWith(".ico")) - { - uplayGameAppInfo.GameIconPath = uplayGameIconPath; - logger.Trace($"UplayLibrary/LoadInstalledGames: Found uplayGameAppInfo.GameUplayIconPath = {uplayGameAppInfo.GameIconPath }"); - } - gotGameIconPath = true; - } - } - // This line contains the filename - else if (uplayEntryLines[i].StartsWith(" relative:") && !gotGameFileName) - { - mc = Regex.Matches(uplayEntryLines[i], @"relative: (.*)"); - if (mc.Count > 0) - { - gameFileName = mc[0].Groups[1].ToString(); - gotGameFileName = true; - logger.Trace($"UplayLibrary/LoadInstalledGames: Found gameFileName = {gameFileName}"); - } - } - // This line contains the registryKey - else if (uplayEntryLines[i].StartsWith(" register: HKEY_LOCAL_MACHINE") && !gotGameId) - { - - // Lookup the GameId within the registry key - mc = Regex.Matches(uplayEntryLines[i], @"Installs\\(\d+)\\InstallDir"); - if (mc.Count > 0) - { - gameId = mc[0].Groups[1].ToString(); - gotGameId = true; - logger.Trace($"UplayLibrary/LoadInstalledGames: Found gameId = {gameId}"); + } - mc = Regex.Matches(uplayEntryLines[i], @"HKEY_LOCAL_MACHINE\\(.*?)\\InstallDir"); - if (mc.Count > 0) - { - gameRegistryKey = mc[0].Groups[1].ToString(); - gameRegistryKey = gameRegistryKey.Replace(@"Ubisoft", @"WOW6432Node\Ubisoft"); - gotGameRegistryKey = true; - logger.Trace($"UplayLibrary/LoadInstalledGames: Found gameRegistryKey = {gameRegistryKey}"); - } - } } - - logger.Trace($"UplayLibrary/LoadInstalledGames: gameId = {gameId}"); - logger.Trace($"UplayLibrary/LoadInstalledGames: gameFileName = {gameFileName}"); - logger.Trace($"UplayLibrary/LoadInstalledGames: gameGameIconPath = {uplayGameAppInfo.GameIconPath}"); - logger.Trace($"UplayLibrary/LoadInstalledGames: gameRegistryKey = {gameRegistryKey}"); - - if (gotGameRegistryKey) + catch (Exception ex) { - // Now we need to lookup the game install path in registry using the game reg we got above - // We assume its 64-bit OS too (not 32bit) - using (RegistryKey uplayGameInstallKey = Registry.LocalMachine.OpenSubKey(gameRegistryKey, RegistryKeyPermissionCheck.ReadSubTree)) - { - // If the key doesn't exist we skip it as the game isn't installed any longer! - if (uplayGameInstallKey == null) - { - logger.Trace($"UplayLibrary/LoadInstalledGames: Skipping Uplay Game {uplayGameAppInfo.GameName} as it isn't installed at the moment (it was uninstalled at some point)"); - continue; - } - - // If we get here, then we have a real game. - foreach (string regKeyName in uplayGameInstallKey.GetValueNames()) - { - logger.Trace($"UplayLibrary/LoadInstalledGames: uplayGameInstallKey[{regKeyName}] = {uplayGameInstallKey.GetValue(regKeyName)}"); - } - - // From that we lookup the actual game path - string gameInstallDir = uplayGameInstallKey.GetValue("InstallDir", "").ToString(); - logger.Trace($"UplayLibrary/LoadInstalledGames: gameInstallDir found = {gameInstallDir}"); - if (!String.IsNullOrWhiteSpace(gameInstallDir)) - { - uplayGameAppInfo.GameInstallDir = Path.GetFullPath(gameInstallDir).TrimEnd('\\'); - logger.Trace($"UplayLibrary/LoadInstalledGames: uplayGameAppInfo.GameInstallDir = {uplayGameAppInfo.GameInstallDir }"); - uplayGameAppInfo.GameExePath = Path.Combine(uplayGameAppInfo.GameInstallDir, gameFileName); - logger.Trace($"UplayLibrary/LoadInstalledGames: uplayGameAppInfo.GameExe = {uplayGameAppInfo.GameExePath}"); - uplayGameAppInfo.GameID = gameId; - logger.Trace($"UplayLibrary/LoadInstalledGames: uplayGameAppInfo.GameID = {uplayGameAppInfo.GameID }"); - } - else - { - logger.Warn($"UplayLibrary/LoadInstalledGames: gameInstallDir is null or all whitespace!"); - } - - // Then we have the gameID, the thumbimage, the icon, the name, the exe path - // And we add the Game to the list of games we have! - _allGames.Add(new UplayGame(uplayGameAppInfo.GameID, uplayGameAppInfo.GameName, uplayGameAppInfo.GameExePath, uplayGameAppInfo.GameIconPath)); - logger.Debug($"UplayLibrary/LoadInstalledGames: Adding Uplay Game with game id {uplayGameAppInfo.GameID}, name {uplayGameAppInfo.GameName}, game exe {uplayGameAppInfo.GameExePath} and icon path {uplayGameAppInfo.GameIconPath}"); - } + // We can't do anything if we hit here. + logger.Error($"UplayLibrary/LoadInstalledGames: Problem deserialising the protobuf Uplay configuration file {uplayConfigFilePath}. Cannot process any games!"); + return false; } - - } + } logger.Info($"UplayLibrary/LoadInstalledGames: Found {_allGames.Count} installed Uplay games"); diff --git a/DisplayMagician/Properties/AssemblyInfo.cs b/DisplayMagician/Properties/AssemblyInfo.cs index 7d2074f..b68add6 100644 --- a/DisplayMagician/Properties/AssemblyInfo.cs +++ b/DisplayMagician/Properties/AssemblyInfo.cs @@ -26,8 +26,8 @@ using System.Resources; [assembly: Guid("e4ceaf5e-ad01-4695-b179-31168eb74c48")] // Version information -[assembly: AssemblyVersion("2.0.1.95")] -[assembly: AssemblyFileVersion("2.0.1.95")] +[assembly: AssemblyVersion("2.0.1.123")] +[assembly: AssemblyFileVersion("2.0.1.123")] [assembly: NeutralResourcesLanguageAttribute( "en" )] [assembly: CLSCompliant(true)]