Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions source/Libraries/XboxLibrary/Models/AuthenticationData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,20 @@ public class Detail
public string developerName;
public DateTime? releaseDate;
public int? minAge;
public List<Availability> availabilities;
}

public class TitleHistory
{
public DateTime? lastTimePlayed;
}

public class Availability
{
public List<string> Platforms;
public string ProductId;
}

public class Title
{
public string titleId;
Expand Down
7 changes: 7 additions & 0 deletions source/Libraries/XboxLibrary/Xbox.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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");
}
}
90 changes: 83 additions & 7 deletions source/Libraries/XboxLibrary/XboxGameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,51 @@ 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)
{
Dispose();
if (userXboxApp)
{
Xbox.OpenXboxPassApp();
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();
}
}
else
{
Expand All @@ -54,12 +81,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
Expand All @@ -73,6 +102,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
Expand Down
67 changes: 63 additions & 4 deletions source/Libraries/XboxLibrary/XboxLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace XboxLibrary
public class XboxLibrary : LibraryPluginBase<XboxLibrarySettingsViewModel>
{
private readonly string pfnInfoCacheDir;
private readonly string pfnToProductIdMapCachePath;

public override LibraryClient Client => new XboxLibraryClient(SettingsViewModel);

Expand All @@ -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)
Expand Down Expand Up @@ -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<MetadataProperty> GetPlatforms(List<string> devices, out bool containsPC, out bool containsConsole)
Expand Down Expand Up @@ -338,17 +340,74 @@ public override IEnumerable<GameMetadata> GetGames(LibraryGetGamesArgs args)
PlayniteApi.Notifications.Remove(ImportErrorMessageId);
}

PersistPcPfnToProductIdMapCache(pcTitles);
return allGames;
}

public void PersistPcPfnToProductIdMapCache(List<TitleHistoryResponse.Title> titles)
{
try
{
var cache = GetPfnToProductIdMapCache();
var cacheChanged = false;
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)
{
cacheChanged = true;
cache[title.pfn] = pcProductId;
}
}

if (cacheChanged)
{
var serializedData = Serialization.ToJson(cache);
FileSystem.WriteStringToFile(pfnToProductIdMapCachePath, serializedData);
}
}
catch (Exception e)
{
Logger.Error(e, "Error while persisting PC Title Id Cache");
};
}

public Dictionary<string, string> GetPfnToProductIdMapCache()
{
if (FileSystem.FileExists(pfnToProductIdMapCachePath))
{
return Serialization.FromJsonFile<Dictionary<string, string>>(pfnToProductIdMapCachePath);
}

return new Dictionary<string, string>();
}

public override IEnumerable<InstallController> 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<UninstallController> GetUninstallActions(GetUninstallActionsArgs args)
Expand Down