// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using Avalonia; using Avalonia.Controls; using Avalonia.VisualTree; using SharpEmu.Libs.VideoOut; namespace SharpEmu.GUI; /// Inline per-game settings navigation, loading and persistence. public partial class MainWindow { private static readonly string[] GameEnvironmentToggleNames = [ "SHARPEMU_BTHID_UNAVAILABLE", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", "SHARPEMU_WRITABLE_APP0", "SHARPEMU_VK_VALIDATION", "SHARPEMU_DUMP_SPIRV", "SHARPEMU_LOG_DIRECT_MEMORY", "SHARPEMU_LOG_IO", "SHARPEMU_LOG_NP", "SHARPEMU_GUEST_IMAGE_CPU_SYNC", ]; private readonly List _gameEnvironmentPassthrough = new(); private IReadOnlyList _gameHostDisplays = []; private bool _isGameSettingsOpen; private bool _isLoadingGameSettings; private bool _updatingGameHostDisplayOptions; private int _gameOptionsIndicatorIndex; private int _gameOptionsSectionIndex; private string? _gameSettingsTitleId; private void WireGameOptions() { GameSettingsButton.Click += (_, _) => OpenSelectedGameSettings(); GameLogLevelBox.ItemsSource = _logLevelChoices; GameWindowModeBox.ItemsSource = _windowModeChoices; GameScalingModeBox.ItemsSource = _scalingModeChoices; GameHdrModeBox.ItemsSource = _hdrModeChoices; var navigationButtons = GameOptionsNavigationButtons(); for (var index = 0; index < navigationButtons.Length; index++) { var section = index; navigationButtons[index].Click += (_, _) => { if (section < GameOptionsSectionPanels().Length) { SetGameOptionsSection(section); } else { CloseGameSettings(); } }; navigationButtons[index].PointerEntered += (_, _) => { if (_isGameSettingsOpen) { SetGameOptionsNavigationIndicator(section); } }; navigationButtons[index].GotFocus += (_, _) => { if (_isGameSettingsOpen) { SetGameOptionsNavigationIndicator(section); } }; } GameOptionsNavHost.PointerExited += (_, _) => { if (_isGameSettingsOpen) { SetGameOptionsNavigationIndicator(_gameOptionsSectionIndex); } }; GameOptionsLaunchButton.Click += (_, _) => { CloseGameSettings(); LaunchSelected(); }; GameOptionsCloseButton.Click += (_, _) => CloseGameSettings(); GameOptionsOpenFolderButton.Click += (_, _) => OpenSelectedGameFolder(); GameOptionsCopyPathButton.Click += async (_, _) => await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path); GameOptionsCopyTitleIdButton.Click += async (_, _) => await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId); GameOptionsRemoveButton.Click += (_, _) => { CloseGameSettings(); RemoveSelectedFromLibrary(); }; GameStrictToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); GameLogLevelBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); GameTraceImportsBox.ValueChanged += (_, _) => PersistOpenGameSettings(); GameLogToFileToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); GameWindowModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); GameDisplayBox.SelectionChanged += (_, _) => OnGameHostDisplayChanged(); GameResolutionBox.SelectionChanged += (_, _) => OnGameHostResolutionChanged(); GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); foreach (var (_, toggle) in GameEnvironmentToggles()) { toggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); } SetGameOptionsSection(0, animateIndicator: false); } private void OpenSelectedGameSettings() { if (GameList.SelectedItem is not GameEntry game) { return; } if (string.IsNullOrWhiteSpace(game.TitleId)) { AppendConsoleLine( "[GUI][WARN] Per-game settings require a title ID, which this game does not have.", WarningLineBrush); return; } _gameSettingsTitleId = game.TitleId; GameOptionsOverlay.DataContext = game; LoadGameSettings(game.TitleId); SetGameOptionsSection(0, animateIndicator: false); GameOptionsLaunchButton.IsEnabled = !_isRunning; GameOptionsCopyTitleIdButton.IsEnabled = !string.IsNullOrWhiteSpace(game.TitleId); _isGameSettingsOpen = true; SetGameOptionsPagesSpan(coversConsoleRow: true); SetGameOptionsOpenClass(BackdropLayer, active: true); SetGameOptionsOpenClass(CarouselHost, active: true); SetGameOptionsOpenClass(LibrarySelectedDetails, active: true); SetGameOptionsOpenClass(GameOptionsOverlay, active: true); GameList.IsHitTestVisible = false; LibraryToolbar.IsHitTestVisible = false; GameOptionsOverlay.IsHitTestVisible = true; GameOptionsGeneralNav.Focus(); } private void CloseGameSettings(bool restoreLibrary = true) { if (!_isGameSettingsOpen) { return; } _isGameSettingsOpen = false; SetGameOptionsPagesSpan(coversConsoleRow: false); SetGameOptionsNavigationIndicator(_gameOptionsIndicatorIndex, animate: false); _gameSettingsTitleId = null; _gameEnvironmentPassthrough.Clear(); SetGameOptionsOpenClass(BackdropLayer, active: false); SetGameOptionsOpenClass(CarouselHost, active: false); SetGameOptionsOpenClass(LibrarySelectedDetails, active: false); SetGameOptionsOpenClass(GameOptionsOverlay, active: false); GameOptionsOverlay.IsHitTestVisible = false; GameOptionsOverlay.DataContext = null; GameList.IsHitTestVisible = true; LibraryToolbar.IsHitTestVisible = true; if (restoreLibrary && _activePageIndex == 0) { GameList.Focus(); } } private void LoadGameSettings(string titleId) { var effective = EffectiveLaunchSettings.Resolve( _settings, PerGameSettings.Load(titleId)); _isLoadingGameSettings = true; _updatingGameHostDisplayOptions = true; try { GameStrictToggle.IsChecked = effective.StrictDynlibResolution; GameLogLevelBox.SelectedItem = FindChoice( _logLevelChoices, effective.LogLevel, "Info"); GameTraceImportsBox.Value = Math.Clamp(effective.ImportTraceLimit, 0, 4096); GameLogToFileToggle.IsChecked = effective.LogToFile; GameWindowModeBox.SelectedItem = FindChoice( _windowModeChoices, effective.WindowMode, "Windowed"); GameScalingModeBox.SelectedItem = FindChoice( _scalingModeChoices, effective.ScalingMode, "Fit"); GameVSyncToggle.IsChecked = effective.VSync; GameHdrModeBox.SelectedItem = FindChoice( _hdrModeChoices, effective.HdrMode, "Auto"); _gameHostDisplays = HostDisplayOptions.BuildDisplays( HostDisplayCatalog.Query(), effective.DisplayIndex); GameDisplayBox.ItemsSource = _gameHostDisplays; var display = HostDisplayOptions.SelectDisplay( _gameHostDisplays, effective.DisplayIndex); GameDisplayBox.SelectedItem = display; PopulateGameHostModes( display, effective.Resolution, effective.RefreshRate); _gameEnvironmentPassthrough.Clear(); foreach (var entry in effective.EnvironmentToggles) { if (!IsKnownGameEnvironmentEntry(entry)) { _gameEnvironmentPassthrough.Add(entry); } } foreach (var (name, toggle) in GameEnvironmentToggles()) { toggle.IsChecked = IsEnvironmentEnabled( effective.EnvironmentToggles, name); } } finally { _updatingGameHostDisplayOptions = false; _isLoadingGameSettings = false; } } private void PersistOpenGameSettings() { if (!_isGameSettingsOpen || _isLoadingGameSettings || _updatingGameHostDisplayOptions || string.IsNullOrWhiteSpace(_gameSettingsTitleId)) { return; } var settings = new PerGameSettings { LogLevel = SelectedComboText(GameLogLevelBox, "Info"), ImportTraceLimit = (int)(GameTraceImportsBox.Value ?? 0), StrictDynlibResolution = GameStrictToggle.IsChecked == true, LogToFile = GameLogToFileToggle.IsChecked == true, WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"), Resolution = SelectedComboText(GameResolutionBox, "1920x1080"), DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display ? display.Index : 0, RefreshRate = SelectedGameRefreshRate(), ScalingMode = SelectedComboText(GameScalingModeBox, "Fit"), VSync = GameVSyncToggle.IsChecked == true, HdrMode = SelectedComboText(GameHdrModeBox, "Auto"), EnvironmentToggles = BuildGameEnvironmentEntries(), }; settings.RemoveInheritedValues(_settings); settings.Save(_gameSettingsTitleId); } private void OnGameHostDisplayChanged() { if (_isLoadingGameSettings || _updatingGameHostDisplayOptions || GameDisplayBox.SelectedItem is not HostDisplayOption display) { return; } _updatingGameHostDisplayOptions = true; try { PopulateGameHostModes( display, GameResolutionBox.SelectedItem as string ?? "1920x1080", SelectedGameRefreshRate()); } finally { _updatingGameHostDisplayOptions = false; } PersistOpenGameSettings(); } private void OnGameHostResolutionChanged() { if (_isLoadingGameSettings || _updatingGameHostDisplayOptions || GameDisplayBox.SelectedItem is not HostDisplayOption display) { return; } _updatingGameHostDisplayOptions = true; try { PopulateGameRefreshRates( display, GameResolutionBox.SelectedItem as string, SelectedGameRefreshRate()); } finally { _updatingGameHostDisplayOptions = false; } PersistOpenGameSettings(); } private void PopulateGameHostModes( HostDisplayOption display, string selectedResolution, int selectedRefreshRate) { var resolutions = HostDisplayOptions.BuildResolutions(display, selectedResolution); GameResolutionBox.ItemsSource = resolutions; GameResolutionBox.SelectedItem = resolutions.FirstOrDefault(resolution => string.Equals(resolution, selectedResolution, StringComparison.OrdinalIgnoreCase)) ?? resolutions[0]; PopulateGameRefreshRates( display, GameResolutionBox.SelectedItem as string, selectedRefreshRate); } private void PopulateGameRefreshRates( HostDisplayOption display, string? resolution, int selectedRefreshRate) { var refreshRates = HostDisplayOptions.BuildRefreshRates( display, resolution, selectedRefreshRate, Localization.Instance.Get("Options.RefreshRate.Automatic")); GameRefreshRateBox.ItemsSource = refreshRates; GameRefreshRateBox.SelectedItem = refreshRates.FirstOrDefault( refreshRate => refreshRate.Value == selectedRefreshRate) ?? refreshRates[0]; } private int SelectedGameRefreshRate() => GameRefreshRateBox.SelectedItem is HostRefreshRateOption refreshRate ? refreshRate.Value : 0; private List BuildGameEnvironmentEntries() { var entries = new List(_gameEnvironmentPassthrough); foreach (var (name, toggle) in GameEnvironmentToggles()) { if (toggle.IsChecked == true) { entries.Add(name); } } return entries; } private static LocalizedChoice FindChoice( IEnumerable choices, string value, string fallback) => choices.FirstOrDefault(choice => string.Equals(choice.Value, value, StringComparison.OrdinalIgnoreCase)) ?? choices.First(choice => string.Equals(choice.Value, fallback, StringComparison.OrdinalIgnoreCase)); private static bool IsEnvironmentEnabled( IEnumerable entries, string name) { foreach (var entry in entries) { var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries); if (!string.Equals(parts[0], name, StringComparison.OrdinalIgnoreCase)) { continue; } return parts.Length == 1 || parts[1] != "0"; } return false; } private static bool IsKnownGameEnvironmentEntry(string entry) { var name = entry.Split('=', 2, StringSplitOptions.TrimEntries)[0]; return GameEnvironmentToggleNames.Contains(name, StringComparer.OrdinalIgnoreCase); } private void SetGameOptionsSection(int section, bool animateIndicator = true) { var buttons = GameOptionsSectionButtons(); var panels = GameOptionsSectionPanels(); section = Math.Clamp(section, 0, buttons.Length - 1); _gameOptionsSectionIndex = section; SetGameOptionsNavigationIndicator(section, animateIndicator); for (var index = 0; index < buttons.Length; index++) { var active = index == section; SetActiveClass(buttons[index], active); SetOptionsPanelInteraction(panels[index], active); } SetActiveClass(GameOptionsBackNav, active: false); } private void SetGameOptionsNavigationIndicator(int section, bool animate = true) { var buttons = GameOptionsNavigationButtons(); _gameOptionsIndicatorIndex = Math.Clamp(section, 0, buttons.Length - 1); var button = buttons[_gameOptionsIndicatorIndex]; MoveNavigationIndicator( GameOptionsNavIndicator, GameOptionsNavHost, button, _gameOptionsIndicatorIndex, animate); } private void SetGameOptionsPagesSpan(bool coversConsoleRow) { Grid.SetRowSpan(PagesHost, coversConsoleRow ? 2 : 1); } private Button[] GameOptionsNavigationButtons() => [ GameOptionsGeneralNav, GameOptionsLoggingNav, GameOptionsRenderingNav, GameOptionsEnvironmentNav, GameOptionsBackNav, ]; private Button[] GameOptionsSectionButtons() => [ GameOptionsGeneralNav, GameOptionsLoggingNav, GameOptionsRenderingNav, GameOptionsEnvironmentNav, ]; private Control[] GameOptionsSectionPanels() => [ GameOptionsGeneralPanel, GameOptionsLoggingPanel, GameOptionsRenderingPanel, GameOptionsEnvironmentPanel, ]; private (string Name, ToggleSwitch Toggle)[] GameEnvironmentToggles() => [ ("SHARPEMU_BTHID_UNAVAILABLE", GameEnvBthidToggle), ("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", GameEnvLoopGuardToggle), ("SHARPEMU_WRITABLE_APP0", GameEnvWritableApp0Toggle), ("SHARPEMU_VK_VALIDATION", GameEnvVkValidationToggle), ("SHARPEMU_DUMP_SPIRV", GameEnvDumpSpirvToggle), ("SHARPEMU_LOG_DIRECT_MEMORY", GameEnvLogDirectMemoryToggle), ("SHARPEMU_LOG_IO", GameEnvLogIoToggle), ("SHARPEMU_LOG_NP", GameEnvLogNpToggle), ("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle), ]; private static void SetGameOptionsOpenClass(Control control, bool active) => SetClass(control, "gameOptionsOpen", active); }