diff --git a/README.md b/README.md index bb51dee13..219e0e598 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ If you are planning to contribute or just want to learn more about this project - **Input** - We currently have support for keyboard, mouse, touch input, JoyCon input support, and nearly all controllers. + We currently have support for keyboard, mouse, touch input, Joy-Con input support, and nearly all controllers. Motion controls are natively supported in most cases; for dual-JoyCon motion support, DS4Windows or BetterJoy are currently required. In all scenarios, you can set up everything inside the input configuration menu. diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs index 7b5f2ca81..e480985b1 100644 --- a/src/ARMeilleure/Translation/Cache/JitCache.cs +++ b/src/ARMeilleure/Translation/Cache/JitCache.cs @@ -24,7 +24,7 @@ namespace ARMeilleure.Translation.Cache private static JitCacheInvalidation _jitCacheInvalidator; - private static CacheMemoryAllocator _cacheAllocator; + private static List _cacheAllocators = []; private static readonly List _cacheEntries = []; @@ -40,37 +40,48 @@ namespace ARMeilleure.Translation.Cache public static void Initialize(IJitMemoryAllocator allocator) { - if (_initialized) - { - return; - } - lock (_lock) { if (_initialized) { - return; + if (OperatingSystem.IsWindows()) + { + JitUnwindWindows.RemoveFunctionTableHandler( + _jitRegions[0].Pointer); + } + + for (int i = 0; i < _jitRegions.Count; i++) + { + _jitRegions[i].Dispose(); + } + + _jitRegions.Clear(); + _cacheAllocators.Clear(); } + else + { + _initialized = true; + } + + _activeRegionIndex = 0; ReservedRegion firstRegion = new(allocator, CacheSize); _jitRegions.Add(firstRegion); - _activeRegionIndex = 0; + + CacheMemoryAllocator firstCacheAllocator = new(CacheSize); + _cacheAllocators.Add(firstCacheAllocator); if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) { _jitCacheInvalidator = new JitCacheInvalidation(allocator); } - _cacheAllocator = new CacheMemoryAllocator(CacheSize); - if (OperatingSystem.IsWindows()) { JitUnwindWindows.InstallFunctionTableHandler( firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize) ); } - - _initialized = true; } } @@ -136,7 +147,7 @@ namespace ARMeilleure.Translation.Cache if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset) { - _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); + _cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size)); _cacheEntries.RemoveAt(entryIndex); } @@ -167,30 +178,24 @@ namespace ARMeilleure.Translation.Cache { codeSize = AlignCodeSize(codeSize); - for (int i = _activeRegionIndex; i < _jitRegions.Count; i++) + int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(codeSize); + + if (allocOffset >= 0) { - int allocOffset = _cacheAllocator.Allocate(codeSize); - - if (allocOffset >= 0) - { - _jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); - _activeRegionIndex = i; - return allocOffset; - } + _jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); + return allocOffset; } int exhaustedRegion = _activeRegionIndex; ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize); _jitRegions.Add(newRegion); _activeRegionIndex = _jitRegions.Count - 1; - - int newRegionNumber = _activeRegionIndex; - Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((long)(newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation)."); - - _cacheAllocator = new CacheMemoryAllocator(CacheSize); + Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize).Bytes()} Total Allocation)."); - int allocOffsetNew = _cacheAllocator.Allocate(codeSize); + _cacheAllocators.Add(new CacheMemoryAllocator(CacheSize)); + + int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(codeSize); if (allocOffsetNew < 0) { throw new OutOfMemoryException("Failed to allocate in new Cache Region!"); diff --git a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs index 01b2aa8ed..15a1051fa 100644 --- a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs +++ b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs @@ -52,6 +52,11 @@ namespace ARMeilleure.Translation.Cache nint context, [MarshalAs(UnmanagedType.LPWStr)] string outOfProcessCallbackDll); + [LibraryImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static unsafe partial bool RtlDeleteFunctionTable( + ulong tableIdentifier); + private static GetRuntimeFunctionCallback _getRuntimeFunctionCallback; private static int _sizeOfRuntimeFunction; @@ -91,6 +96,23 @@ namespace ARMeilleure.Translation.Cache } } + public static void RemoveFunctionTableHandler(nint codeCachePointer) + { + ulong codeCachePtr = (ulong)codeCachePointer.ToInt64(); + + bool result; + + unsafe + { + result = RtlDeleteFunctionTable(codeCachePtr | 3); + } + + if (!result) + { + throw new InvalidOperationException("Failure removing function table callback."); + } + } + private static unsafe RuntimeFunction* FunctionTableHandler(ulong controlPc, nint context) { int offset = (int)((long)controlPc - context.ToInt64()); diff --git a/src/Ryujinx.Common/Helpers/RunningPlatform.cs b/src/Ryujinx.Common/Helpers/RunningPlatform.cs index 8d85c4a3c..7ec2f18df 100644 --- a/src/Ryujinx.Common/Helpers/RunningPlatform.cs +++ b/src/Ryujinx.Common/Helpers/RunningPlatform.cs @@ -5,15 +5,34 @@ using System.Runtime.InteropServices; namespace Ryujinx.Common.Helper { + public enum OperatingSystemType + { + MacOS, + Linux, + Windows + } + public static class RunningPlatform { + public static readonly OperatingSystemType CurrentOS + = IsMacOS + ? OperatingSystemType.MacOS + : IsWindows + ? OperatingSystemType.Windows + : IsLinux + ? OperatingSystemType.Linux + : throw new PlatformNotSupportedException(); + + public static Architecture Architecture => RuntimeInformation.OSArchitecture; + public static Architecture CurrentProcessArchitecture => RuntimeInformation.ProcessArchitecture; + public static bool IsMacOS => OperatingSystem.IsMacOS(); public static bool IsWindows => OperatingSystem.IsWindows(); public static bool IsLinux => OperatingSystem.IsLinux(); - public static bool IsArm => RuntimeInformation.OSArchitecture is Architecture.Arm64; + public static bool IsArm => Architecture is Architecture.Arm64; - public static bool IsX64 => RuntimeInformation.OSArchitecture is Architecture.X64; + public static bool IsX64 => Architecture is Architecture.X64; public static bool IsIntelMac => IsMacOS && IsX64; public static bool IsArmMac => IsMacOS && IsArm; diff --git a/src/Ryujinx.HLE/Loaders/Mods/MemPatch.cs b/src/Ryujinx.HLE/Loaders/Mods/MemPatch.cs index 9a1931433..f601c7749 100644 --- a/src/Ryujinx.HLE/Loaders/Mods/MemPatch.cs +++ b/src/Ryujinx.HLE/Loaders/Mods/MemPatch.cs @@ -71,16 +71,24 @@ namespace Ryujinx.HLE.Loaders.Mods int patchOffset = (int)offset; int patchSize = patch.Length; - if (patchOffset < protectedOffset || patchOffset > memory.Length) + if (patchOffset < protectedOffset) { - continue; // Add warning? + Logger.Warning?.Print(LogClass.ModLoader, $"Attempted to patch protected memory ({patchOffset:x} is within protected boundary of {protectedOffset:x})."); + continue; + } + + if (patchOffset > memory.Length) + { + Logger.Warning?.Print(LogClass.ModLoader, $"Attempted to patch out of bounds memory (offset {patchOffset} ({patchOffset:x}) exceeds memory buffer length {memory.Length})."); + continue; } patchOffset -= protectedOffset; if (patchOffset + patchSize > memory.Length) { - patchSize = memory.Length - patchOffset; // Add warning? + Logger.Warning?.Print(LogClass.ModLoader, $"Patch offset ({patchOffset:x}) + size ({patchSize}) is greater than the size of the memory buffer ({memory.Length}). Attempting to fix this..."); + patchSize = memory.Length - patchOffset; } Logger.Info?.Print(LogClass.ModLoader, $"Patching address offset {patchOffset:x} <= {BitConverter.ToString(patch).Replace('-', ' ')} len={patchSize}"); diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 8cc196a58..b741eb977 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -1113,6 +1113,13 @@ namespace Ryujinx.Ava }); (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true); + + // Reload settings when the game is turned off + // (resets custom settings if there were any) + Program.ReloadConfig(); + + // Reload application list (changes the status of the user setting if it was added or removed during the game) + Dispatcher.UIThread.Post(() => RyujinxApp.MainWindow.LoadApplications()); } public void InitStatus() diff --git a/src/Ryujinx/Assets/Styles/Styles.xaml b/src/Ryujinx/Assets/Styles/Styles.xaml index 5523f551a..eb40e853c 100644 --- a/src/Ryujinx/Assets/Styles/Styles.xaml +++ b/src/Ryujinx/Assets/Styles/Styles.xaml @@ -1,7 +1,8 @@ - + Content="Add" + Classes="red"/> + + + + + + + + + + diff --git a/src/Ryujinx/Assets/Styles/Themes.xaml b/src/Ryujinx/Assets/Styles/Themes.xaml index 3a0bd4217..de7584240 100644 --- a/src/Ryujinx/Assets/Styles/Themes.xaml +++ b/src/Ryujinx/Assets/Styles/Themes.xaml @@ -1,4 +1,4 @@ - @@ -12,11 +12,13 @@ #C1C1C1 #b3ffffff #80cccccc + #FF6347 #A0000000 #fffcd12a #FF2EEAC9 #FFFF4554 #6483F5 + #800080 #C1C1C1 #b3ffffff #80cccccc + #FF6347 #A0000000 #fffcd12a #13c3a4 #FFFF4554 #6483F5 + #800080 #3D3D3D #0FFFFFFF #1EFFFFFF + #FF6347 #A0FFFFFF #fffcd12a #FF2EEAC9 #FFFF4554 #6483F5 + #FFA500 diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 1fc7b474d..7c6e0828b 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -464,7 +464,7 @@ "pl_PL": "", "pt_BR": "Abrir Pasta de Capturas de Tela", "ru_RU": "Открыть папку со скриншотами", - "sv_SE": "", + "sv_SE": "Öppna skärmbildsmappen", "th_TH": "", "tr_TR": "", "uk_UA": "Відкрити теку скріншотів", @@ -1564,7 +1564,7 @@ "pl_PL": "", "pt_BR": "Desenvolvido por {0}", "ru_RU": "Разработана {0}", - "sv_SE": "", + "sv_SE": "Utvecklat av {0}", "th_TH": "", "tr_TR": "", "uk_UA": "Розроблено: {0}", @@ -1864,7 +1864,7 @@ "pl_PL": "", "pt_BR": "Compatibilidade:", "ru_RU": "Совместимость:", - "sv_SE": "", + "sv_SE": "Kompatibilitet:", "th_TH": "", "tr_TR": "", "uk_UA": "Сумісність:", @@ -1889,7 +1889,7 @@ "pl_PL": "", "pt_BR": "ID do Título:", "ru_RU": "ID приложения", - "sv_SE": "", + "sv_SE": "Titel-id:", "th_TH": "", "tr_TR": "", "uk_UA": "ID гри:", @@ -1914,7 +1914,7 @@ "pl_PL": "", "pt_BR": "Jogos Hospedados: {0}", "ru_RU": "Запущенно игр: {0}", - "sv_SE": "", + "sv_SE": "Värdskap för spel: {0}", "th_TH": "", "tr_TR": "", "uk_UA": "Розміщені ігри: {0}", @@ -1939,7 +1939,7 @@ "pl_PL": "", "pt_BR": "Jogadores Online: {0}", "ru_RU": "Игроков онлайн: {0}", - "sv_SE": "", + "sv_SE": "Online-spelare: {0}", "th_TH": "", "tr_TR": "", "uk_UA": "Гравців онлайн: {0}", @@ -2289,7 +2289,7 @@ "pl_PL": "", "pt_BR": "Limpar Cache PPTC", "ru_RU": "Очистить кэш PPTC", - "sv_SE": "", + "sv_SE": "Rensa PPTC-cache", "th_TH": "", "tr_TR": "", "uk_UA": "Очистити кеш PPTC", @@ -2314,7 +2314,7 @@ "pl_PL": "", "pt_BR": "Apaga os arquivos de cache PPTC do aplicativo", "ru_RU": "Удаляет все файлы кэша PPTC для приложения", - "sv_SE": "", + "sv_SE": "Tar bort alla PPTC-cachefiler för applikationen", "th_TH": "", "tr_TR": "", "uk_UA": "Видаляє всі файли кешу PPTC для застосунку", @@ -2747,6 +2747,56 @@ "zh_TW": "建立桌面捷徑,啟動選取的應用程式" } }, + { + "ID": "GameListContextMenuCreateCustomConfiguration", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Create Custom Configuration", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListContextMenuEditCustomConfiguration", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Edit Custom Configuration", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuCreateShortcutToolTipMacOS", "Translations": { @@ -2772,6 +2822,56 @@ "zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式" } }, + { + "ID": "CreateCustomConfigurationToolTip", + "Translations": { + "ar_SA": "ينشئ تكوينًا مستقلًا للعبة الحالية", + "de_DE": "Erstellt eine unabhängige Konfiguration für das aktuelle Spiel", + "el_GR": "Δημιουργεί μια ανεξάρτητη διαμόρφωση για το τρέχον παιχνίδι", + "en_US": "Creates an independent configuration for the selected game", + "es_ES": "Crea una configuración independiente para el juego actual", + "fr_FR": "Crée une configuration indépendante pour le jeu en cours", + "he_IL": "יוצר תצורה עצמאית למשחק הנוכחי", + "it_IT": "Crea una configurazione indipendente per il gioco attuale", + "ja_JP": "現在のゲーム用の独立した設定を作成します", + "ko_KR": "현재 게임에 대한 독립적인 설정을 생성합니다", + "no_NO": "Oppretter en uavhengig konfigurasjon for det gjeldende spillet", + "pl_PL": "Tworzy niezależną konfigurację dla bieżącej gry", + "pt_BR": "Cria uma configuração independente para o jogo atual", + "ru_RU": "Создает независимую конфигурацию для текущей игры", + "sv_SE": "Skapar en oberoende konfiguration för det aktuella spelet", + "th_TH": "สร้างการกำหนดค่าที่เป็นอิสระสำหรับเกมปัจจุบัน", + "tr_TR": "Mevcut oyun için bağımsız bir yapılandırma oluşturur", + "uk_UA": "Створює незалежну конфігурацію для поточної гри", + "zh_CN": "为当前游戏创建独立的配置", + "zh_TW": "為當前遊戲創建獨立的配置" + } + }, + { + "ID": "EditCustomConfigurationToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Edit your existing independent configuration for the selected game", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuShowCompatEntry", "Translations": { @@ -2789,7 +2889,7 @@ "pl_PL": "", "pt_BR": "Mostrar Dados de Compatibilidade", "ru_RU": "Показать записи о совместимости", - "sv_SE": "", + "sv_SE": "Visa kompatibilitetspost", "th_TH": "", "tr_TR": "", "uk_UA": "Iнформація про сумісність", @@ -2814,7 +2914,7 @@ "pl_PL": "", "pt_BR": "Exibe o jogo selecionado na Lista de Compatibilidade, que normalmente pode ser acessada pelo menu Ajuda.", "ru_RU": "Отобразить выбранную игру в списке совместимости, доступ к которому вы обычно можете получить через меню Справки.", - "sv_SE": "", + "sv_SE": "Visa valt spel i kompatibilitetslistan som du normalt sett kan komma åt via hjälpmenyn.", "th_TH": "", "tr_TR": "", "uk_UA": "Показати цю гру в Списку Сумісності. Список сумісності також можна зайти в меню Довідки.", @@ -2839,7 +2939,7 @@ "pl_PL": "", "pt_BR": "Mostrar Informações do Jogo", "ru_RU": "Показать информацию об игре", - "sv_SE": "", + "sv_SE": "Visa spelinformation", "th_TH": "", "tr_TR": "", "uk_UA": "Інформація про гру", @@ -2864,7 +2964,7 @@ "pl_PL": "", "pt_BR": "Exibe estatísticas e detalhes sobre o jogo selecionado.", "ru_RU": "Показывать статистику и подробную информацию о выбранной игре.", - "sv_SE": "", + "sv_SE": "Visa statistik och detaljer om det aktuella spelet.", "th_TH": "", "tr_TR": "", "uk_UA": "Показати статистику та деталі обраної гри.", @@ -3297,6 +3397,31 @@ "zh_TW": "設定" } }, + { + "ID": "SettingsWithInfo", + "Translations": { + "ar_SA": "{0} - إعدادات", + "de_DE": "Einstellungen - {0}", + "el_GR": "Ρυθμίσεις - {0}", + "en_US": "Settings - {0}", + "es_ES": "Configuración - {0}", + "fr_FR": "Paramètres - {0}", + "he_IL": "{0} - הגדרות", + "it_IT": "Impostazioni - {0}", + "ja_JP": "設定 - {0}", + "ko_KR": "설정 - {0}", + "no_NO": "Innstillinger - {0}", + "pl_PL": "Ustawienia - {0}", + "pt_BR": "Configurações - {0}", + "ru_RU": "Параметры - {0}", + "sv_SE": "Inställningar - {0}", + "th_TH": "ตั้งค่า - {0}", + "tr_TR": "Ayarlar - {0}", + "uk_UA": "Налаштування - {0}", + "zh_CN": "设置 - {0}", + "zh_TW": "設定 - {0}" + } + }, { "ID": "SettingsTabGeneral", "Translations": { @@ -3389,7 +3514,7 @@ "pl_PL": "", "pt_BR": "Verificar Atualizações:", "ru_RU": "Проверка наличия обновлений", - "sv_SE": "", + "sv_SE": "Leta efter uppdateringar:", "th_TH": "", "tr_TR": "", "uk_UA": "Перевірка оновлень:", @@ -3414,7 +3539,7 @@ "pl_PL": "", "pt_BR": "Desligado", "ru_RU": "Отключить", - "sv_SE": "", + "sv_SE": "Av", "th_TH": "", "tr_TR": "", "uk_UA": "Вимкнути", @@ -3439,7 +3564,7 @@ "pl_PL": "", "pt_BR": "Ao Abrir", "ru_RU": "При запуске", - "sv_SE": "", + "sv_SE": "Fråga", "th_TH": "", "tr_TR": "", "uk_UA": "Запитувати щоразу", @@ -3464,7 +3589,7 @@ "pl_PL": "", "pt_BR": "2° Plano", "ru_RU": "В фоне", - "sv_SE": "", + "sv_SE": "Bakgrund", "th_TH": "", "tr_TR": "", "uk_UA": "Оновлювати в фоні", @@ -3489,7 +3614,7 @@ "pl_PL": "", "pt_BR": "Ao Perder o Foco:", "ru_RU": "При выходе эмулятора из фокуса", - "sv_SE": "", + "sv_SE": "När emulatorn tappar fokus:", "th_TH": "", "tr_TR": "", "uk_UA": "При втраті фокуса емулятором:", @@ -3514,7 +3639,7 @@ "pl_PL": "", "pt_BR": "Não Fazer Nada", "ru_RU": "Ничего не делать", - "sv_SE": "", + "sv_SE": "Gör ingenting", "th_TH": "", "tr_TR": "", "uk_UA": "Нічого не робити", @@ -3539,7 +3664,7 @@ "pl_PL": "", "pt_BR": "Bloquear Controles", "ru_RU": "Блокировать управление", - "sv_SE": "", + "sv_SE": "Blockera inmatning", "th_TH": "", "tr_TR": "", "uk_UA": "Блокувати введення", @@ -3564,7 +3689,7 @@ "pl_PL": "", "pt_BR": "Ficar Mudo", "ru_RU": "Отключить звук", - "sv_SE": "", + "sv_SE": "Stäng av ljudet", "th_TH": "", "tr_TR": "", "uk_UA": "Вимкнути звук", @@ -3589,7 +3714,7 @@ "pl_PL": "", "pt_BR": "Bloquear Controles & Ficar Mudo", "ru_RU": "Блокировать управление и отключить звук", - "sv_SE": "", + "sv_SE": "Blockera inmatningar och stäng av ljudet", "th_TH": "", "tr_TR": "", "uk_UA": "Блокувати введення та Вимкнути звук", @@ -3614,7 +3739,7 @@ "pl_PL": "", "pt_BR": "Pausar a Emulação", "ru_RU": "Поставить паузу", - "sv_SE": "", + "sv_SE": "Pausa emuleringen", "th_TH": "", "tr_TR": "", "uk_UA": "Поставити на паузу", @@ -3689,7 +3814,7 @@ "pl_PL": "", "pt_BR": "Desativar Controles Quando Estiver Fora de Foco", "ru_RU": "Отключает управление при выходе из фокуса", - "sv_SE": "", + "sv_SE": "Inaktivera inmatning när fokus tappas", "th_TH": "", "tr_TR": "", "uk_UA": "", @@ -4714,7 +4839,7 @@ "pl_PL": "", "pt_BR": "Sincronizar com o Sistema PC", "ru_RU": "Соответствовать времени в системе", - "sv_SE": "", + "sv_SE": "Matcha systemtid", "th_TH": "", "tr_TR": "", "uk_UA": "", @@ -5139,7 +5264,7 @@ "pl_PL": "", "pt_BR": "Ignorar Applet do Controlador", "ru_RU": "Игнорировать апплет контроллера", - "sv_SE": "", + "sv_SE": "Ignorera kontroller-applet", "th_TH": "", "tr_TR": "", "uk_UA": "Ігнорувати Аплет Контролера", @@ -5989,7 +6114,7 @@ "pl_PL": "", "pt_BR": "Habilitar Logs da IU", "ru_RU": "Включить журнал интерфейса", - "sv_SE": "", + "sv_SE": "Aktivera gränssnittsloggar", "th_TH": "", "tr_TR": "", "uk_UA": "Увімкнути журнали інтерфейсу", @@ -6389,7 +6514,7 @@ "pl_PL": "", "pt_BR": "Redefinir Configurações", "ru_RU": "Сбросить настройки", - "sv_SE": "", + "sv_SE": "Nollställ inställningar", "th_TH": "", "tr_TR": "", "uk_UA": "Скинути налаштування", @@ -6414,7 +6539,7 @@ "pl_PL": "", "pt_BR": "Quero redefinir minhas configurações.", "ru_RU": "Я хочу сбросить свои настройки.", - "sv_SE": "", + "sv_SE": "Jag vill nollställa mina inställningar.", "th_TH": "", "tr_TR": "", "uk_UA": "Я хочу скинути налаштування.", @@ -8389,7 +8514,7 @@ "pl_PL": "", "pt_BR": "Velocidade do Arco-íris", "ru_RU": "Скорость переливания", - "sv_SE": "", + "sv_SE": "Regnbågshastighet", "th_TH": "", "tr_TR": "", "uk_UA": "", @@ -12647,6 +12772,31 @@ "zh_TW": "正在下載更新..." } }, + { + "ID": "DialogRebooterMessage", + "Translations": { + "ar_SA": "من فضلك انتظر، المحاكي في طور إعادة التشغيل", + "de_DE": "Bitte warten Sie, der Emulator wird neu gestartet", + "el_GR": "Παρακαλώ περιμένετε, ο εξομοιωτής επανεκκινείται", + "en_US": "Please wait, the emulator is restarting", + "es_ES": "Por favor, espere, el emulador se está reiniciando", + "fr_FR": "Veuillez patienter, l'émulateur est en train de redémarrer", + "he_IL": "אנא המתן, המחקה מתארגן מחדש", + "it_IT": "Attendere prego, l'emulatore si sta riavviando", + "ja_JP": "お待ちください、エミュレーターが再起動しています", + "ko_KR": "잠시만 기다려 주세요, 에뮬레이터가 재시작 중입니다", + "no_NO": "Vennligst vent, emulatoren starter på nytt", + "pl_PL": "Proszę czekać, emulator jest w trakcie ponownego uruchamiania", + "pt_BR": "Por favor, aguarde, o emulador está reiniciando", + "ru_RU": "Пожалуйста, подождите, эмулятор перезапускается", + "sv_SE": "Vänligen vänta, emulatorn startar om", + "th_TH": "กรุณารอสักครู่, ตัวจำลองกำลังเริ่มใหม่", + "tr_TR": "Lütfen bekleyin, emülatör yeniden başlatılıyor", + "uk_UA": "Будь ласка, зачекайте, емулятор перезавантажується", + "zh_CN": "请稍等,模拟器正在重新启动", + "zh_TW": "請稍候,模擬器正在重新啟動" + } + }, { "ID": "DialogUpdaterExtractionMessage", "Translations": { @@ -13664,7 +13814,7 @@ "pl_PL": "", "pt_BR": "Você está prestes a limpar todos os dados PPTC de:\n\n{0}\n\nTem certeza de que deseja continuar?", "ru_RU": "Вы собираетесь удалить все данные PPTC из:\n\n{0}\n\nВы уверены, что хотите продолжить?", - "sv_SE": "", + "sv_SE": "Du är på väg att ta bort allt PPTC-data från:\n\n{0}\n\nÄr du säker på att du vill fortsätta?", "th_TH": "", "tr_TR": "", "uk_UA": "Ви збираєтесь видалити всі дані PPTC з:\n\n{0}\n\nБажаєте продовжити цю операцію?", @@ -16514,7 +16664,7 @@ "pl_PL": "", "pt_BR": "A caixa de diálogo do Applet do controlador não aparecerá se o controle for desconectado enquanto um aplicativo estiver em execução.\n\nDeixe a opção DESLIGADO se não tiver certeza.", "ru_RU": "Диалоговое окно апплета контроллера не будет отображаться, если геймпад отключен во время работы приложения.\n\nОставьте выключенным, если не уверены.", - "sv_SE": "", + "sv_SE": "Handkontroller-appleten kommer inte att visas om gamepaden är frånkopplad under tiden en applikation körs.\n\nLämna AV om du är osäker.", "th_TH": "", "tr_TR": "", "uk_UA": "Діалогове вікно Аплету Контролера не з'явиться, якщо геймпад було відключено під час роботи програми.\n\nЗалиште вимкненим якщо не впевнені.", @@ -16989,7 +17139,7 @@ "pl_PL": "", "pt_BR": "Imprime mensagens de log do Avalonia (UI) no console.", "ru_RU": "Выводит сообщения журнала Avalonia (интерфейс) в консоли.", - "sv_SE": "", + "sv_SE": "Skriver ut loggmeddelanden från Avalonia (användargränssnittet) i konsollen.", "th_TH": "", "tr_TR": "", "uk_UA": "Виводити повідомлення журналу Avalonia (UI) в консоль", @@ -17189,7 +17339,7 @@ "pl_PL": "", "pt_BR": "Abre a pasta de capturas de tela do Ryujinx", "ru_RU": "Открывает папку скриншотов Ryujinx", - "sv_SE": "", + "sv_SE": "Öppna Ryujinx skärmbildsmapp", "th_TH": "", "tr_TR": "", "uk_UA": "Відкрити теку куди зберігаються скріншоти Ryujinx", @@ -17939,7 +18089,7 @@ "pl_PL": "", "pt_BR": "Atualização Disponível!", "ru_RU": "Доступно обновление!", - "sv_SE": "", + "sv_SE": "Uppdatering finns tillgänglig!", "th_TH": "", "tr_TR": "", "uk_UA": "Доступне оновлення!", @@ -19522,6 +19672,31 @@ "zh_TW": "{0} 更新程式" } }, + { + "ID": "RyujinxRebooter", + "Translations": { + "ar_SA": "إعادة تشغيل {0}", + "de_DE": "Neustart von {0}", + "el_GR": "Επανεκκίνηση {0}", + "en_US": "{0} Reboot", + "es_ES": "Reinicio de {0}", + "fr_FR": "Redémarrage de {0}", + "he_IL": "אתחול {0}", + "it_IT": "Riavvio di {0}", + "ja_JP": "{0} 再起動", + "ko_KR": "{0} 재부팅", + "no_NO": "Omstart av {0}", + "pl_PL": "Ponowne uruchomienie {0}", + "pt_BR": "Reinício de {0}", + "ru_RU": "{0} Перезагрузка", + "sv_SE": "Ominläsning av {0}", + "th_TH": "เริ่มต้นใหม่ {0}", + "tr_TR": "{0} Yeniden Başlatma", + "uk_UA": "Перезавантаження {0}", + "zh_CN": "{0} 重启", + "zh_TW": "{0} 重新啟動" + } + }, { "ID": "SettingsTabHotkeys", "Translations": { @@ -23889,7 +24064,7 @@ "pl_PL": "", "pt_BR": "Inicializa e roda sem travamentos ou bugs de GPU de qualquer tipo, e em uma velocidade rápida o suficiente para ser aproveitado em um PC comum.", "ru_RU": "Запускается и работает без любого рода сбоев или графисечких ошибок и на скорости, достаточной для работы на обычном ПК.", - "sv_SE": "", + "sv_SE": "Startar upp och spelas utan några krascher eller GPU-fel av några slag och med en hastighet som är snabb nog för bra upplevelse på en genomsnittlig PC.", "th_TH": "", "tr_TR": "", "uk_UA": "Запускається та оптимально працює (без збоїв або графічних багів) на середньостатистичному комп'ютері.", @@ -23914,7 +24089,7 @@ "pl_PL": "", "pt_BR": "Inicializa e entra no jogo, mas sofre de um ou mais dos seguintes: travamentos, deadlocks, bugs de GPU, áudio ruim que distrai ou é simplesmente muito lento. O jogo ainda pode ser jogado até o fim, mas não da forma como foi criado para ser jogado.", "ru_RU": "Запускается и работает, но возникает одна или несколько из следующих проблем: сбои, взаимоблокировки, ошибки GPU, отвлекающие звуки или просто слишком медленная работа. Возможно, игру всё же удастся пройти до конца, но не так, как она задумана.", - "sv_SE": "", + "sv_SE": "Startar och går in i spelet men lider av ett eller flera av följande: kraschar, deadlocks, GPU-buggar, distraherande dåligt ljud eller är helt enkelt för långsamt. Spelet kan fortfarande spelas hela vägen igenom, men inte så som spelet är avsett att spelas.", "th_TH": "", "tr_TR": "", "uk_UA": "Запускається, але в грі на вас чекатимуть одна або декілька наступних проблем: збої, зависання, графічні баги, спотворений звук або ж гра загалом працюватиме надто повільно. Можливо, її все ще можна пройти, але досвід буде не найкращим.", @@ -23939,7 +24114,7 @@ "pl_PL": "", "pt_BR": "Inicializa e passa da tela de título, mas não entra no jogo principal.", "ru_RU": "Загружается титульный экран и можно перейти дальше, но сама игра не работает.", - "sv_SE": "", + "sv_SE": "Startar upp och går förbi titelskärmen men tar sig inte in i huvudspelet.", "th_TH": "", "tr_TR": "", "uk_UA": "Запускається та проходить початковий екран, але пограти не вийде.", @@ -23964,7 +24139,7 @@ "pl_PL": "", "pt_BR": "Inizializa, mas não passa da tela de título.", "ru_RU": "Загружается, но не проходит дальше титульного экрана.", - "sv_SE": "", + "sv_SE": "Startar upp men tar sig inte förbi titelskärmen.", "th_TH": "", "tr_TR": "", "uk_UA": "Запускається, але не відображає навіть початкового екрану.", @@ -23989,7 +24164,7 @@ "pl_PL": "", "pt_BR": "Não inicializa ou não mostra sinais de atividade.", "ru_RU": "Не запускается или не подаёт признаков жизни.", - "sv_SE": "", + "sv_SE": "Startar inte upp eller visar någon form av aktivitet.", "th_TH": "", "tr_TR": "", "uk_UA": "Взагалі не запускається.", @@ -23997,6 +24172,56 @@ "zh_TW": "" } }, + { + "ID": "GameSpecificConfigurationHeader", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Custom Config", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameSpecificConfigurationGlobal", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "(Global)", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "ExtractAocListHeader", "Translations": { @@ -24039,7 +24264,7 @@ "pl_PL": "", "pt_BR": "Imagem da Presença do Discord", "ru_RU": "Изображение для статуса активности", - "sv_SE": "", + "sv_SE": "Bild för Rich Presence", "th_TH": "", "tr_TR": "", "uk_UA": "Зображення картки активності Discord", @@ -24064,7 +24289,7 @@ "pl_PL": "", "pt_BR": "Presença Dinâmica do Discord", "ru_RU": "Динамический статус активности", - "sv_SE": "", + "sv_SE": "Dynamisk Rich Presence", "th_TH": "", "tr_TR": "", "uk_UA": "Динамічна картка активності Discord", diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index 4c86a6177..f60cff49b 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -54,6 +54,7 @@ namespace Ryujinx.Ava.Common.Locale SetDynamicValues(LocaleKeys.RyujinxInfo, RyujinxApp.FullAppName); SetDynamicValues(LocaleKeys.RyujinxConfirm, RyujinxApp.FullAppName); SetDynamicValues(LocaleKeys.RyujinxUpdater, RyujinxApp.FullAppName); + SetDynamicValues(LocaleKeys.RyujinxRebooter, RyujinxApp.FullAppName); } public string this[LocaleKeys key] diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 9b8ded44b..edd46a7db 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -32,8 +32,10 @@ namespace Ryujinx.Ava public static double DesktopScaleFactor { get; set; } = 1.0; public static string Version { get; private set; } public static string ConfigurationPath { get; private set; } + public static string GlobalConfigurationPath { get; private set; } public static bool PreviewerDetached { get; private set; } public static bool UseHardwareAcceleration { get; private set; } + public static string BackendThreadingArg { get; private set; } [LibraryImport("user32.dll", SetLastError = true)] public static partial int MessageBoxA(nint hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type); @@ -156,11 +158,38 @@ namespace Ryujinx.Ava } } + public static string GetDirGameUserConfig(string gameId, bool rememberGlobalDir = false, bool changeFolderForGame = false) + { + if (string.IsNullOrEmpty(gameId)) + { + return ""; + } + + string gameDir = Path.Combine(AppDataManager.GamesDirPath, gameId, ReleaseInformation.ConfigName); + + // Should load with the game if there is a custom setting for the game + if (rememberGlobalDir) + { + GlobalConfigurationPath = ConfigurationPath; + } + + if (changeFolderForGame) + { + ConfigurationPath = gameDir; + } + + return gameDir; + } + public static void ReloadConfig() { + //It is necessary that when a user setting appears, the global setting remains available + GlobalConfigurationPath = null; + string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); + // Now load the configuration as the other subsystems are now registered if (File.Exists(localConfigurationPath)) { @@ -217,6 +246,11 @@ namespace Ryujinx.Ava _ => ConfigurationState.Instance.Graphics.BackendThreading }; + if (CommandLineState.OverrideBackendThreadingAfterReboot is not null) + { + BackendThreadingArg = CommandLineState.OverrideBackendThreadingAfterReboot; + } + // Check if docked mode was overriden. if (CommandLineState.OverrideDockedMode.HasValue) ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; @@ -232,6 +266,33 @@ namespace Ryujinx.Ava _ => ConfigurationState.Instance.HideCursor, }; + // Check if memoryManagerMode was overridden. + if (CommandLineState.OverrideMemoryManagerMode is not null) + if (Enum.TryParse(CommandLineState.OverrideMemoryManagerMode, true, out MemoryManagerMode result)) + { + ConfigurationState.Instance.System.MemoryManagerMode.Value = result; + } + + // Check if PPTC was overridden. + if (CommandLineState.OverridePPTC is not null) + if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result)) + { + ConfigurationState.Instance.System.EnablePtc.Value = result; + } + + // Check if region was overridden. + if (CommandLineState.OverrideSystemRegion is not null) + if (Enum.TryParse(CommandLineState.OverrideSystemRegion, true, out Ryujinx.HLE.HOS.SystemState.RegionCode result)) + { + ConfigurationState.Instance.System.Region.Value = (Utilities.Configuration.System.Region)result; + } + + //Check if language was overridden. + if (CommandLineState.OverrideSystemLanguage is not null) + if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Ryujinx.HLE.HOS.SystemState.SystemLanguage result)) + { + ConfigurationState.Instance.System.Language.Value = (Utilities.Configuration.System.Language)result; + } // Check if hardware-acceleration was overridden. if (CommandLineState.OverrideHardwareAcceleration != null) diff --git a/src/Ryujinx/Rebooter.cs b/src/Ryujinx/Rebooter.cs new file mode 100644 index 000000000..8c7755a4c --- /dev/null +++ b/src/Ryujinx/Rebooter.cs @@ -0,0 +1,76 @@ +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.Utilities; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava +{ + internal static class Rebooter + { + + private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); + + + public static void RebootAppWithGame(string gamePath, List args) + { + _ = Reboot(gamePath, args); + + } + + private static async Task Reboot(string gamePath, List args) + { + + bool shouldRestart = true; + + TaskDialog taskDialog = new() + { + Header = LocaleManager.Instance[LocaleKeys.RyujinxRebooter], + SubHeader = LocaleManager.Instance[LocaleKeys.DialogRebooterMessage], + IconSource = new SymbolIconSource { Symbol = Symbol.Games }, + XamlRoot = RyujinxApp.MainWindow, + }; + + if (shouldRestart) + { + List arguments = CommandLineState.Arguments.ToList(); + string executableDirectory = AppDomain.CurrentDomain.BaseDirectory; + + var dialogTask = taskDialog.ShowAsync(true); + await Task.Delay(500); + + // Find the process name. + string ryuName = Path.GetFileName(Environment.ProcessPath) ?? string.Empty; + + // Fallback if the executable could not be found. + if (ryuName.Length == 0 || !Path.Exists(Path.Combine(executableDirectory, ryuName))) + { + ryuName = OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx"; + } + + ProcessStartInfo processStart = new(ryuName) + { + UseShellExecute = true, + WorkingDirectory = executableDirectory, + }; + + foreach (var arg in args) + { + processStart.ArgumentList.Add(arg); + } + + processStart.ArgumentList.Add(gamePath); + + Process.Start(processStart); + + Environment.Exit(0); + } + } + } +} diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index c9bd08fa3..a8a6f24c6 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.Applet _parent.SettingsWindow = new SettingsWindow(_parent.VirtualFileSystem, _parent.ContentManager); - await _parent.SettingsWindow.ShowDialog(window); + await StyleableAppWindow.ShowAsync(_parent.SettingsWindow, window); _parent.SettingsWindow = null; diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 3e47a1910..64b30e211 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -19,6 +19,18 @@ Header="{ext:Locale GameListContextMenuCreateShortcut}" Icon="{ext:Icon fa-solid fa-bookmark}" ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" /> + + 0) @@ -386,13 +389,26 @@ namespace Ryujinx.Ava.UI.Controls viewModel.SelectedApplication.Icon ); } - + + public async void EditGameConfiguration_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + { + await StyleableAppWindow.ShowAsync(new GameSpecificSettingsWindow(viewModel)); + + // just checking for file presence + viewModel.SelectedApplication.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false)); + + viewModel.RefreshView(); + } + } + public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args) { if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) await CompatibilityList.Show(viewModel.SelectedApplication.IdString); } - + public async void OpenApplicationData_Click(object sender, RoutedEventArgs args) { if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml index 7f7e3260b..bd20e2969 100644 --- a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml @@ -7,6 +7,7 @@ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" d:DesignHeight="450" d:DesignWidth="800" Focusable="True" @@ -73,12 +74,18 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}"> - + + + + @@ -86,10 +93,28 @@ Margin="5,5,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" - FontSize="16" + FontSize="18" Foreground="{DynamicResource FavoriteApplicationIconColor}" IsVisible="{Binding Favorite}" Symbol="StarFilled" /> + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index c6b7268b9..5ed7acc20 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -6,6 +6,7 @@ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" d:DesignHeight="450" d:DesignWidth="800" Focusable="True" @@ -156,6 +157,13 @@ Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}" TextAlignment="Start" TextWrapping="Wrap"/> + null, }; } + set + { + ListSelectedApplication = value; + GridSelectedApplication = value; + } } public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo; @@ -1085,7 +1090,7 @@ namespace Ryujinx.Ava.UI.ViewModels _rendererWaitEvent.WaitOne(); AppHost?.Start(); - + AppHost?.DisposeContext(); } @@ -1551,8 +1556,50 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool InitializeUserConfig(ApplicationData application) + { + // Code where conditions will be met before loading the user configuration (Global Config) + BackendThreading backendThreadingValue = ConfigurationState.Instance.Graphics.BackendThreading.Value; + string BackendThreadingInit = Program.BackendThreadingArg; + + if (BackendThreadingInit is null) + { + BackendThreadingInit = ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString(); + } + + // If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting. + string idGame = application.IdBaseString; + if (ConfigurationFileFormat.TryLoad(Program.GetDirGameUserConfig(idGame), out ConfigurationFileFormat configurationFileFormat)) + { + // Loads the user configuration, having previously changed the global configuration to the user configuration + ConfigurationState.Instance.Load(configurationFileFormat, Program.GetDirGameUserConfig(idGame, true, true), idGame); + } + + // Code where conditions will be executed after loading user configuration + if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != BackendThreadingInit) + { + + List Arguments = new List + { + "--bt", ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() // BackendThreading + }; + + Rebooter.RebootAppWithGame(application.Path, Arguments); + + return true; + } + + return false; + } + public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct? customNacpData = null) { + + if (InitializeUserConfig(application)) + { + return; + } + if (AppHost != null) { await ContentDialogHelper.CreateInfoDialog( @@ -1568,7 +1615,7 @@ namespace Ryujinx.Ava.UI.ViewModels #if RELEASE await PerformanceCheck(); #endif - + Logger.RestartTime(); SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); @@ -1613,6 +1660,7 @@ namespace Ryujinx.Ava.UI.ViewModels Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; gameThread.Start(); + } public void SwitchToRenderer(bool startFullscreen) => @@ -1699,7 +1747,7 @@ namespace Ryujinx.Ava.UI.ViewModels string titleId = AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(); AmiiboWindow window = new(ShowAll, LastScannedAmiiboId, titleId); - await window.ShowDialog(Window); + await StyleableAppWindow.ShowAsync(window); if (window.IsScanned) { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 689d872a1..d0a6c6d8a 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -1,5 +1,6 @@ using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -27,6 +28,7 @@ using Ryujinx.HLE.HOS.Services.Time.TimeZone; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Threading.Tasks; @@ -68,6 +70,19 @@ namespace Ryujinx.Ava.UI.ViewModels public SettingsHacksViewModel DirtyHacks { get; } + private readonly bool _isGameRunning; + private Bitmap _gameIcon; + private string _gameTitle; + private string _gamePath; + private string _gameId; + public bool IsGameRunning => _isGameRunning; + public Bitmap GameIcon => _gameIcon; + public string GamePath => _gamePath; + public string GameTitle => _gameTitle; + public string GameId => _gameId; + public bool IsGameTitleNotNull => !string.IsNullOrEmpty(GameTitle); + public double PanelOpacity => IsGameTitleNotNull ? 0.5 : 1; + public int ResolutionScale { get => _resolutionScale; @@ -335,7 +350,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsInvalidLdnPassphraseVisible { get; set; } - public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(false) { _virtualFileSystem = virtualFileSystem; _contentManager = contentManager; @@ -348,7 +363,51 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public SettingsViewModel() + public SettingsViewModel( + VirtualFileSystem virtualFileSystem, + ContentManager contentManager, + bool gameRunning, + string gamePath, + string gameName, + string gameId, + byte[] gameIconData, + bool enableToLoadCustomConfig) : this(enableToLoadCustomConfig) + { + _virtualFileSystem = virtualFileSystem; + _contentManager = contentManager; + + if (gameIconData != null && gameIconData.Length > 0) + { + using (var ms = new MemoryStream(gameIconData)) + { + _gameIcon = new Bitmap(ms); + } + } + + _isGameRunning = gameRunning; + _gamePath = gamePath; + _gameTitle = gameName; + _gameId = gameId; + + if (enableToLoadCustomConfig) // During the game. If there is no user config, then load the global config window + { + string gameDir = Program.GetDirGameUserConfig(gameId, false, true); + if (ConfigurationFileFormat.TryLoad(gameDir, out ConfigurationFileFormat configurationFileFormat)) + { + ConfigurationState.Instance.Load(configurationFileFormat, gameDir, gameId); + } + + LoadCurrentConfiguration(); // Needed to load custom configuration + } + + if (Program.PreviewerDetached) + { + Task.Run(LoadTimeZones); + + } + } + + public SettingsViewModel(bool noLoadGlobalConfig = false) { GameDirectories = []; AutoloadDirectories = []; @@ -363,7 +422,9 @@ namespace Ryujinx.Ava.UI.ViewModels if (Program.PreviewerDetached) { Task.Run(LoadAvailableGpus); - LoadCurrentConfiguration(); + + // if (!noLoadGlobalConfig)// Default is false, but loading custom config avoids double call + LoadCurrentConfiguration(); DirtyHacks = new SettingsHacksViewModel(this); } @@ -592,8 +653,8 @@ namespace Ryujinx.Ava.UI.ViewModels config.HideCursor.Value = (HideCursorMode)HideCursor; config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType; config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType; - config.UI.GameDirs.Value = [..GameDirectories]; - config.UI.AutoloadDirs.Value = [..AutoloadDirectories]; + config.UI.GameDirs.Value = [.. GameDirectories]; + config.UI.AutoloadDirs.Value = [.. AutoloadDirectories]; config.UI.BaseStyle.Value = BaseStyleIndex switch { @@ -614,10 +675,10 @@ namespace Ryujinx.Ava.UI.ViewModels // System config.System.Region.Value = (Region)Region; - + if (config.System.Language.Value != (Language)Language) GameListNeedsRefresh = true; - + config.System.Language.Value = (Language)Language; if (_validTzRegions.Contains(TimeZone)) { @@ -696,7 +757,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.Multiplayer.DisableP2p.Value = DisableP2P; config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; config.Multiplayer.LdnServer.Value = LdnServer; - + // Dirty Hacks config.Hacks.Xc2MenuSoftlockFix.Value = DirtyHacks.Xc2MenuSoftlockFix; @@ -712,7 +773,11 @@ namespace Ryujinx.Ava.UI.ViewModels private static void RevertIfNotSaved() { - Program.ReloadConfig(); + // maybe this is an unnecessary check(all options need to be tested) + if (string.IsNullOrEmpty(Program.GlobalConfigurationPath)) + { + Program.ReloadConfig(); + } } public void ApplyButton() @@ -720,6 +785,26 @@ namespace Ryujinx.Ava.UI.ViewModels SaveSettings(); } + public void DeleteConfigGame() + { + string gameDir = Program.GetDirGameUserConfig(GameId,false,false); + + if (File.Exists(gameDir)) + { + File.Delete(gameDir); + } + + RevertIfNotSaved(); + CloseWindow?.Invoke(); + } + + public void SaveUserConfig() + { + SaveSettings(); + RevertIfNotSaved(); // Revert global configuration after saving user configuration + CloseWindow?.Invoke(); + } + public void OkButton() { SaveSettings(); diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index d1931ae2f..2cab0915d 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -19,6 +19,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; @@ -130,9 +131,26 @@ namespace Ryujinx.Ava.UI.Views.Main Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager); Rainbow.Enable(); - - await Window.SettingsWindow.ShowDialog(Window); - + + if (ViewModel.SelectedApplication is null) // Checks if game data exists + { + await StyleableAppWindow.ShowAsync(Window.SettingsWindow); + } + else + { + bool customConfigExists = File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString)); + + if (!ViewModel.IsGameRunning || !customConfigExists) + { + await Window.SettingsWindow.ShowDialog(Window); // The game is not running, or if the user configuration does not exist + } + else + { + // If there is a custom configuration in the folder + await StyleableAppWindow.ShowAsync(new GameSpecificSettingsWindow(ViewModel, customConfigExists)); + } + } + Rainbow.Disable(); Rainbow.Reset(); @@ -158,11 +176,13 @@ namespace Ryujinx.Ava.UI.Views.Main string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString(); - await new CheatWindow( - Window.VirtualFileSystem, - ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText, - name, - ViewModel.SelectedApplication.Path).ShowDialog(Window); + await StyleableAppWindow.ShowAsync( + new CheatWindow( + Window.VirtualFileSystem, + ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText, + name, + ViewModel.SelectedApplication.Path) + ); ViewModel.AppHost.Device.EnableCheats(); } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index 5daa7f69f..dd6858ee6 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -156,6 +156,8 @@ ValueMemberBinding="{Binding Mode=OneWay, Converter={x:Static helpers:TimeZoneConverter.Instance}}" /> + + - + + - + + + - - + + + + + - - + + + + + - - + + + + + - + @@ -64,7 +86,11 @@ - + @@ -81,8 +107,11 @@ + - + @@ -100,7 +129,11 @@ - + + - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +