diff --git a/scripts/windows/launchbox-plugin/Dockerfile b/scripts/windows/launchbox-plugin/Dockerfile
index 1d4e5d33b..ef2a975ca 100644
--- a/scripts/windows/launchbox-plugin/Dockerfile
+++ b/scripts/windows/launchbox-plugin/Dockerfile
@@ -17,4 +17,4 @@ RUN dotnet restore
COPY . .
# Publish the plugin (creates complete artifact)
-RUN dotnet publish -c Release -o /build --no-restore
+RUN dotnet publish -c Release -o /build
diff --git a/scripts/windows/launchbox-plugin/ZaparooLaunchBoxPlugin.csproj b/scripts/windows/launchbox-plugin/ZaparooLaunchBoxPlugin.csproj
index 24d45dfe5..650f957b6 100644
--- a/scripts/windows/launchbox-plugin/ZaparooLaunchBoxPlugin.csproj
+++ b/scripts/windows/launchbox-plugin/ZaparooLaunchBoxPlugin.csproj
@@ -11,9 +11,9 @@
bin\$(Configuration)\
- 1.0.0
- 1.0.0
- 1.0.0
+ 1.2.0
+ 1.2.0
+ 1.2.0
Zaparoo LaunchBox Plugin
diff --git a/scripts/windows/launchbox-plugin/ZaparooPlugin.cs b/scripts/windows/launchbox-plugin/ZaparooPlugin.cs
index 4b76bfe7b..2e4120170 100644
--- a/scripts/windows/launchbox-plugin/ZaparooPlugin.cs
+++ b/scripts/windows/launchbox-plugin/ZaparooPlugin.cs
@@ -20,9 +20,14 @@
using System.IO;
using System.IO.Pipes;
using System.Reflection;
+using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
+using System.Windows.Input;
+using System.Windows.Threading;
+using System.Xml.Linq;
+using System.Xml.XPath;
using Unbroken.LaunchBox.Plugins;
using Unbroken.LaunchBox.Plugins.Data;
@@ -35,6 +40,8 @@ namespace ZaparooLaunchBoxPlugin;
public class ZaparooPlugin : ISystemEventsPlugin, IGameLaunchingPlugin, IGameMenuItemPlugin
{
private const string PipeName = "zaparoo-launchbox-ipc";
+ private const uint WinEventOutOfContext = 0;
+ private const uint EventSystemForeground = 3;
// Static connection state - shared across all plugin instances
private static NamedPipeClientStream? _pipeClient;
@@ -43,17 +50,61 @@ public class ZaparooPlugin : ISystemEventsPlugin, IGameLaunchingPlugin, IGameMen
private static CancellationTokenSource? _cancellationTokenSource;
private static Task? _connectionTask;
private static readonly object _pipeLock = new();
+ private static readonly object _stateLock = new();
private static bool _isShuttingDown;
private static bool _isBigBoxRunning;
-
- // Instance-specific state
- private IGame? _currentGame;
+ private static Dispatcher? _uiDispatcher;
+ private static string _launchBoxRoot = string.Empty;
+ private static IGame? _currentGame;
+ private static IAdditionalApplication? _currentAdditionalApp;
+ private static string? _pendingLaunchGameId;
+ private static string? _pendingLaunchAdditionalAppId;
+ private static bool _pendingLaunchShouldNavigate;
+ private static bool _shouldNavigateBackAfterLaunch;
+ private static bool _shouldShowGameAfterFocusLoss;
+ private static WinEventDelegate? _foregroundDelegate;
+ private static nint _foregroundHook;
public string Name => "Zaparoo LaunchBox Integration";
+ private delegate void WinEventDelegate(
+ nint hWinEventHook,
+ uint eventType,
+ nint hwnd,
+ int idObject,
+ int idChild,
+ uint dwEventThread,
+ uint dwmsEventTime
+ );
+
+ [DllImport("user32.dll")]
+ private static extern nint SetWinEventHook(
+ uint eventMin,
+ uint eventMax,
+ nint hmodWinEventProc,
+ WinEventDelegate lpfnWinEventProc,
+ uint idProcess,
+ uint idThread,
+ uint dwFlags
+ );
+
+ [DllImport("user32.dll")]
+ private static extern bool UnhookWinEvent(nint hWinEventHook);
+
+ [DllImport("user32.dll")]
+ private static extern nint GetForegroundWindow();
+
+ [DllImport("user32.dll")]
+ private static extern int GetWindowText(nint hWnd, StringBuilder text, int count);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
+
// Constructor - try to connect immediately as fallback
public ZaparooPlugin()
{
+ CaptureApplicationContext();
+
// Try to connect immediately in case system events don't fire
System.Threading.Tasks.Task.Run(() =>
{
@@ -69,22 +120,41 @@ public ZaparooPlugin()
public void OnEventRaised(string eventType)
{
- if (eventType == SystemEventTypes.LaunchBoxStartupCompleted)
+ CaptureApplicationContext();
+
+ if (eventType == SystemEventTypes.PluginInitialized)
+ {
+ _uiDispatcher ??= System.Windows.Application.Current?.Dispatcher;
+ }
+ else if (eventType == SystemEventTypes.LaunchBoxStartupCompleted)
{
+ _uiDispatcher ??= System.Windows.Application.Current?.Dispatcher;
_isBigBoxRunning = false;
_isShuttingDown = false;
StartConnectionTask();
}
else if (eventType == SystemEventTypes.BigBoxStartupCompleted)
{
+ _uiDispatcher ??= System.Windows.Application.Current?.Dispatcher;
_isBigBoxRunning = true;
_isShuttingDown = false;
+ EnsureForegroundHook();
StartConnectionTask();
}
+ else if (eventType == SystemEventTypes.SelectionChanged)
+ {
+ ClearCurrentGameIfSelectionChanged();
+ }
+ else if (eventType == SystemEventTypes.GameExited)
+ {
+ NavigateBackAfterZaparooLaunch();
+ }
else if (eventType == SystemEventTypes.LaunchBoxShutdownBeginning ||
eventType == SystemEventTypes.BigBoxShutdownBeginning)
{
_isShuttingDown = true;
+ ClearPendingLaunchState();
+ StopForegroundHook();
DisconnectFromPipe();
}
}
@@ -95,8 +165,18 @@ public void OnEventRaised(string eventType)
public void OnBeforeGameLaunching(IGame game, IAdditionalApplication? app, IEmulator? emulator)
{
- // Track the game that's about to launch
- _currentGame = game;
+ lock (_stateLock)
+ {
+ _currentGame = game;
+ _currentAdditionalApp = app;
+ _shouldNavigateBackAfterLaunch = MatchesPendingLaunch(game, app) && _pendingLaunchShouldNavigate;
+ _shouldShowGameAfterFocusLoss = _shouldNavigateBackAfterLaunch;
+ _pendingLaunchGameId = null;
+ _pendingLaunchAdditionalAppId = null;
+ _pendingLaunchShouldNavigate = false;
+ }
+
+ Log($"Game launching: {game.Title} ({game.Id})");
}
public void OnAfterGameLaunched(IGame game, IAdditionalApplication? app, IEmulator? emulator)
@@ -105,26 +185,37 @@ public void OnAfterGameLaunched(IGame game, IAdditionalApplication? app, IEmulat
SendEvent(new GameStartedEvent
{
Event = "MediaStarted",
- Id = game.Id,
- Title = game.Title,
+ Id = app?.Id ?? game.Id,
+ Title = app?.Name ?? game.Title,
Platform = game.Platform,
- ApplicationPath = game.ApplicationPath
+ ApplicationPath = app?.ApplicationPath ?? game.ApplicationPath
});
}
public void OnGameExited()
{
- // Send game exited event to Zaparoo
- if (_currentGame != null)
+ IGame? game;
+ IAdditionalApplication? app;
+ lock (_stateLock)
+ {
+ game = _currentGame;
+ app = _currentAdditionalApp;
+ _currentGame = null;
+ _currentAdditionalApp = null;
+ _shouldShowGameAfterFocusLoss = false;
+ }
+
+ if (game != null)
{
SendEvent(new GameExitedEvent
{
Event = "MediaStopped",
- Id = _currentGame.Id,
- Title = _currentGame.Title
+ Id = app?.Id ?? game.Id,
+ Title = app?.Name ?? game.Title
});
- _currentGame = null;
}
+
+ NavigateBackAfterZaparooLaunch();
}
#endregion
@@ -180,6 +271,70 @@ public void OnSelected(IGame[] games)
#endregion
+ #region LaunchBox Context and Logging
+
+ private static void CaptureApplicationContext()
+ {
+ _uiDispatcher ??= System.Windows.Application.Current?.Dispatcher;
+
+ if (!string.IsNullOrEmpty(_launchBoxRoot))
+ {
+ return;
+ }
+
+ try
+ {
+ string? root = Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName);
+ if (string.IsNullOrEmpty(root))
+ {
+ root = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ }
+
+ if (!string.IsNullOrEmpty(root) &&
+ (root.EndsWith($"{Path.DirectorySeparatorChar}core", StringComparison.OrdinalIgnoreCase) ||
+ root.EndsWith($"{Path.AltDirectorySeparatorChar}core", StringComparison.OrdinalIgnoreCase)))
+ {
+ root = Directory.GetParent(root)?.FullName;
+ }
+
+ _launchBoxRoot = root ?? string.Empty;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Zaparoo plugin: Failed to capture LaunchBox root: {ex.Message}");
+ }
+ }
+
+ private static void Log(string message)
+ {
+ try
+ {
+ CaptureApplicationContext();
+ string root = _launchBoxRoot;
+ if (string.IsNullOrEmpty(root))
+ {
+ root = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
+ }
+
+ if (string.IsNullOrEmpty(root))
+ {
+ System.Diagnostics.Debug.WriteLine($"Zaparoo plugin: {message}");
+ return;
+ }
+
+ string logDir = Path.Combine(root, "Logs");
+ Directory.CreateDirectory(logDir);
+ string logPath = Path.Combine(logDir, "ZaparooLaunchBoxPlugin.txt");
+ File.AppendAllText(logPath, $"[{DateTime.Now}] {message}{Environment.NewLine}");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Zaparoo plugin: {message} (log failed: {ex.Message})");
+ }
+ }
+
+ #endregion
+
#region Named Pipe Communication
private void StartConnectionTask()
@@ -407,41 +562,97 @@ private void HandleCommand(string json)
return;
}
- switch (command.Command)
+ string commandName = command.Command ?? string.Empty;
+ switch (commandName.ToLowerInvariant())
{
- case "Launch":
+ case "launch":
if (!string.IsNullOrEmpty(command.Id))
{
LaunchGameById(command.Id);
}
+ else
+ {
+ SendCommandError("Launch", "Missing required field 'Id' for launch");
+ }
break;
- case "GetPlatforms":
+ case "showplatforms":
+ ShowPlatforms();
+ break;
+
+ case "showallgames":
+ ShowAllGames();
+ break;
+
+ case "showplatform":
+ if (!string.IsNullOrEmpty(command.Platform))
+ {
+ ShowPlatform(command.Platform);
+ }
+ else
+ {
+ SendCommandError("ShowPlatform", "Missing required field 'Platform' for showplatform");
+ }
+ break;
+
+ case "showplaylist":
+ if (!string.IsNullOrEmpty(command.Playlist))
+ {
+ ShowPlaylist(command.Playlist);
+ }
+ else
+ {
+ SendCommandError("ShowPlaylist", "Missing required field 'Playlist' for showplaylist");
+ }
+ break;
+
+ case "search":
+ if (!string.IsNullOrEmpty(command.Query))
+ {
+ Search(command.Query);
+ }
+ else
+ {
+ SendCommandError("Search", "Missing required field 'Query' for search");
+ }
+ break;
+
+ case "openmanual":
+ OpenManual(command.Id);
+ break;
+
+ case "getplatforms":
SendPlatformsList();
break;
- case "GetGames":
+ case "getgames":
SendGamesList();
break;
- case "GetGamesForPlatform":
+ case "getgamesforplatform":
if (!string.IsNullOrEmpty(command.Platform))
{
SendGamesForPlatform(command.Platform);
}
+ else
+ {
+ SendCommandError("GetGamesForPlatform", "Missing required field 'Platform' for getgamesforplatform");
+ }
break;
- case "Ping":
+ case "ping":
// Heartbeat to keep connection alive - no action needed
break;
default:
+ SendCommandError(commandName, "Unknown command");
break;
}
}
- catch (Exception)
+ catch (Exception ex)
{
- // Ignore command errors
+ Log($"Command failed: {ex.Message}");
+ SendCommandError("Command", ex.Message);
}
}
@@ -449,18 +660,18 @@ private void LaunchGameById(string gameId)
{
try
{
+ if (IsGameRunning())
+ {
+ Log($"Ignoring launch for {gameId}: a game is already running");
+ SendCommandError("Launch", "A game is already running");
+ return;
+ }
+
// First try to find as a regular game
var game = PluginHelper.DataManager.GetGameById(gameId);
if (game != null)
{
- if (_isBigBoxRunning)
- {
- PluginHelper.BigBoxMainViewModel.PlayGame(game, null, null, null);
- }
- else
- {
- PluginHelper.LaunchBoxMainViewModel.PlayGame(game, null, null, null);
- }
+ LaunchGame(game, null);
return;
}
@@ -468,22 +679,354 @@ private void LaunchGameById(string gameId)
var (parentGame, additionalApp) = FindAdditionalApplicationById(gameId);
if (additionalApp != null && parentGame != null)
{
- if (_isBigBoxRunning)
+ LaunchGame(parentGame, additionalApp);
+ return;
+ }
+
+ Log($"Game or additional app not found: {gameId}");
+ SendCommandError("Launch", $"Game or additional app not found: {gameId}");
+ }
+ catch (Exception ex)
+ {
+ Log($"Failed to launch game {gameId}: {ex.Message}");
+ SendCommandError("Launch", ex.Message);
+ }
+ }
+
+ private void LaunchGame(IGame game, IAdditionalApplication? app)
+ {
+ InvokeOnUiThread(() =>
+ {
+ if (_isBigBoxRunning || PluginHelper.StateManager?.IsBigBox == true)
+ {
+ EnsureForegroundHook();
+ BeginPendingLaunch(game, app, navigateAfterLaunch: true);
+
+ try
+ {
+ Log($"Launching BigBox game: {game.Title} ({game.Id})");
+ PluginHelper.BigBoxMainViewModel.PlayGame(game, app, null, null);
+ }
+ catch
{
- PluginHelper.BigBoxMainViewModel.PlayGame(parentGame, additionalApp, null, null);
+ ClearPendingLaunch(game, app);
+ throw;
}
- else
+ return;
+ }
+
+ BeginPendingLaunch(game, app, navigateAfterLaunch: false);
+ try
+ {
+ Log($"Launching LaunchBox game: {game.Title} ({game.Id})");
+ PluginHelper.LaunchBoxMainViewModel.PlayGame(game, app, null, null);
+ }
+ catch
+ {
+ ClearPendingLaunch(game, app);
+ throw;
+ }
+ }, "Launch game");
+ }
+
+ private static void InvokeOnUiThread(Action action, string description)
+ {
+ CaptureApplicationContext();
+ Dispatcher? dispatcher = _uiDispatcher ?? System.Windows.Application.Current?.Dispatcher;
+ if (dispatcher == null)
+ {
+ Log($"Cannot run UI action without dispatcher: {description}");
+ return;
+ }
+
+ if (dispatcher.CheckAccess())
+ {
+ action();
+ return;
+ }
+
+ dispatcher.Invoke(action);
+ }
+
+ private static bool IsGameRunning()
+ {
+ lock (_stateLock)
+ {
+ return _currentGame != null || _pendingLaunchGameId != null;
+ }
+ }
+
+ private static bool MatchesPendingLaunch(IGame game, IAdditionalApplication? app)
+ {
+ return _pendingLaunchGameId == game.Id && _pendingLaunchAdditionalAppId == app?.Id;
+ }
+
+ private static void ClearPendingLaunchState()
+ {
+ lock (_stateLock)
+ {
+ _pendingLaunchGameId = null;
+ _pendingLaunchAdditionalAppId = null;
+ _pendingLaunchShouldNavigate = false;
+ _shouldNavigateBackAfterLaunch = false;
+ _shouldShowGameAfterFocusLoss = false;
+ _currentGame = null;
+ _currentAdditionalApp = null;
+ }
+ }
+
+ private static void BeginPendingLaunch(IGame game, IAdditionalApplication? app, bool navigateAfterLaunch)
+ {
+ lock (_stateLock)
+ {
+ _pendingLaunchGameId = game.Id;
+ _pendingLaunchAdditionalAppId = app?.Id;
+ _pendingLaunchShouldNavigate = navigateAfterLaunch;
+ _shouldNavigateBackAfterLaunch = false;
+ _shouldShowGameAfterFocusLoss = false;
+ }
+
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromSeconds(30));
+ lock (_stateLock)
+ {
+ if (_pendingLaunchGameId == game.Id && _pendingLaunchAdditionalAppId == app?.Id)
{
- PluginHelper.LaunchBoxMainViewModel.PlayGame(parentGame, additionalApp, null, null);
+ Log($"Clearing stale pending launch: {game.Title} ({game.Id})");
+ _pendingLaunchGameId = null;
+ _pendingLaunchAdditionalAppId = null;
+ _pendingLaunchShouldNavigate = false;
+ _shouldNavigateBackAfterLaunch = false;
+ _shouldShowGameAfterFocusLoss = false;
}
+ }
+ });
+ }
+
+ private static void ClearPendingLaunch(IGame game, IAdditionalApplication? app)
+ {
+ lock (_stateLock)
+ {
+ if (_pendingLaunchGameId == game.Id && _pendingLaunchAdditionalAppId == app?.Id)
+ {
+ _pendingLaunchGameId = null;
+ _pendingLaunchAdditionalAppId = null;
+ _pendingLaunchShouldNavigate = false;
+ _shouldNavigateBackAfterLaunch = false;
+ _shouldShowGameAfterFocusLoss = false;
+ }
+ }
+ }
+
+ private static void ClearCurrentGameIfSelectionChanged()
+ {
+ // CLI Launcher uses selection changes to repair stale in-game state, but Zaparoo relies on
+ // OnGameExited to emit MediaStopped and to keep duplicate launches blocked while a game runs.
+ // Leave running state intact here; stale pending launches are handled by their timeout.
+ }
+
+ private static void EnsureForegroundHook()
+ {
+ lock (_stateLock)
+ {
+ if (_foregroundHook != 0)
+ {
return;
}
- System.Diagnostics.Debug.WriteLine($"Zaparoo plugin: Game or additional app not found: {gameId}");
+ try
+ {
+ _foregroundDelegate = OnForegroundChanged;
+ _foregroundHook = SetWinEventHook(
+ EventSystemForeground,
+ EventSystemForeground,
+ 0,
+ _foregroundDelegate,
+ 0,
+ 0,
+ WinEventOutOfContext
+ );
+
+ if (_foregroundHook == 0)
+ {
+ _foregroundDelegate = null;
+ Log("Failed to install foreground hook");
+ }
+ }
+ catch (Exception ex)
+ {
+ _foregroundDelegate = null;
+ _foregroundHook = 0;
+ Log($"Failed to install foreground hook: {ex.Message}");
+ }
+ }
+ }
+
+ private static void StopForegroundHook()
+ {
+ lock (_stateLock)
+ {
+ try
+ {
+ if (_foregroundHook != 0)
+ {
+ UnhookWinEvent(_foregroundHook);
+ _foregroundHook = 0;
+ }
+
+ _foregroundDelegate = null;
+ }
+ catch (Exception ex)
+ {
+ Log($"Failed to remove foreground hook: {ex.Message}");
+ }
+ }
+ }
+
+ private static void OnForegroundChanged(
+ nint hWinEventHook,
+ uint eventType,
+ nint hwnd,
+ int idObject,
+ int idChild,
+ uint dwEventThread,
+ uint dwmsEventTime
+ )
+ {
+ if (IsLaunchBoxOrBigBoxActiveWindow())
+ {
+ return;
+ }
+
+ IGame? gameToShow = null;
+ lock (_stateLock)
+ {
+ if (_shouldShowGameAfterFocusLoss && _currentGame != null)
+ {
+ _shouldShowGameAfterFocusLoss = false;
+ gameToShow = _currentGame;
+ }
+ }
+
+ if (gameToShow == null)
+ {
+ return;
+ }
+
+ InvokeOnUiThread(() =>
+ {
+ Log($"Showing launched game after focus loss: {gameToShow.Title}");
+ PluginHelper.BigBoxMainViewModel.ShowGame(gameToShow, FilterType.Local);
+ }, "Show launched BigBox game");
+ }
+
+ private static bool IsLaunchBoxOrBigBoxActiveWindow()
+ {
+ try
+ {
+ string title = GetActiveWindowTitle();
+ return title.Equals("launchbox", StringComparison.OrdinalIgnoreCase) ||
+ title.Equals("launchbox big box", StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static string GetActiveWindowTitle()
+ {
+ nint window = GetForegroundWindow();
+ if (window == 0)
+ {
+ return string.Empty;
+ }
+
+ var title = new StringBuilder(256);
+ return GetWindowText(window, title, title.Capacity) > 0 ? title.ToString() : string.Empty;
+ }
+
+ private static void NavigateBackAfterZaparooLaunch()
+ {
+ bool shouldNavigate;
+ lock (_stateLock)
+ {
+ shouldNavigate = _shouldNavigateBackAfterLaunch;
+ _shouldNavigateBackAfterLaunch = false;
+ _shouldShowGameAfterFocusLoss = false;
+ }
+
+ if (!shouldNavigate || !IsBigBox())
+ {
+ return;
+ }
+
+ InvokeOnUiThread(() =>
+ {
+ if (!SendBigBoxBackKey())
+ {
+ Log("Falling back to ShowAllGames after BigBox Back was unavailable");
+ PluginHelper.BigBoxMainViewModel.ShowAllGames();
+ }
+ }, "Navigate back after BigBox launch");
+ }
+
+ private static bool SendBigBoxBackKey()
+ {
+ string? setting = ReadBigBoxSetting("KeyboardBack");
+ if (string.IsNullOrWhiteSpace(setting) || setting == "0")
+ {
+ return false;
+ }
+
+ if (!int.TryParse(setting, out int keyValue))
+ {
+ Log($"Invalid KeyboardBack setting: {setting}");
+ return false;
+ }
+
+ byte virtualKey = (byte)KeyInterop.VirtualKeyFromKey((Key)keyValue);
+ if (virtualKey == 0)
+ {
+ return false;
+ }
+
+ if (!IsLaunchBoxOrBigBoxActiveWindow())
+ {
+ Log("Skipping BigBox Back key because BigBox is not foreground");
+ return false;
+ }
+
+ Log($"Navigating BigBox back with virtual key: {virtualKey}");
+ keybd_event(virtualKey, 0, 0, 0);
+ keybd_event(virtualKey, 0, 2, 0);
+ return true;
+ }
+
+ private static string? ReadBigBoxSetting(string name)
+ {
+ try
+ {
+ CaptureApplicationContext();
+ if (string.IsNullOrEmpty(_launchBoxRoot))
+ {
+ return null;
+ }
+
+ string path = Path.Combine(_launchBoxRoot, "Data", "BigBoxSettings.xml");
+ if (!File.Exists(path))
+ {
+ return null;
+ }
+
+ XDocument doc = XDocument.Load(path);
+ return doc.XPathSelectElement($"/LaunchBox/BigBoxSettings/{name}")?.Value;
}
catch (Exception ex)
{
- System.Diagnostics.Debug.WriteLine($"Zaparoo plugin: Failed to launch game {gameId}: {ex.Message}");
+ Log($"Failed to read BigBox setting {name}: {ex.Message}");
+ return null;
}
}
@@ -502,6 +1045,164 @@ private void LaunchGameById(string gameId)
return (null, null);
}
+ private void ShowPlatforms()
+ {
+ InvokeOnUiThread(() =>
+ {
+ if (!IsBigBox())
+ {
+ SendCommandError("ShowPlatforms", "Platform navigation only works in BigBox");
+ return;
+ }
+
+ if (IsGameRunning())
+ {
+ SendCommandError("ShowPlatforms", "A game is already running");
+ return;
+ }
+
+ PluginHelper.BigBoxMainViewModel.ShowPlatforms();
+ }, "Show BigBox platforms");
+ }
+
+ private void ShowAllGames()
+ {
+ InvokeOnUiThread(() =>
+ {
+ if (!IsBigBox())
+ {
+ SendCommandError("ShowAllGames", "All Games navigation only works in BigBox");
+ return;
+ }
+
+ if (IsGameRunning())
+ {
+ SendCommandError("ShowAllGames", "A game is already running");
+ return;
+ }
+
+ PluginHelper.BigBoxMainViewModel.ShowAllGames();
+ }, "Show BigBox all games");
+ }
+
+ private void ShowPlatform(string platformName)
+ {
+ InvokeOnUiThread(() =>
+ {
+ if (!IsBigBox())
+ {
+ SendCommandError("ShowPlatform", "Platform navigation only works in BigBox");
+ return;
+ }
+
+ if (IsGameRunning())
+ {
+ SendCommandError("ShowPlatform", "A game is already running");
+ return;
+ }
+
+ IPlatform? platform = PluginHelper.DataManager.GetPlatformByName(platformName);
+ if (platform == null)
+ {
+ SendCommandError("ShowPlatform", $"Platform not found: {platformName}");
+ return;
+ }
+
+ PluginHelper.BigBoxMainViewModel.ShowGames(platform);
+ }, "Show BigBox platform");
+ }
+
+ private void ShowPlaylist(string playlistName)
+ {
+ InvokeOnUiThread(() =>
+ {
+ if (!IsBigBox())
+ {
+ SendCommandError("ShowPlaylist", "Playlist navigation only works in BigBox");
+ return;
+ }
+
+ if (IsGameRunning())
+ {
+ SendCommandError("ShowPlaylist", "A game is already running");
+ return;
+ }
+
+ IPlaylist? playlist = PluginHelper.DataManager.GetAllPlaylists()
+ .FirstOrDefault(p => p.Name.Equals(playlistName, StringComparison.OrdinalIgnoreCase));
+ if (playlist == null)
+ {
+ SendCommandError("ShowPlaylist", $"Playlist not found: {playlistName}");
+ return;
+ }
+
+ PluginHelper.BigBoxMainViewModel.ShowGames(FilterType.PlatformOrCategoryOrPlaylist, playlist.Name);
+ }, "Show BigBox playlist");
+ }
+
+ private void Search(string query)
+ {
+ InvokeOnUiThread(() =>
+ {
+ if (!IsBigBox())
+ {
+ SendCommandError("Search", "Search command only works in BigBox");
+ return;
+ }
+
+ if (IsGameRunning())
+ {
+ SendCommandError("Search", "A game is already running");
+ return;
+ }
+
+ PluginHelper.BigBoxMainViewModel.Search(query);
+ }, "Search BigBox");
+ }
+
+ private void OpenManual(string? gameId)
+ {
+ InvokeOnUiThread(() =>
+ {
+ IGame? game = null;
+ if (!string.IsNullOrEmpty(gameId))
+ {
+ game = PluginHelper.DataManager.GetGameById(gameId);
+ }
+
+ if (game == null)
+ {
+ lock (_stateLock)
+ {
+ game = _currentGame;
+ }
+ }
+
+ if (game == null)
+ {
+ IGame[]? selectedGames = PluginHelper.StateManager?.GetAllSelectedGames();
+ game = selectedGames is { Length: > 0 } ? selectedGames[0] : null;
+ }
+
+ if (game == null)
+ {
+ SendCommandError("OpenManual", "No game is selected or running");
+ return;
+ }
+
+ string? result = game.OpenManual();
+ if (!string.IsNullOrEmpty(result))
+ {
+ Log($"OpenManual result for {game.Title}: {result}");
+ }
+ }, "Open manual");
+ }
+
+ private static bool IsBigBox()
+ {
+ return _isBigBoxRunning || PluginHelper.StateManager?.IsBigBox == true;
+ }
+
private void SendPlatformsList()
{
try
@@ -612,6 +1313,16 @@ private void SendGamesForPlatform(string platform)
}
}
+ private void SendCommandError(string command, string message)
+ {
+ Log($"{command} error: {message}");
+ SendEvent(new ErrorEvent
+ {
+ Command = command,
+ Error = message
+ });
+ }
+
#endregion
#region Message Types
@@ -645,6 +1356,15 @@ private class PluginCommand
public string? Command { get; set; }
public string? Id { get; set; }
public string? Platform { get; set; }
+ public string? Playlist { get; set; }
+ public string? Query { get; set; }
+ }
+
+ private class ErrorEvent
+ {
+ public string Event { get; set; } = "Error";
+ public string Command { get; set; } = string.Empty;
+ public string Error { get; set; } = string.Empty;
}
private class PlatformInfo