From f29d9edea940a53627382a93df1566e0b2b27394 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 22 Oct 2024 00:13:51 -0600 Subject: [PATCH 1/3] Xbox: Support to open game page on Xbox App when Installing --- .../XboxLibrary/Models/AuthenticationData.cs | 7 ++ source/Libraries/XboxLibrary/Xbox.cs | 7 ++ .../XboxLibrary/XboxGameController.cs | 94 ++++++++++++++++--- source/Libraries/XboxLibrary/XboxLibrary.cs | 62 +++++++++++- 4 files changed, 155 insertions(+), 15 deletions(-) diff --git a/source/Libraries/XboxLibrary/Models/AuthenticationData.cs b/source/Libraries/XboxLibrary/Models/AuthenticationData.cs index 3e4033b5..ef44ffed 100644 --- a/source/Libraries/XboxLibrary/Models/AuthenticationData.cs +++ b/source/Libraries/XboxLibrary/Models/AuthenticationData.cs @@ -75,6 +75,7 @@ public class Detail public string developerName; public DateTime? releaseDate; public int? minAge; + public List availabilities; } public class TitleHistory @@ -82,6 +83,12 @@ public class TitleHistory public DateTime? lastTimePlayed; } + public class Availability + { + public List Platforms; + public string ProductId; + } + public class Title { public string titleId; diff --git a/source/Libraries/XboxLibrary/Xbox.cs b/source/Libraries/XboxLibrary/Xbox.cs index c3cecdd9..a744320a 100644 --- a/source/Libraries/XboxLibrary/Xbox.cs +++ b/source/Libraries/XboxLibrary/Xbox.cs @@ -1,6 +1,7 @@ using Playnite.Common; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -14,6 +15,7 @@ public class Xbox private const string xboxPassAppFN = "Microsoft.GamingApp_8wekyb3d8bbwe"; private static bool? isXboxPassAppInstalled; + public static bool IsRunning => Process.GetProcessesByName("XboxPcApp").Length > 0; public static bool IsXboxPassAppInstalled { get @@ -58,6 +60,11 @@ public static void OpenXboxPassApp() } } + public static void OpenGamePage(string productId) + { + ProcessStarter.StartUrl($"msxbox://game/?productId={productId}"); + } + public static string Icon { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"icon.png"); } } diff --git a/source/Libraries/XboxLibrary/XboxGameController.cs b/source/Libraries/XboxLibrary/XboxGameController.cs index 7bf3128d..9e09b373 100644 --- a/source/Libraries/XboxLibrary/XboxGameController.cs +++ b/source/Libraries/XboxLibrary/XboxGameController.cs @@ -18,16 +18,23 @@ public class XboxInstallController : InstallController { private readonly bool userXboxApp; private CancellationTokenSource watcherToken; + private readonly string productId; + private readonly string logsDirectoryWatcherPath; + private FileSystemWatcher fileSystemWatcher; - public XboxInstallController(Game game, bool useXboxApp) : base(game) + public XboxInstallController(Game game, bool useXboxApp, string productId) : base(game) { this.userXboxApp = useXboxApp; - Name = useXboxApp ? "Install using Xbox app" : "Install using MS Store"; - } + this.productId = productId; + logsDirectoryWatcherPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Packages", + "Microsoft.GamingApp_8wekyb3d8bbwe", + "AC", + "Temp" + ); - public override void Dispose() - { - watcherToken?.Cancel(); + Name = useXboxApp ? "Install using Xbox app" : "Install using MS Store"; } public override void Install(InstallActionArgs args) @@ -35,11 +42,27 @@ public override void Install(InstallActionArgs args) Dispose(); if (userXboxApp) { - Xbox.OpenXboxPassApp(); - } - else - { - ProcessStarter.StartUrl($"ms-windows-store://pdp/?PFN={Game.GameId}"); + if (productId.IsNullOrEmpty()) + { + Xbox.OpenXboxPassApp(); + } + else if (Xbox.IsRunning) + { + // If the Xbox app is already running, it's possible to open the game page without issues. + Xbox.OpenGamePage(productId); + } + else + { + // Note: A recent update to the Xbox app introduced a bug where the URI to open the product page fails + // until the app has fully initialized. This issue did not occur in earlier versions of the app. + // + // To determine when the app has fully initialized, we monitor for changes in a specific log file, + // which is created or modified only after the initialization is complete. + // Alternative approaches, such as monitoring running processes, tracking changes in application windows, + // or detecting locked files, were ineffective for addressing this scenario. + Xbox.OpenXboxPassApp(); + InitializeFileSystemWatcher(); + } } StartInstallWatcher(); @@ -54,12 +77,14 @@ await Task.Run(async () => { if (watcherToken.IsCancellationRequested) { + DisableFileSystemWatcher(); return; } var app = Programs.GetUWPApps().FirstOrDefault(a => a.AppId == Game.GameId); if (app != null) { + DisableFileSystemWatcher(); var installInfo = new GameInstallationData { InstallDirectory = app.WorkDir @@ -73,6 +98,53 @@ await Task.Run(async () => } }); } + + private void DisableFileSystemWatcher() + { + if (fileSystemWatcher != null) + { + fileSystemWatcher.EnableRaisingEvents = false; + } + } + + private void InitializeFileSystemWatcher() + { + if (!FileSystem.DirectoryExists(logsDirectoryWatcherPath)) + { + return; + } + + fileSystemWatcher = new FileSystemWatcher(logsDirectoryWatcherPath) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + Filter = "*.*", + EnableRaisingEvents = true + }; + + fileSystemWatcher.Changed += OnFileChanged; + fileSystemWatcher.Created += OnFileChanged; + fileSystemWatcher.Renamed += OnFileChanged; + } + + private void OnFileChanged(object sender, FileSystemEventArgs e) + { + DisableFileSystemWatcher(); + Xbox.OpenGamePage(productId); + } + + public override void Dispose() + { + watcherToken?.Cancel(); + if (fileSystemWatcher != null) + { + fileSystemWatcher.EnableRaisingEvents = false; + fileSystemWatcher.Changed -= OnFileChanged; + fileSystemWatcher.Created -= OnFileChanged; + fileSystemWatcher.Renamed -= OnFileChanged; + fileSystemWatcher.Dispose(); + fileSystemWatcher = null; + } + } } public class XboxUninstallController : UninstallController diff --git a/source/Libraries/XboxLibrary/XboxLibrary.cs b/source/Libraries/XboxLibrary/XboxLibrary.cs index 4fa8fe03..5f1827bb 100644 --- a/source/Libraries/XboxLibrary/XboxLibrary.cs +++ b/source/Libraries/XboxLibrary/XboxLibrary.cs @@ -20,6 +20,7 @@ namespace XboxLibrary public class XboxLibrary : LibraryPluginBase { private readonly string pfnInfoCacheDir; + private readonly string pfnToProductIdMapCachePath; public override LibraryClient Client => new XboxLibraryClient(SettingsViewModel); @@ -34,6 +35,7 @@ public XboxLibrary(IPlayniteAPI api) : base( { SettingsViewModel = new XboxLibrarySettingsViewModel(this, api); pfnInfoCacheDir = Path.Combine(GetPluginUserDataPath(), "PfnInfoCache"); + pfnToProductIdMapCachePath = Path.Combine(GetPluginUserDataPath(), "pfnToProductIdMapCache.json"); } public override ISettings GetSettings(bool firstRunSettings) @@ -131,8 +133,8 @@ internal GameMetadata GetGameMetadataFromTitle(TitleHistoryResponse.Title title) public void WriteAppDataCache(TitleHistoryResponse.Title data) { var filePath = Path.Combine(pfnInfoCacheDir, data.pfn + ".json"); - FileSystem.PrepareSaveFile(filePath); - File.WriteAllText(filePath, Serialization.ToJson(data)); + var serializedData = Serialization.ToJson(data); + FileSystem.WriteStringToFile(filePath, serializedData); } private static HashSet GetPlatforms(List devices, out bool containsPC, out bool containsConsole) @@ -338,17 +340,69 @@ public override IEnumerable GetGames(LibraryGetGamesArgs args) PlayniteApi.Notifications.Remove(ImportErrorMessageId); } + PersistPcPfnToProductIdMapCache(pcTitles); return allGames; } + public void PersistPcPfnToProductIdMapCache(List titles) + { + try + { + var cache = GetPfnToProductIdMapCache(); + foreach (var title in titles) + { + var pcProductId = title.detail.availabilities + .FirstOrDefault(a => a.Platforms.Contains("PC"))?.ProductId; + if (pcProductId.IsNullOrEmpty()) + { + continue; + } + + if (!cache.TryGetValue(title.pfn, out var existingCache) || existingCache != pcProductId) + { + cache[title.pfn] = pcProductId; + } + } + + var serializedData = Serialization.ToJson(cache); + FileSystem.WriteStringToFile(pfnToProductIdMapCachePath, serializedData); + } + catch (Exception e) + { + Logger.Error(e, "Error while persisting PC Title Id Cache"); + }; + } + + public Dictionary GetPfnToProductIdMapCache() + { + if (FileSystem.FileExists(pfnToProductIdMapCachePath)) + { + return Serialization.FromJsonFile>(pfnToProductIdMapCachePath); + } + + return new Dictionary(); + } + public override IEnumerable GetInstallActions(GetInstallActionsArgs args) { - if (args.Game.PluginId != Id || args.Game.GameId.StartsWith("CONSOLE")) + var game = args.Game; + if (game.PluginId != Id || game.GameId.StartsWith("CONSOLE")) { yield break; } - yield return new XboxInstallController(args.Game, SettingsViewModel.Settings.XboxAppClientPriorityLaunch && Xbox.IsXboxPassAppInstalled); + var shouldUseXboxApp = SettingsViewModel.Settings.XboxAppClientPriorityLaunch && Xbox.IsXboxPassAppInstalled; + var productId = string.Empty; + if (shouldUseXboxApp) + { + var pfnToProductIdMapCache = GetPfnToProductIdMapCache(); + if (pfnToProductIdMapCache.TryGetValue(game.GameId, out var cachedProductId)) + { + productId = cachedProductId; + } + } + + yield return new XboxInstallController(args.Game, shouldUseXboxApp, productId); } public override IEnumerable GetUninstallActions(GetUninstallActionsArgs args) From c4e76377d0984265dc536153f24a85adaa64bcb2 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 22 Oct 2024 01:07:09 -0600 Subject: [PATCH 2/3] Restore MS Store app launch Uri removed by mistake --- source/Libraries/XboxLibrary/XboxGameController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/Libraries/XboxLibrary/XboxGameController.cs b/source/Libraries/XboxLibrary/XboxGameController.cs index 9e09b373..2f63aacc 100644 --- a/source/Libraries/XboxLibrary/XboxGameController.cs +++ b/source/Libraries/XboxLibrary/XboxGameController.cs @@ -64,6 +64,10 @@ public override void Install(InstallActionArgs args) InitializeFileSystemWatcher(); } } + else + { + ProcessStarter.StartUrl($"ms-windows-store://pdp/?PFN={Game.GameId}"); + } StartInstallWatcher(); } From 0c360be3e059c2d4e7bd118b64d0c665e3fe4a10 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 22 Oct 2024 01:13:05 -0600 Subject: [PATCH 3/3] Only save cache if it changed --- source/Libraries/XboxLibrary/XboxLibrary.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/source/Libraries/XboxLibrary/XboxLibrary.cs b/source/Libraries/XboxLibrary/XboxLibrary.cs index 5f1827bb..0718b5e7 100644 --- a/source/Libraries/XboxLibrary/XboxLibrary.cs +++ b/source/Libraries/XboxLibrary/XboxLibrary.cs @@ -349,6 +349,7 @@ public void PersistPcPfnToProductIdMapCache(List tit try { var cache = GetPfnToProductIdMapCache(); + var cacheChanged = false; foreach (var title in titles) { var pcProductId = title.detail.availabilities @@ -360,12 +361,16 @@ public void PersistPcPfnToProductIdMapCache(List tit if (!cache.TryGetValue(title.pfn, out var existingCache) || existingCache != pcProductId) { + cacheChanged = true; cache[title.pfn] = pcProductId; } } - var serializedData = Serialization.ToJson(cache); - FileSystem.WriteStringToFile(pfnToProductIdMapCachePath, serializedData); + if (cacheChanged) + { + var serializedData = Serialization.ToJson(cache); + FileSystem.WriteStringToFile(pfnToProductIdMapCachePath, serializedData); + } } catch (Exception e) {